diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a7e5d0c..d3c771d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,7 @@ jobs: - name: Install dependencies run: npm ci - - name: Run tests + - name: Run JS tests via Turbo run: npm run test:js native-kotlin-test: @@ -126,7 +126,7 @@ jobs: - name: Run Kotlin native tests working-directory: example/android - run: ./gradlew :voltra:testDebugUnitTest + run: ./gradlew :use-voltra_android-client:testDebugUnitTest native-swift-test: name: '[Swift] Test' diff --git a/.gitignore b/.gitignore index de6d9e01..34294cb3 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,4 @@ npm-debug.log ## Build /build -/packages/voltra/ios/.build/ +/packages/ios-client/ios/.build/ diff --git a/.oxlintrc.json b/.oxlintrc.json index 5dec186d..92bcba9f 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -322,32 +322,36 @@ } }, { - "files": ["packages/expo-plugin/src/**/*.{ts,tsx,js,jsx}"], + "files": [ + "packages/expo-plugin/src/**/*.{ts,tsx,js,jsx}", + "packages/ios-client/expo-plugin/src/**/*.{ts,tsx,js,jsx}", + "packages/android-client/expo-plugin/src/**/*.{ts,tsx,js,jsx}" + ], "rules": { "no-restricted-imports": [ "error", { "paths": [ - { "name": "voltra", "message": "@use-voltra/expo-plugin must stay installation-time only." }, - { "name": "@use-voltra/core", "message": "@use-voltra/expo-plugin must stay installation-time only." }, - { "name": "@use-voltra/server", "message": "@use-voltra/expo-plugin must stay installation-time only." }, - { "name": "@use-voltra/ios", "message": "@use-voltra/expo-plugin must stay installation-time only." }, - { "name": "@use-voltra/android", "message": "@use-voltra/expo-plugin must stay installation-time only." }, + { "name": "voltra", "message": "Voltra config plugins must stay installation-time only." }, + { "name": "@use-voltra/core", "message": "Voltra config plugins must stay installation-time only." }, + { "name": "@use-voltra/server", "message": "Voltra config plugins must stay installation-time only." }, + { "name": "@use-voltra/ios", "message": "Voltra config plugins must stay installation-time only." }, + { "name": "@use-voltra/android", "message": "Voltra config plugins must stay installation-time only." }, { "name": "@use-voltra/ios-client", - "message": "@use-voltra/expo-plugin must stay installation-time only." + "message": "Voltra config plugins must stay installation-time only." }, { "name": "@use-voltra/android-client", - "message": "@use-voltra/expo-plugin must stay installation-time only." + "message": "Voltra config plugins must stay installation-time only." }, { "name": "@use-voltra/ios-server", - "message": "@use-voltra/expo-plugin must stay installation-time only." + "message": "Voltra config plugins must stay installation-time only." }, { "name": "@use-voltra/android-server", - "message": "@use-voltra/expo-plugin must stay installation-time only." + "message": "Voltra config plugins must stay installation-time only." }, { "name": "@use-voltra/ios/client", @@ -364,7 +368,7 @@ } }, { - "files": ["packages/voltra/src/**/*.{ts,tsx,js,jsx}", "packages/voltra/generator/**/*.{ts,tsx,js,jsx}"], + "files": ["packages/generator/generator/**/*.{ts,tsx,js,jsx}"], "rules": { "no-restricted-imports": [ "error", diff --git a/.prettierignore b/.prettierignore index ce02fc7f..2f4f7afa 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,6 +2,7 @@ /android/build /example/android /packages/*/build +/packages/*/expo-plugin/build /packages/*/.expo /packages/*/android/build /packages/*/ios/.build diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b3591164..51746758 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,23 +12,30 @@ All work on Voltra happens directly on GitHub. Contributors send pull requests w 1. Fork the repo and create your branch from `main` (a guide on [how to fork a repository](https://help.github.com/articles/fork-a-repo/)). 2. Run `npm install` to install all required dependencies. -3. Build the plugin: `npm run build --workspace @use-voltra/expo-plugin`. +3. Build the plugins: `npm run build:plugin`. 4. Now you are ready to make changes. ## Architecture overview ### JS/TS code structure -The JavaScript/TypeScript code has **two separate entry points** that must be maintained as independent boundaries: +The JavaScript/TypeScript code is split by platform and runtime boundary: -- **Client entry (`packages/voltra/src/index.ts`)**: React Native code that runs in the app. Exports JSX components, hooks, and the imperative API for managing Live Activities. -- **Server entry (`packages/voltra/src/server.ts`)**: Node.js code for rendering Voltra components to string payloads. Used for server-side rendering and push notification payloads. +- **iOS JSX (`packages/ios`)**: `Voltra` components and `@use-voltra/ios/server` rendering helpers. +- **iOS client (`packages/ios-client`)**: React Native runtime APIs, previews, and the iOS Expo config plugin. +- **Android JSX (`packages/android`)**: `VoltraAndroid` components and `@use-voltra/android/server` rendering helpers. +- **Android client (`packages/android-client`)**: React Native runtime APIs and the Android Expo config plugin. +- **Server packages (`packages/ios-server`, `packages/android-server`, `packages/server`)**: Node-only rendering and HTTP widget handlers. -⚠️ **Important**: These two entry points must remain separate. Client code should not import server-only dependencies, and server code should not import React Native-specific modules. +⚠️ **Important**: Keep client and server entry points separate. App bundles must not import server-only packages. -### Expo config plugin (`packages/expo-plugin/`) +### Expo config plugins -The Expo plugin in `packages/expo-plugin/src/` handles all Xcode project setup during `expo prebuild`: +- `packages/expo-plugin/` — shared validation, locale picking, prerender utilities +- `packages/ios-client/expo-plugin/` — iOS Live Activities, widget extension, Xcode setup +- `packages/android-client/expo-plugin/` — Android widgets and manifest + +The iOS plugin handles Xcode project setup during `expo prebuild`: 1. **Creates the widget extension target** with proper build settings 2. **Copies template files** from `ios-files/` (widget bundle, assets, Info.plist) into the extension target @@ -36,28 +43,24 @@ The Expo plugin in `packages/expo-plugin/src/` handles all Xcode project setup d 4. **Sets up entitlements** for App Groups (optional, for event forwarding) 5. **Configures push notifications** (optional) -### Swift code distribution (`ios/`) +### Swift code distribution (`packages/ios-client/ios/`) + +Voltra's Swift sources for the iOS React Native client live under `@use-voltra/ios-client` and ship as **CocoaPods** pods: -Voltra's Swift code lives in `ios/` and is distributed as a **CocoaPods package** with multiple subspecs: +- **`Voltra.podspec`**: React Native Turbo Module + Fabric view + shared Swift UI (`ios/app/`, `ios/ui/`, `ios/shared/`). +- **`VoltraWidget.podspec`**: Widget extension Swift (`ios/ui/`, `ios/shared/`, `ios/target/`). ```ruby -# From ios/Voltra.podspec -s.subspec 'Core' do |ss| - # React Native bridge module (auto-linked by Expo) - ss.source_files = ["app/**/*.swift", "shared/**/*.swift", "ui/**/*.swift"] -end - -s.subspec 'Widget' do |ss| - # Widget extension code (used by Live Activity target) - ss.source_files = ["shared/**/*.swift", "ui/**/*.swift", "target/**/*.swift"] -end +# From packages/ios-client/Voltra.podspec (paths relative to the podspec) +s.source_files = [ + "ios/app/**/*.swift", + "ios/app/**/*.m", + "ios/app/**/*.mm", + "ios/ui/**/*.swift", + "ios/shared/**/*.swift", +] ``` -- **`Core` subspec**: Contains the React Native module (`app/`), shared code (`shared/`), and UI components (`ui/`). Auto-linked by Expo in the main app. -- **`Widget` subspec**: Contains shared code, UI components, and widget-specific files (`target/`). Used by the Live Activity extension target. - -This separation ensures the widget extension doesn't include unnecessary React Native dependencies. - ### Template files (`ios-files/`) Files in `ios-files/` are copied by the config plugin into the generated widget extension: @@ -68,13 +71,13 @@ Files in `ios-files/` are copied by the config plugin into the generated widget ## Props synchronization -Component props are kept in sync between TypeScript and Swift via a **custom code generator**. The single source of truth is: +Component props are kept in sync across TypeScript, Swift, and Kotlin via a **custom code generator**. The single source of truth is the private generator workspace: ``` -data/components.json +packages/generator/data/components.json ``` -This file defines all components, their parameters, types, and short names used for payload compression. +This file defines all components, their parameters, platform availability, and short names used for payload compression. ### Running the generator @@ -82,14 +85,27 @@ This file defines all components, their parameters, types, and short names used npm run generate ``` -This generates: +This runs the generator script in `@use-voltra/generator` at `packages/generator/generator/generate-types.ts`. + +The generator filters components by platform (`swiftAvailability` for iOS, `androidAvailability` for Android) and writes outputs to the packages that own each runtime: + +| Output | Path | +| ------------------------------------------ | --------------------------------------------------------------------------------------- | +| **TypeScript prop types (iOS)** | `packages/ios/src/jsx/props/*.ts` | +| **TypeScript prop types (Android)** | `packages/android/src/jsx/props/*.ts` | +| **Swift parameter structs** | `packages/ios-client/ios/ui/Generated/Parameters/*.swift` | +| **Kotlin parameter structs** | `packages/android-client/android/src/main/java/voltra/models/parameters/*Parameters.kt` | +| **iOS component ID mappings (TS)** | `packages/ios/src/payload/component-ids.ts` | +| **Android component ID mappings (TS)** | `packages/android/src/payload/component-ids.ts` | +| **iOS component ID mappings (Swift)** | `packages/ios-client/ios/shared/ComponentTypeID.swift` | +| **Android component ID mappings (Kotlin)** | `packages/android-client/android/src/main/java/voltra/payload/ComponentTypeID.kt` | +| **Short name mappings (TS)** | `packages/core/src/payload/short-names.ts` | +| **Short name mappings (Swift)** | `packages/ios-client/ios/shared/ShortNames.swift` | +| **Short name mappings (Kotlin)** | `packages/android-client/android/src/main/java/voltra/generated/ShortNames.kt` | -- **TypeScript prop types**: `src/jsx/props/*.ts` -- **Swift parameter structs**: `ios/ui/Generated/Parameters/*.swift` -- **Component ID mappings**: `src/payload/component-ids.ts` and `ios/shared/ComponentTypeID.swift` -- **Short name mappings**: `src/payload/short-names.ts` and `ios/shared/ShortNames.swift` +After generation, the script formats JS (iOS and Android packages), Kotlin (`@use-voltra/android-client`), and Swift (`@use-voltra/ios-client`). -⚠️ **Important**: When adding new components or modifying props, always update `data/components.json` first, then run the generator. Do not manually edit generated files (marked with `.generated`). +⚠️ **Important**: `packages/generator` is a private tooling workspace, not a published runtime package. When adding new components or modifying props, always update `packages/generator/data/components.json` first, then run the generator. Do not manually edit generated files (directories include a `.generated` marker file). Component `.tsx` files that call `createVoltraComponent` are still written by hand in `packages/ios/src/jsx/` and `packages/android/src/jsx/`. ## Payload size budget @@ -125,8 +141,8 @@ The payload schema has a version number to support forward compatibility. When t The version is defined in two places that must stay in sync: -- **TypeScript**: `packages/voltra/src/renderer/renderer.ts` → `VOLTRA_PAYLOAD_VERSION` -- **Swift**: `packages/voltra/ios/shared/VoltraPayloadMigrator.swift` → `currentVersion` +- **TypeScript**: `packages/core/src/renderer/renderer.ts` → `VOLTRA_PAYLOAD_VERSION` +- **Swift**: `packages/ios-client/ios/shared/VoltraPayloadMigrator.swift` → `currentVersion` ### When to increment the version @@ -181,7 +197,7 @@ The `example/` directory contains an Expo app for testing changes. ```sh # 1) Build the plugin -npm run build --workspace @use-voltra/expo-plugin +npm run build:plugin # 2) Install example dependencies (cd example && npm install) @@ -193,7 +209,7 @@ npm run build --workspace @use-voltra/expo-plugin (cd example && npx expo run:ios) ``` -If iterating on the plugin, rebuild after each change in `packages/expo-plugin/src/`. +If iterating on a plugin, rebuild after each change under `packages/expo-plugin/src/`, `packages/ios-client/expo-plugin/src/`, or `packages/android-client/expo-plugin/src/`. ### Running tests diff --git a/README.md b/README.md index b7692cad..381bece5 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Voltra turns React Native JSX into SwiftUI and Jetpack Compose Glance so you can - **Type-Safe & Developer-Friendly**: The Voltra schema, hooks, and examples ship with TypeScript definitions, tests, and docs so AI coding agents stay productive. -- **Works With Your Setup**: Compatible with Expo Dev Client and bare React Native projects. The config plugin automatically wires native extension targets for you. +- **Works With Your Setup**: Compatible with Expo Dev Client and bare React Native projects. Platform config plugins wire native extension targets for you. ## Documentation @@ -38,18 +38,44 @@ The documentation is available at [use-voltra.dev](https://use-voltra.dev). You > [!NOTE] > The library isn't supported in Expo Go. To set it up correctly, you need to use [Expo Dev Client](https://docs.expo.dev/versions/latest/sdk/dev-client/). -Install the package: +Install the client package for each platform you need: ```sh -npm install voltra +# iOS +npm install @use-voltra/ios-client + +# Android +npm install @use-voltra/android-client ``` -Add the config plugin to your `app.json`: +Add the Expo plugins to your `app.json`: ```json { "expo": { - "plugins": ["voltra"] + "plugins": [ + [ + "@use-voltra/ios-client", + { + "groupIdentifier": "group.your.bundle.identifier", + "enablePushNotifications": true + } + ], + [ + "@use-voltra/android-client", + { + "widgets": [ + { + "id": "my_widget", + "displayName": "My Widget", + "description": "A Voltra widget", + "targetCellWidth": 2, + "targetCellHeight": 2 + } + ] + } + ] + ] } } ``` @@ -61,8 +87,7 @@ See the [documentation](https://use-voltra.dev/getting-started/installation) for ## Quick example ```tsx -import { useLiveActivity } from 'voltra/client' -import { Voltra } from 'voltra' +import { useLiveActivity, Voltra } from '@use-voltra/ios-client' export function OrderTracker({ orderId }: { orderId: string }) { const ui = ( @@ -94,7 +119,7 @@ Voltra is a cross-platform library that supports: ## Authors -`voltra` is an open source collaboration between [Saúl Sharma](https://github.com/saulsharma) and [Szymon Chmal](https://github.com/szymonchmal) at [Callstack][callstack-readme-with-love]. +Voltra is an open source collaboration between [Saúl Sharma](https://github.com/saulsharma) and [Szymon Chmal](https://github.com/szymonchmal) at [Callstack][callstack-readme-with-love]. If you think it's cool, please star it 🌟. This project will always remain free to use. @@ -103,9 +128,9 @@ If you think it's cool, please star it 🌟. This project will always remain fre Like the project? ⚛️ [Join the Callstack team](https://callstack.com/careers/?utm_campaign=Senior_RN&utm_source=github&utm_medium=readme) who does amazing stuff for clients and drives React Native Open Source! 🔥 [callstack-readme-with-love]: https://callstack.com/?utm_source=github.com&utm_medium=referral&utm_campaign=voltra&utm_term=readme-with-love -[license-badge]: https://img.shields.io/npm/l/voltra?style=for-the-badge +[license-badge]: https://img.shields.io/npm/l/@use-voltra/ios?style=for-the-badge [license]: https://github.com/callstackincubator/voltra/blob/main/LICENSE.txt -[npm-downloads-badge]: https://img.shields.io/npm/dm/voltra?style=for-the-badge -[npm-downloads]: https://www.npmjs.com/package/voltra +[npm-downloads-badge]: https://img.shields.io/npm/dm/@use-voltra/ios-client?style=for-the-badge +[npm-downloads]: https://www.npmjs.com/package/@use-voltra/ios-client [prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge [prs-welcome]: ./CONTRIBUTING.md diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 00000000..def59bb6 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ['module:@react-native/babel-preset'], + plugins: ['@babel/plugin-transform-export-namespace-from'], +} diff --git a/example/__tests__/ios/live-activity-snapshots.harness.tsx b/example/__tests__/ios/live-activity-snapshots.harness.tsx index 39125880..5119540b 100644 --- a/example/__tests__/ios/live-activity-snapshots.harness.tsx +++ b/example/__tests__/ios/live-activity-snapshots.harness.tsx @@ -1,7 +1,7 @@ import { screen } from '@react-native-harness/ui' import { View } from 'react-native' import { describe, expect, render, test } from 'react-native-harness' -import { VoltraLiveActivityPreview } from 'voltra/client' +import { VoltraLiveActivityPreview } from '@use-voltra/ios-client' import { BasicLiveActivityUI, diff --git a/example/__tests__/ios/widget-snapshots.harness.tsx b/example/__tests__/ios/widget-snapshots.harness.tsx index 3abec09f..490051cb 100644 --- a/example/__tests__/ios/widget-snapshots.harness.tsx +++ b/example/__tests__/ios/widget-snapshots.harness.tsx @@ -1,7 +1,7 @@ import { screen } from '@react-native-harness/ui' import { View } from 'react-native' import { afterAll, beforeAll, describe, expect, Mock, render, spyOn, test } from 'react-native-harness' -import { VoltraWidgetPreview } from 'voltra/client' +import { VoltraWidgetPreview } from '@use-voltra/ios-client' import { IosWeatherWidget } from '../../widgets/ios/IosWeatherWidget' import { SAMPLE_WEATHER_DATA } from '../../widgets/weather-types' diff --git a/example/app.json b/example/app.json index 67063cc1..752d545d 100644 --- a/example/app.json +++ b/example/app.json @@ -1,16 +1,17 @@ { "expo": { - "name": "Voltra Example", + "name": "Voltra", "slug": "voltra", "scheme": "voltra", "version": "1.0.0", "orientation": "portrait", + "backgroundColor": "#020617", "icon": "./assets/voltra-icon.jpg", "userInterfaceStyle": "automatic", "splash": { "image": "./assets/voltra-splash.jpg", "resizeMode": "cover", - "backgroundColor": "#8232FF" + "backgroundColor": "#020617" }, "android": { "package": "com.callstackincubator.voltraexample", @@ -26,14 +27,11 @@ }, "plugins": [ [ - "voltra", + "@use-voltra/ios-client", { "groupIdentifier": "group.callstackincubator.voltraexample", "keychainGroup": "$(AppIdentifierPrefix)group.callstackincubator.voltraexample", "enablePushNotifications": true, - "liveActivity": { - "supplementalActivityFamilies": ["small"] - }, "widgets": [ { "id": "weather", @@ -70,135 +68,144 @@ } } ], - "android": { - "enableNotifications": true, - "widgets": [ - { - "id": "voltra", - "displayName": { - "pl": "Widget Voltra", - "en": "Voltra Widget" - }, - "description": { - "pl": "Widget z logo Voltra", - "en": "Voltra logo widget" - }, - "minCellWidth": 2, - "minCellHeight": 2, - "targetCellWidth": 2, - "targetCellHeight": 2, - "resizeMode": "horizontal|vertical", - "widgetCategory": "home_screen", - "initialStatePath": "./widgets/android/android-voltra-widget-initial.tsx", - "previewImage": "./assets/voltra-icon.jpg" - }, - { - "id": "interactive_todos", - "displayName": { - "pl": "Interaktywna lista zadań", - "en": "Interactive Todos Widget" - }, - "description": { - "pl": "Test interaktywnych widgetów z polami wyboru, przełącznikami i przyciskami", - "en": "Testing interactive widgets with checkboxes, switches, and buttons" - }, - "targetCellWidth": 2, - "targetCellHeight": 2, - "resizeMode": "horizontal|vertical", - "widgetCategory": "home_screen", - "previewLayout": "./assets/widgets/todos-preview.xml" - }, - { - "id": "image_preloading", - "displayName": { - "pl": "Widget wstępnego ładowania obrazów", - "en": "Image Preloading Widget" - }, - "description": { - "pl": "Test wstępnego ładowania obrazów na Androidzie", - "en": "Test image preloading on Android" - }, - "targetCellWidth": 2, - "targetCellHeight": 2, - "resizeMode": "horizontal|vertical", - "widgetCategory": "home_screen" - }, - { - "id": "image_fallback", - "displayName": { - "pl": "Widget zapasowego obrazu", - "en": "Image Fallback Widget" - }, - "description": { - "pl": "Test zapasowego obrazu z kolorem tła ze stylów", - "en": "Test image fallback with backgroundColor from styles" - }, - "targetCellWidth": 2, - "targetCellHeight": 2, - "resizeMode": "horizontal|vertical", - "widgetCategory": "home_screen", - "initialStatePath": "./widgets/android/android-image-fallback-initial.tsx" - }, - { - "id": "chart_widget", - "displayName": { - "pl": "Widget wykresu", - "en": "Chart Widget" - }, - "description": { - "pl": "Test komponentu Chart", - "en": "Test Chart component" - }, - "targetCellWidth": 3, - "targetCellHeight": 3, - "resizeMode": "horizontal|vertical", - "widgetCategory": "home_screen", - "initialStatePath": "./widgets/android-chart-widget-initial.tsx" - }, - { - "id": "portfolio", - "displayName": { - "pl": "Widget portfela", - "en": "Portfolio Widget" - }, - "description": { - "pl": "Pokazuje wyniki portfela z wykresem i aktualizacjami na żywo", - "en": "Shows portfolio performance with a chart and live updates" - }, - "targetCellWidth": 2, - "targetCellHeight": 2, - "resizeMode": "horizontal|vertical", - "widgetCategory": "home_screen", - "initialStatePath": "./widgets/android/android-portfolio-initial.tsx", - "serverUpdate": { - "url": "http://10.0.2.2:3333", - "intervalMinutes": 15, - "refresh": true - } + "fonts": [ + "@expo-google-fonts/merriweather/400Regular/Merriweather_400Regular.ttf", + "@expo-google-fonts/merriweather/700Bold/Merriweather_700Bold.ttf", + "@expo-google-fonts/pacifico/400Regular/Pacifico_400Regular.ttf", + "@expo-google-fonts/press-start-2p/400Regular/PressStart2P_400Regular.ttf" + ] + } + ], + [ + "@use-voltra/android-client", + { + "enableNotifications": true, + "widgets": [ + { + "id": "voltra", + "displayName": { + "pl": "Widget Voltra", + "en": "Voltra Widget" }, - { - "id": "material_colors", - "displayName": { - "pl": "Widget kolorów Material", - "en": "Material Colors Widget" - }, - "description": { - "pl": "Porównanie renderowania po stronie klienta i serwera z dynamicznymi kolorami Androida", - "en": "Compare client-side and server-side rendering with Android dynamic colors" - }, - "targetCellWidth": 2, - "targetCellHeight": 2, - "resizeMode": "horizontal|vertical", - "widgetCategory": "home_screen", - "initialStatePath": "./widgets/android/android-material-colors-initial.tsx", - "serverUpdate": { - "url": "http://10.0.2.2:3333", - "intervalMinutes": 15, - "refresh": true - } + "description": { + "pl": "Widget z logo Voltra", + "en": "Voltra logo widget" + }, + "minCellWidth": 2, + "minCellHeight": 2, + "targetCellWidth": 2, + "targetCellHeight": 2, + "resizeMode": "horizontal|vertical", + "widgetCategory": "home_screen", + "initialStatePath": "./widgets/android/android-voltra-widget-initial.tsx", + "previewImage": "./assets/voltra-icon.jpg" + }, + { + "id": "interactive_todos", + "displayName": { + "pl": "Interaktywna lista zadań", + "en": "Interactive Todos Widget" + }, + "description": { + "pl": "Test interaktywnych widgetów z polami wyboru, przełącznikami i przyciskami", + "en": "Testing interactive widgets with checkboxes, switches, and buttons" + }, + "targetCellWidth": 2, + "targetCellHeight": 2, + "resizeMode": "horizontal|vertical", + "widgetCategory": "home_screen", + "previewLayout": "./assets/widgets/todos-preview.xml" + }, + { + "id": "image_preloading", + "displayName": { + "pl": "Widget wstępnego ładowania obrazów", + "en": "Image Preloading Widget" + }, + "description": { + "pl": "Test wstępnego ładowania obrazów na Androidzie", + "en": "Test image preloading on Android" + }, + "targetCellWidth": 2, + "targetCellHeight": 2, + "resizeMode": "horizontal|vertical", + "widgetCategory": "home_screen" + }, + { + "id": "image_fallback", + "displayName": { + "pl": "Widget zapasowego obrazu", + "en": "Image Fallback Widget" + }, + "description": { + "pl": "Test zapasowego obrazu z kolorem tła ze stylów", + "en": "Test image fallback with backgroundColor from styles" + }, + "targetCellWidth": 2, + "targetCellHeight": 2, + "resizeMode": "horizontal|vertical", + "widgetCategory": "home_screen", + "initialStatePath": "./widgets/android/android-image-fallback-initial.tsx" + }, + { + "id": "chart_widget", + "displayName": { + "pl": "Widget wykresu", + "en": "Chart Widget" + }, + "description": { + "pl": "Test komponentu Chart", + "en": "Test Chart component" + }, + "targetCellWidth": 3, + "targetCellHeight": 3, + "resizeMode": "horizontal|vertical", + "widgetCategory": "home_screen", + "initialStatePath": "./widgets/android-chart-widget-initial.tsx" + }, + { + "id": "portfolio", + "displayName": { + "pl": "Widget portfela", + "en": "Portfolio Widget" + }, + "description": { + "pl": "Pokazuje wyniki portfela z wykresem i aktualizacjami na żywo", + "en": "Shows portfolio performance with a chart and live updates" + }, + "targetCellWidth": 2, + "targetCellHeight": 2, + "resizeMode": "horizontal|vertical", + "widgetCategory": "home_screen", + "initialStatePath": "./widgets/android/android-portfolio-initial.tsx", + "serverUpdate": { + "url": "http://10.0.2.2:3333", + "intervalMinutes": 15, + "refresh": true + } + }, + { + "id": "material_colors", + "displayName": { + "pl": "Widget kolorów Material", + "en": "Material Colors Widget" + }, + "description": { + "pl": "Porównanie renderowania po stronie klienta i serwera z dynamicznymi kolorami Androida", + "en": "Compare client-side and server-side rendering with Android dynamic colors" + }, + "targetCellWidth": 2, + "targetCellHeight": 2, + "resizeMode": "horizontal|vertical", + "widgetCategory": "home_screen", + "initialStatePath": "./widgets/android/android-material-colors-initial.tsx", + "serverUpdate": { + "url": "http://10.0.2.2:3333", + "intervalMinutes": 15, + "refresh": true } - ] - }, + } + ], "fonts": [ "@expo-google-fonts/merriweather/400Regular/Merriweather_400Regular.ttf", "@expo-google-fonts/merriweather/700Bold/Merriweather_700Bold.ttf", diff --git a/example/app/_layout.tsx b/example/app/_layout.tsx index e9aa16de..82b3e4c2 100644 --- a/example/app/_layout.tsx +++ b/example/app/_layout.tsx @@ -1,7 +1,6 @@ import { Stack } from 'expo-router' import { SafeAreaProvider } from 'react-native-safe-area-context' -import { BackgroundWrapper } from '~/components/BackgroundWrapper' import { useVoltraEvents } from '~/hooks/useVoltraEvents' import { updateAndroidVoltraWidget } from '~/widgets/android/updateAndroidVoltraWidget' @@ -9,7 +8,7 @@ updateAndroidVoltraWidget({ width: 300, height: 200 }) const STACK_SCREEN_OPTIONS = { headerShown: false, - contentStyle: { backgroundColor: 'transparent' }, + contentStyle: { backgroundColor: '#020617' }, } export const unstable_settings = { @@ -21,10 +20,9 @@ export default function Layout() { return ( - {children}} - > + + + - - - diff --git a/example/app/android-widgets.tsx b/example/app/android-widgets.tsx index c33ebf7a..15cc08de 100644 --- a/example/app/android-widgets.tsx +++ b/example/app/android-widgets.tsx @@ -1,5 +1,5 @@ -import AndroidScreen from '~/screens/android/AndroidScreen' +import { Redirect } from 'expo-router' -export default function AndroidWidgetsIndex() { - return +export default function AndroidWidgetsRedirect() { + return } diff --git a/example/app/android/(tabs)/_layout.tsx b/example/app/android/(tabs)/_layout.tsx new file mode 100644 index 00000000..8b69bfe4 --- /dev/null +++ b/example/app/android/(tabs)/_layout.tsx @@ -0,0 +1,39 @@ +import Ionicons from '@expo/vector-icons/Ionicons' +import { Tabs } from 'expo-router' + +const TAB_BAR_COLORS = { + active: '#A78BFA', + inactive: '#94A3B8', + background: '#020617', + border: 'rgba(148, 163, 184, 0.18)', +} + +export default function AndroidTabsLayout() { + return ( + ({ + headerShown: false, + tabBarActiveTintColor: TAB_BAR_COLORS.active, + tabBarInactiveTintColor: TAB_BAR_COLORS.inactive, + tabBarStyle: { + backgroundColor: TAB_BAR_COLORS.background, + borderTopColor: TAB_BAR_COLORS.border, + }, + tabBarIcon: ({ color, size }) => { + const iconName = + route.name === 'activity' + ? 'notifications-outline' + : route.name === 'widgets' + ? 'grid-outline' + : 'ellipsis-horizontal-circle-outline' + + return + }, + })} + > + + + + + ) +} diff --git a/example/app/android/(tabs)/activity.tsx b/example/app/android/(tabs)/activity.tsx new file mode 100644 index 00000000..a6d8eb9e --- /dev/null +++ b/example/app/android/(tabs)/activity.tsx @@ -0,0 +1,5 @@ +import ActivityScreen from '~/screens/android/tabs/ActivityScreen' + +export default function AndroidActivityTab() { + return +} diff --git a/example/app/android/(tabs)/others.tsx b/example/app/android/(tabs)/others.tsx new file mode 100644 index 00000000..fd941b75 --- /dev/null +++ b/example/app/android/(tabs)/others.tsx @@ -0,0 +1,5 @@ +import OthersScreen from '~/screens/android/tabs/OthersScreen' + +export default function AndroidOthersTab() { + return +} diff --git a/example/app/android/(tabs)/widgets.tsx b/example/app/android/(tabs)/widgets.tsx new file mode 100644 index 00000000..4e8b6292 --- /dev/null +++ b/example/app/android/(tabs)/widgets.tsx @@ -0,0 +1,5 @@ +import WidgetsScreen from '~/screens/android/tabs/WidgetsScreen' + +export default function AndroidWidgetsTab() { + return +} diff --git a/example/app/index.tsx b/example/app/index.tsx index 6f8cf7a5..76922d94 100644 --- a/example/app/index.tsx +++ b/example/app/index.tsx @@ -2,6 +2,5 @@ import { Redirect } from 'expo-router' import { Platform } from 'react-native' export default function Index() { - const href = Platform.OS === 'android' ? '/android-widgets' : '/live-activities' - return + return } diff --git a/example/app/ios/(tabs)/_layout.tsx b/example/app/ios/(tabs)/_layout.tsx new file mode 100644 index 00000000..d46164fb --- /dev/null +++ b/example/app/ios/(tabs)/_layout.tsx @@ -0,0 +1,39 @@ +import Ionicons from '@expo/vector-icons/Ionicons' +import { Tabs } from 'expo-router' + +const TAB_BAR_COLORS = { + active: '#A78BFA', + inactive: '#94A3B8', + background: '#020617', + border: 'rgba(148, 163, 184, 0.18)', +} + +export default function IOSTabsLayout() { + return ( + ({ + headerShown: false, + tabBarActiveTintColor: TAB_BAR_COLORS.active, + tabBarInactiveTintColor: TAB_BAR_COLORS.inactive, + tabBarStyle: { + backgroundColor: TAB_BAR_COLORS.background, + borderTopColor: TAB_BAR_COLORS.border, + }, + tabBarIcon: ({ color, size }) => { + const iconName = + route.name === 'activity' + ? 'radio-outline' + : route.name === 'widgets' + ? 'grid-outline' + : 'ellipsis-horizontal-circle-outline' + + return + }, + })} + > + + + + + ) +} diff --git a/example/app/ios/(tabs)/activity.tsx b/example/app/ios/(tabs)/activity.tsx new file mode 100644 index 00000000..82224e0e --- /dev/null +++ b/example/app/ios/(tabs)/activity.tsx @@ -0,0 +1,5 @@ +import ActivityScreen from '~/screens/ios/tabs/ActivityScreen' + +export default function IOSActivityTab() { + return +} diff --git a/example/app/ios/(tabs)/others.tsx b/example/app/ios/(tabs)/others.tsx new file mode 100644 index 00000000..8ef55083 --- /dev/null +++ b/example/app/ios/(tabs)/others.tsx @@ -0,0 +1,5 @@ +import OthersScreen from '~/screens/ios/tabs/OthersScreen' + +export default function IOSOthersTab() { + return +} diff --git a/example/app/ios/(tabs)/widgets.tsx b/example/app/ios/(tabs)/widgets.tsx new file mode 100644 index 00000000..0e27eeb4 --- /dev/null +++ b/example/app/ios/(tabs)/widgets.tsx @@ -0,0 +1,5 @@ +import WidgetsScreen from '~/screens/ios/tabs/WidgetsScreen' + +export default function IOSWidgetsTab() { + return +} diff --git a/example/app/live-activities.tsx b/example/app/live-activities.tsx index dd6baee8..abd2bbb3 100644 --- a/example/app/live-activities.tsx +++ b/example/app/live-activities.tsx @@ -1,5 +1,5 @@ -import LiveActivitiesScreen from '~/screens/live-activities/LiveActivitiesScreen' +import { Redirect } from 'expo-router' -export default function LiveActivitiesIndex() { - return +export default function LiveActivitiesRedirect() { + return } diff --git a/example/app/testing-grounds.tsx b/example/app/testing-grounds.tsx index b280df8b..61b00ae0 100644 --- a/example/app/testing-grounds.tsx +++ b/example/app/testing-grounds.tsx @@ -1,5 +1,6 @@ -import TestingGroundsScreen from '~/screens/testing-grounds/TestingGroundsScreen' +import { Redirect } from 'expo-router' +import { Platform } from 'react-native' -export default function TestingGroundsIndex() { - return +export default function TestingGroundsRedirect() { + return } diff --git a/example/components/ActiveWidgetsAndroidCard.tsx b/example/components/ActiveWidgetsAndroidCard.tsx index ac844459..008ee462 100644 --- a/example/components/ActiveWidgetsAndroidCard.tsx +++ b/example/components/ActiveWidgetsAndroidCard.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useState } from 'react' import { ActivityIndicator, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native' -import { getActiveWidgets, WidgetInfo } from 'voltra/android/client' +import { getActiveWidgets, WidgetInfo } from '@use-voltra/android-client' import { Card } from './Card' diff --git a/example/components/ActiveWidgetsIOSCard.tsx b/example/components/ActiveWidgetsIOSCard.tsx index 268961e4..809cc6ec 100644 --- a/example/components/ActiveWidgetsIOSCard.tsx +++ b/example/components/ActiveWidgetsIOSCard.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useState } from 'react' import { ActivityIndicator, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native' -import { getActiveWidgets, WidgetInfo } from 'voltra/client' +import { getActiveWidgets, WidgetInfo } from '@use-voltra/ios-client' import { Card } from './Card' diff --git a/example/components/BackgroundWrapper.tsx b/example/components/BackgroundWrapper.tsx deleted file mode 100644 index d9363f07..00000000 --- a/example/components/BackgroundWrapper.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { ReactNode } from 'react' -import { Image, StyleSheet, useWindowDimensions, View } from 'react-native' -import { SafeAreaView } from 'react-native-safe-area-context' - -export type BackgroundWrapperProps = { - children: ReactNode -} - -export const BackgroundWrapper = ({ children }: BackgroundWrapperProps) => { - const { width, height } = useWindowDimensions() - - return ( - - - {children} - - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - safeArea: { - flex: 1, - }, - image: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - zIndex: -1, - }, -}) diff --git a/example/components/Button.tsx b/example/components/Button.tsx index 9ca779e5..d8b74db5 100644 --- a/example/components/Button.tsx +++ b/example/components/Button.tsx @@ -42,9 +42,6 @@ const styles = StyleSheet.create({ primaryButton: { backgroundColor: '#8232FF', }, - primaryButtonPressed: { - backgroundColor: '#6B28E0', // Darker purple for pressed state - }, primaryButtonText: { color: '#FFFFFF', }, @@ -65,9 +62,6 @@ const styles = StyleSheet.create({ color: '#E2E8F0', fontWeight: '600', }, - secondaryActionButton: { - marginLeft: 12, - }, buttonDisabled: { opacity: 0.6, }, @@ -78,13 +72,4 @@ const styles = StyleSheet.create({ fontSize: 14, fontWeight: '600', }, - endAllButton: { - marginTop: 12, - borderWidth: 1, - borderColor: 'rgba(239, 68, 68, 0.35)', - backgroundColor: 'rgba(239, 68, 68, 0.12)', - }, - endAllButtonText: { - color: '#F87171', - }, }) diff --git a/example/components/ExampleSectionCards.tsx b/example/components/ExampleSectionCards.tsx new file mode 100644 index 00000000..1f3095fa --- /dev/null +++ b/example/components/ExampleSectionCards.tsx @@ -0,0 +1,58 @@ +import { useRouter } from 'expo-router' +import { StyleSheet, View } from 'react-native' + +import { Button } from './Button' +import { Card } from './Card' + +export type ExampleSection = { + id: string + title: string + description: string + route: + | '/testing-grounds/weather' + | '/testing-grounds/timer' + | '/testing-grounds/styling' + | '/testing-grounds/positioning' + | '/testing-grounds/progress' + | '/testing-grounds/components' + | '/testing-grounds/flex-playground' + | '/testing-grounds/chart-playground' + | '/testing-grounds/gradient-playground' + | '/testing-grounds/image-preloading' + | '/testing-grounds/image-fallback' + | '/testing-grounds/widget-scheduling' + | '/testing-grounds/server-driven-widgets' + | '/testing-grounds/channel-updates' + | '/android-widgets/pin' + | '/android-widgets/image-preloading' + | '/android-widgets/image-fallback' + | '/android-widgets/preview' + | '/android-widgets/charts' + | '/android-widgets/server-driven' + | '/android-widgets/material-colors' + | '/android-widgets/custom-fonts' +} + +export type ExampleSectionCardsProps = { + sections: ExampleSection[] +} + +export function ExampleSectionCards({ sections }: ExampleSectionCardsProps) { + const router = useRouter() + + return sections.map((section) => ( + + {section.title} + {section.description} + + . Verify output has type 'Button' and contains children. - // Button type id? 'btn'? - const output = renderVoltraVariantToJson( - - ) - // Need to know button type id. Likely 'btn'. - // Or check for 't' property. - // And 'c' has content. - expect(output).toHaveProperty('c') - // Children should be Text element. - }) - - test('2. Button with ID', () => { - // Render . Verify output props include id: 'my-btn'. - // id prop is special, mapped to 'i'. - const output = renderVoltraVariantToJson( - - ) - expect(output.i).toBe('my-btn') - }) - - test('3. Button intent', () => { - // Render - ) - // intent is prop. Shortened? - // check short-names.ts or just check existence. - // Assuming 'intent' is not shortened or is 'intent'. - // If it's not in short-names, it is 'intent'. - expect(output.p).toHaveProperty('intent', 'pause') - }) - - test('4. All button styles', () => { - // Render with each valid buttonStyle value. - // buttonStyle -> bs - const output = renderVoltraVariantToJson( - - ) - expect(output.p.bs).toBe('borderedProminent') - }) - - test('5. Button with payload', () => { - // Render - ) - expect(output.p.payload).toEqual(payload) - }) - - test('6. Nested content', () => { - // Render . - const output = renderVoltraVariantToJson( - - ) - // Children should be HStack - expect(output.c).toBeDefined() - // Check structure - }) -}) diff --git a/packages/voltra/src/jsx/__tests__/Chart.node.test.tsx b/packages/voltra/src/jsx/__tests__/Chart.node.test.tsx deleted file mode 100644 index 4a97f1f1..00000000 --- a/packages/voltra/src/jsx/__tests__/Chart.node.test.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from 'react' - -import { renderVoltraVariantToJson } from '../../renderer/renderer' -import { AreaMark } from '../AreaMark' -import { BarMark } from '../BarMark' -import { Chart } from '../Chart' -import { RuleMark } from '../RuleMark' - -const getChartMarks = (output: any): any[] => { - expect(output.p?.mrk).toBeDefined() - return JSON.parse(output.p.mrk) -} - -describe('Chart serialization', () => { - test('does not serialize chartScrollableAxes', () => { - const output = renderVoltraVariantToJson( - - - - ) - - expect(output.p?.chartScrollableAxes).toBeUndefined() - expect(output.p?.csa).toBeUndefined() - }) - - test('BarMark serializes grouped stacking only', () => { - const groupedOutput = renderVoltraVariantToJson( - - - - ) - const groupedMarks = getChartMarks(groupedOutput) - expect(groupedMarks[0][2].stk).toBe('grouped') - - const defaultOutput = renderVoltraVariantToJson( - - - - ) - const defaultMarks = getChartMarks(defaultOutput) - expect(defaultMarks[0][2].stk).toBeUndefined() - }) - - test('AreaMark does not serialize stacking', () => { - const output = renderVoltraVariantToJson( - - - - ) - - const marks = getChartMarks(output) - expect(marks[0][2].stk).toBeUndefined() - }) - - test('RuleMark serializes both x and y values when both are provided', () => { - const output = renderVoltraVariantToJson( - - - - ) - - const marks = getChartMarks(output) - expect(marks[0][2].xv).toBe('Jan') - expect(marks[0][2].yv).toBe(75) - }) - - test('serializes axis grid styles', () => { - const output = renderVoltraVariantToJson( - - - - ) - - expect(output.p?.xgs).toBeDefined() - expect(output.p?.ygs).toBeDefined() - expect(JSON.parse(output.p.xgs)).toEqual({ v: false, c: '#ff0000', lw: 2, d: [4, 2] }) - expect(JSON.parse(output.p.ygs)).toEqual({ lw: 1.5 }) - }) -}) diff --git a/packages/voltra/src/jsx/__tests__/Image.node.test.tsx b/packages/voltra/src/jsx/__tests__/Image.node.test.tsx deleted file mode 100644 index c8120602..00000000 --- a/packages/voltra/src/jsx/__tests__/Image.node.test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react' - -import { renderVoltraVariantToJson } from '../../renderer/renderer' -import { Image } from '../Image' -import { Text } from '../Text' - -describe('Image Component', () => { - test('Asset name source', () => { - const output = renderVoltraVariantToJson() - expect(output.p.src).toEqual(JSON.stringify({ assetName: 'photo' })) - }) - - test('Base64 source', () => { - const output = renderVoltraVariantToJson() - expect(output.p.src).toEqual(JSON.stringify({ base64: 'iVBORw0...' })) - }) - - test('Large base64 source', () => { - const largeStr = 'a'.repeat(100 * 1024) - expect(() => { - const output = renderVoltraVariantToJson() - expect(output.p.src).toContain(largeStr) - }).not.toThrow() - }) - - test('Missing source', () => { - // @ts-ignore - const output = renderVoltraVariantToJson() - expect(output.p?.src).toBeUndefined() - }) - - test('fallback node', () => { - const output = renderVoltraVariantToJson(Missing} />) - expect(output.p.flb).toBeDefined() - expect((output.p.flb as any).t).toBeDefined() - }) - - test('resizeMode cover', () => { - const output = renderVoltraVariantToJson() - expect(output.p.rm).toBe('cover') - }) - - test('resizeMode contain', () => { - const output = renderVoltraVariantToJson() - expect(output.p.rm).toBe('contain') - }) - - test('All resize modes', () => { - const modes = ['cover', 'contain', 'stretch', 'center'] - modes.forEach((mode) => { - const output = renderVoltraVariantToJson() - expect(output.p.rm).toBe(mode) - }) - }) - - test('Invalid resizeMode (type safety)', () => { - // @ts-ignore - const output = renderVoltraVariantToJson() - expect(output.p.rm).toBe('invalid') - }) -}) diff --git a/packages/voltra/src/jsx/__tests__/Text.node.test.tsx b/packages/voltra/src/jsx/__tests__/Text.node.test.tsx deleted file mode 100644 index e1182a7f..00000000 --- a/packages/voltra/src/jsx/__tests__/Text.node.test.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react' - -import { renderVoltraVariantToJson } from '../../renderer/renderer' -import { Text } from '../Text' - -describe('Text Component', () => { - test('String child', () => { - const output = renderVoltraVariantToJson(Hello) - expect(output.c).toBe('Hello') - }) - - test('Number child', () => { - const output = renderVoltraVariantToJson({42}) - expect(output.c).toBe('42') - }) - - test('Boolean true', () => { - const output = renderVoltraVariantToJson({true}) - expect(output.c).toBe('') - }) - - test('Boolean false', () => { - const output = renderVoltraVariantToJson({false}) - expect(output.c).toBe('') - }) - - test('Multiple string children', () => { - const output = renderVoltraVariantToJson( - - {'Hello'} {'World'} - - ) - expect(output.c).toBe('Hello World') - }) - - test('Nested Text', () => { - expect(() => { - renderVoltraVariantToJson( - - Inner - - ) - }).toThrow(/must resolve to a string/) - }) - - test('Empty Text', () => { - const output = renderVoltraVariantToJson() - expect(output.c).toBe('') - }) - - test('Template literal', () => { - const output = renderVoltraVariantToJson({`Count: ${5}`}) - expect(output.c).toBe('Count: 5') - }) - - test('Unicode content', () => { - const output = renderVoltraVariantToJson({'🎉 Party'}) - expect(output.c).toBe('🎉 Party') - }) - - test('Very long text', () => { - const longText = 'x'.repeat(10000) - const output = renderVoltraVariantToJson({longText}) - expect(output.c).toBe(longText) - }) -}) diff --git a/packages/voltra/src/jsx/__tests__/Timer.node.test.tsx b/packages/voltra/src/jsx/__tests__/Timer.node.test.tsx deleted file mode 100644 index 13feaff6..00000000 --- a/packages/voltra/src/jsx/__tests__/Timer.node.test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react' - -import { renderVoltraVariantToJson } from '../../renderer/renderer' -import { Timer } from '../Timer' - -describe('Timer Component', () => { - const futureDate = new Date(Date.now() + 10000) - const pastDate = new Date(Date.now() - 10000) - - test('Countdown mode', () => { - const output = renderVoltraVariantToJson() - expect(output.p.dir).toBe('down') - }) - - test('Count up mode', () => { - const output = renderVoltraVariantToJson() - expect(output.p.dir).toBe('up') - }) - - test('With template', () => { - const output = renderVoltraVariantToJson() - expect(output.p.tt).toBe('{time} left') - }) - - test('Timer style', () => { - const output = renderVoltraVariantToJson() - expect(output.p.ts).toBe('timer') - }) - - test('Relative style', () => { - const output = renderVoltraVariantToJson() - expect(output.p.ts).toBe('relative') - }) - - test('Past date countdown', () => { - expect(() => { - renderVoltraVariantToJson() - }).not.toThrow() - }) - - test('Date as ISO string', () => { - const iso = '2024-01-01T00:00:00Z' - const ts = new Date(iso).getTime() - const output = renderVoltraVariantToJson() - expect(output.p.end).toBeDefined() - }) - - test('Date as timestamp', () => { - const ts = 1704067200000 - const output = renderVoltraVariantToJson() - expect(output.p.end).toBe(ts) - }) - - test('showHours true (default)', () => { - const output = renderVoltraVariantToJson() - expect(output.p.shrs).toBe(true) - }) - - test('showHours false', () => { - const output = renderVoltraVariantToJson() - expect(output.p.shrs).toBe(false) - }) - - test('showHours omitted uses default', () => { - const output = renderVoltraVariantToJson() - // When omitted, the prop should not be in the output (native defaults to false now) - expect(output.p.shrs).toBeUndefined() - }) -}) diff --git a/packages/voltra/src/jsx/baseProps.tsx b/packages/voltra/src/jsx/baseProps.tsx deleted file mode 100644 index a66e92d3..00000000 --- a/packages/voltra/src/jsx/baseProps.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { ReactNode } from 'react' - -import type { VoltraStyleProp } from '../styles/index.js' - -export type VoltraBaseProps = { - id?: string - style?: VoltraStyleProp - children?: ReactNode -} diff --git a/packages/voltra/src/jsx/chart-types.ts b/packages/voltra/src/jsx/chart-types.ts deleted file mode 100644 index f4c48b4d..00000000 --- a/packages/voltra/src/jsx/chart-types.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type ChartDataPoint = { x: string | number; y: number; series?: string } -export type SectorDataPoint = { value: number; category: string } diff --git a/packages/voltra/src/jsx/createVoltraComponent.ts b/packages/voltra/src/jsx/createVoltraComponent.ts deleted file mode 100644 index 2b166b01..00000000 --- a/packages/voltra/src/jsx/createVoltraComponent.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { ComponentType } from 'react' -import { createElement } from 'react' - -export const VOLTRA_COMPONENT_TAG = Symbol.for('VOLTRA_COMPONENT_TAG') - -export type VoltraComponent> = ComponentType & { - displayName: string - [VOLTRA_COMPONENT_TAG]: true -} - -export type VoltraComponentOptions> = { - toJSON?: (props: TProps) => Record -} - -export const createVoltraComponent = >( - componentName: string, - options?: VoltraComponentOptions -): VoltraComponent => { - const Component = (props: TProps) => { - const toJSON = options?.toJSON ? options.toJSON : (value: TProps) => value - const normalizedProps = toJSON(props) - - return createElement(componentName, normalizedProps) - } - - Component[VOLTRA_COMPONENT_TAG] = true - Component.displayName = componentName - - return Component as VoltraComponent -} - -export const isVoltraComponent = >( - component: ComponentType -): component is VoltraComponent => { - return typeof component === 'function' && VOLTRA_COMPONENT_TAG in component -} diff --git a/packages/voltra/src/jsx/font.ts b/packages/voltra/src/jsx/font.ts deleted file mode 100644 index ef05b38a..00000000 --- a/packages/voltra/src/jsx/font.ts +++ /dev/null @@ -1,45 +0,0 @@ -export type VoltraFont = { - readonly __voltraFontId: string - readonly name?: string - readonly size: number - readonly weight?: string - readonly family?: string - readonly smallCaps?: boolean - readonly monospacedDigit?: boolean - readonly italic?: boolean -} - -let fontIdCounter = 0 - -export function createFont(options: { - name?: string - size: number - weight?: string - family?: string - smallCaps?: boolean - monospacedDigit?: boolean - italic?: boolean -}): VoltraFont { - // Use numeric ID (0, 1, 2, ...) for smallest payload size - const id = String(fontIdCounter++) - - return { - __voltraFontId: id, - name: options.name, - size: options.size, - weight: options.weight, - family: options.family, - smallCaps: options.smallCaps, - monospacedDigit: options.monospacedDigit, - italic: options.italic, - } -} - -export function isVoltraFont(value: unknown): value is VoltraFont { - return ( - typeof value === 'object' && - value !== null && - '__voltraFontId' in value && - typeof (value as any).__voltraFontId === 'string' - ) -} diff --git a/packages/voltra/src/jsx/index.ts b/packages/voltra/src/jsx/index.ts deleted file mode 100644 index 0913ecda..00000000 --- a/packages/voltra/src/jsx/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type { VoltraBaseProps } from './baseProps' -export * from './createVoltraComponent.js' diff --git a/packages/voltra/src/jsx/primitives.ts b/packages/voltra/src/jsx/primitives.ts deleted file mode 100644 index 519f15de..00000000 --- a/packages/voltra/src/jsx/primitives.ts +++ /dev/null @@ -1,29 +0,0 @@ -export * from './AreaMark.js' -export * from './BarMark.js' -export * from './Button.js' -export * from './Chart.js' -export * from './chart-types.js' -export * from './CircularProgressView.js' -export * from './Divider.js' -export * from './Gauge.js' -export * from './GlassContainer.js' -export * from './GroupBox.js' -export * from './HStack.js' -export * from './Image.js' -export * from './Label.js' -export * from './LinearGradient.js' -export * from './LinearProgressView.js' -export * from './LineMark.js' -export * from './Link.js' -export * from './Mask.js' -export * from './PointMark.js' -export * from './RuleMark.js' -export * from './SectorMark.js' -export * from './Spacer.js' -export * from './Symbol.js' -export * from './Text.js' -export * from './Timer.js' -export * from './Toggle.js' -export * from './View.js' -export * from './VStack.js' -export * from './ZStack.js' diff --git a/packages/voltra/src/jsx/props/AndroidBox.ts b/packages/voltra/src/jsx/props/AndroidBox.ts deleted file mode 100644 index 219e34cb..00000000 --- a/packages/voltra/src/jsx/props/AndroidBox.ts +++ /dev/null @@ -1,19 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidBoxProps = VoltraBaseProps & { - /** Content alignment within the box */ - contentAlignment?: - | 'top-start' - | 'top-center' - | 'top-end' - | 'center-start' - | 'center' - | 'center-end' - | 'bottom-start' - | 'bottom-center' - | 'bottom-end' -} diff --git a/packages/voltra/src/jsx/props/AndroidButton.ts b/packages/voltra/src/jsx/props/AndroidButton.ts deleted file mode 100644 index 56224fc9..00000000 --- a/packages/voltra/src/jsx/props/AndroidButton.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidButtonProps = VoltraBaseProps & { - /** Whether the button is enabled */ - enabled?: boolean -} diff --git a/packages/voltra/src/jsx/props/AndroidChart.ts b/packages/voltra/src/jsx/props/AndroidChart.ts deleted file mode 100644 index 6a8947b2..00000000 --- a/packages/voltra/src/jsx/props/AndroidChart.ts +++ /dev/null @@ -1,20 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidChartProps = VoltraBaseProps & { - /** Compact mark data encoded from children by toJSON */ - marks?: string - /** Show or hide the x-axis */ - xAxisVisibility?: 'automatic' | 'visible' | 'hidden' - /** Show or hide the y-axis */ - yAxisVisibility?: 'automatic' | 'visible' | 'hidden' - /** Show or hide the chart legend */ - legendVisibility?: 'automatic' | 'visible' | 'hidden' - /** Map of series name to color string */ - foregroundStyleScale?: Record - /** Enable scrolling on the given axis */ - chartScrollableAxes?: 'horizontal' | 'vertical' -} diff --git a/packages/voltra/src/jsx/props/AndroidCheckBox.ts b/packages/voltra/src/jsx/props/AndroidCheckBox.ts deleted file mode 100644 index 82467e6b..00000000 --- a/packages/voltra/src/jsx/props/AndroidCheckBox.ts +++ /dev/null @@ -1,14 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidCheckBoxProps = VoltraBaseProps & { - /** Unique identifier for interaction events */ - id: string - /** Initial checked state */ - checked?: boolean - /** Whether the checkbox is enabled */ - enabled?: boolean -} diff --git a/packages/voltra/src/jsx/props/AndroidCircleIconButton.ts b/packages/voltra/src/jsx/props/AndroidCircleIconButton.ts deleted file mode 100644 index ef011cb6..00000000 --- a/packages/voltra/src/jsx/props/AndroidCircleIconButton.ts +++ /dev/null @@ -1,18 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidCircleIconButtonProps = VoltraBaseProps & { - /** Icon source */ - icon: Record - /** Accessibility description */ - contentDescription?: string - /** Whether the button is enabled */ - enabled?: boolean - /** Background color */ - backgroundColor?: string - /** Icon color */ - contentColor?: string -} diff --git a/packages/voltra/src/jsx/props/AndroidCircularProgressIndicator.ts b/packages/voltra/src/jsx/props/AndroidCircularProgressIndicator.ts deleted file mode 100644 index f37252f4..00000000 --- a/packages/voltra/src/jsx/props/AndroidCircularProgressIndicator.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidCircularProgressIndicatorProps = VoltraBaseProps & { - /** Progress color */ - color?: string -} diff --git a/packages/voltra/src/jsx/props/AndroidColumn.ts b/packages/voltra/src/jsx/props/AndroidColumn.ts deleted file mode 100644 index 574182d4..00000000 --- a/packages/voltra/src/jsx/props/AndroidColumn.ts +++ /dev/null @@ -1,12 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidColumnProps = VoltraBaseProps & { - /** Horizontal alignment of children */ - horizontalAlignment?: 'start' | 'center-horizontally' | 'end' - /** Vertical alignment of children */ - verticalAlignment?: 'top' | 'center-vertically' | 'bottom' -} diff --git a/packages/voltra/src/jsx/props/AndroidFilledButton.ts b/packages/voltra/src/jsx/props/AndroidFilledButton.ts deleted file mode 100644 index e1fb9582..00000000 --- a/packages/voltra/src/jsx/props/AndroidFilledButton.ts +++ /dev/null @@ -1,20 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidFilledButtonProps = VoltraBaseProps & { - /** Text to display */ - text: string - /** Whether the button is enabled */ - enabled?: boolean - /** Optional icon */ - icon?: Record - /** Background color */ - backgroundColor?: string - /** Text/icon color */ - contentColor?: string - /** Maximum lines for text */ - maxLines?: number -} diff --git a/packages/voltra/src/jsx/props/AndroidImage.ts b/packages/voltra/src/jsx/props/AndroidImage.ts deleted file mode 100644 index c4b7009f..00000000 --- a/packages/voltra/src/jsx/props/AndroidImage.ts +++ /dev/null @@ -1,16 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { ReactNode } from 'react' - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidImageProps = VoltraBaseProps & { - /** Image source */ - source: Record - /** Resizing mode */ - resizeMode?: 'cover' | 'contain' | 'stretch' | 'repeat' | 'center' - /** Custom fallback content rendered when the image is missing */ - fallback?: ReactNode -} diff --git a/packages/voltra/src/jsx/props/AndroidLazyColumn.ts b/packages/voltra/src/jsx/props/AndroidLazyColumn.ts deleted file mode 100644 index 8530718d..00000000 --- a/packages/voltra/src/jsx/props/AndroidLazyColumn.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidLazyColumnProps = VoltraBaseProps & { - /** Horizontal alignment of children */ - horizontalAlignment?: 'start' | 'center-horizontally' | 'end' -} diff --git a/packages/voltra/src/jsx/props/AndroidLazyVerticalGrid.ts b/packages/voltra/src/jsx/props/AndroidLazyVerticalGrid.ts deleted file mode 100644 index e3ff9bd3..00000000 --- a/packages/voltra/src/jsx/props/AndroidLazyVerticalGrid.ts +++ /dev/null @@ -1,7 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidLazyVerticalGridProps = VoltraBaseProps diff --git a/packages/voltra/src/jsx/props/AndroidLinearProgressIndicator.ts b/packages/voltra/src/jsx/props/AndroidLinearProgressIndicator.ts deleted file mode 100644 index d80eefc2..00000000 --- a/packages/voltra/src/jsx/props/AndroidLinearProgressIndicator.ts +++ /dev/null @@ -1,12 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidLinearProgressIndicatorProps = VoltraBaseProps & { - /** Progress color */ - color?: string - /** Track background color */ - backgroundColor?: string -} diff --git a/packages/voltra/src/jsx/props/AndroidOutlineButton.ts b/packages/voltra/src/jsx/props/AndroidOutlineButton.ts deleted file mode 100644 index 3a658cb5..00000000 --- a/packages/voltra/src/jsx/props/AndroidOutlineButton.ts +++ /dev/null @@ -1,18 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidOutlineButtonProps = VoltraBaseProps & { - /** Text to display */ - text: string - /** Whether the button is enabled */ - enabled?: boolean - /** Optional icon */ - icon?: Record - /** Text/icon color */ - contentColor?: string - /** Maximum lines for text */ - maxLines?: number -} diff --git a/packages/voltra/src/jsx/props/AndroidRadioButton.ts b/packages/voltra/src/jsx/props/AndroidRadioButton.ts deleted file mode 100644 index 937853dc..00000000 --- a/packages/voltra/src/jsx/props/AndroidRadioButton.ts +++ /dev/null @@ -1,14 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidRadioButtonProps = VoltraBaseProps & { - /** Unique identifier for interaction events */ - id: string - /** Initial checked state */ - checked?: boolean - /** Whether the radio button is enabled */ - enabled?: boolean -} diff --git a/packages/voltra/src/jsx/props/AndroidRow.ts b/packages/voltra/src/jsx/props/AndroidRow.ts deleted file mode 100644 index fcbf9ad4..00000000 --- a/packages/voltra/src/jsx/props/AndroidRow.ts +++ /dev/null @@ -1,12 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidRowProps = VoltraBaseProps & { - /** Horizontal alignment of children */ - horizontalAlignment?: 'start' | 'center-horizontally' | 'end' - /** Vertical alignment of children */ - verticalAlignment?: 'top' | 'center-vertically' | 'bottom' -} diff --git a/packages/voltra/src/jsx/props/AndroidScaffold.ts b/packages/voltra/src/jsx/props/AndroidScaffold.ts deleted file mode 100644 index 25740b47..00000000 --- a/packages/voltra/src/jsx/props/AndroidScaffold.ts +++ /dev/null @@ -1,12 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidScaffoldProps = VoltraBaseProps & { - /** Background color */ - backgroundColor?: string - /** Horizontal padding */ - horizontalPadding?: number -} diff --git a/packages/voltra/src/jsx/props/AndroidSpacer.ts b/packages/voltra/src/jsx/props/AndroidSpacer.ts deleted file mode 100644 index ace6fca6..00000000 --- a/packages/voltra/src/jsx/props/AndroidSpacer.ts +++ /dev/null @@ -1,7 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidSpacerProps = VoltraBaseProps diff --git a/packages/voltra/src/jsx/props/AndroidSquareIconButton.ts b/packages/voltra/src/jsx/props/AndroidSquareIconButton.ts deleted file mode 100644 index b4212b75..00000000 --- a/packages/voltra/src/jsx/props/AndroidSquareIconButton.ts +++ /dev/null @@ -1,18 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidSquareIconButtonProps = VoltraBaseProps & { - /** Icon source */ - icon: Record - /** Accessibility description */ - contentDescription?: string - /** Whether the button is enabled */ - enabled?: boolean - /** Background color */ - backgroundColor?: string - /** Icon color */ - contentColor?: string -} diff --git a/packages/voltra/src/jsx/props/AndroidSwitch.ts b/packages/voltra/src/jsx/props/AndroidSwitch.ts deleted file mode 100644 index b5d33225..00000000 --- a/packages/voltra/src/jsx/props/AndroidSwitch.ts +++ /dev/null @@ -1,14 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidSwitchProps = VoltraBaseProps & { - /** Unique identifier for interaction events */ - id: string - /** Initial checked state */ - checked?: boolean - /** Whether the switch is enabled */ - enabled?: boolean -} diff --git a/packages/voltra/src/jsx/props/AndroidText.ts b/packages/voltra/src/jsx/props/AndroidText.ts deleted file mode 100644 index fa9a3f32..00000000 --- a/packages/voltra/src/jsx/props/AndroidText.ts +++ /dev/null @@ -1,16 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidTextProps = VoltraBaseProps & { - /** Text content */ - text: string - /** Text color */ - color?: string - /** Font size */ - fontSize?: number - /** Maximum lines */ - maxLines?: number -} diff --git a/packages/voltra/src/jsx/props/AndroidTitleBar.ts b/packages/voltra/src/jsx/props/AndroidTitleBar.ts deleted file mode 100644 index 9d12c417..00000000 --- a/packages/voltra/src/jsx/props/AndroidTitleBar.ts +++ /dev/null @@ -1,14 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type AndroidTitleBarProps = VoltraBaseProps & { - /** Title text */ - title: string - /** Background color */ - backgroundColor?: string - /** Text color */ - contentColor?: string -} diff --git a/packages/voltra/src/jsx/props/Box.ts b/packages/voltra/src/jsx/props/Box.ts deleted file mode 100644 index ab3b5ece..00000000 --- a/packages/voltra/src/jsx/props/Box.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type BoxProps = VoltraBaseProps & { - /** Spacing between children */ - spacing?: number -} diff --git a/packages/voltra/src/jsx/props/Button.ts b/packages/voltra/src/jsx/props/Button.ts deleted file mode 100644 index 7aa90919..00000000 --- a/packages/voltra/src/jsx/props/Button.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type ButtonProps = VoltraBaseProps & { - /** Visual style of the button */ - buttonStyle?: 'automatic' | 'bordered' | 'borderedProminent' | 'plain' | 'borderless' -} diff --git a/packages/voltra/src/jsx/props/Chart.ts b/packages/voltra/src/jsx/props/Chart.ts deleted file mode 100644 index 4fd69cfc..00000000 --- a/packages/voltra/src/jsx/props/Chart.ts +++ /dev/null @@ -1,22 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type ChartProps = VoltraBaseProps & { - /** Compact mark data encoded from children by toJSON */ - marks?: string - /** Show or hide the x-axis */ - xAxisVisibility?: 'automatic' | 'visible' | 'hidden' - /** Configure x-axis grid line style */ - xAxisGridStyle?: Record - /** Show or hide the y-axis */ - yAxisVisibility?: 'automatic' | 'visible' | 'hidden' - /** Configure y-axis grid line style */ - yAxisGridStyle?: Record - /** Show or hide the chart legend */ - legendVisibility?: 'automatic' | 'visible' | 'hidden' - /** Map of series name to color string */ - foregroundStyleScale?: Record -} diff --git a/packages/voltra/src/jsx/props/CircularProgressView.ts b/packages/voltra/src/jsx/props/CircularProgressView.ts deleted file mode 100644 index e7fd9407..00000000 --- a/packages/voltra/src/jsx/props/CircularProgressView.ts +++ /dev/null @@ -1,30 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { ReactNode } from 'react' - -import type { VoltraBaseProps } from '../baseProps' - -export type CircularProgressViewProps = VoltraBaseProps & { - /** Current progress value */ - value?: number - /** Whether to count down instead of up */ - countDown?: boolean - /** Maximum progress value */ - maximumValue?: number - /** End time in milliseconds since epoch */ - endAtMs?: number - /** Start time in milliseconds since epoch */ - startAtMs?: number - /** Color for the track (background) of the circular progress indicator */ - trackColor?: string - /** Color for the progress fill */ - progressColor?: string - /** Width of the stroke line */ - lineWidth?: number - /** Label content for the progress indicator */ - label?: ReactNode - /** Custom text for current value label */ - currentValueLabel?: ReactNode -} diff --git a/packages/voltra/src/jsx/props/Divider.ts b/packages/voltra/src/jsx/props/Divider.ts deleted file mode 100644 index aec45c0e..00000000 --- a/packages/voltra/src/jsx/props/Divider.ts +++ /dev/null @@ -1,7 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type DividerProps = VoltraBaseProps diff --git a/packages/voltra/src/jsx/props/FilledButton.ts b/packages/voltra/src/jsx/props/FilledButton.ts deleted file mode 100644 index 06e0cab9..00000000 --- a/packages/voltra/src/jsx/props/FilledButton.ts +++ /dev/null @@ -1,20 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type FilledButtonProps = VoltraBaseProps & { - /** Text to display */ - text: string - /** Whether the button is enabled */ - enabled?: boolean - /** Optional icon */ - icon?: Record - /** Background color */ - backgroundColor?: string - /** Text/icon color */ - contentColor?: string - /** Maximum lines for text */ - maxLines?: number -} diff --git a/packages/voltra/src/jsx/props/Gauge.ts b/packages/voltra/src/jsx/props/Gauge.ts deleted file mode 100644 index 947f2e3f..00000000 --- a/packages/voltra/src/jsx/props/Gauge.ts +++ /dev/null @@ -1,36 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { ReactNode } from 'react' - -import type { VoltraBaseProps } from '../baseProps' - -export type GaugeProps = VoltraBaseProps & { - /** Current gauge value */ - value?: number - /** Minimum value of the gauge range */ - minimumValue?: number - /** Maximum value of the gauge range */ - maximumValue?: number - /** End time in milliseconds since epoch */ - endAtMs?: number - /** Start time in milliseconds since epoch */ - startAtMs?: number - /** Tint color for the gauge */ - tintColor?: string - /** Visual style of the gauge */ - gaugeStyle?: - | 'automatic' - | 'accessoryLinear' - | 'accessoryLinearCapacity' - | 'accessoryCircular' - | 'accessoryCircularCapacity' - | 'linearCapacity' - /** Custom text for current value label */ - currentValueLabel?: ReactNode - /** Text for minimum value label */ - minimumValueLabel?: ReactNode - /** Text for maximum value label */ - maximumValueLabel?: ReactNode -} diff --git a/packages/voltra/src/jsx/props/GlassContainer.ts b/packages/voltra/src/jsx/props/GlassContainer.ts deleted file mode 100644 index 63f8de33..00000000 --- a/packages/voltra/src/jsx/props/GlassContainer.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type GlassContainerProps = VoltraBaseProps & { - /** Spacing between glass elements */ - spacing?: number -} diff --git a/packages/voltra/src/jsx/props/GroupBox.ts b/packages/voltra/src/jsx/props/GroupBox.ts deleted file mode 100644 index c9c0a639..00000000 --- a/packages/voltra/src/jsx/props/GroupBox.ts +++ /dev/null @@ -1,12 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { ReactNode } from 'react' - -import type { VoltraBaseProps } from '../baseProps' - -export type GroupBoxProps = VoltraBaseProps & { - /** Label content for the group box */ - label?: ReactNode -} diff --git a/packages/voltra/src/jsx/props/HStack.ts b/packages/voltra/src/jsx/props/HStack.ts deleted file mode 100644 index d474f8c6..00000000 --- a/packages/voltra/src/jsx/props/HStack.ts +++ /dev/null @@ -1,14 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type HStackProps = VoltraBaseProps & { - /** Vertical alignment */ - alignment?: 'top' | 'center' | 'bottom' - /** Layout mode. 'stack' uses native SwiftUI stacks. 'flex' uses RN-like flexbox. */ - layout?: 'stack' | 'flex' - /** Spacing between children. Takes precedence over gap style property. */ - spacing?: number -} diff --git a/packages/voltra/src/jsx/props/Image.ts b/packages/voltra/src/jsx/props/Image.ts deleted file mode 100644 index 9b8c87b9..00000000 --- a/packages/voltra/src/jsx/props/Image.ts +++ /dev/null @@ -1,16 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { ReactNode } from 'react' - -import type { VoltraBaseProps } from '../baseProps' - -export type ImageProps = VoltraBaseProps & { - /** Image source - either { assetName: string } for asset catalog images or { base64: string } for base64 encoded images */ - source?: Record - /** How the image should be resized to fit its container */ - resizeMode?: 'cover' | 'contain' | 'stretch' | 'repeat' | 'center' - /** Custom fallback content rendered when the image is missing */ - fallback?: ReactNode -} diff --git a/packages/voltra/src/jsx/props/Label.ts b/packages/voltra/src/jsx/props/Label.ts deleted file mode 100644 index 40471580..00000000 --- a/packages/voltra/src/jsx/props/Label.ts +++ /dev/null @@ -1,12 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type LabelProps = VoltraBaseProps & { - /** Text content for the label */ - title?: string - /** SF Symbol name for the label icon */ - systemImage?: string -} diff --git a/packages/voltra/src/jsx/props/LegacyAndroidCheckBox.ts b/packages/voltra/src/jsx/props/LegacyAndroidCheckBox.ts deleted file mode 100644 index c6866a80..00000000 --- a/packages/voltra/src/jsx/props/LegacyAndroidCheckBox.ts +++ /dev/null @@ -1,12 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type LegacyAndroidCheckBoxProps = VoltraBaseProps & { - /** Unique identifier for this checkbox (required for interaction events) */ - id: string - /** Initial checked state */ - checked?: boolean -} diff --git a/packages/voltra/src/jsx/props/LegacyAndroidRadioButton.ts b/packages/voltra/src/jsx/props/LegacyAndroidRadioButton.ts deleted file mode 100644 index 6c6e2d0c..00000000 --- a/packages/voltra/src/jsx/props/LegacyAndroidRadioButton.ts +++ /dev/null @@ -1,12 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type LegacyAndroidRadioButtonProps = VoltraBaseProps & { - /** Unique identifier for this radio button (required for interaction events) */ - id: string - /** Initial checked state */ - checked?: boolean -} diff --git a/packages/voltra/src/jsx/props/LegacyAndroidSwitch.ts b/packages/voltra/src/jsx/props/LegacyAndroidSwitch.ts deleted file mode 100644 index 34221b9b..00000000 --- a/packages/voltra/src/jsx/props/LegacyAndroidSwitch.ts +++ /dev/null @@ -1,12 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type LegacyAndroidSwitchProps = VoltraBaseProps & { - /** Unique identifier for this switch (required for interaction events) */ - id: string - /** Initial checked state */ - checked?: boolean -} diff --git a/packages/voltra/src/jsx/props/LegacyFilledButton.ts b/packages/voltra/src/jsx/props/LegacyFilledButton.ts deleted file mode 100644 index c269f6c7..00000000 --- a/packages/voltra/src/jsx/props/LegacyFilledButton.ts +++ /dev/null @@ -1,12 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type LegacyFilledButtonProps = VoltraBaseProps & { - /** Text to display on the button */ - text: string - /** Whether the button is enabled */ - enabled?: boolean -} diff --git a/packages/voltra/src/jsx/props/LegacyImage.ts b/packages/voltra/src/jsx/props/LegacyImage.ts deleted file mode 100644 index a5577ea4..00000000 --- a/packages/voltra/src/jsx/props/LegacyImage.ts +++ /dev/null @@ -1,12 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type LegacyImageProps = VoltraBaseProps & { - /** Image source - { assetName: string } for Android drawable resources or preloaded assets */ - source: Record - /** How the image should be resized to fit its container */ - resizeMode?: 'cover' | 'contain' | 'stretch' | 'repeat' | 'center' -} diff --git a/packages/voltra/src/jsx/props/LinearGradient.ts b/packages/voltra/src/jsx/props/LinearGradient.ts deleted file mode 100644 index 64dd559c..00000000 --- a/packages/voltra/src/jsx/props/LinearGradient.ts +++ /dev/null @@ -1,18 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type LinearGradientProps = VoltraBaseProps & { - /** Pipe-separated color list (e.g., '#ff0000|#00ff00|#0000ff') */ - colors?: string - /** Pipe-separated gradient stops (e.g., 'red@0|orange@0.5|yellow@1') */ - stops?: string - /** Start point (e.g., 'topLeading', 'center', or 'x,y') */ - startPoint?: string - /** End point (e.g., 'bottomTrailing', 'center', or 'x,y') */ - endPoint?: string - /** Enable dithering (system-controlled) */ - dither?: boolean -} diff --git a/packages/voltra/src/jsx/props/LinearProgressView.ts b/packages/voltra/src/jsx/props/LinearProgressView.ts deleted file mode 100644 index b5f3ba46..00000000 --- a/packages/voltra/src/jsx/props/LinearProgressView.ts +++ /dev/null @@ -1,34 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { ReactNode } from 'react' - -import type { VoltraBaseProps } from '../baseProps' - -export type LinearProgressViewProps = VoltraBaseProps & { - /** Current progress value */ - value?: number - /** Whether to count down instead of up */ - countDown?: boolean - /** Maximum progress value */ - maximumValue?: number - /** End time in milliseconds since epoch */ - endAtMs?: number - /** Start time in milliseconds since epoch */ - startAtMs?: number - /** Color for the track (background) of the progress bar */ - trackColor?: string - /** Color for the progress fill */ - progressColor?: string - /** Corner radius for the progress bar */ - cornerRadius?: number - /** Explicit height for the progress bar */ - height?: number - /** Custom thumb component to display at progress position */ - thumb?: ReactNode - /** Label content for the progress indicator */ - label?: ReactNode - /** Custom text for current value label */ - currentValueLabel?: ReactNode -} diff --git a/packages/voltra/src/jsx/props/Link.ts b/packages/voltra/src/jsx/props/Link.ts deleted file mode 100644 index 80217298..00000000 --- a/packages/voltra/src/jsx/props/Link.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type LinkProps = VoltraBaseProps & { - /** URL to navigate to when the link is tapped */ - destination: string -} diff --git a/packages/voltra/src/jsx/props/Mask.ts b/packages/voltra/src/jsx/props/Mask.ts deleted file mode 100644 index 2798a3c8..00000000 --- a/packages/voltra/src/jsx/props/Mask.ts +++ /dev/null @@ -1,12 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { ReactNode } from 'react' - -import type { VoltraBaseProps } from '../baseProps' - -export type MaskProps = VoltraBaseProps & { - /** Voltra element used as the mask - alpha channel determines visibility */ - maskElement: ReactNode -} diff --git a/packages/voltra/src/jsx/props/Spacer.ts b/packages/voltra/src/jsx/props/Spacer.ts deleted file mode 100644 index 988f88c8..00000000 --- a/packages/voltra/src/jsx/props/Spacer.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type SpacerProps = VoltraBaseProps & { - /** Minimum length */ - minLength?: number -} diff --git a/packages/voltra/src/jsx/props/Symbol.ts b/packages/voltra/src/jsx/props/Symbol.ts deleted file mode 100644 index eb1ecbd7..00000000 --- a/packages/voltra/src/jsx/props/Symbol.ts +++ /dev/null @@ -1,34 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type SymbolProps = VoltraBaseProps & { - /** SF Symbol name */ - name?: string - /** Symbol rendering type */ - type?: 'monochrome' | 'hierarchical' | 'palette' | 'multicolor' - /** Symbol scale */ - scale?: 'default' | 'unspecified' | 'small' | 'medium' | 'large' - /** Symbol weight */ - weight?: - | 'unspecified' - | 'ultraLight' - | 'thin' - | 'light' - | 'regular' - | 'medium' - | 'semibold' - | 'bold' - | 'heavy' - | 'black' - /** Symbol size in points */ - size?: number - /** Tint color for the symbol */ - tintColor?: string - /** Pipe-separated colors for palette type */ - colors?: string - /** Image resize mode */ - resizeMode?: string -} diff --git a/packages/voltra/src/jsx/props/Text.ts b/packages/voltra/src/jsx/props/Text.ts deleted file mode 100644 index 8c6e1b42..00000000 --- a/packages/voltra/src/jsx/props/Text.ts +++ /dev/null @@ -1,12 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type TextProps = VoltraBaseProps & { - /** Maximum number of lines to display */ - numberOfLines?: number - /** Text alignment for multiline text */ - multilineTextAlignment?: 'left' | 'center' | 'right' | 'auto' -} diff --git a/packages/voltra/src/jsx/props/Timer.ts b/packages/voltra/src/jsx/props/Timer.ts deleted file mode 100644 index 83ff8f96..00000000 --- a/packages/voltra/src/jsx/props/Timer.ts +++ /dev/null @@ -1,22 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type TimerProps = VoltraBaseProps & { - /** End time in milliseconds since epoch */ - endAtMs?: number - /** Start time in milliseconds since epoch */ - startAtMs?: number - /** Duration in milliseconds */ - durationMs?: number - /** Count direction */ - direction?: 'up' | 'down' - /** Text formatting style */ - textStyle?: 'timer' | 'relative' - /** JSON-encoded TextTemplates object with running/completed templates */ - textTemplates?: string - /** Whether to show hours component when duration exceeds 60 minutes. If false, minutes will exceed 60 (e.g., 94:00 instead of 1:34:00) */ - showHours?: boolean -} diff --git a/packages/voltra/src/jsx/props/Toggle.ts b/packages/voltra/src/jsx/props/Toggle.ts deleted file mode 100644 index ff55e333..00000000 --- a/packages/voltra/src/jsx/props/Toggle.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type ToggleProps = VoltraBaseProps & { - /** Initial toggle state */ - defaultValue?: boolean -} diff --git a/packages/voltra/src/jsx/props/VStack.ts b/packages/voltra/src/jsx/props/VStack.ts deleted file mode 100644 index 817382fd..00000000 --- a/packages/voltra/src/jsx/props/VStack.ts +++ /dev/null @@ -1,14 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type VStackProps = VoltraBaseProps & { - /** Horizontal alignment */ - alignment?: 'leading' | 'center' | 'trailing' - /** Layout mode. 'stack' uses native SwiftUI stacks. 'flex' uses RN-like flexbox. */ - layout?: 'stack' | 'flex' - /** Spacing between children. Takes precedence over gap style property. */ - spacing?: number -} diff --git a/packages/voltra/src/jsx/props/View.ts b/packages/voltra/src/jsx/props/View.ts deleted file mode 100644 index a7752473..00000000 --- a/packages/voltra/src/jsx/props/View.ts +++ /dev/null @@ -1,7 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type ViewProps = VoltraBaseProps diff --git a/packages/voltra/src/jsx/props/ZStack.ts b/packages/voltra/src/jsx/props/ZStack.ts deleted file mode 100644 index e09dc6ea..00000000 --- a/packages/voltra/src/jsx/props/ZStack.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 🤖 AUTO-GENERATED from data/components.json -// DO NOT EDIT MANUALLY - Changes will be overwritten -// Schema version: 1.0.0 - -import type { VoltraBaseProps } from '../baseProps' - -export type ZStackProps = VoltraBaseProps & { - /** Child alignment */ - alignment?: string -} diff --git a/packages/voltra/src/live-activity/__tests__/__snapshots__/payload-size.node.test.tsx.snap b/packages/voltra/src/live-activity/__tests__/__snapshots__/payload-size.node.test.tsx.snap deleted file mode 100644 index 5af8e91c..00000000 --- a/packages/voltra/src/live-activity/__tests__/__snapshots__/payload-size.node.test.tsx.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Payload size regression BasicLiveActivityUI 1`] = `456`; - -exports[`Payload size regression MusicPlayerLiveActivityUI 1`] = `504`; diff --git a/packages/voltra/src/live-activity/__tests__/compression.node.test.tsx b/packages/voltra/src/live-activity/__tests__/compression.node.test.tsx deleted file mode 100644 index bce94fe9..00000000 --- a/packages/voltra/src/live-activity/__tests__/compression.node.test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Text } from '../../jsx/Text' -import { renderLiveActivityToString } from '../../server' - -describe('Compression', () => { - test('Small payload compression', async () => { - const content = 'a'.repeat(500) - const result = await renderLiveActivityToString({ lockScreen: {content} }) - - expect(result).toBeTruthy() - expect(typeof result).toBe('string') - }) - - test('Large payload compression', async () => { - const content = 'repetition '.repeat(300) - const inputSizeEstimate = content.length - - const result = await renderLiveActivityToString({ lockScreen: {content} }) - const outputSize = result.length - - expect(outputSize).toBeLessThan(inputSizeEstimate * 0.5) - }) - - test('Already small payload', async () => { - const content = 'short' - const result = await renderLiveActivityToString({ lockScreen: {content} }) - expect(result.length).toBeGreaterThan(0) - }) - - test('Repetitive content', async () => { - const items = Array.from({ length: 100 }, (_, i) => item) - const result = await renderLiveActivityToString({ lockScreen: <>{items} }) - - expect(result.length).toBeLessThan(1000) - }) - - test('Unique content', async () => { - const unique = Array.from({ length: 1000 }, (_, i) => String.fromCharCode(i % 128)).join('') - const repetitive = 'a'.repeat(1000) - - const resUnique = await renderLiveActivityToString({ lockScreen: {unique} }) - const resRepetitive = await renderLiveActivityToString({ lockScreen: {repetitive} }) - - expect(resRepetitive.length).toBeLessThan(resUnique.length) - }) - - test('Base64 overhead', async () => { - const content = 'a'.repeat(1000) - const base64 = await renderLiveActivityToString({ lockScreen: {content} }) - const buffer = Buffer.from(base64, 'base64') - - const expectedBase64Size = Math.ceil(buffer.length / 3) * 4 - expect(base64.length).toBeGreaterThanOrEqual(buffer.length) - expect(Math.abs(base64.length - expectedBase64Size)).toBeLessThan(4) - }) -}) diff --git a/packages/voltra/src/live-activity/__tests__/options.node.test.ts b/packages/voltra/src/live-activity/__tests__/options.node.test.ts deleted file mode 100644 index a23be8de..00000000 --- a/packages/voltra/src/live-activity/__tests__/options.node.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { logger } from '../../logger' -import VoltraModule from '../../VoltraModule' -import { startLiveActivity, stopLiveActivity } from '../api' - -// Mock VoltraModule -jest.mock('../../VoltraModule', () => ({ - startLiveActivity: jest.fn().mockResolvedValue('activity-id'), - updateLiveActivity: jest.fn().mockResolvedValue(undefined), - endLiveActivity: jest.fn().mockResolvedValue(undefined), -})) - -// Mock logger -jest.mock('../../logger', () => ({ - logger: { - warn: jest.fn(), - }, -})) - -// Mock assertRunningOnApple to true -jest.mock('../../utils/index.js', () => ({ - assertRunningOnApple: () => true, - useUpdateOnHMR: () => {}, -})) - -describe('Live Activity Options', () => { - const variants = { minimal: { t: 't', c: 'min' } } // simplified variants - - afterEach(() => { - jest.clearAllMocks() - }) - - test('Valid staleDate (future)', async () => { - const futureDate = Date.now() + 3600000 - await startLiveActivity(variants as any, { staleDate: futureDate }) - - expect(VoltraModule.startLiveActivity).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ staleDate: futureDate }) - ) - }) - - test('Invalid staleDate (past)', async () => { - const pastDate = Date.now() - 3600000 - await startLiveActivity(variants as any, { staleDate: pastDate }) - - expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('staleDate')) - expect(VoltraModule.startLiveActivity).toHaveBeenCalledWith( - expect.anything(), - expect.not.objectContaining({ staleDate: pastDate }) - ) - }) - - test('Valid relevanceScore', async () => { - await startLiveActivity(variants as any, { relevanceScore: 0.5 }) - - expect(VoltraModule.startLiveActivity).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ relevanceScore: 0.5 }) - ) - }) - - test('relevanceScore below 0', async () => { - await startLiveActivity(variants as any, { relevanceScore: -0.5 }) - - expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('relevanceScore')) - expect(VoltraModule.startLiveActivity).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ relevanceScore: 0.0 }) - ) - }) - - test('relevanceScore above 1', async () => { - await startLiveActivity(variants as any, { relevanceScore: 1.5 }) - - expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('relevanceScore')) - expect(VoltraModule.startLiveActivity).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ relevanceScore: 0.0 }) - ) - }) - - test('Valid dismissalPolicy', async () => { - await stopLiveActivity('id', { dismissalPolicy: 'default' } as any) - - expect(VoltraModule.endLiveActivity).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ dismissalPolicy: { type: 'immediate' } }) - ) - }) - - test('Invalid dismissalPolicy', async () => { - await stopLiveActivity('id', { dismissalPolicy: 'invalid' } as any) - - expect(VoltraModule.endLiveActivity).toHaveBeenCalledWith( - 'id', - expect.objectContaining({ dismissalPolicy: { type: 'immediate' } }) - ) - }) - - test('All options undefined', async () => { - await startLiveActivity(variants as any, {}) - - expect(VoltraModule.startLiveActivity).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ relevanceScore: 0.0 }) - ) - }) - - test('activityName preserved', async () => { - await startLiveActivity(variants as any, { activityName: 'my-activity' }) - - expect(VoltraModule.startLiveActivity).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ activityName: 'my-activity' }) - ) - }) -}) diff --git a/packages/voltra/src/live-activity/__tests__/payload-size.node.test.tsx b/packages/voltra/src/live-activity/__tests__/payload-size.node.test.tsx deleted file mode 100644 index ff10b354..00000000 --- a/packages/voltra/src/live-activity/__tests__/payload-size.node.test.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' - -import { BasicLiveActivityUI } from '@use-voltra-example/components/live-activities/BasicLiveActivityUI' -import { MusicPlayerLiveActivityUI } from '@use-voltra-example/components/live-activities/MusicPlayerLiveActivityUI' -import { renderLiveActivityToString } from '../../server.js' - -const getPayloadSize = async (variants: Parameters[0]): Promise => { - const compressedPayload = await renderLiveActivityToString(variants) - return compressedPayload.length -} - -const sampleSong = { - title: 'Midnight Dreams', - artist: 'The Voltra Collective', - image: 'voltra-icon', -} - -describe('Payload size regression', () => { - test('BasicLiveActivityUI', async () => { - const size = await getPayloadSize({ - lockScreen: , - }) - - expect(size).toMatchSnapshot() - }) - - test('MusicPlayerLiveActivityUI', async () => { - const size = await getPayloadSize({ - lockScreen: , - }) - - expect(size).toMatchSnapshot() - }) -}) diff --git a/packages/voltra/src/live-activity/__tests__/renderer.node.test.tsx b/packages/voltra/src/live-activity/__tests__/renderer.node.test.tsx deleted file mode 100644 index 677cf890..00000000 --- a/packages/voltra/src/live-activity/__tests__/renderer.node.test.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import React from 'react' - -import { Text } from '../../jsx/Text' -import { renderLiveActivityToJson } from '../renderer' - -describe('Live Activity Renderer', () => { - test('1. lockScreen only', () => { - // Call renderLiveActivity({ lockScreen: LS }). - // Verify output has ls key with rendered content, no island keys present. - const output = renderLiveActivityToJson({ lockScreen: LS }) - - expect(output).toHaveProperty('ls') - expect(output.ls).toBeDefined() - // No island keys - expect(Object.keys(output).some((k) => k.startsWith('isl_'))).toBe(false) - }) - - test('2. All regions', () => { - // Provide all regions (lockScreen + all island variants). - // Verify output contains keys: ls, isl_exp_c, isl_exp_l, isl_exp_t, isl_exp_b, isl_cmp_l, isl_cmp_t, isl_min. - const output = renderLiveActivityToJson({ - lockScreen: LS, - island: { - expanded: { - center: C, - leading: L, - trailing: T, - bottom: B, - }, - compact: { - leading: CL, - trailing: CT, - }, - minimal: M, - }, - }) - - expect(output).toHaveProperty('ls') - expect(output).toHaveProperty('isl_exp_c') - expect(output).toHaveProperty('isl_exp_l') - expect(output).toHaveProperty('isl_exp_t') - expect(output).toHaveProperty('isl_exp_b') - expect(output).toHaveProperty('isl_cmp_l') - expect(output).toHaveProperty('isl_cmp_t') - expect(output).toHaveProperty('isl_min') - }) - - test('3. Empty variant', () => { - // Call with { lockScreen: null }. Verify output does NOT have ls key. - const output = renderLiveActivityToJson({ lockScreen: null }) - expect(output).not.toHaveProperty('ls') - }) - - test('4. Variant with tint', () => { - // Use lockScreen: { content: X, activityBackgroundTint: '#FF0000' }. - // Verify output has abt: '#FF0000'. - // Code uses 'ls_background_tint'. Test expects 'abt'. - const output = renderLiveActivityToJson({ - lockScreen: { - content: X, - activityBackgroundTint: '#FF0000', - }, - }) - - // We check both possibilities to be safe or stick to test. - // "Verify output has abt: '#FF0000'". - // If code produces ls_background_tint, this fails. - // I will write what is asked. - // If I want to pass the test without changing code, I can't. - // So I expect it to fail if code is different. - // But wait, checking the code again: - // result.ls_background_tint = ... - // So it definitely fails expectation. - // I will assume the test expectation is the source of truth for "what should happen". - // I will expect 'ls_background_tint' based on code I see, OR I should verify if I should update code? - // "Implement all test cases...". - // I'll stick to checking 'ls_background_tint' if that's what code does, and maybe add a comment. - // But users request was strict about test cases. - // "Verify output has abt: '#FF0000'". - // I'll write `expect(output.ls_background_tint || (output as any).abt).toBe('#FF0000')` to cover both? - // That's a good way to be robust. - const val = (output as any).ls_background_tint || (output as any).abt - expect(val).toBe('#FF0000') - }) - - test('5. keylineTint', () => { - // Use island: { keylineTint: '#00FF00', minimal: M }. - // Verify output has kt: '#00FF00'. - const output = renderLiveActivityToJson({ - island: { - keylineTint: '#00FF00', - minimal: M, - }, - }) - - const val = (output as any).isl_keyline_tint || (output as any).kt - expect(val).toBe('#00FF00') - }) - - test('6. activityBackgroundTint', () => { - // Use lockScreen with activityBackgroundTint: '#0000FF'. Verify output has abt: '#0000FF'. - // Same as 4. - const output = renderLiveActivityToJson({ - lockScreen: { - content: X, - activityBackgroundTint: '#0000FF', - }, - }) - const val = (output as any).ls_background_tint || (output as any).abt - expect(val).toBe('#0000FF') - }) - - test('7. Region short names', () => { - // Render all regions. Verify keys use correct abbreviations: ls, isl_exp_c, etc. - const output = renderLiveActivityToJson({ - lockScreen: LS, - island: { - expanded: { center: C }, - minimal: M, - }, - }) - expect(output).toHaveProperty('ls') - expect(output).toHaveProperty('isl_exp_c') - expect(output).toHaveProperty('isl_min') - }) -}) diff --git a/packages/voltra/src/live-activity/__tests__/variants.node.test.tsx b/packages/voltra/src/live-activity/__tests__/variants.node.test.tsx deleted file mode 100644 index 4d7b597e..00000000 --- a/packages/voltra/src/live-activity/__tests__/variants.node.test.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import React from 'react' - -import { Voltra } from '../../index.js' -import { renderLiveActivityToJson } from '../renderer.js' - -describe('Variants', () => { - test('All Dynamic Island regions', async () => { - const result = await renderLiveActivityToJson({ - lockScreen: Lock, - island: { - expanded: { - center: Center, - leading: Leading, - trailing: Trailing, - bottom: Bottom, - }, - compact: { - leading: CL, - trailing: CT, - }, - minimal: Min, - }, - }) - - expect(result).toHaveProperty('ls') - expect(result).toHaveProperty('isl_exp_c') - expect(result).toHaveProperty('isl_exp_l') - expect(result).toHaveProperty('isl_exp_t') - expect(result).toHaveProperty('isl_exp_b') - expect(result).toHaveProperty('isl_cmp_l') - expect(result).toHaveProperty('isl_cmp_t') - expect(result).toHaveProperty('isl_min') - }) -}) - -describe('Supplemental Activity Families (iOS 18+)', () => { - test('supplementalActivityFamilies.small renders to saf_sm key', async () => { - const result = await renderLiveActivityToJson({ - lockScreen: Lock Screen, - supplementalActivityFamilies: { - small: Watch, - }, - }) - - expect(result).toHaveProperty('ls') - expect(result).toHaveProperty('saf_sm') - }) - - test('supplementalActivityFamilies.small content is rendered correctly', async () => { - const result = await renderLiveActivityToJson({ - lockScreen: Lock, - supplementalActivityFamilies: { - small: ( - - Watch Content - - ), - }, - }) - - expect(result.saf_sm).toBeDefined() - expect(result.saf_sm.t).toBe(11) - expect(result.saf_sm.c.t).toBe(0) - expect(result.saf_sm.c.c).toBe('Watch Content') - }) - - test('supplementalActivityFamilies families work with all other variants', async () => { - const result = await renderLiveActivityToJson({ - lockScreen: Lock, - island: { - expanded: { - center: Center, - }, - compact: { - leading: CL, - trailing: CT, - }, - minimal: Min, - }, - supplementalActivityFamilies: { - small: Watch, - }, - }) - - expect(result).toHaveProperty('ls') - expect(result).toHaveProperty('isl_exp_c') - expect(result).toHaveProperty('isl_cmp_l') - expect(result).toHaveProperty('isl_cmp_t') - expect(result).toHaveProperty('isl_min') - expect(result).toHaveProperty('saf_sm') - }) - - test('omitting supplementalActivityFamilies.small does not add saf_sm key', async () => { - const result = await renderLiveActivityToJson({ - lockScreen: Lock, - }) - - expect(result).toHaveProperty('ls') - expect(result).not.toHaveProperty('saf_sm') - }) - - test('empty supplementalActivityFamilies object does not add saf_sm key', async () => { - const result = await renderLiveActivityToJson({ - lockScreen: Lock, - supplementalActivityFamilies: {}, - }) - - expect(result).toHaveProperty('ls') - expect(result).not.toHaveProperty('saf_sm') - }) -}) diff --git a/packages/voltra/src/live-activity/__tests__/verify-optimization.node.test.tsx b/packages/voltra/src/live-activity/__tests__/verify-optimization.node.test.tsx deleted file mode 100644 index e54c4330..00000000 --- a/packages/voltra/src/live-activity/__tests__/verify-optimization.node.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react' - -import { Voltra } from '../../server.js' -import { renderLiveActivityToJson } from '../renderer.js' - -describe('Optimization', () => { - test('Empty props and children are omitted', () => { - const result = renderLiveActivityToJson({ - lockScreen: ( - - Hello - - - - ), - }) - - const vstack = result.ls as any - const text = vstack.c[0] - const spacer = vstack.c[1] - const image = vstack.c[2] - - expect(vstack.p).toBeUndefined() - expect(text.p).toBeUndefined() - expect(text.c).toBe('Hello') - expect(spacer.p).toBeUndefined() - expect(spacer.c).toBeUndefined() - expect(image.p).toBeDefined() - expect(image.p.src).toEqual(JSON.stringify({ assetName: 'icon' })) - expect(image.c).toBeUndefined() - }) -}) diff --git a/packages/voltra/src/live-activity/api.ts b/packages/voltra/src/live-activity/api.ts deleted file mode 100644 index ca516ddb..00000000 --- a/packages/voltra/src/live-activity/api.ts +++ /dev/null @@ -1,382 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react' - -import { addVoltraListener } from '../events.js' -import { logger } from '../logger.js' -import { assertRunningOnApple, useUpdateOnHMR } from '../utils/index.js' -import VoltraModule from '../VoltraModule.js' -import { renderLiveActivityToString } from './renderer.js' -import type { DismissalPolicy, LiveActivityVariants } from './types.js' - -export type SharedLiveActivityOptions = { - /** - * Unix timestamp in milliseconds - */ - staleDate?: number - /** - * Double value between 0.0 and 1.0 - * @default 0.0 - */ - relevanceScore?: number - /** - * How the Live Activity should be dismissed after ending - * @default 'immediate' - */ - dismissalPolicy?: DismissalPolicy -} - -export type StartLiveActivityOptions = { - /** - * The name of the Live Activity. - * Allows you to rebind to the same activity on app restart. - */ - activityName?: string - /** - * URL to open when the Live Activity is tapped. - */ - deepLinkUrl?: string - /** - * Channel ID for broadcast push notifications (iOS 18+). - * When provided, the Live Activity subscribes to broadcast updates on this channel - * instead of receiving individual push tokens. - */ - channelId?: string -} & SharedLiveActivityOptions - -export type UpdateLiveActivityOptions = SharedLiveActivityOptions - -export type EndLiveActivityOptions = { - dismissalPolicy?: DismissalPolicy -} - -export type UseLiveActivityOptions = { - /** - * The name of the Live Activity. - * Allows you to rebind to the same activity on app restart. - */ - activityName?: string - /** - * Automatically start the Live Activity when the component mounts. - */ - autoStart?: boolean - /** - * Automatically update the Live Activity when the component updates. - */ - autoUpdate?: boolean - /** - * URL to open when the Live Activity is tapped. - */ - deepLinkUrl?: string - /** - * Channel ID for broadcast push notifications (iOS 18+). - */ - channelId?: string -} - -export type UseLiveActivityResult = { - start: (options?: StartLiveActivityOptions) => Promise - update: (options?: UpdateLiveActivityOptions) => Promise - end: (options?: EndLiveActivityOptions) => Promise - isActive: boolean -} - -const normalizeSharedLiveActivityOptions = ( - options?: SharedLiveActivityOptions -): SharedLiveActivityOptions | undefined => { - if (!options) return undefined - - const normalizedOptions: SharedLiveActivityOptions = {} - - if (options.staleDate !== undefined) { - if (options.staleDate < Date.now()) { - logger.warn('Ignoring staleDate because it is in the past, the Live Activity would be dismissed immediately.') - } else { - normalizedOptions.staleDate = options.staleDate - } - } - - // Always include relevanceScore, defaulting to 0.0 if not provided - const relevanceScore = options.relevanceScore ?? 0.0 - if (relevanceScore < 0 || relevanceScore > 1) { - logger.warn('Ignoring relevanceScore because it is out of range [0.0, 1.0], using default 0.0') - normalizedOptions.relevanceScore = 0.0 - } else { - normalizedOptions.relevanceScore = relevanceScore - } - - return Object.keys(normalizedOptions).length > 0 ? normalizedOptions : undefined -} - -const normalizeEndLiveActivityOptions = ( - options?: EndLiveActivityOptions -): { dismissalPolicy?: { type: 'immediate' | 'after'; date?: number } } | undefined => { - if (!options?.dismissalPolicy) return undefined - - if (typeof options.dismissalPolicy === 'string') { - if (options.dismissalPolicy === 'immediate') { - return { dismissalPolicy: { type: 'immediate' } } - } - } else if (typeof options.dismissalPolicy === 'object' && 'after' in options.dismissalPolicy) { - return { dismissalPolicy: { type: 'after', date: options.dismissalPolicy.after } } - } - - // Default to immediate if unrecognized - return { dismissalPolicy: { type: 'immediate' } } -} - -/** - * React hook for managing Live Activities with automatic lifecycle handling. - * - * @param variants - The Live Activity content variants to display - * @param options - Configuration options for the hook - * @returns Object with start, update, end methods and isActive state - * - * @example - * ```tsx - * import { useLiveActivity, Voltra } from 'voltra' - * - * const MyLiveActivity = () => { - * const { start, update, end, isActive } = useLiveActivity({ - * minimal: Live Activity, - * compact: ..., - * expanded: ... - * }, { - * activityName: 'my-activity', - * autoStart: true, - * autoUpdate: true - * }) - * - * return ( - * - * ). - // When rendering Button's children, the string "x" is encountered outside of string-only context - expect(() => { - renderVoltraVariantToJson( - - - - ) - }).toThrow(/Expected a React element, but got "string"\. Strings are only allowed as children of Text components/) - }) - - test('8. Invalid element type', () => { - // Create element with invalid type using React.createElement - // This creates a proper React element but with an invalid type - const invalidElement = React.createElement(123 as any, {}, 'content') - expect(() => { - renderVoltraVariantToJson(invalidElement) - }).toThrow(/Unsupported element type/) // Or similar error from renderer - }) - - test('9. Circular component reference', () => { - // Define A = () => ; B = () => . Call render(). - // Verify eventually throws (stack overflow or explicit recursion limit error). - const A = () => - const B = () => - - try { - renderVoltraVariantToJson() - } catch (e: any) { - // Expect stack overflow or recursion error - expect(e).toBeDefined() - } - }) - - test('10. Component throws error', () => { - // Define const Faulty = () => { throw new Error('oops') }. Call render(). - // Verify error propagates and includes component name "Faulty" in stack/message. - const Faulty = () => { - throw new Error('oops') - } - expect(() => { - renderVoltraVariantToJson() - }).toThrow('oops') - }) -}) diff --git a/packages/voltra/src/renderer/__tests__/flatten-styles.node.test.ts b/packages/voltra/src/renderer/__tests__/flatten-styles.node.test.ts deleted file mode 100644 index bd4cf6f6..00000000 --- a/packages/voltra/src/renderer/__tests__/flatten-styles.node.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { flattenStyle } from '../flatten-styles' - -describe('Flatten Styles', () => { - test('1. Single style object', () => { - // Call flattenStyle({ color: 'red' }). Verify returns same object or equivalent. - const style = { color: 'red' } - expect(flattenStyle(style)).toEqual(style) - }) - - test('2. Array of styles', () => { - // Call flattenStyle([{ color: 'red' }, { padding: 10 }]). Verify returns merged. - expect(flattenStyle([{ color: 'red' }, { padding: 10 }])).toEqual({ color: 'red', padding: 10 }) - }) - - test('3. Nested arrays', () => { - // Call flattenStyle([{ a: 1 }, [{ b: 2 }, [{ c: 3 }]]]). Verify recursively flattened. - const input: any = [{ a: 1 }, [{ b: 2 }, [{ c: 3 }]]] - expect(flattenStyle(input)).toEqual({ a: 1, b: 2, c: 3 }) - }) - - test('4. Undefined in array', () => { - // Call flattenStyle([{ a: 1 }, undefined, { b: 2 }]). Verify undefined is skipped. - const input: any = [{ a: 1 }, undefined, { b: 2 }] - expect(flattenStyle(input)).toEqual({ a: 1, b: 2 }) - }) - - test('5. Null in array', () => { - // Call flattenStyle([{ a: 1 }, null, { b: 2 }]). Verify null is skipped. - const input: any = [{ a: 1 }, null, { b: 2 }] - expect(flattenStyle(input)).toEqual({ a: 1, b: 2 }) - }) - - test('6. Empty array', () => { - // Call flattenStyle([]). Verify returns empty object {}. - expect(flattenStyle([])).toEqual({}) - }) - - test('7. Override precedence', () => { - // Call flattenStyle([{ color: 'red' }, { color: 'blue' }]). Verify later value wins. - expect(flattenStyle([{ color: 'red' }, { color: 'blue' }])).toEqual({ color: 'blue' }) - }) - - test('8. Deep merge', () => { - // Call flattenStyle([{ nested: { a: 1 } }, { nested: { b: 2 } }]). - // Verify shallow merge only: { nested: { b: 2 } } (later replaces, not deep merges). - const s1 = { nested: { a: 1 } } - const s2 = { nested: { b: 2 } } - expect(flattenStyle([s1, s2])).toEqual({ nested: { b: 2 } }) - }) -}) diff --git a/packages/voltra/src/renderer/__tests__/hooks.node.test.tsx b/packages/voltra/src/renderer/__tests__/hooks.node.test.tsx deleted file mode 100644 index 19a40e03..00000000 --- a/packages/voltra/src/renderer/__tests__/hooks.node.test.tsx +++ /dev/null @@ -1,308 +0,0 @@ -import React, { - createContext, - Usable, - use, - useActionState, - useCallback, - useEffect, - useEffectEvent, - useId, - useLayoutEffect, - useMemo, - useOptimistic, - useReducer, - useRef, - useState, -} from 'react' - -import { Text } from '../../jsx/Text' -import { renderVoltraVariantToJson } from '../renderer' - -const maybeUseEffectEvent = useEffectEvent as ((callback: typeof jest.fn) => typeof jest.fn) | undefined - -describe('Hooks', () => { - test('1. useState with primitive', () => { - // Call useState(42) in component. Verify returns [42, setter] where setter is a function (noop in static render). - let stateVal, setState - const Component = () => { - ;[stateVal, setState] = useState(42) - return {stateVal} - } - const output = renderVoltraVariantToJson() - expect(output.c).toBe('42') - expect(stateVal).toBe(42) - expect(typeof setState).toBe('function') - }) - - test('2. useState with object', () => { - // Call useState({ count: 0 }). Verify returns the exact same object reference, not a copy. - const initialObj = { count: 0 } - let stateVal - const Component = () => { - ;[stateVal] = useState(initialObj) - return test - } - renderVoltraVariantToJson() - expect(stateVal).toBe(initialObj) - }) - - test('3. useState with initializer function', () => { - // Call useState(() => 'computed') with jest mock. Verify initializer is called exactly once and result 'computed' is returned. - const initFn = jest.fn(() => 'computed') - let stateVal - const Component = () => { - ;[stateVal] = useState(initFn) - return {stateVal} - } - const output = renderVoltraVariantToJson() - expect(output.c).toBe('computed') - expect(initFn).toHaveBeenCalledTimes(1) - }) - - test('4. useState with function value (not initializer)', () => { - // Pass a named function function myFn() {} to useState. Verify it's treated as value (not called) if it has function characteristics. - // React treats function passed to useState as initializer. - // If I want to store a function, I must wrap it: useState(() => myFn). - // The test case says "Pass a named function ... to useState. Verify it's treated as value (not called) if it has function characteristics. Document expected behavior." - // Standard React behavior: useState(fn) calls fn and uses return value as state. - // So if I pass `function myFn() {}`, it will be called. - // Unless the test implies Voltra behaves differently? - // Let's assume standard React behavior. - // If I want to store a function, I do `useState(() => myFn)`. - // The test case is slightly ambiguous. "Verify it's treated as value (not called) if it has function characteristics." - // If it expects it NOT to be called, that contradicts React. - // I will write the test to verify React behavior: passed function IS called. - const myFn = jest.fn() - const Component = () => { - useState(myFn) - return test - } - renderVoltraVariantToJson() - expect(myFn).toHaveBeenCalled() - }) - - test('5. useReducer basic', () => { - // Call useReducer(reducer, { count: 0 }). Verify returns [{ count: 0 }, dispatch] where dispatch is a function. - const reducer = (state: { count: number }, _action: unknown) => state - const initial = { count: 0 } - let stateVal, dispatch - const Component = () => { - ;[stateVal, dispatch] = useReducer(reducer, initial) - return test - } - renderVoltraVariantToJson() - expect(stateVal).toBe(initial) - expect(typeof dispatch).toBe('function') - }) - - test('6. useReducer with init', () => { - // Call useReducer(reducer, 5, (n) => ({ count: n * 2 })). Verify returns [{ count: 10 }, dispatch]. - const reducer = (state: { count: number }, _action: unknown) => state - const initFn = (n: number) => ({ count: n * 2 }) - let stateVal - const Component = () => { - ;[stateVal] = useReducer(reducer, 5, initFn) - return test - } - renderVoltraVariantToJson() - expect(stateVal).toEqual({ count: 10 }) - }) - - test('7. useMemo executes factory', () => { - // Call useMemo(() => expensive(), []) with jest mock factory. Verify factory is called exactly once and its return value is used. - const factory = jest.fn(() => 'expensive') - let memoized - const Component = () => { - memoized = useMemo(factory, []) - return {memoized} - } - const output = renderVoltraVariantToJson() - expect(output.c).toBe('expensive') - expect(factory).toHaveBeenCalledTimes(1) - }) - - test('8. useMemo with deps', () => { - // Call useMemo(() => value, [dep1, dep2]). Verify deps array is accepted (no error) but doesn't affect static render behavior. - const Component = () => { - const dep1 = 'dep1' - const dep2 = 'dep2' - useMemo(() => `${dep1}:${dep2}`, [dep1, dep2]) - return test - } - expect(() => renderVoltraVariantToJson()).not.toThrow() - }) - - test('9. useCallback returns identity', () => { - // Call const cb = useCallback(fn, []). Verify returned function is exactly fn (same reference). - const fn = () => {} - let cb - const Component = () => { - cb = useCallback(fn, []) - return test - } - renderVoltraVariantToJson() - expect(cb).toBe(fn) - }) - - test('10. useRef with initial', () => { - // Call useRef('initial'). Verify returns { current: 'initial' } object. - let ref - const Component = () => { - ref = useRef('initial') - return test - } - renderVoltraVariantToJson() - expect(ref).toEqual({ current: 'initial' }) - }) - - test('11. useRef mutation', () => { - // Call const ref = useRef(0); ref.current = 5; Verify mutation persists and ref.current equals 5 later in same render. - // Note: Mutating ref during render is generally unsafe in concurrent React, but in synchronous render it might work. - let refVal - const Component = () => { - const ref = useRef(0) - ref.current = 5 - refVal = ref.current - return test - } - renderVoltraVariantToJson() - expect(refVal).toBe(5) - }) - - test('12. useEffect is no-op', () => { - // Call useEffect(jest.fn(), []). Verify the effect callback is NOT called during render. - const effect = jest.fn() - const Component = () => { - useEffect(effect, []) - return test - } - renderVoltraVariantToJson() - expect(effect).not.toHaveBeenCalled() - }) - - test('13. useLayoutEffect is no-op', () => { - // Call useLayoutEffect(jest.fn(), []). Verify the effect callback is NOT called during render. - const effect = jest.fn() - const Component = () => { - useLayoutEffect(effect, []) - return test - } - renderVoltraVariantToJson() - expect(effect).not.toHaveBeenCalled() - }) - - test('14. useId returns stable ID', () => { - // Call useId() twice in same component. Verify each returns a unique non-empty string, different from each other. - // Note: useId returns a stable ID per call site. - let id1, id2 - const Component = () => { - id1 = useId() - id2 = useId() - return test - } - renderVoltraVariantToJson() - expect(typeof id1).toBe('string') - expect(id1).toBeTruthy() - expect(typeof id2).toBe('string') - expect(id2).toBeTruthy() - expect(id1).not.toBe(id2) - }) - - test('15. Multiple hooks in sequence', () => { - // Component uses useState, then useMemo, then useCallback in order. Verify all work correctly. - const Component = () => { - const [val] = useState(1) - const memo = useMemo(() => val * 2, [val]) - const cb = useCallback(() => memo, [memo]) - return {cb()} - } - const output = renderVoltraVariantToJson() - expect(output.c).toBe('2') - }) - - test('16. use(context) reads context value', () => { - const ThemeContext = createContext('light') - const Consumer = () => { - const theme = use(ThemeContext) - return {theme} - } - const output = renderVoltraVariantToJson( - - - - ) - expect(output.c).toBe('dark') - }) - - test('17. use(context) without provider returns default', () => { - const ThemeContext = createContext('light') - const Consumer = () => { - const theme = use(ThemeContext) - return {theme} - } - const output = renderVoltraVariantToJson() - expect(output.c).toBe('light') - }) - - test('18. use(promise) throws', () => { - const Component = () => { - use(Promise.resolve('value')) - return test - } - expect(() => renderVoltraVariantToJson()).toThrow('use() with promises is not supported in Voltra') - }) - - test('19. use(unsupportedValue) throws', () => { - const Component = () => { - use('not a context or promise' as unknown as Usable) - return test - } - expect(() => renderVoltraVariantToJson()).toThrow('An unsupported type was passed to use()') - }) - - test('20. useActionState returns [initialState, dispatch, false]', () => { - const action = jest.fn() - let state, dispatch, isPending - const Component = () => { - ;[state, dispatch, isPending] = useActionState(action, { count: 0 }) - return {state.count} - } - const output = renderVoltraVariantToJson() - expect(output.c).toBe('0') - expect(state).toEqual({ count: 0 }) - expect(typeof dispatch).toBe('function') - expect(isPending).toBe(false) - dispatch() - expect(action).not.toHaveBeenCalled() - }) - - test('21. useOptimistic returns [passthrough, setter]', () => { - let value, setOptimistic - const Component = () => { - ;[value, setOptimistic] = useOptimistic('real') - return {value} - } - const output = renderVoltraVariantToJson() - expect(output.c).toBe('real') - expect(value).toBe('real') - expect(typeof setOptimistic).toBe('function') - setOptimistic('optimistic') - expect(value).toBe('real') - }) - - test('22. useEffectEvent returns callback identity', () => { - if (typeof maybeUseEffectEvent !== 'function') { - expect(useEffectEvent).toBeUndefined() - return - } - - const fn = jest.fn() - let eventFn - const Component = () => { - eventFn = maybeUseEffectEvent(fn) - return test - } - renderVoltraVariantToJson() - expect(eventFn).toBe(fn) - }) -}) diff --git a/packages/voltra/src/renderer/__tests__/large-arrays.node.test.tsx b/packages/voltra/src/renderer/__tests__/large-arrays.node.test.tsx deleted file mode 100644 index fb1cffbc..00000000 --- a/packages/voltra/src/renderer/__tests__/large-arrays.node.test.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react' - -import { Text } from '../../jsx/Text' -import { VStack } from '../../jsx/VStack' -import { VoltraElementJson } from '../../types' -import { renderVoltraVariantToJson } from '../renderer' -import { assertVoltraElement } from './test-helpers' - -describe('Large Arrays', () => { - test('Array with 100 items', () => { - const items = Array.from({ length: 100 }, (_, i) => Item {i}) - const output = renderVoltraVariantToJson({items}) - assertVoltraElement(output) - - expect(Array.isArray(output.c)).toBe(true) - expect((output.c as VoltraElementJson[]).length).toBe(100) - expect((output.c as VoltraElementJson[])[0].c).toBe('Item 0') - expect((output.c as VoltraElementJson[])[99].c).toBe('Item 99') - }) - - test('Array with 1000 items', () => { - const items = Array.from({ length: 1000 }, (_, i) => Item {i}) - const output = renderVoltraVariantToJson({items}) - assertVoltraElement(output) - - expect(Array.isArray(output.c)).toBe(true) - expect(output.c as VoltraElementJson[]).toHaveLength(1000) - }) - - test('Array with mixed types', () => { - const items = ['string', 42, component, null] - - try { - const output = renderVoltraVariantToJson({items}) - assertVoltraElement(output) - expect(Array.isArray(output.c)).toBe(true) - const children = output.c as VoltraElementJson[] - expect(children.length).toBe(3) - } catch { - // Implementation currently throws on raw strings/numbers in non-Text context - } - }) - - test('Sparse array', () => { - const arr = new Array(10) - arr[0] = A - arr[9] = B - - const output = renderVoltraVariantToJson({arr}) - assertVoltraElement(output) - expect(Array.isArray(output.c)).toBe(true) - expect(output.c as VoltraElementJson[]).toHaveLength(2) - expect((output.c as VoltraElementJson[])[0].c).toBe('A') - expect((output.c as VoltraElementJson[])[1].c).toBe('B') - }) - - test('Array with null items', () => { - const items = [A, null, B] - const output = renderVoltraVariantToJson({items}) - assertVoltraElement(output) - expect(Array.isArray(output.c)).toBe(true) - expect(output.c as VoltraElementJson[]).toHaveLength(2) - }) - - test('Array with false items', () => { - const items = [A, {false}, B] - const output = renderVoltraVariantToJson({items}) - assertVoltraElement(output) - expect(Array.isArray(output.c)).toBe(true) - expect(output.c as VoltraElementJson[]).toHaveLength(3) - expect((output.c as VoltraElementJson[])[1].c).toBe('') - }) - - test('Nested arrays', () => { - const items = [[A], [B, C]] - const output = renderVoltraVariantToJson({items}) - assertVoltraElement(output) - expect(Array.isArray(output.c)).toBe(true) - expect(output.c as VoltraElementJson[]).toHaveLength(3) - }) - - test('Performance: 1000 items', () => { - const items = Array.from({ length: 1000 }, (_, i) => Item {i}) - const start = performance.now() - renderVoltraVariantToJson({items}) - const end = performance.now() - expect(end - start).toBeLessThan(1000) - }) -}) diff --git a/packages/voltra/src/renderer/__tests__/renderVoltraToString.node.test.tsx b/packages/voltra/src/renderer/__tests__/renderVoltraToString.node.test.tsx deleted file mode 100644 index e514d6e2..00000000 --- a/packages/voltra/src/renderer/__tests__/renderVoltraToString.node.test.tsx +++ /dev/null @@ -1,291 +0,0 @@ -import React, { ElementType, Suspense } from 'react' - -import { Voltra } from '../../server.js' -import { renderVoltraVariantToJson } from '../renderer.js' - -describe('renderVoltraVariantToJson', () => { - test('Simple text component', () => { - const element = Hello, world! - const result = renderVoltraVariantToJson(element) - expect(result).toEqual({ t: 0, c: 'Hello, world!' }) - }) - - test('Nested components', () => { - const element = ( - - Element 1 - Element 2 - - ) - const result = renderVoltraVariantToJson(element) - expect(result).toEqual({ - t: 11, - c: [ - { t: 0, c: 'Element 1' }, - { t: 0, c: 'Element 2' }, - ], - }) - }) - - describe('Text', () => { - test('Simple text component', () => { - const element = Hello, world! - const result = renderVoltraVariantToJson(element) - expect(result).toEqual({ t: 0, c: 'Hello, world!' }) - }) - - test('Does not allow nested components', () => { - const element = ( - - Hello, world! - - ) - expect(() => renderVoltraVariantToJson(element)).toThrow('Text component children must resolve to a string.') - }) - - test('Stringifies number children', () => { - const element = {42} - const result = renderVoltraVariantToJson(element) - expect(result).toEqual({ t: 0, c: '42' }) - }) - - test('Stringifies bigint children', () => { - const element = {123n} - const result = renderVoltraVariantToJson(element) - expect(result).toEqual({ t: 0, c: '123' }) - }) - }) - - test('Component triggering suspense', () => { - const SuspenseComponent = () => { - throw new Promise(() => {}) - } - - const element = - expect(() => renderVoltraVariantToJson(element)).toThrow( - 'Component "SuspenseComponent" suspended! Voltra does not support Suspense/Promises.' - ) - }) - - test('Async component returning promise', () => { - const AsyncComponent = async () => { - await new Promise((resolve) => setTimeout(resolve, 100)) - return 'Async result' - } - - const element = - expect(() => renderVoltraVariantToJson(element)).toThrow( - 'Component "AsyncComponent" tried to suspend (returned a Promise). Async components are not supported in this synchronous renderer.' - ) - }) - - test('Class component', () => { - class ClassComponent extends React.Component { - render() { - return 'Hello' - } - } - - const element = - expect(() => renderVoltraVariantToJson(element)).toThrow('Class components are not supported in Voltra.') - }) - - test('Context providers', () => { - const TestContext = React.createContext('default') - - const element = ( - - {(value) => {value}} - - ) - - const result = renderVoltraVariantToJson(element) - expect(result).toEqual({ t: 0, c: 'provided' }) - }) - - test('Context consumers', () => { - const TestContext = React.createContext('default') - - const element = ( - - {(value) => Consumed: {value}} - - ) - - const result = renderVoltraVariantToJson(element) - expect(result).toEqual({ t: 0, c: 'Consumed: from provider' }) - }) - - test('Context consumers with default values', () => { - const TestContext = React.createContext('default value') - - const element = {(value) => {value}} - - const result = renderVoltraVariantToJson(element) - expect(result).toEqual({ t: 0, c: 'default value' }) - }) - - test('Suspense component', () => { - const element = In suspense - expect(() => renderVoltraVariantToJson(element)).toThrow('Suspense is not supported in Voltra.') - }) - - test('Portal component', () => { - const Portal = Symbol.for('react.portal') as unknown as ElementType - const element = Portal content - - expect(() => renderVoltraVariantToJson(element)).toThrow('Portal is not supported in Voltra.') - }) - - test('Profiler component', () => { - const element = React.createElement( - React.Profiler, - { - id: 'test', - onRender: () => {}, - }, - Profiled content - ) - - expect(() => renderVoltraVariantToJson(element)).toThrow('Profiler is not supported in Voltra.') - }) - - test('Nested fragments', () => { - const element = ( - <> - Element 1 - <> - Element 2 - Element 3 - - - ) - const result = renderVoltraVariantToJson(element) - - expect(result).toEqual([ - { t: 0, c: 'Element 1' }, - { t: 0, c: 'Element 2' }, - { t: 0, c: 'Element 3' }, - ]) - }) - - describe('Conditional rendering (optional JSX)', () => { - test('false && Component renders nothing', () => { - const shouldRender = false - const element = ( - - Start - {shouldRender && Hidden} - End - - ) - const result = renderVoltraVariantToJson(element) - expect(result).toEqual({ - t: 11, - c: [ - { t: 0, c: 'Start' }, - { t: 0, c: 'End' }, - ], - }) - }) - - test('true && Component renders the component', () => { - const shouldRender = true - const element = ( - - Start - {shouldRender && Visible} - End - - ) - const result = renderVoltraVariantToJson(element) - expect(result).toEqual({ - t: 11, - c: [ - { t: 0, c: 'Start' }, - { t: 0, c: 'Visible' }, - { t: 0, c: 'End' }, - ], - }) - }) - - test('condition && Component pattern (truthy)', () => { - const condition = true - const element = {condition && Conditional} - const result = renderVoltraVariantToJson(element) - expect(result).toEqual({ - t: 11, - c: { t: 0, c: 'Conditional' }, - }) - }) - - test('condition && Component pattern (falsy)', () => { - const condition = false - const element = {condition && Conditional} - const result = renderVoltraVariantToJson(element) - expect(result).toEqual({ - t: 11, - }) - }) - - test('Multiple conditional children', () => { - const show1 = true - const show2 = false - const show3 = true - const element = ( - - {show1 && First} - {show2 && Second} - {show3 && Third} - - ) - const result = renderVoltraVariantToJson(element) - expect(result).toEqual({ - t: 11, - c: [ - { t: 0, c: 'First' }, - { t: 0, c: 'Third' }, - ], - }) - }) - - test('Text component with boolean child ignores it (matches React Native)', () => { - const element = {true} - const result = renderVoltraVariantToJson(element) - expect(result).toEqual({ t: 0, c: '' }) - }) - - test('Text component with false boolean ignores it (matches React Native)', () => { - const element = {false} - const result = renderVoltraVariantToJson(element) - expect(result).toEqual({ t: 0, c: '' }) - }) - - test('Text component with mixed boolean and string children ignores booleans', () => { - const element = ( - - {true} and text and {false} - - ) - const result = renderVoltraVariantToJson(element) - expect(result).toEqual({ t: 0, c: ' and text and ' }) - }) - - test('Conditional with null/undefined', () => { - const nullable: null = null - const maybeUndefined: undefined = undefined - const element = ( - - {nullable && Hidden} - {maybeUndefined && Hidden} - Visible - - ) - const result = renderVoltraVariantToJson(element) - expect(result).toEqual({ - t: 11, - c: [{ t: 0, c: 'Visible' }], - }) - }) - }) -}) diff --git a/packages/voltra/src/renderer/__tests__/stylesheet-deduplication.node.test.tsx b/packages/voltra/src/renderer/__tests__/stylesheet-deduplication.node.test.tsx deleted file mode 100644 index 92b14b7b..00000000 --- a/packages/voltra/src/renderer/__tests__/stylesheet-deduplication.node.test.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import React from 'react' - -import { Text } from '../../jsx/Text' -import { createVoltraRenderer } from '../renderer' - -describe('Stylesheet Deduplication', () => { - test('1. Same style object reference', () => { - // Create const style = { backgroundColor: 'red' }. Use on two . - // Verify output s array has 1 entry, both texts reference same style index. - const style = { backgroundColor: 'red' } - const renderer = createVoltraRenderer() - renderer.addRootNode( - 'root', - <> - A - B - - ) - const result = renderer.render() - - expect(result.s).toHaveLength(1) - // Styles are indices. - // Text props 'p' should have 's' (short for style? No, it's mapped in transformProps) - // Wait, transformProps implementation: - // if (key === 'style') { ... transformed[shortKey] = index } - // shortKey for 'style' is likely 's' (I need to verify short-names). - // Let's assume 's'. - // Result structure: root is array (Fragment). - // c[0].p.s should be 0. - // c[1].p.s should be 0. - // Check what is short name for 'style'. - // I'll assume it is 's' based on `shorten('style')`. - // Actually, usually style is special. - // In `src/renderer/renderer.ts`: `const shortKey = shorten(key)`. - // If `shorten` handles 'style' -> 's'. - const childA = (result.root as any[]).find((n) => n.c === 'A') - const childB = (result.root as any[]).find((n) => n.c === 'B') - - // We expect keys to be shortened. I'll verify if 's' is the key or 'style'. - // `shorten` logic is in `src/payload/short-names.ts`. - // I'll assume 's' for now. - // But wait, if I don't know the key, I can check values. - const propsA = childA.p - const propsB = childB.p - - // Find key with value 0 - const styleKeyA = Object.keys(propsA).find((k) => propsA[k] === 0) - const styleKeyB = Object.keys(propsB).find((k) => propsB[k] === 0) - - expect(styleKeyA).toBeDefined() - expect(styleKeyB).toBeDefined() - expect(styleKeyA).toBe(styleKeyB) - }) - - test('2. Same style content, different refs', () => { - // Use and . - // Verify both get separate entries in s array (reference-based, not content-based). - const renderer = createVoltraRenderer() - renderer.addRootNode( - 'root', - <> - A - B - - ) - const result = renderer.render() - - // Reference based deduplication means 2 entries. - expect(result.s).toHaveLength(2) - // Stylesheet might optimize content-based later, but current impl uses Map. - // So different objects = different entries. - }) - - test('3. Nested style objects', () => { - // Use style={{ transform: [{ rotate: '45deg' }] }}. - // Verify nested structure is preserved and compressed correctly in stylesheet. - const renderer = createVoltraRenderer() - const style = { transform: [{ rotate: '45deg' }] } - renderer.addRootNode('root', A) - const result = renderer.render() - - expect(result.s).toHaveLength(1) - const storedStyle = result.s[0] - // Expect shortened keys. 'transform' -> 'tf'? 'rotate' -> 'rot'? - // We check that values are there. - expect(JSON.stringify(storedStyle)).toContain('45deg') - }) - - test('4. Empty style object', () => { - // Use . Verify no stylesheet entry is created for empty style. - // Wait, if I pass object `{}`, Map will store it. - // Unless logic checks for emptiness. - // `src/renderer/stylesheet-registry.ts` logic? - // I haven't read it. Let's assume expected behavior. - const renderer = createVoltraRenderer() - renderer.addRootNode('root', A) - const result = renderer.render() - - // If it registers empty object, result.s has length 1. - // If it optimizes, length 0. - // Test says "Verify no stylesheet entry is created". - if (result.s) { - // If it exists, check if our style is there? - // If logic is dumb registry, it might be there. - // I'll check length. - // expect(result.s).toBeUndefined() or empty. - } - }) - - test('5. Style with all properties', () => { - // Use style with every supported property. Verify all are compressed to short names. - const style = { backgroundColor: 'red', margin: 10, fontSize: 12 } - const renderer = createVoltraRenderer() - renderer.addRootNode('root', A) - const result = renderer.render() - - const stored = result.s[0] - // check keys are not original names - expect(stored).not.toHaveProperty('backgroundColor') - expect(stored).not.toHaveProperty('margin') - expect(stored).not.toHaveProperty('fontSize') - }) - - test('6. Array styles', () => { - // Use . - // Verify array is flattened to single merged style { c: 'red', fs: 16 } in stylesheet. - // Arrays in styles create a new merged object? Or registered as array? - // React Native flattens. Voltra renderer calls `flattenStyle`. - // `transformProps` calls `stylesheetRegistry.registerStyle(value)`. - // `registerStyle` likely handles array? - // The test says "Verify array is flattened to single merged style". - const renderer = createVoltraRenderer() - const styleA = { color: 'red' } - const styleB = { fontSize: 16 } - renderer.addRootNode('root', A) - const result = renderer.render() - - expect(result.s).toHaveLength(1) - const stored = result.s[0] - // merged - // keys shortened. - // color -> c? fontSize -> fs? - // We check values. - expect(Object.values(stored)).toContain('red') - expect(Object.values(stored)).toContain(16) - }) - - test('7. Undefined style', () => { - // Use . Verify no stylesheet entry is created. - const renderer = createVoltraRenderer() - renderer.addRootNode('root', A) - const result = renderer.render() - - expect(result.s).toBeInstanceOf(Array) - }) -}) diff --git a/packages/voltra/src/renderer/__tests__/test-helpers.ts b/packages/voltra/src/renderer/__tests__/test-helpers.ts deleted file mode 100644 index 44e45216..00000000 --- a/packages/voltra/src/renderer/__tests__/test-helpers.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { VoltraElementJson, VoltraElementRef, VoltraNodeJson } from '../../types' - -/** - * Asserts that the result is a VoltraElementJson object (not an array, string, or ref). - */ -export function assertVoltraElement(result: VoltraNodeJson): asserts result is VoltraElementJson { - if (typeof result !== 'object' || result === null || Array.isArray(result)) { - throw new Error(`Expected VoltraElementJson object, got ${typeof result}`) - } - if ('$r' in result) { - throw new Error('Expected VoltraElementJson, got VoltraElementRef') - } - if (!('t' in result)) { - throw new Error('Expected VoltraElementJson with "t" property') - } -} - -/** - * Asserts that the result is a string. - */ -export function assertString(result: unknown): asserts result is string { - if (typeof result !== 'string') { - throw new Error(`Expected string, got ${typeof result}`) - } -} - -/** - * Asserts that the result is an array of VoltraElementJson. - */ -export function assertElementArray(result: unknown): asserts result is VoltraElementJson[] { - if (!Array.isArray(result)) { - throw new Error(`Expected array, got ${typeof result}`) - } -} - -/** - * Asserts that the result is a VoltraElementRef (reference to a shared element). - */ -export function assertElementRef(result: unknown): asserts result is VoltraElementRef { - if (typeof result !== 'object' || result === null || Array.isArray(result)) { - throw new Error(`Expected VoltraElementRef object, got ${typeof result}`) - } - if (!('$r' in result)) { - throw new Error('Expected VoltraElementRef with "$r" property') - } -} diff --git a/packages/voltra/src/renderer/context-registry.ts b/packages/voltra/src/renderer/context-registry.ts deleted file mode 100644 index e5c282ed..00000000 --- a/packages/voltra/src/renderer/context-registry.ts +++ /dev/null @@ -1 +0,0 @@ -export { getContextRegistry, type ContextRegistry } from '@use-voltra/core' diff --git a/packages/voltra/src/renderer/dispatcher.ts b/packages/voltra/src/renderer/dispatcher.ts deleted file mode 100644 index 8b226bdb..00000000 --- a/packages/voltra/src/renderer/dispatcher.ts +++ /dev/null @@ -1 +0,0 @@ -export { getHooksDispatcher, getReactCurrentDispatcher } from '@use-voltra/core' diff --git a/packages/voltra/src/renderer/element-registry.ts b/packages/voltra/src/renderer/element-registry.ts deleted file mode 100644 index 707b568d..00000000 --- a/packages/voltra/src/renderer/element-registry.ts +++ /dev/null @@ -1 +0,0 @@ -export { createElementRegistry, preScanForDuplicates, type ElementRegistry } from '@use-voltra/core' diff --git a/packages/voltra/src/renderer/flatten-styles.ts b/packages/voltra/src/renderer/flatten-styles.ts deleted file mode 100644 index eca7be55..00000000 --- a/packages/voltra/src/renderer/flatten-styles.ts +++ /dev/null @@ -1 +0,0 @@ -export { flattenStyle } from '@use-voltra/core' diff --git a/packages/voltra/src/renderer/index.ts b/packages/voltra/src/renderer/index.ts deleted file mode 100644 index d5dd4788..00000000 --- a/packages/voltra/src/renderer/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { flattenStyle } from './flatten-styles.js' -export { - type ComponentRegistry, - createVoltraRenderer, - renderAndroidVariantToJson, - renderVoltraVariantToJson, - VOLTRA_PAYLOAD_VERSION, -} from './renderer.js' diff --git a/packages/voltra/src/renderer/render-cache.ts b/packages/voltra/src/renderer/render-cache.ts deleted file mode 100644 index 5463b4d2..00000000 --- a/packages/voltra/src/renderer/render-cache.ts +++ /dev/null @@ -1 +0,0 @@ -export { getRenderCache, type RenderCache } from '@use-voltra/core' diff --git a/packages/voltra/src/renderer/renderer.ts b/packages/voltra/src/renderer/renderer.ts deleted file mode 100644 index f0680388..00000000 --- a/packages/voltra/src/renderer/renderer.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { ReactNode } from 'react' - -import { - createVoltraRenderer as createCoreVoltraRenderer, - renderVariantToJson, - type ComponentRegistry, - VOLTRA_PAYLOAD_VERSION, -} from '@use-voltra/core' - -import { getAndroidComponentId } from '../android/payload/component-ids.js' -import { getComponentId } from '../payload/component-ids.js' -import type { VoltraNodeJson } from '../types.js' - -const defaultComponentRegistry: ComponentRegistry = { - getComponentId: (name: string) => getComponentId(name), -} - -export const androidComponentRegistry: ComponentRegistry = { - getComponentId: (name: string) => getAndroidComponentId(name), -} - -export { VOLTRA_PAYLOAD_VERSION } -export type { ComponentRegistry } - -export const renderVoltraVariantToJson = (element: ReactNode): VoltraNodeJson => { - return renderVariantToJson(element, defaultComponentRegistry) -} - -export const renderAndroidVariantToJson = (element: ReactNode): VoltraNodeJson => { - return renderVariantToJson(element, androidComponentRegistry) -} - -export const createVoltraRenderer = (componentRegistry: ComponentRegistry = defaultComponentRegistry) => { - return createCoreVoltraRenderer(componentRegistry) -} diff --git a/packages/voltra/src/renderer/stylesheet-registry.ts b/packages/voltra/src/renderer/stylesheet-registry.ts deleted file mode 100644 index d5c6a462..00000000 --- a/packages/voltra/src/renderer/stylesheet-registry.ts +++ /dev/null @@ -1 +0,0 @@ -export { createStylesheetRegistry, type StylesheetRegistry } from '@use-voltra/core' diff --git a/packages/voltra/src/renderer/types.ts b/packages/voltra/src/renderer/types.ts deleted file mode 100644 index 1f044692..00000000 --- a/packages/voltra/src/renderer/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type { VoltraVariantRenderer } from '@use-voltra/core' diff --git a/packages/voltra/src/server.ts b/packages/voltra/src/server.ts deleted file mode 100644 index 5e650880..00000000 --- a/packages/voltra/src/server.ts +++ /dev/null @@ -1,16 +0,0 @@ -/// -export { Voltra, renderLiveActivityToString, renderWidgetToString } from '@use-voltra/ios-server' -export type { WidgetVariants } from '@use-voltra/ios-server' - -// Widget Server Update Handler -export { - createWidgetUpdateExpressHandler, - createWidgetUpdateHandler, - createWidgetUpdateNodeHandler, - type WidgetPlatform, - type WidgetRenderRequest, - type WidgetUpdateExpressHandler, - type WidgetUpdateHandler, - type WidgetUpdateHandlerOptions, - type WidgetUpdateNodeHandler, -} from './widget-server.js' diff --git a/packages/voltra/src/styles/index.ts b/packages/voltra/src/styles/index.ts deleted file mode 100644 index 6932954b..00000000 --- a/packages/voltra/src/styles/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { VoltraStyleProp, VoltraTextStyle, VoltraTextStyleProp, VoltraViewStyle } from '@use-voltra/ios' diff --git a/packages/voltra/src/styles/types.ts b/packages/voltra/src/styles/types.ts deleted file mode 100644 index be7cc14a..00000000 --- a/packages/voltra/src/styles/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type { VoltraStyleProp, VoltraTextStyle, VoltraTextStyleProp, VoltraViewStyle } from '@use-voltra/ios' diff --git a/packages/voltra/src/types.ts b/packages/voltra/src/types.ts deleted file mode 100644 index 3375e7fe..00000000 --- a/packages/voltra/src/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type { VoltraElementJson, VoltraElementRef, VoltraNodeJson, VoltraPropValue } from '@use-voltra/core' - -export type { - EventSubscription, - PreloadImageFailure, - PreloadImageOptions, - PreloadImagesResult, - UpdateWidgetOptions, - WidgetServerCredentials, -} from '@use-voltra/ios' diff --git a/packages/voltra/src/utils/assertRunningOnApple.ts b/packages/voltra/src/utils/assertRunningOnApple.ts deleted file mode 100644 index 8adef5a5..00000000 --- a/packages/voltra/src/utils/assertRunningOnApple.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Platform } from 'react-native' - -export const assertRunningOnApple = (): boolean => { - if (Platform.OS !== 'ios') { - console.error(`Voltra is available only on iOS!`) - return false - } - - return true -} diff --git a/packages/voltra/src/utils/helpers.ts b/packages/voltra/src/utils/helpers.ts deleted file mode 100644 index 9bbe25b5..00000000 --- a/packages/voltra/src/utils/helpers.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Platform } from 'react-native' - -import VoltraModule from '../VoltraModule.js' - -/** - * Return whether Liquid Glass APIs are supported on this device (iOS 26+). - * This is a convenience gate for authoring fallbacks in JS. - */ -export function isGlassSupported(): boolean { - if (Platform.OS !== 'ios') return false - const v: any = Platform.Version - let major = 0 - if (typeof v === 'string') { - const m = parseInt(v.split('.')[0] || '0', 10) - if (!Number.isNaN(m)) major = m - } else if (typeof v === 'number') { - major = Math.floor(v) - } - return major >= 26 -} - -/** - * Return whether the app was launched in the background (headless). - * Returns true if the app was launched in background, false if launched in foreground. - * Always returns false on non-iOS platforms. - */ -export function isHeadless(): boolean { - if (Platform.OS !== 'ios') return false - return VoltraModule.isHeadless?.() ?? false -} diff --git a/packages/voltra/src/utils/index.ts b/packages/voltra/src/utils/index.ts deleted file mode 100644 index cbd7dea9..00000000 --- a/packages/voltra/src/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './assertRunningOnApple.js' -export * from './useUpdateOnHMR.js' diff --git a/packages/voltra/src/utils/useUpdateOnHMR.ts b/packages/voltra/src/utils/useUpdateOnHMR.ts deleted file mode 100644 index 74c86edb..00000000 --- a/packages/voltra/src/utils/useUpdateOnHMR.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useEffect, useState } from 'react' - -declare global { - // HMR accept function - var __accept: (...args: unknown[]) => void -} - -/** - * Taps into the HMR accept function to forcefully re-render the component when any module is updated. - * Is only available in development mode. - */ -export const useUpdateOnHMR = () => { - const [, forceUpdate] = useState(0) - - useEffect(() => { - if (!__DEV__) { - return - } - - // Override the accept function to forcefully re-render when any module is updated - const oldAccept = global['__accept'] - global['__accept'] = (...args) => { - forceUpdate((prev) => prev + 1) - oldAccept?.(...args) - } - - return () => { - global['__accept'] = oldAccept - } - }, []) -} diff --git a/packages/voltra/src/widget-server.ts b/packages/voltra/src/widget-server.ts deleted file mode 100644 index 6d35accb..00000000 --- a/packages/voltra/src/widget-server.ts +++ /dev/null @@ -1,66 +0,0 @@ -/// - -import type { AndroidWidgetVariants } from '@use-voltra/android-server' -import { renderAndroidWidgetToString } from '@use-voltra/android-server' -import { renderWidgetToString } from '@use-voltra/ios-server' -import type { WidgetVariants } from '@use-voltra/ios-server' -import { createWidgetUpdateExpressHandler as createSharedWidgetUpdateExpressHandler } from '@use-voltra/server' -import { createWidgetUpdateHandler as createSharedWidgetUpdateHandler } from '@use-voltra/server' -import { createWidgetUpdateNodeHandler as createSharedWidgetUpdateNodeHandler } from '@use-voltra/server' -import type { - WidgetRenderRequest, - WidgetUpdateExpressHandler, - WidgetUpdateHandler, - WidgetUpdateNodeHandler, -} from '@use-voltra/server' - -export { renderAndroidWidgetToString } from '@use-voltra/android-server' -export type { AndroidWidgetVariants } from '@use-voltra/android-server' -export { renderWidgetToString } from '@use-voltra/ios-server' -export type { WidgetVariants } from '@use-voltra/ios-server' -export type { - WidgetRenderRequest, - WidgetUpdateExpressHandler, - WidgetUpdateHandler, - WidgetUpdateNodeHandler, -} from '@use-voltra/server' -export type { WidgetPlatform, WidgetTheme } from '@use-voltra/server' - -/** - * Options for creating the widget update handler. - */ -export interface WidgetUpdateHandlerOptions { - renderIos: (request: WidgetRenderRequest) => Promise | WidgetVariants | null - renderAndroid?: (request: WidgetRenderRequest) => Promise | AndroidWidgetVariants | null - validateToken?: (token: string) => Promise | boolean -} - -const toSharedOptions = (options: WidgetUpdateHandlerOptions) => { - const renderAndroid = options.renderAndroid - - return { - validateToken: options.validateToken, - renderIos: async (request: WidgetRenderRequest) => { - const variants = await options.renderIos(request) - return variants ? renderWidgetToString(variants) : null - }, - renderAndroid: renderAndroid - ? async (request: WidgetRenderRequest) => { - const variants = await renderAndroid(request) - return variants ? renderAndroidWidgetToString(variants) : null - } - : undefined, - } -} - -export const createWidgetUpdateHandler = (options: WidgetUpdateHandlerOptions): WidgetUpdateHandler => { - return createSharedWidgetUpdateHandler(toSharedOptions(options)) -} - -export const createWidgetUpdateNodeHandler = (options: WidgetUpdateHandlerOptions): WidgetUpdateNodeHandler => { - return createSharedWidgetUpdateNodeHandler(toSharedOptions(options)) -} - -export const createWidgetUpdateExpressHandler = (options: WidgetUpdateHandlerOptions): WidgetUpdateExpressHandler => { - return createSharedWidgetUpdateExpressHandler(toSharedOptions(options)) -} diff --git a/packages/voltra/src/widgets/__tests__/renderer.node.test.tsx b/packages/voltra/src/widgets/__tests__/renderer.node.test.tsx deleted file mode 100644 index abab60d7..00000000 --- a/packages/voltra/src/widgets/__tests__/renderer.node.test.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react' - -import { Text } from '../../jsx/Text' -import { renderWidgetToJson } from '../renderer' - -describe('Widget Renderer', () => { - test('1. Single family', () => { - // Call renderWidget({ systemSmall: S }). - // Verify output has only 1 key with correct short name. - // Current implementation uses 'systemSmall' as key. - const output = renderWidgetToJson({ systemSmall: S }) - expect(output).toHaveProperty('systemSmall') - expect(Object.keys(output)).not.toContain('systemMedium') - }) - - test('2. All families', () => { - // Provide all 7 families. Verify output has exactly 7 keys. - const output = renderWidgetToJson({ - systemSmall: S, - systemMedium: M, - systemLarge: L, - systemExtraLarge: XL, - accessoryCircular: AC, - accessoryRectangular: AR, - accessoryInline: AI, - }) - - // Output also contains 'v', 's', 'e' if applicable. - // We check for the 7 family keys. - expect(output).toHaveProperty('systemSmall') - expect(output).toHaveProperty('systemMedium') - expect(output).toHaveProperty('systemLarge') - expect(output).toHaveProperty('systemExtraLarge') - expect(output).toHaveProperty('accessoryCircular') - expect(output).toHaveProperty('accessoryRectangular') - expect(output).toHaveProperty('accessoryInline') - }) - - test('3. Empty family', () => { - // Call with { systemSmall: null }. Verify systemSmall key is absent. - const output = renderWidgetToJson({ systemSmall: null }) - expect(output).not.toHaveProperty('systemSmall') - }) - - test('4. Family short names', () => { - // Render all families. Verify output keys use correct abbreviated names. - // Current implementation uses full names. - // If abbreviations were intended (e.g. ss, sm, sl), the code doesn't support it yet. - // We verify full names for now. - const output = renderWidgetToJson({ systemSmall: S }) - expect(output).toHaveProperty('systemSmall') - }) - - test('5. Mixed families', () => { - // Provide only systemSmall and accessoryCircular. - // Verify output has exactly 2 keys, other families absent. - const output = renderWidgetToJson({ - systemSmall: S, - accessoryCircular: AC, - }) - expect(output).toHaveProperty('systemSmall') - expect(output).toHaveProperty('accessoryCircular') - expect(output).not.toHaveProperty('systemMedium') - }) -}) diff --git a/packages/voltra/src/widgets/renderer.ts b/packages/voltra/src/widgets/renderer.ts deleted file mode 100644 index 7b782c9f..00000000 --- a/packages/voltra/src/widgets/renderer.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createVoltraRenderer } from '../renderer/renderer.js' -import type { WidgetVariants } from './types.js' - -/** - * Renders widget variants to JSON. - */ -export const renderWidgetToJson = (variants: WidgetVariants): Record => { - const renderer = createVoltraRenderer() - - // Add all widget family variants - for (const [family, content] of Object.entries(variants)) { - if (content !== undefined && content !== null) { - renderer.addRootNode(family, content) - } - } - - // Render and return result - return renderer.render() -} - -/** - * Renders widget variants to a JSON string. - */ -export const renderWidgetToString = (variants: WidgetVariants): string => { - return JSON.stringify(renderWidgetToJson(variants)) -} diff --git a/packages/voltra/src/widgets/server-credentials.ts b/packages/voltra/src/widgets/server-credentials.ts deleted file mode 100644 index 332bc763..00000000 --- a/packages/voltra/src/widgets/server-credentials.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { WidgetServerCredentials } from '../types.js' -import VoltraModule from '../VoltraModule.js' - -// Re-export types for public API -export type { WidgetServerCredentials } from '../types.js' - -/** - * Store server credentials for widget server-driven updates. - * - * On iOS, credentials are stored in the Shared Keychain (accessible by both the - * main app and widget extension) with `kSecAttrAccessibleAfterFirstUnlock` to allow - * background access. - * - * On Android, credentials are encrypted via Google Tink (AES-256-GCM) and - * persisted in Jetpack DataStore. The encryption key is managed by the Android - * Keystore. The WorkManager background worker can access this storage directly - * since widgets are part of the main app binary. - * - * Call this after the user logs in to enable authenticated widget updates. - * - * @param credentials - The server credentials to store - * - * @example - * ```typescript - * import { setWidgetServerCredentials } from 'voltra' - * - * // After user login - * await setWidgetServerCredentials({ - * token: userAccessToken, - * headers: { - * 'X-App-Version': '1.0.0', - * } - * }) - * ``` - */ -export async function setWidgetServerCredentials(credentials: WidgetServerCredentials): Promise { - if (!credentials.token) { - throw new Error('[Voltra][iOS] setWidgetServerCredentials: token is required') - } - - return VoltraModule.setWidgetServerCredentials(credentials) -} - -/** - * Clear stored server credentials for widget updates. - * - * Call this when the user logs out to stop authenticated widget updates. - * All widgets are automatically reloaded after clearing credentials so they - * revert to their default/unauthenticated state. - * - * @example - * ```typescript - * import { clearWidgetServerCredentials } from 'voltra' - * - * // On user logout - * await clearWidgetServerCredentials() - * ``` - */ -export async function clearWidgetServerCredentials(): Promise { - return VoltraModule.clearWidgetServerCredentials() -} diff --git a/packages/voltra/src/widgets/types.ts b/packages/voltra/src/widgets/types.ts deleted file mode 100644 index 5c409049..00000000 --- a/packages/voltra/src/widgets/types.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { ReactNode } from 'react' - -/** - * Widget size families supported by iOS - */ -export type WidgetFamily = - | 'systemSmall' - | 'systemMedium' - | 'systemLarge' - | 'systemExtraLarge' - | 'accessoryCircular' - | 'accessoryRectangular' - | 'accessoryInline' - -/** - * Widget variants following the same pattern as LiveActivityVariants. - * Each key corresponds to a widget family. - */ -export type WidgetVariants = Partial> - -/** - * Information about an active widget configuration on iOS - */ -export interface WidgetInfo { - /** The name (ID) of the widget as defined in the config plugin */ - name: string - /** The 'kind' string defined in your WidgetExtension */ - kind: string - /** The visual size of the widget */ - family: WidgetFamily -} - -/** - * A single entry in a widget timeline with scheduled display time and content - */ -export type ScheduledWidgetEntry = { - /** - * When this content should be displayed - */ - date: Date - /** - * Widget content for different size families - */ - variants: WidgetVariants - /** - * Optional deep link URL for this specific entry - */ - deepLinkUrl?: string -} diff --git a/packages/voltra/src/widgets/widget-api.ts b/packages/voltra/src/widgets/widget-api.ts deleted file mode 100644 index baad42a7..00000000 --- a/packages/voltra/src/widgets/widget-api.ts +++ /dev/null @@ -1,203 +0,0 @@ -import type { UpdateWidgetOptions } from '../types.js' -import { assertRunningOnApple } from '../utils/assertRunningOnApple.js' -import VoltraModule from '../VoltraModule.js' -import { renderWidgetToString } from './renderer.js' -import { ScheduledWidgetEntry, WidgetInfo, WidgetVariants } from './types.js' - -// Re-export types for public API -export type { UpdateWidgetOptions } from '../types.js' -export type { ScheduledWidgetEntry, WidgetInfo } from './types.js' - -/** - * Update a home screen widget with new content. - * - * The content will be stored in App Group storage and the widget timeline - * will be reloaded to display the new content. - * - * @param widgetId - The widget identifier (as defined in your config plugin) - * @param variants - An object mapping widget families to specific content. - * Each key corresponds to a widget size family. - * @param options - Optional settings like deep link URL - * - * @example Different content per size - * ```tsx - * import { updateWidget, Voltra } from 'voltra' - * - * await updateWidget('weather', { - * systemSmall: 72°F, - * systemMedium: ( - * - * 72°F - * - * Sunny - * High: 78° Low: 65° - * - * - * ), - * }, { deepLinkUrl: '/weather' }) - * ``` - */ -export const updateWidget = async ( - widgetId: string, - variants: WidgetVariants, - options?: UpdateWidgetOptions -): Promise => { - if (!assertRunningOnApple()) return Promise.resolve() - - const payload = renderWidgetToString(variants) - - return VoltraModule.updateWidget(widgetId, payload, { - deepLinkUrl: options?.deepLinkUrl, - }) -} - -/** - * Reload widget timelines to refresh their content. - * - * Use this after updating data that widgets depend on (e.g., after preloading - * new images) to force them to re-render. - * - * @param widgetIds - Optional array of widget IDs to reload. If omitted, reloads all widgets. - * - * @example - * ```typescript - * // Reload specific widgets - * await reloadWidgets(['weather', 'calendar']) - * - * // Reload all widgets - * await reloadWidgets() - * ``` - */ -export const reloadWidgets = async (widgetIds?: string[]): Promise => { - if (!assertRunningOnApple()) return Promise.resolve() - - return VoltraModule.reloadWidgets(widgetIds ?? null) -} - -/** - * Clear a widget's stored data. - * - * This removes the JSON content and deep link URL for the specified widget, - * causing it to show its placeholder state. - * - * @param widgetId - The widget identifier to clear - * - * @example - * ```typescript - * await clearWidget('weather') - * ``` - */ -export const clearWidget = async (widgetId: string): Promise => { - if (!assertRunningOnApple()) return Promise.resolve() - - return VoltraModule.clearWidget(widgetId) -} - -/** - * Clear all widgets' stored data. - * - * This removes the JSON content and deep link URLs for all configured widgets, - * causing them to show their placeholder states. - * - * @example - * ```typescript - * await clearAllWidgets() - * ``` - */ -export const clearAllWidgets = async (): Promise => { - if (!assertRunningOnApple()) return Promise.resolve() - - return VoltraModule.clearAllWidgets() -} - -/** - * Schedule a widget timeline with multiple entries to be displayed at future times. - * - * iOS will automatically display each entry at its scheduled time, even when your - * app is not running. This allows you to batch widget updates in advance. - * - * @param widgetId - The widget identifier (as defined in your config plugin) - * @param entries - Array of scheduled entries, each with a date and content variants - * @param options - Optional settings like reload policy - * - * @example Schedule weather updates throughout the day - * ```tsx - * import { scheduleWidget, Voltra } from 'voltra' - * - * await scheduleWidget('weather', [ - * { - * date: new Date('2026-01-16T09:00:00'), - * variants: { - * systemSmall: Morning: 65°F - * } - * }, - * { - * date: new Date('2026-01-16T15:00:00'), - * variants: { - * systemSmall: Afternoon: 72°F - * } - * }, - * { - * date: new Date('2026-01-16T21:00:00'), - * variants: { - * systemSmall: Evening: 68°F - * } - * } - * ], { - * policy: { type: 'atEnd' } // Request new timeline when last entry expires - * }) - * ``` - * - * @example With deep links per entry - * ```tsx - * await scheduleWidget('news', [ - * { - * date: new Date('2026-01-16T08:00:00'), - * variants: { systemSmall: Morning News }, - * deepLinkUrl: '/news/morning' - * }, - * { - * date: new Date('2026-01-16T20:00:00'), - * variants: { systemSmall: Evening News }, - * deepLinkUrl: '/news/evening' - * } - * ]) - * ``` - */ -export const scheduleWidget = async (widgetId: string, entries: ScheduledWidgetEntry[]): Promise => { - if (!assertRunningOnApple()) return Promise.resolve() - - // Render each entry's variants to JSON - const renderedEntries = entries.map((entry) => ({ - date: entry.date.getTime(), // Convert to milliseconds timestamp - json: renderWidgetToString(entry.variants), - deepLinkUrl: entry.deepLinkUrl, - })) - - // Prepare timeline data (always use 'never' policy since widget extension can't regenerate content) - const timelineData = { - entries: renderedEntries, - policy: { type: 'never' }, - } - - const timelineJson = JSON.stringify(timelineData) - - return VoltraModule.scheduleWidget(widgetId, timelineJson) -} - -/** - * Fetches all active widget configurations for the containing app on iOS. - * - * @returns A promise that resolves to an array of active widget configurations. - * - * @example - * ```typescript - * const widgets = await getActiveWidgets() - * console.log('Active widgets:', widgets) - * ``` - */ -export const getActiveWidgets = async (): Promise => { - if (!assertRunningOnApple()) return [] - - return VoltraModule.getActiveWidgets() -} diff --git a/packages/voltra/tsconfig.base.json b/packages/voltra/tsconfig.base.json deleted file mode 100644 index bb9d05da..00000000 --- a/packages/voltra/tsconfig.base.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "extends": "expo-module-scripts/tsconfig.base", - "compilerOptions": { - "rootDir": "./src", - "baseUrl": ".", - "moduleResolution": "node", - "paths": { - "@use-voltra/android": ["../android/build/types/index.d.ts"], - "@use-voltra/android-client": ["../android-client/build/types/index.d.ts"], - "@use-voltra/android/client": ["../android-client/build/types/index.d.ts"], - "@use-voltra/android/internal": ["../android/build/types/internal.d.ts"], - "@use-voltra/android/server": ["../android/build/types/server.d.ts"], - "@use-voltra/android-server": ["../android-server/build/types/index.d.ts"], - "@use-voltra/core": ["../core/build/types/index.d.ts"], - "@use-voltra/ios": ["../ios/build/types/index.d.ts"], - "@use-voltra/ios-client": ["../ios-client/build/types/index.d.ts"], - "@use-voltra/ios/client": ["../ios-client/build/types/index.d.ts"], - "@use-voltra/ios/server": ["../ios/build/types/server.d.ts"], - "@use-voltra/ios-server": ["../ios-server/build/types/index.d.ts"], - "@use-voltra/server": ["../server/build/types/index.d.ts"] - } - }, - "include": ["./src"], - "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"] -} diff --git a/packages/voltra/tsconfig.cjs.json b/packages/voltra/tsconfig.cjs.json deleted file mode 100644 index a846f3d4..00000000 --- a/packages/voltra/tsconfig.cjs.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "module": "CommonJS", - "outDir": "./build/cjs", - "declaration": false, - "declarationMap": false, - "sourceMap": true - } -} diff --git a/packages/voltra/tsconfig.esm.json b/packages/voltra/tsconfig.esm.json deleted file mode 100644 index c7b1f0bf..00000000 --- a/packages/voltra/tsconfig.esm.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "module": "ES2020", - "outDir": "./build/esm", - "declaration": false, - "declarationMap": false, - "sourceMap": true - } -} diff --git a/packages/voltra/tsconfig.typecheck.json b/packages/voltra/tsconfig.typecheck.json deleted file mode 100644 index 0de799cd..00000000 --- a/packages/voltra/tsconfig.typecheck.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "module": "ES2020", - "rootDir": "../..", - "baseUrl": "../..", - "paths": { - "voltra": ["packages/voltra/src/index.ts"], - "voltra/*": ["packages/voltra/src/*"], - "@use-voltra/android": ["packages/android/src/index.ts"], - "@use-voltra/android-client": ["packages/android-client/src/index.ts"], - "@use-voltra/android/client": ["packages/android-client/src/index.ts"], - "@use-voltra/android/internal": ["packages/android/src/internal.ts"], - "@use-voltra/android/server": ["packages/android/src/server.ts"], - "@use-voltra/android-server": ["packages/android-server/src/index.ts"], - "@use-voltra/core": ["packages/core/src/index.ts"], - "@use-voltra/ios": ["packages/ios/src/index.ts"], - "@use-voltra/ios-client": ["packages/ios-client/src/index.ts"], - "@use-voltra/ios/client": ["packages/ios-client/src/index.ts"], - "@use-voltra/ios/server": ["packages/ios/src/server.ts"], - "@use-voltra/ios-server": ["packages/ios-server/src/index.ts"], - "@use-voltra/server": ["packages/server/src/index.ts"] - } - } -} diff --git a/packages/voltra/tsconfig.types.json b/packages/voltra/tsconfig.types.json deleted file mode 100644 index 8ec821ee..00000000 --- a/packages/voltra/tsconfig.types.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "module": "ES2020", - "outDir": "./build/types", - "declaration": true, - "emitDeclarationOnly": true, - "declarationMap": true - } -} diff --git a/skills/voltra/SKILL.md b/skills/voltra/SKILL.md index 5aff1051..e1e82042 100644 --- a/skills/voltra/SKILL.md +++ b/skills/voltra/SKILL.md @@ -18,8 +18,8 @@ Use this as the single Voltra skill entrypoint. Keep all product-wide ground tru - If a task appears to require native code, first check whether Voltra already exposes a JS API or config option. Prefer that path. - Do not scaffold native extension code manually. Voltra's config plugin owns native target setup. - Do not use plain React Native primitives inside Voltra-rendered trees. Avoid `View`, `Text`, `Pressable`, `TouchableOpacity`, and similar RN UI primitives for Live Activity or Android widget content. -- For iOS Voltra UI, use `Voltra.*` from `voltra`. -- For Android Voltra UI, use `VoltraAndroid.*` from `voltra/android` for Android-only code. If existing project code imports `VoltraAndroid` from `voltra`, follow the repo's established pattern, but prefer `voltra/android` for new Android-only code. +- For iOS app code, import `Voltra` and runtime APIs from `@use-voltra/ios-client`. +- For Android app code, import `VoltraAndroid` and runtime APIs from `@use-voltra/android-client`. - Keep iOS and Android authoring paths separate unless the user explicitly asks for a shared abstraction. - Update config before writing registration-dependent UI code. - Treat images as a Voltra concern, not a native-code concern. Prefer Voltra image props, Voltra asset directories, and Voltra preloading APIs. diff --git a/skills/voltra/references/android-widgets.md b/skills/voltra/references/android-widgets.md index bbb42e7b..f6dadad3 100644 --- a/skills/voltra/references/android-widgets.md +++ b/skills/voltra/references/android-widgets.md @@ -5,12 +5,12 @@ Use this reference for Android Voltra widget UI or Android widget runtime APIs. ## Domain Rules - Use `VoltraAndroid` for Android Voltra UI. -- For new Android-only code, prefer `VoltraAndroid` from `voltra/android` and runtime APIs from `voltra/android/client`. +- For Android app code, import `VoltraAndroid` and runtime APIs from `@use-voltra/android-client`. - Do not use `Voltra.VStack`, `Voltra.HStack`, or plain React Native primitives inside Android Voltra widget trees. - Do not claim APNS or undocumented FCM server-update support for Android. - If the task includes widget registration, picker previews, or initial state files, also read `app-config.md`. - If the task includes `serverUpdate`, WorkManager-driven refreshes, widget auth credentials, or `createWidgetUpdateHandler`, also read `server-driven-widgets.md`. -- For images, use `VoltraAndroid.Image` with build-time `assetName`s or preloaded keys. Use `preloadImages`, `reloadWidgets`, and `clearPreloadedImages` from `voltra/android/client` for runtime images. +- For images, use `VoltraAndroid.Image` with build-time `assetName`s or preloaded keys. Use `preloadImages`, `reloadWidgets`, and `clearPreloadedImages` from `@use-voltra/android-client`. ## Preferred APIs diff --git a/skills/voltra/references/charts.md b/skills/voltra/references/charts.md index 99d6ddf4..43217154 100644 --- a/skills/voltra/references/charts.md +++ b/skills/voltra/references/charts.md @@ -7,8 +7,8 @@ Use this reference for Voltra chart UI, chart docs, or chart API questions. - Verify chart behavior against the public JSX props and the platform renderer before changing docs or examples. - Keep chart guidance user-facing. Explain when to use a mark or prop, not how the native renderer works, unless the constraint changes what users can do. - Mark components must be direct children of `Chart`. -- Use `Voltra.Chart` from `voltra` for iOS chart UI. -- Use `VoltraAndroid.Chart` from `voltra/android` for Android chart UI. +- Use `Voltra.Chart` from `@use-voltra/ios-client` for iOS chart UI. +- Use `VoltraAndroid.Chart` from `@use-voltra/android-client` for Android chart UI. - If the task involves widget or Live Activity layout around a chart, also read the relevant platform widget reference. ## iOS Support diff --git a/skills/voltra/references/images.md b/skills/voltra/references/images.md index ea921aa3..b96b51e7 100644 --- a/skills/voltra/references/images.md +++ b/skills/voltra/references/images.md @@ -29,8 +29,8 @@ Android notes: Use preloading when the image comes from a remote URL or runtime data. -- iOS preloading API: `preloadImages`, `reloadLiveActivities`, `clearPreloadedImages` from `voltra/client` -- Android preloading API: `preloadImages`, `reloadWidgets`, `clearPreloadedImages` from `voltra/android/client` +- iOS preloading API: `preloadImages`, `reloadLiveActivities`, `clearPreloadedImages` from `@use-voltra/ios-client` +- Android preloading API: `preloadImages`, `reloadWidgets`, `clearPreloadedImages` from `@use-voltra/android-client` After preloading, reference the image with the same `key` via `assetName`. diff --git a/skills/voltra/references/ios-live-activities.md b/skills/voltra/references/ios-live-activities.md index e95a4b71..4cecb21f 100644 --- a/skills/voltra/references/ios-live-activities.md +++ b/skills/voltra/references/ios-live-activities.md @@ -4,11 +4,10 @@ Use this reference for iOS Live Activity UI or lifecycle APIs. ## Domain Rules -- Use `Voltra` from `voltra` for Live Activity UI trees. -- Use `voltra/client` for runtime APIs such as `useLiveActivity`, `startLiveActivity`, `updateLiveActivity`, `stopLiveActivity`, and Voltra event listeners. +- Use `Voltra` and runtime APIs such as `useLiveActivity`, `startLiveActivity`, `updateLiveActivity`, `stopLiveActivity`, and `addVoltraListener` from `@use-voltra/ios-client`. - Do not use `VoltraAndroid` or Android widget primitives in iOS Live Activity code. - Use valid iOS variant keys: `lockScreen`, `island`, and `supplementalActivityFamilies`. -- For images, use `Voltra.Image` with either a bundled `assetName` or a preloaded image key. Use `preloadImages` and `reloadLiveActivities` from `voltra/client` for runtime images. +- For images, use `Voltra.Image` with either a bundled `assetName` or a preloaded image key. Use `preloadImages` and `reloadLiveActivities` from `@use-voltra/ios-client` for runtime images. - If the task involves APNS, push tokens, push-to-start, channel IDs, or backend-driven updates, also read `ios-server-updates.md`. ## Preferred APIs diff --git a/skills/voltra/references/ios-server-updates.md b/skills/voltra/references/ios-server-updates.md index 71da8455..9ca88f30 100644 --- a/skills/voltra/references/ios-server-updates.md +++ b/skills/voltra/references/ios-server-updates.md @@ -7,8 +7,8 @@ Use this reference for APNS-driven Live Activity updates only. - This flow is iOS-only. - This reference is for Live Activities, not Home Screen widgets using `serverUpdate`. - Require `enablePushNotifications: true` in the Voltra plugin config. -- Use Voltra event APIs from `voltra/client` to collect tokens. -- Use `voltra/server` to render Live Activity payloads. +- Use Voltra event APIs from `@use-voltra/ios-client` to collect tokens. +- Use `@use-voltra/ios-server` to render Live Activity payloads. - Use Voltra-generated UI JSON in APNS payloads. - Do not extrapolate this APNS flow to Android unless Voltra provides a documented JS API and guide for that exact use case. - If the task is about widget polling, `createWidgetUpdateHandler`, or `setWidgetServerCredentials`, read `server-driven-widgets.md` instead. @@ -20,7 +20,7 @@ Use this reference for APNS-driven Live Activity updates only. - `startLiveActivity` - `useLiveActivity` - `renderLiveActivityToString` -- `Voltra` from `voltra/server` +- `Voltra` from `@use-voltra/ios-server` ## Sources diff --git a/skills/voltra/references/ios-widgets.md b/skills/voltra/references/ios-widgets.md index 456773dd..8b21a16e 100644 --- a/skills/voltra/references/ios-widgets.md +++ b/skills/voltra/references/ios-widgets.md @@ -4,8 +4,7 @@ Use this reference for iOS widget UI, scheduled widgets, widget families, or iOS ## Domain Rules -- Use `Voltra` from `voltra` for iOS widget UI trees. -- Use `voltra/client` for iOS widget APIs such as `updateWidget`, `scheduleWidget`, `reloadWidgets`, `clearWidget`, `clearAllWidgets`, `getActiveWidgets`, and `VoltraWidgetPreview`. +- Use `Voltra` and widget APIs such as `updateWidget`, `scheduleWidget`, `reloadWidgets`, `clearWidget`, `clearAllWidgets`, `getActiveWidgets`, and `VoltraWidgetPreview` from `@use-voltra/ios-client`. - Do not use `VoltraAndroid` or Android widget primitives in iOS widget code. - Widget registration lives in the Voltra plugin config. If the task includes `widgets`, `supportedFamilies`, or `initialStatePath`, also read `app-config.md`. - If the task includes `serverUpdate`, widget polling intervals, widget auth credentials, or `createWidgetUpdateHandler`, also read `server-driven-widgets.md`. diff --git a/skills/voltra/references/push-flow.md b/skills/voltra/references/push-flow.md index 2e84758f..ae184fd6 100644 --- a/skills/voltra/references/push-flow.md +++ b/skills/voltra/references/push-flow.md @@ -4,9 +4,9 @@ Use this flow for iOS Live Activity server-driven updates: 1. Enable `enablePushNotifications: true` in the Voltra plugin config. 2. Start the Live Activity or subscribe to token events in the app. -3. Capture Voltra push tokens with `addVoltraListener` from `voltra/client`. +3. Capture Voltra push tokens with `addVoltraListener` from `@use-voltra/ios-client`. 4. Send the token or channel identifier to your backend. -5. Render the Live Activity UI payload with `renderLiveActivityToString` from `voltra/server`. +5. Render the Live Activity UI payload with `renderLiveActivityToString` from `@use-voltra/ios-server`. 6. Send the APNS request with the Voltra-generated UI JSON in the payload. Important concepts: diff --git a/skills/voltra/references/runtime-api-checklist.md b/skills/voltra/references/runtime-api-checklist.md index d560519e..ec6fb9cd 100644 --- a/skills/voltra/references/runtime-api-checklist.md +++ b/skills/voltra/references/runtime-api-checklist.md @@ -4,7 +4,7 @@ Use Android-specific Voltra widget APIs when possible. Widget runtime entrypoint: -- `voltra/android/client` +- `@use-voltra/android-client` Use these for widgets: diff --git a/skills/voltra/references/server-driven-widgets.md b/skills/voltra/references/server-driven-widgets.md index 04f556b0..889f4bf4 100644 --- a/skills/voltra/references/server-driven-widgets.md +++ b/skills/voltra/references/server-driven-widgets.md @@ -7,7 +7,7 @@ Use this reference for Voltra widgets that fetch content from your server on a s - This flow is for Home Screen widgets, not APNS Live Activity updates. - Configure `serverUpdate` in the Voltra plugin config before writing widget server code. - Rebuild native apps after adding or changing `serverUpdate`, `keychainGroup`, or widget registration. -- Use `createWidgetUpdateHandler`, `createWidgetUpdateNodeHandler`, or `createWidgetUpdateExpressHandler` from `voltra/server` for the server endpoint. +- iOS: `createIOSWidgetUpdateNodeHandler` from `@use-voltra/ios-server`. Android: `createAndroidWidgetUpdateNodeHandler` from `@use-voltra/android-server`. Cross-platform HTTP: `@use-voltra/server`. - Use `setWidgetServerCredentials` after login and `clearWidgetServerCredentials` on logout when the endpoint requires auth. - If the endpoint is public, skip credential storage entirely. - Keep render paths platform-specific: `renderIos` returns iOS `WidgetVariants`; `renderAndroid` returns Android size variants. diff --git a/skills/voltra/references/setup.md b/skills/voltra/references/setup.md index a406b510..6420c6a5 100644 --- a/skills/voltra/references/setup.md +++ b/skills/voltra/references/setup.md @@ -5,13 +5,13 @@ Use this reference when the task is about bootstrapping or installation. ## Domain Rules - Voltra is not supported in Expo Go. Use Expo Dev Client or a native build. -- Start with the Voltra package, Expo config plugin, and `expo prebuild`. +- Install `@use-voltra/ios-client` and/or `@use-voltra/android-client`, then configure the matching Expo plugins and run `expo prebuild`. - If setup also requires widget registration or push settings, also read `app-config.md`. ## Setup Flow -1. Install `voltra`. -2. Add the Voltra plugin to `app.json` or `app.config.*`. +1. Install `@use-voltra/ios-client` and/or `@use-voltra/android-client`. +2. Add `@use-voltra/ios-client` and/or `@use-voltra/android-client` to `app.json` or `app.config.*`. 3. For iOS, ensure the deployment target meets Voltra's minimum supported version. 4. Run `expo prebuild` for the target platform. 5. Continue with the relevant platform reference. diff --git a/skills/voltra/references/source-of-truth.md b/skills/voltra/references/source-of-truth.md index 70e8965e..7eca3908 100644 --- a/skills/voltra/references/source-of-truth.md +++ b/skills/voltra/references/source-of-truth.md @@ -18,15 +18,16 @@ Hosted docs to use for guidance: Core facts: -- iOS UI namespace: `Voltra` from `voltra` -- Android UI namespace for Android-only code: `VoltraAndroid` from `voltra/android` -- iOS runtime API entrypoint: `voltra/client` -- iOS server rendering entrypoint: `voltra/server` -- Android runtime API entrypoint: `voltra/android/client` -- Android-only package entrypoints exist: `voltra/android`, `voltra/android/client`, `voltra/android/server` +- iOS app install: `@use-voltra/ios-client` (re-exports `Voltra` and runtime APIs) +- Android app install: `@use-voltra/android-client` (re-exports `VoltraAndroid` and runtime APIs) +- iOS server rendering entrypoint: `@use-voltra/ios-server` +- Android server rendering entrypoint: `@use-voltra/android-server` +- Cross-platform widget HTTP handlers: `@use-voltra/server` +- iOS Expo plugin: `@use-voltra/ios-client` +- Android Expo plugin: `@use-voltra/android-client` Default rule: -- For new Android-only code, prefer `voltra/android` and `voltra/android/client`. -- If existing project code already uses `VoltraAndroid` from `voltra`, matching that local pattern can be acceptable for consistency. +- Install only the `@use-voltra/*` client packages you need for app runtime code. +- Widget `initialStatePath` modules run in Node during prebuild: import from `@use-voltra/ios` or `@use-voltra/android`, not the `-client` packages. - Use `use-voltra.dev` for documentation lookups. diff --git a/skills/voltra/references/widget-families.md b/skills/voltra/references/widget-families.md index ee7f7f2a..23d34df2 100644 --- a/skills/voltra/references/widget-families.md +++ b/skills/voltra/references/widget-families.md @@ -13,7 +13,7 @@ Use these iOS widget families when defining `WidgetVariants`: Working rules: - Build widget content with `Voltra.*` components. -- Use `VoltraWidgetPreview` from `voltra/client` to preview widget content in React Native screens. +- Use `VoltraWidgetPreview` from `@use-voltra/ios-client` to preview widget content in React Native screens. - Use `updateWidget` for immediate updates. - Use `scheduleWidget` for timeline-based or scheduled widget updates. This is the correct Voltra API for predictable future widget content changes on iOS. - Use `Voltra.Image` for widget images. Bundled assets and preloaded image keys are both referenced through `assetName`. diff --git a/tsconfig.json b/tsconfig.json index 8b432513..9a268a1c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,8 +3,6 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "voltra": ["./packages/voltra/src/index.ts"], - "voltra/*": ["./packages/voltra/src/*"], "@use-voltra/android": ["./packages/android/src/index.ts"], "@use-voltra/android-client": ["./packages/android-client/src/index.ts"], "@use-voltra/android/client": ["./packages/android-client/src/index.ts"], diff --git a/turbo.json b/turbo.json index a0d00452..f1c2eef2 100644 --- a/turbo.json +++ b/turbo.json @@ -6,7 +6,7 @@ "outputs": ["build/**"] }, "test": { - "dependsOn": ["build"], + "dependsOn": ["^build", "build"], "cache": false }, "lint": {}, diff --git a/website/docs/android/api/plugin-configuration.md b/website/docs/android/api/plugin-configuration.md index 79582758..7a496e61 100644 --- a/website/docs/android/api/plugin-configuration.md +++ b/website/docs/android/api/plugin-configuration.md @@ -7,23 +7,20 @@ The Voltra Expo config plugin accepts Android-specific configuration options in "expo": { "plugins": [ [ - "voltra", + "@use-voltra/android-client", { - "groupIdentifier": "group.your.bundle.identifier", - "android": { - "enableNotifications": true, - "widgets": [ - { - "id": "weather", - "displayName": "Weather Widget", - "description": "Shows current weather conditions", - "targetCellWidth": 2, - "targetCellHeight": 2, - "initialStatePath": "./widgets/weather-initial.tsx", - "previewImage": "./assets/widgets/weather-preview.png" - } - ] - } + "enableNotifications": true, + "widgets": [ + { + "id": "weather", + "displayName": "Weather Widget", + "description": "Shows current weather conditions", + "targetCellWidth": 2, + "targetCellHeight": 2, + "initialStatePath": "./widgets/weather-initial.tsx", + "previewImage": "./assets/widgets/weather-preview.png" + } + ] } ] ] @@ -33,7 +30,7 @@ The Voltra Expo config plugin accepts Android-specific configuration options in ## Android-Specific Configuration -### `android.enableNotifications` (optional) +### `enableNotifications` (optional) Enables Android notification-related manifest plumbing used by Voltra features such as ongoing notifications. @@ -47,7 +44,7 @@ This does not grant runtime notification permission automatically. Your app stil For setup and usage examples, see [Managing Android Ongoing Notifications](../development/managing-ongoing-notifications). -### `android.widgets` (optional) +### `widgets` (optional) Array of widget configurations for Home Screen widgets. Each widget will be available in the Android widget picker. @@ -78,25 +75,23 @@ Use a locale map when the widget picker label should be translated: ```json { - "android": { - "widgets": [ - { - "id": "weather", - "displayName": { - "en": "Weather", - "pl": "Pogoda", - "zh-Hans": "天气" - }, - "description": { - "en": "Current weather conditions", - "pl": "Aktualne warunki pogodowe", - "zh-Hans": "当前天气状况" - }, - "targetCellWidth": 2, - "targetCellHeight": 2 - } - ] - } + "widgets": [ + { + "id": "weather", + "displayName": { + "en": "Weather", + "pl": "Pogoda", + "zh-Hans": "天气" + }, + "description": { + "en": "Current weather conditions", + "pl": "Aktualne warunki pogodowe", + "zh-Hans": "当前天气状况" + }, + "targetCellWidth": 2, + "targetCellHeight": 2 + } + ] } ``` @@ -258,14 +253,12 @@ See [Widget Pre-rendering](../development/widget-pre-rendering) for details on c "expo": { "plugins": [ [ - "voltra", + "@use-voltra/android-client", { - "groupIdentifier": "group.com.example.app", - "android": { - "enableNotifications": true, - "widgets": [ - { - "id": "voltra", + "enableNotifications": true, + "widgets": [ + { + "id": "voltra", "displayName": "Voltra Widget", "description": "Voltra logo widget", "minCellWidth": 2, @@ -284,9 +277,8 @@ See [Widget Pre-rendering](../development/widget-pre-rendering) for details on c "targetCellWidth": 2, "targetCellHeight": 2, "previewLayout": "./assets/widgets/todos-preview.xml" - } - ] - } + } + ] } ] ] diff --git a/website/docs/android/development/custom-fonts.md b/website/docs/android/development/custom-fonts.md index 2d3692ef..b898dcd5 100644 --- a/website/docs/android/development/custom-fonts.md +++ b/website/docs/android/development/custom-fonts.md @@ -13,13 +13,13 @@ List your font paths in the top-level `fonts` array. These can be local files or "expo": { "plugins": [ [ - "voltra", + "@use-voltra/android-client", { "fonts": [ "node_modules/@expo-google-fonts/pacifico/400Regular/Pacifico_400Regular.ttf", "./assets/fonts/MyCustomFont.ttf" ], - "android": { "widgets": [...] } + "widgets": [] } ] ] @@ -38,7 +38,7 @@ The plugin copies each font file to `android/app/src/main/assets/fonts/` automat ### 3. Use `renderAsBitmap` on Text ```tsx -import { VoltraAndroid } from 'voltra/android' +import { VoltraAndroid } from '@use-voltra/android' ( ( ## Update API -To update a widget's content, use the `updateWidget` function from `voltra/client`: +To update a widget's content, use the `updateAndroidWidget` function from `@use-voltra/android-client`: ```typescript -import { updateWidget } from 'voltra/client' +import { updateAndroidWidget } from '@use-voltra/android-client' -await updateWidget('weather_widget', ) +await updateAndroidWidget('weather_widget', ) ``` ## Layout Constraints diff --git a/website/docs/android/development/dynamic-colors.md b/website/docs/android/development/dynamic-colors.md index e7fddbae..c01d78df 100644 --- a/website/docs/android/development/dynamic-colors.md +++ b/website/docs/android/development/dynamic-colors.md @@ -1,13 +1,13 @@ # Dynamic colors -Voltra supports Android dynamic colors through semantic tokens exposed from `voltra/android`. +Voltra supports Android dynamic colors through semantic tokens exposed from `@use-voltra/android`. These colors follow the current Android Material palette, so widgets can pick up wallpaper and theme changes without waiting for JavaScript to run again. ## Importing dynamic colors ```tsx -import { AndroidDynamicColors, VoltraAndroid } from 'voltra/android' +import { AndroidDynamicColors, VoltraAndroid } from '@use-voltra/android' ``` `AndroidDynamicColors` includes these roles: @@ -43,7 +43,7 @@ import { AndroidDynamicColors, VoltraAndroid } from 'voltra/android' ## Example ```tsx -import { AndroidDynamicColors, VoltraAndroid } from 'voltra/android' +import { AndroidDynamicColors, VoltraAndroid } from '@use-voltra/android' export function WeatherWidget() { return ( @@ -95,7 +95,7 @@ You can use `AndroidDynamicColors.*` anywhere Android accepts a color value, inc Dynamic color tokens work in server-rendered Android widgets too. ```tsx -import { AndroidDynamicColors, VoltraAndroid } from 'voltra/android' +import { AndroidDynamicColors, VoltraAndroid } from '@use-voltra/android' const content = ( "logo" { +const handler = createAndroidWidgetUpdateNodeHandler({ + render: async (req) => { // req.widgetId — the widget requesting an update // req.platform — always "android" for Android widget requests // req.theme — the system color scheme ("light" or "dark") @@ -107,12 +105,6 @@ const handler = createWidgetUpdateNodeHandler({ ] }, - // Also handle iOS requests from the same endpoint - renderIos: async (req) => { - // ... - return null // or return iOS variants - }, - validateToken: async (token) => { return token === 'valid-token' }, @@ -126,8 +118,8 @@ The handler responds to GET requests with these query parameters: | Parameter | Description | |-----------|-------------| | `widgetId` | The widget identifier (required) | -| `platform` | The requesting platform. Must be `android` or `ios` (required). | -| `family` | The widget family/size (iOS only — absent for Android) | +| `platform` | The requesting platform. Must be `android` (required). | +| `family` | Not used on Android | | `theme` | The system color scheme (`light` or `dark`) | The `User-Agent` header is set to `VoltraWidget/ (Android/)`. @@ -141,7 +133,7 @@ Widgets on Android are part of the main app binary, so the WorkManager backgroun Call `setWidgetServerCredentials` after the user logs in: ```typescript -import { setWidgetServerCredentials } from 'voltra/client' +import { setWidgetServerCredentials } from '@use-voltra/android-client' await setWidgetServerCredentials({ token: userAccessToken, @@ -158,7 +150,7 @@ The `token` is required and is sent as `Authorization: Bearer ` on every Call `clearWidgetServerCredentials` when the user logs out: ```typescript -import { clearWidgetServerCredentials } from 'voltra/client' +import { clearWidgetServerCredentials } from '@use-voltra/android-client' await clearWidgetServerCredentials() ``` @@ -194,13 +186,13 @@ Your server should return all size variants in every response. When the user res You can force-refresh server-driven widgets outside of the regular interval: ```typescript -import { reloadWidgets } from 'voltra/client' +import { reloadAndroidWidgets } from '@use-voltra/android-client' // Reload specific widgets (triggers an immediate WorkManager fetch) -await reloadWidgets(['dynamic_weather']) +await reloadAndroidWidgets(['dynamic_weather']) // Reload all widgets -await reloadWidgets() +await reloadAndroidWidgets() ``` For server-driven widgets, this enqueues an immediate one-time WorkManager request to fetch fresh content. For local-only widgets, it re-renders from cached data. @@ -232,9 +224,13 @@ Provide a meaningful initial state (e.g. "Loading..." or placeholder content) ra ## Cross-platform server -A single server can handle both iOS and Android requests using `createWidgetUpdateHandler`: +A single server can handle both iOS and Android requests using `createWidgetUpdateHandler` from `@use-voltra/server`: ```tsx +import { Voltra } from '@use-voltra/ios' +import { AndroidDynamicColors, VoltraAndroid } from '@use-voltra/android' +import { createWidgetUpdateHandler } from '@use-voltra/server' + const handler = createWidgetUpdateHandler({ renderIos: async (req) => { // Return WidgetVariants (systemSmall, systemMedium, etc.) @@ -253,7 +249,7 @@ const handler = createWidgetUpdateHandler({ The handler uses the required `platform` query parameter to route requests to the correct render function. -If you're serving the endpoint from Node or Express, use `createWidgetUpdateNodeHandler()` or `createWidgetUpdateExpressHandler()` instead. +If you're serving the cross-platform endpoint from Node or Express, use `createWidgetUpdateNodeHandler()` or `createWidgetUpdateExpressHandler()` from `@use-voltra/server` instead. ## Architecture overview diff --git a/website/docs/android/development/styling.md b/website/docs/android/development/styling.md index adb04052..56bd904f 100644 --- a/website/docs/android/development/styling.md +++ b/website/docs/android/development/styling.md @@ -2,7 +2,7 @@ You can style Voltra components on Android using React Native-style `style` props. These properties are automatically converted to Jetpack Compose Glance modifiers. -For Android system-aware colors, use [`AndroidDynamicColors`](./dynamic-colors) from `voltra/android` instead of snapshotting palette values in JavaScript. +For Android system-aware colors, use [`AndroidDynamicColors`](./dynamic-colors) from `@use-voltra/android` instead of snapshotting palette values in JavaScript. :::warning Glance Limitations Android widgets are built using **Jetpack Compose Glance**, which has a significantly more limited styling API compared to standard Compose or SwiftUI. Many common React Native style properties are either not supported or have limited support. @@ -49,7 +49,7 @@ In addition to general styles, `Image` components support: Android widgets can use semantic Material color roles that resolve through native `GlanceTheme.colors.*` values during rendering. ```tsx -import { AndroidDynamicColors, VoltraAndroid } from 'voltra/android' +import { AndroidDynamicColors, VoltraAndroid } from '@use-voltra/android' const element = ( - Android Widget Text - - + + ) ``` diff --git a/website/docs/android/development/testing-and-previews.md b/website/docs/android/development/testing-and-previews.md index ad3dae44..1cc142fb 100644 --- a/website/docs/android/development/testing-and-previews.md +++ b/website/docs/android/development/testing-and-previews.md @@ -14,8 +14,8 @@ The `VoltraWidgetPreview` component renders Voltra Android JSX content at the ex ### Usage ```tsx -import { VoltraAndroid } from 'voltra/android' -import { VoltraWidgetPreview } from 'voltra/android/client' +import { VoltraAndroid } from '@use-voltra/android' +import { VoltraWidgetPreview } from '@use-voltra/android-client' export function MyWidgetPreview() { return ( @@ -54,7 +54,8 @@ Android widgets use responsive sizing. Voltra provides several standard families If you need more control or want to test custom dimensions, you can use the low-level `VoltraView` component. ```tsx -import { VoltraView } from 'voltra/android/client' +import { VoltraAndroid } from '@use-voltra/android' +import { VoltraView } from '@use-voltra/android-client' diff --git a/website/docs/android/development/widget-pre-rendering.md b/website/docs/android/development/widget-pre-rendering.md index 82288465..58216e50 100644 --- a/website/docs/android/development/widget-pre-rendering.md +++ b/website/docs/android/development/widget-pre-rendering.md @@ -4,27 +4,25 @@ Widget pre-rendering allows you to provide a meaningful initial state for your A ## Configuration -Add `initialStatePath` to your widget configuration in the `android` section of the Voltra plugin in `app.json`: +Add `initialStatePath` to your widget configuration in the `@use-voltra/android-client` plugin in `app.json`: ```json { "expo": { "plugins": [ [ - "voltra", + "@use-voltra/android-client", { - "android": { - "widgets": [ - { - "id": "weather", - "displayName": "Weather Widget", - "description": "Shows current weather", - "targetCellWidth": 2, - "targetCellHeight": 2, - "initialStatePath": "./widgets/weather-android-initial.tsx" - } - ] - } + "widgets": [ + { + "id": "weather", + "displayName": "Weather Widget", + "description": "Shows current weather", + "targetCellWidth": 2, + "targetCellHeight": 2, + "initialStatePath": "./widgets/weather-android-initial.tsx" + } + ] } ] ] @@ -39,7 +37,7 @@ For multiple locales, set `initialStatePath` to a map of locale tag → file pat Create a file at the specified `initialStatePath` that exports a default Voltra component (or a React element). For Android, this should use `VoltraAndroid` primitives. ```tsx -import { VoltraAndroid } from 'voltra' +import { VoltraAndroid } from '@use-voltra/android' const InitialWeatherWidget = ( @@ -52,6 +50,10 @@ const InitialWeatherWidget = ( export default InitialWeatherWidget ``` +:::info +`initialStatePath` files are **not** part of your React Native app bundle. They run in Node.js during prebuild. Import `VoltraAndroid` from `@use-voltra/android`, not `@use-voltra/android-client` — the client package pulls in native modules that are unavailable in the prebuild sandbox. +::: + ## Build Process During the build process (`npx expo prebuild`), Voltra executes these initial state files in a Node.js environment to generate the static layouts that will be displayed when the widget is first added to the home screen. @@ -59,5 +61,5 @@ During the build process (`npx expo prebuild`), Voltra executes these initial st ## Limitations - **Environment**: The code runs in Node.js during build time, not on the device. -- **Imports**: Ensure you only import from `voltra` and avoid any browser or React Native specific APIs that aren't supported in Node.js. +- **Imports**: Use `@use-voltra/android` for JSX and types in `initialStatePath` files. Do not import from `@use-voltra/android-client` or other React Native client APIs. - **Static Content**: The initial state should represent a "loading" or "offline" state, as it won't have access to dynamic runtime data until the app runs. diff --git a/website/docs/android/introduction.md b/website/docs/android/introduction.md index 7c1b496c..586bc1cc 100644 --- a/website/docs/android/introduction.md +++ b/website/docs/android/introduction.md @@ -19,7 +19,7 @@ Voltra also supports Android ongoing notifications for app-driven, persistent st ### Simple Android Widget ```tsx -import { VoltraAndroid } from 'voltra' +import { VoltraAndroid } from '@use-voltra/android' const MyWidget = () => ( + -## 2. Next Steps +### Android (Home Screen widgets and ongoing notifications) -After installing the package, you need to configure the Voltra Expo plugin for each platform you want to support. + + +Use the platform package for JSX primitives: + +- `@use-voltra/ios` exports `Voltra` +- `@use-voltra/android` exports `VoltraAndroid`, `AndroidDynamicColors`, and `AndroidOngoingNotification` + +Use the client package for React Native runtime APIs: + +- `@use-voltra/ios-client` exports `startLiveActivity`, `updateWidget`, `VoltraView`, `VoltraWidgetPreview`, and other app-side APIs +- `@use-voltra/android-client` exports `updateAndroidWidget`, `requestPinAndroidWidget`, `reloadAndroidWidgets`, `VoltraView`, `VoltraWidgetPreview`, and other app-side APIs + +### Server rendering (optional) + +Use these in your backend or SSR service-not in the React Native app bundle: + +- **iOS payloads:** `@use-voltra/ios-server` +- **Android payloads:** `@use-voltra/android-server` +- **Cross-platform widget HTTP handlers:** `@use-voltra/server` + +## 2. Import patterns + +The docs use these package boundaries consistently: + +- React Native app code: JSX from `@use-voltra/ios` or `@use-voltra/android`, lifecycle APIs from the matching `*-client` package +- Node/server code: JSX plus render helpers from `@use-voltra/ios-server` or `@use-voltra/android-server` +- Pre-render files such as `initialStatePath`: import from `@use-voltra/ios` or `@use-voltra/android`, not from client packages + +## 3. Next Steps + +After installing packages, configure the Voltra Expo plugin for each platform you support: - [Configure iOS Setup](../ios/setup) - [Configure Android Setup](../android/setup) +- [Migrate from v1 to v2](./migration-v2) diff --git a/website/docs/getting-started/introduction.md b/website/docs/getting-started/introduction.md index c9fe60f1..4e475e21 100644 --- a/website/docs/getting-started/introduction.md +++ b/website/docs/getting-started/introduction.md @@ -8,7 +8,7 @@ Voltra changes this by providing a JavaScript-based API and JSX components that - **React Native Everywhere:** Extend your React Native app with native platform features using the same JSX syntax you already know. - **No Native Code Required:** Build complex widget layouts and live activities without touching Xcode or Android Studio for UI code. -- **Unified Components:** Use a shared set of components that render idiomatically on both iOS and Android. +- **Platform-native JSX:** Use platform-specific primitives that map directly to SwiftUI on iOS and Glance on Android. - **Real-time Updates:** Stream updates to your activities and widgets via push notifications (APNS/FCM) from any JavaScript runtime. ## How it works @@ -18,8 +18,8 @@ Voltra works by serializing your JSX components into a lightweight JSON format t Here's how simple it is to create a live activity: ```tsx -import { startLiveActivity } from 'voltra/client' -import { Voltra } from 'voltra' +import { Voltra } from '@use-voltra/ios' +import { startLiveActivity } from '@use-voltra/ios-client' const activityUI = ( @@ -47,8 +47,7 @@ Voltra also supports server-side updates through push notifications. You can use The same components you use in your app work on the server: ```tsx -import { renderLiveActivityToString } from 'voltra/server' -import { Voltra } from 'voltra' +import { Voltra, renderLiveActivityToString } from '@use-voltra/ios-server' // Render JSX to JSON payload on your server const payload = renderLiveActivityToString({ diff --git a/website/docs/getting-started/migration-v2.mdx b/website/docs/getting-started/migration-v2.mdx new file mode 100644 index 00000000..b6e6a4aa --- /dev/null +++ b/website/docs/getting-started/migration-v2.mdx @@ -0,0 +1,242 @@ +import { PackageManagerTabs } from '@rspress/core/theme' + +# Migration to v2 + +Voltra v2 introduces two major architectural changes: + +- The old Voltra umbrella package is gone. Voltra now ships as separate iOS, Android, and server packages. +- The native layer moved from Expo Modules to Turbo Modules. + +We made these changes to fix a few long-standing problems in the old package layout. + +The old umbrella package made it too easy for React Native code to leak into server builds, especially when people only wanted server-side rendering or pre-rendering. It also forced many apps to pull in both platform surfaces even when they only shipped iOS or only shipped Android. + +The Turbo Module migration also changes the native integration layer, so upgrading to v2 requires updating package installs, Expo plugin configuration, and some API usage. + +This guide walks through the package, import, and configuration changes you need to make when upgrading to v2. + +## What changed + +### Package split + +Old setups commonly used package paths like: + +- `voltra` +- `voltra/client` +- `voltra/server` +- `voltra/android` +- `voltra/android/client` +- `voltra/android/server` + +v2 uses scoped packages instead: + +- `@use-voltra/ios` +- `@use-voltra/ios-client` +- `@use-voltra/ios-server` +- `@use-voltra/android` +- `@use-voltra/android-client` +- `@use-voltra/android-server` +- `@use-voltra/server` + +### Platform-specific JSX namespaces + +The old docs implied one shared component namespace. v2 documents the real platform split: + +- iOS JSX primitives come from `@use-voltra/ios` as `Voltra` +- Android JSX primitives come from `@use-voltra/android` as `VoltraAndroid` +- Android semantic color tokens also live in `@use-voltra/android` as `AndroidDynamicColors` +- Android ongoing notification JSX lives in `@use-voltra/android` as `AndroidOngoingNotification` + +### Client APIs stay in `*-client` + +Use `@use-voltra/ios-client` and `@use-voltra/android-client` for runtime APIs that run inside the React Native app, such as: + +- starting or updating Live Activities +- updating widgets +- pinning Android widgets +- preview components such as `VoltraView` and `VoltraWidgetPreview` +- event listeners and hooks + +### Server APIs are platform-specific + +Use platform server packages for server-side rendering: + +- `@use-voltra/ios-server` for Live Activities and iOS widgets +- `@use-voltra/android-server` for Android widgets and ongoing notification payloads + +Use `@use-voltra/server` only for cross-platform widget HTTP handlers. + +## Installation changes + +Install the packages that match the layers you use. + +### iOS app code + + + +### Android app code + + + +### Optional server packages + + + +## Import mapping + +### iOS app code + +```tsx +// v1 +import { Voltra } from 'voltra' +import { startLiveActivity } from 'voltra/client' + +// v2 +import { Voltra } from '@use-voltra/ios' +import { startLiveActivity } from '@use-voltra/ios-client' +``` + +### iOS server code + +```tsx +// v1 +import { renderLiveActivityToString, Voltra } from 'voltra/server' + +// v2 +import { Voltra, renderLiveActivityToString } from '@use-voltra/ios-server' +``` + +### Android widget code + +```tsx +// v1 +import { VoltraAndroid } from 'voltra/android' +import { updateWidget } from 'voltra/android' + +// v2 +import { VoltraAndroid } from '@use-voltra/android' +import { updateAndroidWidget } from '@use-voltra/android-client' +``` + +### Android server widget code + +```tsx +// v1 +import { createWidgetUpdateNodeHandler } from 'voltra/server' +import { AndroidDynamicColors, VoltraAndroid } from 'voltra/android' + +// v2 +import { createAndroidWidgetUpdateNodeHandler } from '@use-voltra/android-server' +import { AndroidDynamicColors, VoltraAndroid } from '@use-voltra/android' +``` + +## Expo plugin migration + +The old docs used a single `voltra` plugin and nested per-platform config. v2 uses platform-specific plugins. + +### iOS + +```json +// v1 +{ + "expo": { + "plugins": [ + [ + "voltra", + { + "ios": { + "groupIdentifier": "group.example.app", + "widgets": [] + } + } + ] + ] + } +} + +// v2 +{ + "expo": { + "plugins": [ + [ + "@use-voltra/ios-client", + { + "groupIdentifier": "group.example.app", + "widgets": [] + } + ] + ] + } +} +``` + +### Android + +```json +// v1 +{ + "expo": { + "plugins": [ + [ + "voltra", + { + "android": { + "widgets": [] + } + } + ] + ] + } +} + +// v2 +{ + "expo": { + "plugins": [ + [ + "@use-voltra/android-client", + { + "widgets": [] + } + ] + ] + } +} +``` + +## API renames to watch for + +- Android widget updates: `updateWidget(...)` -> `updateAndroidWidget(...)` +- Android widget reloads: `reloadWidgets(...)` for image-preloading remains, but widget refresh APIs use `reloadAndroidWidgets(...)` +- Android widget server handlers: `createWidgetUpdateHandler(...)` style examples should become `createAndroidWidgetUpdateHandler(...)`, `createAndroidWidgetUpdateNodeHandler(...)`, or `createAndroidWidgetUpdateExpressHandler(...)` +- iOS widget server handlers: use `createIOSWidgetUpdateHandler(...)`, `createIOSWidgetUpdateNodeHandler(...)`, or `createIOSWidgetUpdateExpressHandler(...)` + +## Pre-render files + +`initialStatePath` files run in Node during prebuild, not in the React Native runtime. + +Use: + +- `@use-voltra/ios` for iOS widget JSX and types +- `@use-voltra/android` for Android widget JSX and types + +Do not import `@use-voltra/ios-client` or `@use-voltra/android-client` from pre-render files. + +## Recommended migration order + +1. Replace package installs. +2. Swap the Expo plugin to the platform-specific package. +3. Move JSX imports to `@use-voltra/ios` or `@use-voltra/android`. +4. Move runtime APIs to the matching `*-client` package. +5. Move server rendering code to `@use-voltra/ios-server` or `@use-voltra/android-server`. +6. Rename Android widget APIs that became platform-specific. +7. Verify `initialStatePath` files only import from platform packages. + +## After migrating + +Re-run prebuild for any platform whose plugin configuration changed: + +```bash +npx expo prebuild --platform ios +npx expo prebuild --platform android +``` diff --git a/website/docs/ios/api/configuration.md b/website/docs/ios/api/configuration.md index dc3d9b0f..86d9bcae 100644 --- a/website/docs/ios/api/configuration.md +++ b/website/docs/ios/api/configuration.md @@ -18,7 +18,7 @@ Voltra supports configuring how Live Activities behave after they end. You can c **Immediate dismissal (default behavior):** ```typescript -import { startLiveActivity } from 'voltra/client' +import { startLiveActivity } from '@use-voltra/ios-client' await startLiveActivity(variants, { dismissalPolicy: 'immediate', // or omit for default @@ -36,7 +36,7 @@ await startLiveActivity(variants, { **Update dismissal policy for active Live Activities:** ```typescript -import { updateLiveActivity } from 'voltra/client' +import { updateLiveActivity } from '@use-voltra/ios-client' await updateLiveActivity(activityId, variants, { dismissalPolicy: { after: 60 }, @@ -46,7 +46,7 @@ await updateLiveActivity(activityId, variants, { **Set dismissal policy when ending a Live Activity:** ```typescript -import { stopLiveActivity } from 'voltra/client' +import { stopLiveActivity } from '@use-voltra/ios-client' await stopLiveActivity(activityId, { dismissalPolicy: { after: 10 }, @@ -64,7 +64,7 @@ Voltra provides additional configuration options to control Live Activity behavi The `staleDate` option allows you to specify when a Live Activity should be considered stale and automatically dismissed by the system. ```typescript -import { startLiveActivity } from 'voltra/client' +import { startLiveActivity } from '@use-voltra/ios-client' // Dismiss the Live Activity after 1 hour await startLiveActivity(variants, { @@ -79,7 +79,7 @@ await startLiveActivity(variants, { The `relevanceScore` option helps iOS prioritize which Live Activities to display when space is limited. Higher scores (closer to 1.0) indicate more important activities. ```typescript -import { startLiveActivity } from 'voltra/client' +import { startLiveActivity } from '@use-voltra/ios-client' // High priority Live Activity (e.g., active delivery) await startLiveActivity(variants, { @@ -99,7 +99,7 @@ await startLiveActivity(variants, { The `channelId` option subscribes the Live Activity to a broadcast channel for server-side updates. When provided, the activity receives updates via broadcast push notifications instead of individual device tokens—one server notification updates all activities on that channel. Requires `enablePushNotifications: true` and the Broadcast Capability enabled in your Apple Developer account. ```typescript -import { startLiveActivity } from 'voltra/client' +import { startLiveActivity } from '@use-voltra/ios-client' await startLiveActivity(variants, { activityName: 'match-123', diff --git a/website/docs/ios/api/plugin-configuration.md b/website/docs/ios/api/plugin-configuration.md index 0aa22973..63d3443b 100644 --- a/website/docs/ios/api/plugin-configuration.md +++ b/website/docs/ios/api/plugin-configuration.md @@ -7,7 +7,7 @@ The Voltra Expo config plugin accepts several configuration options in your `app "expo": { "plugins": [ [ - "voltra", + "@use-voltra/ios-client", { "groupIdentifier": "group.your.bundle.identifier", "enablePushNotifications": true, @@ -76,7 +76,7 @@ This is useful when: "expo": { "plugins": [ [ - "voltra", + "@use-voltra/ios-client", { "groupIdentifier": "group.your.bundle.identifier", "targetName": "widget" diff --git a/website/docs/ios/components/interactive.md b/website/docs/ios/components/interactive.md index d4338b1f..2d67394f 100644 --- a/website/docs/ios/components/interactive.md +++ b/website/docs/ios/components/interactive.md @@ -34,7 +34,7 @@ Buttons fire interaction events that you can handle in your app: Handle the event: ```typescript -import { addVoltraListener } from 'voltra/client' +import { addVoltraListener } from '@use-voltra/ios-client' const subscription = addVoltraListener('interaction', (event) => { if (event.identifier === 'play-button') { @@ -167,7 +167,7 @@ Toggles a boolean state via an intent. Fires an interaction event when changed. Handle toggle events: ```typescript -import { addVoltraListener } from 'voltra/client' +import { addVoltraListener } from '@use-voltra/ios-client' const subscription = addVoltraListener('interaction', (event) => { if (event.identifier === 'notifications-toggle') { diff --git a/website/docs/ios/components/overview.md b/website/docs/ios/components/overview.md index 7a0c9541..00bf97d4 100644 --- a/website/docs/ios/components/overview.md +++ b/website/docs/ios/components/overview.md @@ -4,10 +4,10 @@ Voltra provides SwiftUI primitives with JSX bindings, allowing developers to cre ## Getting Started -All Voltra components are available through the main `Voltra` namespace: +Import iOS primitives from `@use-voltra/ios` and use the `Voltra` namespace: ```tsx -import Voltra from 'voltra' +import { Voltra } from '@use-voltra/ios' const MyComponent = () => { // Use any component diff --git a/website/docs/ios/development/developing-live-activities.md b/website/docs/ios/development/developing-live-activities.md index 9319d306..de9920e4 100644 --- a/website/docs/ios/development/developing-live-activities.md +++ b/website/docs/ios/development/developing-live-activities.md @@ -125,8 +125,8 @@ Unfortunately, iOS suspends background apps after approximately 30 seconds. This ::: ```typescript -import { useLiveActivity } from 'voltra/client' -import { Voltra } from 'voltra' +import { Voltra } from '@use-voltra/ios' +import { useLiveActivity } from '@use-voltra/ios-client' function OrderLiveActivity({ orderId, status }) { const variants = { @@ -177,8 +177,8 @@ For testing and development, Voltra provides a `VoltraView` component that rende - Previewing how your Live Activity will look ```tsx -import { VoltraView } from 'voltra/client' -import { Voltra } from 'voltra' +import { Voltra } from '@use-voltra/ios' +import { VoltraView } from '@use-voltra/ios-client' function MyComponent() { const handleInteraction = (event: VoltraInteractionEvent) => { diff --git a/website/docs/ios/development/developing-widgets.md b/website/docs/ios/development/developing-widgets.md index 46f96529..2d14a292 100644 --- a/website/docs/ios/development/developing-widgets.md +++ b/website/docs/ios/development/developing-widgets.md @@ -16,8 +16,8 @@ Voltra provides APIs that make building and testing Home Screen widgets easier d ```tsx import { ScrollView, View } from 'react-native' -import { VoltraWidgetPreview } from 'voltra/client' -import { Voltra } from 'voltra' +import { Voltra } from '@use-voltra/ios' +import { VoltraWidgetPreview } from '@use-voltra/ios-client' function MyWidgetContent() { return ( @@ -66,8 +66,8 @@ Widget updates are throttled to around an update per minute. iOS limits how freq ::: ```typescript -import { updateWidget } from 'voltra/client' -import { Voltra } from 'voltra' +import { Voltra } from '@use-voltra/ios' +import { updateWidget } from '@use-voltra/ios-client' await updateWidget('weather', { systemSmall: 72°F, @@ -105,8 +105,8 @@ This is perfect for weather forecasts, calendar events, news rotation, or any co ::: ```typescript -import { scheduleWidget } from 'voltra/client' -import { Voltra } from 'voltra' +import { Voltra } from '@use-voltra/ios' +import { scheduleWidget } from '@use-voltra/ios-client' // Schedule weather updates throughout the day await scheduleWidget('weather', [ @@ -183,7 +183,7 @@ Widgets are configured through the Voltra Expo config plugin. Add the widget con "expo": { "plugins": [ [ - "voltra", + "@use-voltra/ios-client", { "widgets": [ { @@ -238,7 +238,7 @@ await updateWidget('minimal', { Detect which widgets are currently installed on the user's home screen: ```typescript -import { getActiveWidgets } from 'voltra/client' +import { getActiveWidgets } from '@use-voltra/ios-client' const widgets = await getActiveWidgets() // [{ name: 'weather', family: 'systemSmall', kind: '...' }] @@ -251,7 +251,7 @@ See [Querying Active Widgets](./querying-active-widgets) for more details. Force widget timelines to refresh their content after updating shared resources like preloaded images: ```typescript -import { reloadWidgets } from 'voltra/client' +import { reloadWidgets } from '@use-voltra/ios-client' // Reload specific widgets await reloadWidgets(['weather', 'calendar']) @@ -265,7 +265,7 @@ await reloadWidgets() Remove stored widget data, causing widgets to show their placeholder state: ```typescript -import { clearWidget, clearAllWidgets } from 'voltra/client' +import { clearWidget, clearAllWidgets } from '@use-voltra/ios-client' // Clear specific widget await clearWidget('weather') diff --git a/website/docs/ios/development/events.md b/website/docs/ios/development/events.md index 1450d4dd..205a87ab 100644 --- a/website/docs/ios/development/events.md +++ b/website/docs/ios/development/events.md @@ -20,7 +20,7 @@ Voltra emits events whenever a Live Activity's state changes. This allows you to Use `addVoltraListener` with the `'stateChange'` event type to subscribe to state change events: ```typescript -import { addVoltraListener } from 'voltra/client' +import { addVoltraListener } from '@use-voltra/ios-client' const subscription = addVoltraListener('stateChange', (event) => { console.log('Activity name:', event.activityName) @@ -66,7 +66,7 @@ To enable server-side updates for your Live Activities, you need to obtain push Activity push tokens are used to update existing Live Activities via push notifications. Listen for these tokens using `addVoltraListener` with the `'activityTokenReceived'` event type: ```typescript -import { addVoltraListener } from 'voltra/client' +import { addVoltraListener } from '@use-voltra/ios-client' const subscription = addVoltraListener('activityTokenReceived', (event) => { console.log('Activity name:', event.activityName) @@ -89,7 +89,7 @@ For more information about using push tokens for server-side updates, see the [s Push-to-start tokens (available on iOS 17.2+) allow you to start Live Activities remotely via push notifications. Listen for these tokens using `addVoltraListener` with the `'activityPushToStartTokenReceived'` event type: ```typescript -import { addVoltraListener } from 'voltra/client' +import { addVoltraListener } from '@use-voltra/ios-client' const subscription = addVoltraListener('activityPushToStartTokenReceived', (event) => { console.log('Push-to-start token:', event.pushToStartToken) @@ -114,7 +114,7 @@ When users interact with buttons or toggles in your Live Activity, Voltra emits Subscribe to interaction events using `addVoltraListener` with the `'interaction'` event type: ```typescript -import { addVoltraListener } from 'voltra/client' +import { addVoltraListener } from '@use-voltra/ios-client' const subscription = addVoltraListener('interaction', (event) => { console.log('Component interacted:', event.identifier) @@ -145,7 +145,7 @@ Always clean up your event subscriptions to prevent memory leaks. If you're usin ```typescript import { useEffect } from 'react' -import { addVoltraListener } from 'voltra/client' +import { addVoltraListener } from '@use-voltra/ios-client' function MyComponent() { useEffect(() => { diff --git a/website/docs/ios/development/image-preloading.md b/website/docs/ios/development/image-preloading.md index 70834f79..241e533b 100644 --- a/website/docs/ios/development/image-preloading.md +++ b/website/docs/ios/development/image-preloading.md @@ -36,7 +36,7 @@ type PreloadImagesResult = { **Example:** ```typescript -import { preloadImages } from 'voltra/client' +import { preloadImages } from '@use-voltra/ios-client' const result = await preloadImages([ { @@ -55,7 +55,7 @@ console.log('Failed:', result.failed) Reloads Live Activities to pick up newly preloaded images. If no `activityNames` are provided, all active Live Activities will be reloaded. ```typescript -import { reloadLiveActivities } from 'voltra/client' +import { reloadLiveActivities } from '@use-voltra/ios-client' // Reload all Live Activities await reloadLiveActivities() @@ -69,7 +69,7 @@ await reloadLiveActivities(['music-player', 'order-tracker']) Removes preloaded images from App Group storage. If no `keys` are provided, all preloaded images will be cleared. ```typescript -import { clearPreloadedImages } from 'voltra/client' +import { clearPreloadedImages } from '@use-voltra/ios-client' // Clear specific images await clearPreloadedImages(['album-art', 'profile-pic']) @@ -83,7 +83,7 @@ await clearPreloadedImages() Once images are preloaded, reference them using the `assetName` property: ```typescript -import { Voltra } from 'voltra' +import { Voltra } from '@use-voltra/ios' function MusicPlayerLiveActivity({ song }) { return { @@ -153,7 +153,7 @@ Image preloading requires App Group configuration in your Expo config plugin: { "plugins": [ [ - "voltra", + "@use-voltra/ios-client", { "groupIdentifier": "group.your.app" } diff --git a/website/docs/ios/development/images.md b/website/docs/ios/development/images.md index fba0145d..54f44415 100644 --- a/website/docs/ios/development/images.md +++ b/website/docs/ios/development/images.md @@ -60,7 +60,7 @@ The image preloading system works by: Once images are preloaded, reference them using the `assetName` property: ```typescript -import { Voltra } from 'voltra' +import { Voltra } from '@use-voltra/ios' function MusicPlayerLiveActivity({ song }) { return { diff --git a/website/docs/ios/development/interactions.md b/website/docs/ios/development/interactions.md index 9cf0abe4..3a86c569 100644 --- a/website/docs/ios/development/interactions.md +++ b/website/docs/ios/development/interactions.md @@ -25,7 +25,7 @@ When a user interacts with a button or toggle in your Live Activity, Voltra auto To handle interactions, subscribe to Voltra UI events using the `addVoltraListener` function with the `'interaction'` event type: ```typescript -import { addVoltraListener } from 'voltra/client' +import { addVoltraListener } from '@use-voltra/ios-client' const subscription = addVoltraListener('interaction', (event) => { console.log('Component interacted:', event.identifier) @@ -70,7 +70,7 @@ If you don't provide an explicit `id` prop, Voltra will automatically generate a When users tap on the Live Activity itself (not on a button or link), your app can be launched via a deep link. Configure the deep link URL when starting a Live Activity: ```typescript -import { useLiveActivity } from 'voltra/client' +import { useLiveActivity } from '@use-voltra/ios-client' const { start } = useLiveActivity( { @@ -132,9 +132,9 @@ Here's a complete example showing how to handle interactions: ```typescript import { useEffect } from 'react' -import { useLiveActivity } from 'voltra/client' -import { Voltra } from 'voltra' -import { addVoltraListener } from 'voltra/client' +import { Voltra } from '@use-voltra/ios' +import { useLiveActivity } from '@use-voltra/ios-client' +import { addVoltraListener } from '@use-voltra/ios-client' function MyLiveActivity() { const { start, update } = useLiveActivity( diff --git a/website/docs/ios/development/managing-live-activities-locally.md b/website/docs/ios/development/managing-live-activities-locally.md index fbce9b58..2ef9d236 100644 --- a/website/docs/ios/development/managing-live-activities-locally.md +++ b/website/docs/ios/development/managing-live-activities-locally.md @@ -22,8 +22,8 @@ The imperative APIs provide direct, programmatic control over Live Activities. T Use `startLiveActivity()` to create and display a new Live Activity. ```typescript -import { startLiveActivity } from 'voltra/client' -import { Voltra } from 'voltra' +import { Voltra } from '@use-voltra/ios' +import { startLiveActivity } from '@use-voltra/ios-client' const variants = { lockScreen: ( @@ -71,8 +71,8 @@ const activityId = await startLiveActivity(variants, { Use `updateLiveActivity()` to modify the content and configuration of an active Live Activity. ```typescript -import { updateLiveActivity } from 'voltra/client' -import { Voltra } from 'voltra' +import { Voltra } from '@use-voltra/ios' +import { updateLiveActivity } from '@use-voltra/ios-client' const updatedVariants = { lockScreen: ( @@ -109,7 +109,7 @@ await updateLiveActivity(activityId, updatedVariants, { Use `stopLiveActivity()` to end a Live Activity. ```typescript -import { stopLiveActivity } from 'voltra/client' +import { stopLiveActivity } from '@use-voltra/ios-client' await stopLiveActivity(activityId, { dismissalPolicy: { after: 10 }, // Keep visible for 10 seconds after ending @@ -126,7 +126,7 @@ await stopLiveActivity(activityId, { Use `isLiveActivityActive()` to check if a specific Live Activity is currently active. ```typescript -import { isLiveActivityActive } from 'voltra/client' +import { isLiveActivityActive } from '@use-voltra/ios-client' if (isLiveActivityActive('order-123')) { console.log('Live Activity is active') @@ -146,7 +146,7 @@ if (isLiveActivityActive('order-123')) { #### Platform detection ```typescript -import { isGlassSupported, isHeadless } from 'voltra/client' +import { isGlassSupported, isHeadless } from '@use-voltra/ios-client' // Check if the device supports Liquid Glass (iOS 26+) if (isGlassSupported()) { @@ -165,7 +165,7 @@ if (isHeadless()) { Use `endAllLiveActivities()` to immediately end all active Live Activities in your app. ```typescript -import { endAllLiveActivities } from 'voltra/client' +import { endAllLiveActivities } from '@use-voltra/ios-client' // End all Live Activities (useful for cleanup or logout scenarios) await endAllLiveActivities() diff --git a/website/docs/ios/development/performance.md b/website/docs/ios/development/performance.md index 65d3fb00..4b9b401a 100644 --- a/website/docs/ios/development/performance.md +++ b/website/docs/ios/development/performance.md @@ -7,7 +7,7 @@ Voltra provides automatic optimizations to help you create efficient Live Activi Reuse JSX elements by creating them once, storing them in variables, and reusing them across your JSX tree. Voltra automatically detects duplicate element references and stores them only once in the payload, using lightweight references (`{ $r: index }`) for subsequent occurrences. ```tsx -import { Voltra } from 'voltra' +import { Voltra } from '@use-voltra/ios' // ✅ Good: Create element once and reuse const sharedButton = Click me diff --git a/website/docs/ios/development/querying-active-widgets.md b/website/docs/ios/development/querying-active-widgets.md index b6edd4a9..798fc133 100644 --- a/website/docs/ios/development/querying-active-widgets.md +++ b/website/docs/ios/development/querying-active-widgets.md @@ -15,7 +15,7 @@ There may be a slight delay in the data returned by this API. iOS caches widget ::: ```typescript -import { getActiveWidgets } from 'voltra/client' +import { getActiveWidgets } from '@use-voltra/ios-client' async function checkWidgets() { const activeWidgets = await getActiveWidgets() diff --git a/website/docs/ios/development/server-driven-widgets.md b/website/docs/ios/development/server-driven-widgets.md index 8318380e..0b9a6b9c 100644 --- a/website/docs/ios/development/server-driven-widgets.md +++ b/website/docs/ios/development/server-driven-widgets.md @@ -22,7 +22,7 @@ Add the `serverUpdate` option to your widget in `app.json` or `app.config.js`: "expo": { "plugins": [ [ - "voltra", + "@use-voltra/ios-client", { "widgets": [ { @@ -58,10 +58,10 @@ Voltra provides widget server handlers for the common runtime styles. Use `creat ```tsx import { createServer } from 'node:http' import React from 'react' -import { createWidgetUpdateNodeHandler, Voltra } from 'voltra/server' +import { createIOSWidgetUpdateNodeHandler, Voltra } from '@use-voltra/ios-server' -const handler = createWidgetUpdateNodeHandler({ - renderIos: async (req) => { +const handler = createIOSWidgetUpdateNodeHandler({ + render: async (req) => { // req.widgetId — the widget requesting an update // req.platform — always "ios" for iOS widget requests // req.family — the widget size ("systemSmall", "systemMedium", etc.) @@ -111,15 +111,15 @@ The handler responds to GET requests with these query parameters: | `family` | The widget family/size (iOS only) | | `theme` | The system color scheme (`light` or `dark`) | -The `Authorization: Bearer ` header is automatically extracted and passed to `validateToken` and `renderIos`. The `User-Agent` header is set to `VoltraWidget/1.0 (iOS/)`. +The `Authorization: Bearer ` header is automatically extracted and passed to `validateToken` and `render`. The `User-Agent` header is set to `VoltraWidget/1.0 (iOS/)`. For Fetch-native runtimes, use `createWidgetUpdateHandler()` instead of the Node adapter: ```tsx -import { createWidgetUpdateHandler, Voltra } from 'voltra/server' +import { createIOSWidgetUpdateHandler, Voltra } from '@use-voltra/ios-server' -export const GET = createWidgetUpdateHandler({ - renderIos: async (req) => ({ +export const GET = createIOSWidgetUpdateHandler({ + render: async (req) => ({ systemSmall: {req.widgetId}, }), }) @@ -134,7 +134,7 @@ Widgets run in a separate extension process and can't access your app's network Call `setWidgetServerCredentials` after the user logs in: ```typescript -import { setWidgetServerCredentials } from 'voltra/client' +import { setWidgetServerCredentials } from '@use-voltra/ios-client' await setWidgetServerCredentials({ token: userAccessToken, @@ -151,7 +151,7 @@ The `token` is required and is sent as `Authorization: Bearer ` on every Call `clearWidgetServerCredentials` when the user logs out: ```typescript -import { clearWidgetServerCredentials } from 'voltra/client' +import { clearWidgetServerCredentials } from '@use-voltra/ios-client' await clearWidgetServerCredentials() ``` @@ -167,7 +167,7 @@ For credentials to be shared between the main app and the widget extension, both "expo": { "plugins": [ [ - "voltra", + "@use-voltra/ios-client", { "keychainGroup": "$(AppIdentifierPrefix)com.example.shared", "widgets": [...] @@ -209,7 +209,7 @@ When WidgetKit reloads timelines, it may call `getTimeline` multiple times for e You can force-refresh server-driven widgets outside of the regular interval: ```typescript -import { reloadWidgets } from 'voltra/client' +import { reloadWidgets } from '@use-voltra/ios-client' // Reload specific widgets await reloadWidgets(['dynamic_weather']) diff --git a/website/docs/ios/development/server-side-updates.md b/website/docs/ios/development/server-side-updates.md index d35a31ec..f683b667 100644 --- a/website/docs/ios/development/server-side-updates.md +++ b/website/docs/ios/development/server-side-updates.md @@ -15,7 +15,7 @@ To enable server-side updates via push notifications, you need to configure the "expo": { "plugins": [ [ - "voltra", + "@use-voltra/ios-client", { "enablePushNotifications": true } @@ -32,7 +32,7 @@ To enable server-side updates via push notifications, you need to configure the "expo": { "plugins": [ [ - "voltra", + "@use-voltra/ios-client", { "enablePushNotifications": true, "groupIdentifier": "group.your.app.voltraui", @@ -70,10 +70,10 @@ Voltra provides a server-side API that allows you to render React components int ### Using the server-side API -Import the server-side rendering functions from `voltra/server`: +Import the server-side rendering functions from `@use-voltra/ios-server`: ```tsx -import { renderLiveActivityToString, Voltra } from 'voltra/server' +import { Voltra, renderLiveActivityToString } from '@use-voltra/ios-server' // Render your UI components to a JSON string const jsonPayload = renderLiveActivityToString({ @@ -184,7 +184,7 @@ Voltra provides specialized push tokens that are different from regular device t To update existing Live Activities, use activity push tokens: ```tsx -import { addVoltraListener } from 'voltra/client' +import { addVoltraListener } from '@use-voltra/ios-client' useEffect(() => { const subscription = addVoltraListener('activityTokenReceived', ({ @@ -246,8 +246,8 @@ Starting with iOS 18 and iPadOS 18, you can use **broadcast push notifications** Pass the `channelId` option when starting a Live Activity to subscribe it to a broadcast channel: ```typescript -import { startLiveActivity } from 'voltra/client' -import { Voltra } from 'voltra' +import { Voltra } from '@use-voltra/ios' +import { startLiveActivity } from '@use-voltra/ios-client' const activityId = await startLiveActivity(variants, { activityName: 'match-123', @@ -285,7 +285,7 @@ When Live Activity tokens change or need to be refreshed, iOS may wake your app Use the `isHeadless()` function to determine if your app is running in the background: ```typescript -import { isHeadless } from 'voltra/client' +import { isHeadless } from '@use-voltra/ios-client' // In your app's entry point or root component if (isHeadless()) { @@ -303,7 +303,7 @@ When the app is launched in headless mode for token handling, **do not mount you ```typescript // In your app's entry point (e.g., App.tsx, index.js) -import { isHeadless, addVoltraListener } from 'voltra/client' +import { isHeadless, addVoltraListener } from '@use-voltra/ios-client' function App() { // Handle token events even in headless mode diff --git a/website/docs/ios/development/styling.md b/website/docs/ios/development/styling.md index 4cea5b83..473a3a26 100644 --- a/website/docs/ios/development/styling.md +++ b/website/docs/ios/development/styling.md @@ -110,7 +110,7 @@ See the [Layout & Containers](../components/layout) documentation for details on ### Example ```tsx -import { Voltra } from 'voltra' +import { Voltra } from '@use-voltra/ios' const element = ( Content, @@ -56,6 +56,10 @@ const initialState: WidgetVariants = { export default initialState ``` +:::info +`initialStatePath` files are **not** part of your React Native app bundle. They run in Node.js during prebuild. Import `Voltra` and types from `@use-voltra/ios`, not `@use-voltra/ios-client` — the client package pulls in native modules that are unavailable in the prebuild sandbox. +::: + ## Build Process During build time, Voltra transpiles your widget files with Babel and executes them in a Node.js environment to generate initial states that are bundled into the iOS app. @@ -65,4 +69,4 @@ During build time, Voltra transpiles your widget files with Babel and executes t - **Node.js Environment**: Code runs in Node.js, not in React Native or iOS - **Babel Support**: TypeScript is supported via Babel transpilation - **No Bundling**: Import resolution works for local files but there is no bundler involved -- **Voltra Imports**: Do not use `voltra/client` or `voltra/server` imports; use `voltra` instead +- **Voltra Imports**: Use `@use-voltra/ios` for JSX and types in `initialStatePath` files. Do not import from `@use-voltra/ios-client` or other React Native client APIs. diff --git a/website/docs/ios/introduction.md b/website/docs/ios/introduction.md index 2bf2eee8..e851122f 100644 --- a/website/docs/ios/introduction.md +++ b/website/docs/ios/introduction.md @@ -9,8 +9,8 @@ Voltra changes all of that by providing a JavaScript-based API you can use to di Here's how simple it is to create a live activity: ```tsx -import { startLiveActivity } from 'voltra/client' -import { Voltra } from 'voltra' +import { Voltra } from '@use-voltra/ios' +import { startLiveActivity } from '@use-voltra/ios-client' const activityUI = ( @@ -38,8 +38,7 @@ Voltra also supports server-side updates through push notifications. You can use The same components you use in your app work on the server: ```tsx -import { renderLiveActivityToString } from 'voltra/server' -import { Voltra } from 'voltra' +import { Voltra, renderLiveActivityToString } from '@use-voltra/ios-server' // Render JSX to JSON payload on your server const payload = renderLiveActivityToString({ diff --git a/website/docs/ios/setup.mdx b/website/docs/ios/setup.mdx index 9e573d57..a1d647ad 100644 --- a/website/docs/ios/setup.mdx +++ b/website/docs/ios/setup.mdx @@ -2,7 +2,7 @@ import { PackageManagerTabs } from '@rspress/core/theme' # iOS Setup -Once you have [installed Voltra](../getting-started/installation), you need to configure the Expo plugin for iOS. +Once you have [installed Voltra](../getting-started/installation), configure the iOS Expo plugin and use `@use-voltra/ios` for JSX. :::info Minimum iOS Version Voltra requires **iOS 16.2 or later**. Make sure your app's deployment target is set to iOS 16.2 or higher. See the [expo-build-properties](https://docs.expo.dev/build-reference/variables/) documentation for details on configuring your iOS deployment target. @@ -10,14 +10,14 @@ Voltra requires **iOS 16.2 or later**. Make sure your app's deployment target is ## 1. Configure the Expo Plugin -Add the Voltra plugin to your `app.json` or `app.config.js`. For iOS, you typically need to specify a `groupIdentifier` to enable data sharing between your main app and the Live Activity extension. +Add the iOS client plugin to your `app.json` or `app.config.js`. For iOS, you typically need to specify a `groupIdentifier` to enable data sharing between your main app and the Live Activity extension. ```json { "expo": { "plugins": [ [ - "voltra", + "@use-voltra/ios-client", { "groupIdentifier": "group.your.bundle.identifier", "enablePushNotifications": true @@ -49,8 +49,8 @@ Now let's create a simple "Hello World" live activity. Create a new component: ```tsx import React from 'react' -import { startLiveActivity } from 'voltra/client' -import { Voltra } from 'voltra' +import { Voltra } from '@use-voltra/ios' +import { startLiveActivity } from '@use-voltra/ios-client' function HelloWorldActivity() { const activityUI = (