├── .changeset ├── README.md └── config.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── firebase-hosting-merge.yml │ ├── firebase-hosting-pull-request.yml │ ├── release-to-npm.yml │ └── release.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .nvmrc ├── LICENSE ├── README.md ├── apps ├── kitchen-sink │ ├── .eslintrc.js │ ├── .firebaserc │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── craco.config.js │ ├── firebase.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── images │ │ │ └── lottie1.json │ │ ├── index.html │ │ ├── logo.svg │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.css │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── ensemble │ │ │ ├── config.yaml │ │ │ ├── locales │ │ │ │ ├── en.yaml │ │ │ │ └── hi.yaml │ │ │ ├── screens │ │ │ │ ├── actions.yaml │ │ │ │ ├── customWidgets.yaml │ │ │ │ ├── forms.yaml │ │ │ │ ├── help.yaml │ │ │ │ ├── home.yaml │ │ │ │ ├── layouts.yaml │ │ │ │ ├── menu.yaml │ │ │ │ ├── product.yaml │ │ │ │ ├── testActions.yaml │ │ │ │ └── widgets.yaml │ │ │ ├── scripts │ │ │ │ ├── common.js │ │ │ │ └── test.js │ │ │ ├── theme.yaml │ │ │ ├── types.d.ts │ │ │ └── widgets │ │ │ │ ├── Button.yaml │ │ │ │ ├── Header.yaml │ │ │ │ └── StyledText.yaml │ │ ├── index.css │ │ ├── index.tsx │ │ ├── logo.svg │ │ ├── react-app-env.d.ts │ │ ├── reportWebVitals.ts │ │ └── setupTests.ts │ └── tsconfig.json ├── preview │ ├── .env.development │ ├── .env.production │ ├── .eslintrc.js │ ├── .firebaserc │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── craco.config.js │ ├── firebase.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ ├── robots.txt │ │ └── schema │ │ │ └── react │ │ │ └── .gitkeep │ ├── src │ │ ├── App.css │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── AppPreview.tsx │ │ ├── AppSelector.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ ├── reportWebVitals.ts │ │ ├── setupTests.ts │ │ └── useAppConsole.tsx │ └── tsconfig.json └── starter │ ├── .env.development │ ├── .env.production │ ├── .eslintrc.js │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── craco.config.js │ ├── package.json │ ├── public │ ├── ensemble.config.json │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── ensemble │ │ ├── config.yaml │ │ ├── index.ts │ │ ├── screens │ │ │ ├── help.yaml │ │ │ ├── home.yaml │ │ │ └── menu.yaml │ │ ├── scripts │ │ │ └── hello.js │ │ ├── theme.yaml │ │ ├── types.d.ts │ │ └── widgets │ │ │ └── Header.yaml │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ └── setupTests.ts │ └── tsconfig.json ├── package.json ├── packages ├── cli │ ├── .eslintrc.json │ ├── .gitignore │ ├── .mocharc.json │ ├── .prettierrc.json │ ├── CHANGELOG.md │ ├── README.md │ ├── bin │ │ ├── dev.cmd │ │ ├── dev.js │ │ ├── run.cmd │ │ └── run.js │ ├── package.json │ ├── src │ │ ├── commands │ │ │ ├── apps │ │ │ │ ├── get.ts │ │ │ │ └── list.ts │ │ │ ├── hello │ │ │ │ ├── index.ts │ │ │ │ └── world.ts │ │ │ ├── login.ts │ │ │ └── update-password.ts │ │ ├── firebase.ts │ │ ├── index.ts │ │ └── utils.ts │ ├── test │ │ ├── commands │ │ │ ├── apps │ │ │ │ ├── get.test.ts │ │ │ │ └── list.test.ts │ │ │ ├── hello │ │ │ │ ├── index.test.ts │ │ │ │ └── world.test.ts │ │ │ └── login.test.ts │ │ └── tsconfig.json │ └── tsconfig.json ├── eslint-config-custom │ ├── CHANGELOG.md │ ├── README.md │ ├── library.js │ ├── next.js │ ├── package.json │ └── react.js ├── framework │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ ├── __resources__ │ │ │ │ ├── helloworld.yaml │ │ │ │ ├── mycustomwidget.yaml │ │ │ │ ├── newtheme.yaml │ │ │ │ └── oldtheme.yaml │ │ │ ├── evaluate.test.ts │ │ │ └── parser.test.ts │ │ ├── api │ │ │ ├── data.ts │ │ │ ├── index.ts │ │ │ ├── modal.ts │ │ │ ├── navigate.tsx │ │ │ └── toast.ts │ │ ├── appConfig.ts │ │ ├── data │ │ │ ├── fetcher.ts │ │ │ └── index.ts │ │ ├── date │ │ │ ├── dateFormatter.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ │ ├── getPrettyDate.ts │ │ │ │ ├── getPrettyDuration.ts │ │ │ │ └── getPrettyTime.ts │ │ ├── evaluate │ │ │ ├── binding.ts │ │ │ ├── context.ts │ │ │ ├── evaluate.ts │ │ │ ├── index.ts │ │ │ └── mock.ts │ │ ├── hooks │ │ │ ├── __tests__ │ │ │ │ ├── useEnsembleStorage.test.ts │ │ │ │ ├── useEnsembleUser.test.ts │ │ │ │ ├── useRegisterBindings.test.ts │ │ │ │ ├── useScreenData.test.ts │ │ │ │ └── useWidgetId.test.ts │ │ │ ├── index.ts │ │ │ ├── useApplicationContext.tsx │ │ │ ├── useCommandCallback.ts │ │ │ ├── useCustomScope.tsx │ │ │ ├── useDeviceObserver.tsx │ │ │ ├── useEnsembleStorage.ts │ │ │ ├── useEnsembleUser.ts │ │ │ ├── useEvaluate.ts │ │ │ ├── useEventContext.tsx │ │ │ ├── useFonts.ts │ │ │ ├── useHtmlPassThrough.tsx │ │ │ ├── useLanguageScope.tsx │ │ │ ├── useRegisterBindings.ts │ │ │ ├── useScreenContext.tsx │ │ │ ├── useScreenData.ts │ │ │ ├── useStyles.ts │ │ │ ├── useTemplateData.ts │ │ │ ├── useThemeContext.tsx │ │ │ ├── useWidgetId.ts │ │ │ └── useWidgetState.ts │ │ ├── i18n.ts │ │ ├── index.ts │ │ ├── loader.ts │ │ ├── parser.ts │ │ ├── serializer.ts │ │ ├── shared │ │ │ ├── __tests__ │ │ │ │ └── common.test.ts │ │ │ ├── actions.ts │ │ │ ├── api.ts │ │ │ ├── common.ts │ │ │ ├── dto.ts │ │ │ ├── ensemble.d.ts │ │ │ ├── index.ts │ │ │ └── models.ts │ │ └── state │ │ │ ├── __tests__ │ │ │ └── widget.test.ts │ │ │ ├── application.ts │ │ │ ├── index.ts │ │ │ ├── platform.ts │ │ │ ├── screen.ts │ │ │ ├── user.ts │ │ │ └── widget.ts │ ├── tsconfig.json │ ├── tsdoc.json │ └── tsup.config.ts ├── js-commons │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── browser │ │ │ └── index.ts │ │ └── core │ │ │ ├── dto.ts │ │ │ ├── firebase.ts │ │ │ ├── index.ts │ │ │ ├── local-files.ts │ │ │ ├── service.ts │ │ │ └── transporter.ts │ ├── tsconfig.json │ ├── tsdoc.json │ └── tsup.config.ts ├── runtime │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── setupTests.ts │ ├── src │ │ ├── EnsembleApp.tsx │ │ ├── ThemeProvider.tsx │ │ ├── __tests__ │ │ │ ├── EnsembleApp.test.tsx │ │ │ ├── __resources__ │ │ │ │ └── helloworld.yaml │ │ │ └── registry.test.tsx │ │ ├── index.tsx │ │ ├── registry.tsx │ │ ├── runtime │ │ │ ├── __tests__ │ │ │ │ ├── customWidget.test.tsx │ │ │ │ └── screen.test.tsx │ │ │ ├── body.tsx │ │ │ ├── customWidget.tsx │ │ │ ├── entry.tsx │ │ │ ├── error.tsx │ │ │ ├── footer.tsx │ │ │ ├── header.tsx │ │ │ ├── hooks │ │ │ │ ├── __tests__ │ │ │ │ │ ├── useActionGroup.test.tsx │ │ │ │ │ ├── useEnsembleAction.test.tsx │ │ │ │ │ ├── useExecuteCode.test.tsx │ │ │ │ │ ├── useInvokeApi.test.tsx │ │ │ │ │ └── useUploadFile.test.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── useCloseAllDialogs.ts │ │ │ │ ├── useCloseAllModalScreens.ts │ │ │ │ ├── useEnsembleAction.tsx │ │ │ │ ├── useNavigateExternalScreen.ts │ │ │ │ ├── useNavigateModal.tsx │ │ │ │ ├── useNavigateScreen.ts │ │ │ │ ├── useNavigateUrl.ts │ │ │ │ └── useShowToast.tsx │ │ │ ├── index.ts │ │ │ ├── invokeApi.tsx │ │ │ ├── locationApi.tsx │ │ │ ├── menu.tsx │ │ │ ├── mock.tsx │ │ │ ├── modal │ │ │ │ ├── index.tsx │ │ │ │ └── utils.ts │ │ │ ├── navigation.tsx │ │ │ ├── runtime.tsx │ │ │ ├── screen.tsx │ │ │ ├── showDialog.ts │ │ │ └── websocket.tsx │ │ ├── shared │ │ │ ├── coreSchema.ts │ │ │ ├── hasSchema.ts │ │ │ ├── icons.tsx │ │ │ ├── layoutSchema.ts │ │ │ ├── screenSchema.ts │ │ │ ├── styleSchema.ts │ │ │ ├── styles.ts │ │ │ └── types.ts │ │ └── widgets │ │ │ ├── Avatar │ │ │ ├── Avatar.tsx │ │ │ └── utils │ │ │ │ ├── generateInitials.ts │ │ │ │ └── stringToColors.ts │ │ │ ├── Button.tsx │ │ │ ├── Card.tsx │ │ │ ├── Carousel.tsx │ │ │ ├── Charts │ │ │ ├── BarChart.tsx │ │ │ ├── DoughnutChart.tsx │ │ │ ├── LineChart.tsx │ │ │ ├── StackBarChart.tsx │ │ │ ├── index.tsx │ │ │ └── utils │ │ │ │ └── getMergedOptions.ts │ │ │ ├── Collapsible.tsx │ │ │ ├── Column.tsx │ │ │ ├── Conditional.tsx │ │ │ ├── DataGrid │ │ │ ├── DataCell.tsx │ │ │ ├── DataGrid.tsx │ │ │ └── index.ts │ │ │ ├── Divider.tsx │ │ │ ├── FittedColumn.tsx │ │ │ ├── FittedRow.tsx │ │ │ ├── Flex.tsx │ │ │ ├── Flow.tsx │ │ │ ├── Form │ │ │ ├── Checkbox.tsx │ │ │ ├── Date │ │ │ │ ├── Date.tsx │ │ │ │ ├── DateRange.tsx │ │ │ │ ├── __tests__ │ │ │ │ │ └── Date.test.tsx │ │ │ │ └── utils │ │ │ │ │ ├── DateConstants.ts │ │ │ │ │ ├── DatePickerContext.ts │ │ │ │ │ └── isDateValid.ts │ │ │ ├── Dropdown.tsx │ │ │ ├── Form.tsx │ │ │ ├── FormItem.tsx │ │ │ ├── MultiSelect.tsx │ │ │ ├── Radio.tsx │ │ │ ├── TextInput.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Checkbox.test.tsx │ │ │ │ ├── Dropdown.test.tsx │ │ │ │ ├── Form.test.tsx │ │ │ │ ├── MultiSelect.test.tsx │ │ │ │ ├── Radio.test.tsx │ │ │ │ ├── TextInput.test.tsx │ │ │ │ └── __shared__ │ │ │ │ │ └── fixtures.tsx │ │ │ ├── index.ts │ │ │ └── types.d.ts │ │ │ ├── GridView.tsx │ │ │ ├── Html.tsx │ │ │ ├── Icon.tsx │ │ │ ├── Image.tsx │ │ │ ├── ImageCropper.tsx │ │ │ ├── LoadingContainer.tsx │ │ │ ├── Lottie.tsx │ │ │ ├── Markdown.tsx │ │ │ ├── PopupMenu.tsx │ │ │ ├── Progress.tsx │ │ │ ├── QrCode.tsx │ │ │ ├── Row.tsx │ │ │ ├── Search.tsx │ │ │ ├── SignInWithGoogle.tsx │ │ │ ├── Skeleton.tsx │ │ │ ├── Slider.tsx │ │ │ ├── Spacer.tsx │ │ │ ├── Stack.tsx │ │ │ ├── Stepper │ │ │ ├── StepType.tsx │ │ │ ├── Stepper.tsx │ │ │ └── index.ts │ │ │ ├── Switch.tsx │ │ │ ├── TabBar.tsx │ │ │ ├── Tag.tsx │ │ │ ├── Text.tsx │ │ │ ├── ToggleButton.tsx │ │ │ ├── ToolTip.tsx │ │ │ ├── __tests__ │ │ │ ├── Button.test.tsx │ │ │ ├── Collapsible.test.tsx │ │ │ ├── Conditional.test.tsx │ │ │ └── Text.test.tsx │ │ │ └── index.ts │ ├── tsconfig.json │ ├── tsdoc.json │ └── turbo │ │ └── generators │ │ ├── config.ts │ │ └── templates │ │ └── component.hbs └── tsconfig │ ├── CHANGELOG.md │ ├── base.json │ ├── nextjs.json │ ├── package.json │ ├── react-app.json │ ├── react-library.json │ └── tsdoc.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts └── generateSchema.ts ├── tsconfig.json └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [], 11 | "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { 12 | "onlyUpdatePeerDependentsWhenOutOfRange": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: bug, p1 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: p1 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Describe your changes 2 | 3 | ### Screenshots [Optional] 4 | 5 | ## Issue ticket number and link 6 | 7 | Closes # 8 | 9 | ## Checklist before requesting a review 10 | 11 | - [ ] I have performed a self-review of my code 12 | - [ ] I have added tests 13 | - [ ] I have added a changeset `pnpm changeset add` 14 | - [ ] I have added example usage in the kitchen sink app 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | build: 11 | # Skip on automated changeset release 12 | if: github.ref != 'changeset-release/main' 13 | name: Build and Test 14 | timeout-minutes: 15 15 | runs-on: ubuntu-latest 16 | # To use Remote Caching, uncomment the next lines and follow the steps below. 17 | # env: 18 | # TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 19 | # TURBO_TEAM: ${{ vars.TURBO_TEAM }} 20 | 21 | steps: 22 | - name: Check out code 23 | uses: actions/checkout@v3 24 | - uses: pnpm/action-setup@v4.0.0 25 | with: 26 | version: 6.32.2 27 | - name: Setup Node.js environment 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version-file: ".nvmrc" 31 | cache: "pnpm" 32 | - name: Install dependencies 33 | run: pnpm install 34 | 35 | - name: Build 36 | run: pnpm build 37 | 38 | - name: Lint 39 | run: pnpm lint 40 | 41 | - name: Test 42 | run: pnpm test 43 | -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-merge.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Deploy to Firebase Hosting on merge 5 | "on": 6 | push: 7 | branches: 8 | - main 9 | jobs: 10 | build_and_deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: pnpm/action-setup@v4.0.0 15 | with: 16 | version: 6.32.2 17 | - name: Setup Node.js environment 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version-file: ".nvmrc" 21 | cache: "pnpm" 22 | - name: Install dependencies 23 | run: pnpm install 24 | - name: Build Packages 25 | run: pnpm run build --filter=./packages 26 | - name: Generate Schema 27 | run: pnpm run schema:generate 28 | - name: Build Preview Dev 29 | run: pnpm run build:dev 30 | working-directory: ./apps/preview 31 | - uses: FirebaseExtended/action-hosting-deploy@v0 32 | with: 33 | repoToken: "${{ secrets.GITHUB_TOKEN }}" 34 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_ENSEMBLE_WEB_STUDIO_DEV }}" 35 | channelId: live 36 | projectId: ensemble-web-studio-dev 37 | entryPoint: ./apps/preview 38 | firebaseToolsVersion: 13.35.1 39 | - name: Build Kitchen Sink 40 | run: pnpm run build 41 | working-directory: ./apps/kitchen-sink 42 | - uses: FirebaseExtended/action-hosting-deploy@v0 43 | with: 44 | repoToken: "${{ secrets.GITHUB_TOKEN }}" 45 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_ENSEMBLE_WEB_STUDIO_DEV }}" 46 | channelId: live 47 | projectId: ensemble-web-studio-dev 48 | entryPoint: ./apps/kitchen-sink 49 | firebaseToolsVersion: 13.35.1 -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-pull-request.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Deploy to Firebase Hosting on PR 5 | "on": pull_request 6 | jobs: 7 | build_and_preview: 8 | if: "${{ github.event.pull_request.head.repo.full_name == github.repository }}" 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: pnpm/action-setup@v4.0.0 13 | with: 14 | version: 6.32.2 15 | - name: Setup Node.js environment 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version-file: ".nvmrc" 19 | cache: "pnpm" 20 | - name: Install dependencies 21 | run: pnpm install 22 | - name: Build Packages 23 | run: pnpm run build --filter=./packages 24 | - name: Build Kitchen Sink 25 | run: pnpm run build 26 | working-directory: ./apps/kitchen-sink 27 | - uses: FirebaseExtended/action-hosting-deploy@v0 28 | with: 29 | repoToken: "${{ secrets.GITHUB_TOKEN }}" 30 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_ENSEMBLE_WEB_STUDIO_DEV }}" 31 | projectId: ensemble-web-studio-dev 32 | entryPoint: ./apps/kitchen-sink 33 | firebaseToolsVersion: 13.35.1 -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: pnpm/action-setup@v4.0.0 17 | with: 18 | version: 6.32.2 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version-file: ".nvmrc" 22 | cache: "pnpm" 23 | - name: Install dependencies 24 | run: pnpm install 25 | # TODO only do this when changesets are empty 26 | - name: Build Packages 27 | run: pnpm run build --filter=./packages 28 | - name: Create Release Pull Request 29 | uses: changesets/action@v1 30 | with: 31 | publish: pnpm release 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | - name: Generate Schema 35 | run: pnpm run schema:generate 36 | - name: Build Applications 37 | run: pnpm run build 38 | - uses: FirebaseExtended/action-hosting-deploy@v0 39 | with: 40 | repoToken: "${{ secrets.GITHUB_TOKEN }}" 41 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_ENSEMBLE_WEB_STUDIO }}" 42 | channelId: live 43 | projectId: ensemble-web-studio 44 | entryPoint: ./apps/preview 45 | firebaseToolsVersion: 13.35.1 46 | - name: Package Starter 47 | working-directory: ./apps/starter 48 | run: | 49 | version=$(jq -r '.version' package.json) 50 | zip -r "ensemble-starter-${version}.zip" build 51 | cp "ensemble-starter-${version}.zip" ensemble-starter-latest.zip 52 | - id: "auth" 53 | uses: "google-github-actions/auth@v2" 54 | with: 55 | credentials_json: ${{ secrets.GCP_SERVICE_ACCT_KEY_JSON }} 56 | - id: "upload-file" 57 | uses: "google-github-actions/upload-cloud-storage@v2" 58 | with: 59 | path: "apps/starter" 60 | destination: "ensemble-react-starter" 61 | parent: false 62 | glob: "*.zip" 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | dist 16 | 17 | # misc 18 | .DS_Store 19 | *.pem 20 | .vscode 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # turbo 35 | .turbo 36 | 37 | # vercel 38 | .vercel 39 | .idea 40 | 41 | .firebase 42 | 43 | # schemas 44 | apps/preview/public/schema/react/*.json 45 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm exec lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true 2 | @ensembleui:registry = https://npm.pkg.github.com 3 | //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.12.1 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Ensemble 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /apps/kitchen-sink/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["custom/react"], 3 | rules: { 4 | "unicorn/filename-case": "off", 5 | }, 6 | ignorePatterns: ["*.config.js", "src/ensemble/scripts/*.js"], 7 | }; 8 | -------------------------------------------------------------------------------- /apps/kitchen-sink/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "ensemble-web-studio-dev", 4 | "dev": "ensemble-web-studio-dev", 5 | "prod": "ensemble-web-studio" 6 | }, 7 | "targets": { 8 | "ensemble-web-studio-dev": { 9 | "hosting": { 10 | "react-kitchen-sink": [ 11 | "react-kitchen-sink-dev" 12 | ] 13 | } 14 | }, 15 | "ensemble-web-studio": { 16 | "hosting": { 17 | "react-kitchen-sink": [ 18 | "react-kitchen-sink" 19 | ] 20 | } 21 | } 22 | }, 23 | "etags": {} 24 | } 25 | -------------------------------------------------------------------------------- /apps/kitchen-sink/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /apps/kitchen-sink/craco.config.js: -------------------------------------------------------------------------------- 1 | const NodePolyfillPlugin = require("node-polyfill-webpack-plugin"); 2 | 3 | module.exports = { 4 | webpack: { 5 | configure: { 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.yaml$/i, 10 | type: "asset/source", 11 | }, 12 | { 13 | resourceQuery: /raw/, 14 | type: "asset/source", 15 | }, 16 | ], 17 | }, 18 | resolve: { 19 | fallback: { 20 | fs: false, 21 | }, 22 | }, 23 | }, 24 | plugins: { 25 | add: [ 26 | new NodePolyfillPlugin({ 27 | excludeAliases: ["console"], 28 | }), 29 | ], 30 | }, 31 | }, 32 | devServer: { 33 | port: 4000, 34 | }, 35 | jest: { 36 | moduleNameMapper: { 37 | "^lodash-es$": "lodash", 38 | }, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /apps/kitchen-sink/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "target": "react-kitchen-sink", 4 | "public": "build", 5 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 6 | "rewrites": [ 7 | { 8 | "source": "**", 9 | "destination": "/index.html" 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/kitchen-sink/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ensembleui/react-kitchen-sink", 3 | "version": "0.0.124", 4 | "private": true, 5 | "dependencies": { 6 | "@ensembleui/react-framework": "workspace:*", 7 | "@ensembleui/react-runtime": "workspace:*", 8 | "react": "^18.2.0", 9 | "react-dom": "^18.2.0", 10 | "web-vitals": "^2.1.4" 11 | }, 12 | "devDependencies": { 13 | "@craco/craco": "^7.1.0", 14 | "@testing-library/jest-dom": "^5.17.0", 15 | "@testing-library/react": "^13.4.0", 16 | "@testing-library/user-event": "^13.5.0", 17 | "@types/jest": "^27.5.2", 18 | "@types/node": "^16.18.45", 19 | "@types/react": "^18.2.21", 20 | "@types/react-dom": "^18.2.7", 21 | "eslint-config-custom": "workspace:*", 22 | "https-browserify": "^1.0.0", 23 | "node-polyfill-webpack-plugin": "^2.0.1", 24 | "path-browserify": "^1.0.1", 25 | "react-scripts": "5.0.1", 26 | "stream-browserify": "^3.0.0", 27 | "stream-http": "^3.2.0", 28 | "tsconfig": "workspace:*", 29 | "typescript": "^4.9.5", 30 | "url": "^0.11.2", 31 | "util": "^0.12.5" 32 | }, 33 | "scripts": { 34 | "start": "craco start", 35 | "dev": "craco start", 36 | "build": "craco build", 37 | "test": "craco test --watchAll=false", 38 | "eject": "react-scripts eject" 39 | }, 40 | "lint-staged": { 41 | "*.{ts,tsx}": "eslint --fix" 42 | }, 43 | "eslintConfig": { 44 | "extends": [ 45 | "react-app", 46 | "react-app/jest" 47 | ] 48 | }, 49 | "browserslist": { 50 | "production": [ 51 | ">0.2%", 52 | "not dead", 53 | "not op_mini all" 54 | ], 55 | "development": [ 56 | "last 1 chrome version", 57 | "last 1 firefox version", 58 | "last 1 safari version" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /apps/kitchen-sink/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnsembleUI/ensemble-react/d11f8fe7c3bbb7dd9ec03f74e2c4be85845e125a/apps/kitchen-sink/public/favicon.ico -------------------------------------------------------------------------------- /apps/kitchen-sink/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 28 | 35 | React App 36 | 37 | 38 | 39 |
40 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /apps/kitchen-sink/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /apps/kitchen-sink/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /apps/kitchen-sink/src/App.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Roboto:400,400italic,500,500italic,700,700italic,900,900italic,300italic,300,100italic,100); 2 | 3 | body { 4 | font-family: 'Roboto', sans-serif; 5 | } 6 | 7 | .App-logo { 8 | height: 40vmin; 9 | pointer-events: none; 10 | } 11 | 12 | @media (prefers-reduced-motion: no-preference) { 13 | .App-logo { 14 | animation: App-logo-spin infinite 20s linear; 15 | } 16 | } 17 | 18 | .App-header { 19 | background-color: #282c34; 20 | min-height: 100vh; 21 | display: flex; 22 | flex-direction: column; 23 | align-items: center; 24 | justify-content: center; 25 | font-size: calc(10px + 2vmin); 26 | color: white; 27 | } 28 | 29 | .App-link { 30 | color: #61dafb; 31 | } 32 | 33 | @keyframes App-logo-spin { 34 | from { 35 | transform: rotate(0deg); 36 | } 37 | to { 38 | transform: rotate(360deg); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/kitchen-sink/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import "react"; 2 | 3 | test.todo("Renders home page"); 4 | -------------------------------------------------------------------------------- /apps/kitchen-sink/src/ensemble/config.yaml: -------------------------------------------------------------------------------- 1 | environmentVariables: 2 | googleOAuthId: 726646987043-9i1it0ll0neojkf7f9abkagbe66kqe4a.apps.googleusercontent.com 3 | randomId: 8729435004 4 | 5 | secretVariables: 6 | dummyOauthSecret: 1234567890 7 | -------------------------------------------------------------------------------- /apps/kitchen-sink/src/ensemble/locales/en.yaml: -------------------------------------------------------------------------------- 1 | kitchenSink: Hello Sagar 2 | -------------------------------------------------------------------------------- /apps/kitchen-sink/src/ensemble/locales/hi.yaml: -------------------------------------------------------------------------------- 1 | kitchenSink: नमस्कार सागर 2 | -------------------------------------------------------------------------------- /apps/kitchen-sink/src/ensemble/screens/customWidgets.yaml: -------------------------------------------------------------------------------- 1 | View: 2 | header: 3 | title: 4 | Header: 5 | 6 | onLoad: 7 | executeCode: | 8 | const date = '2024-04-08T18:54:09.063Z'; // change date to see different results 9 | console.log(ensemble.formatter.now().humanize(date)); 10 | 11 | body: 12 | Column: 13 | styles: 14 | names: page 15 | children: 16 | - Text: 17 | styles: 18 | names: heading-1 19 | text: Extending with Custom Widgets 20 | - Markdown: 21 | text: More to come! In the meantime, checkout the Ensemble [documentation](https://docs.ensembleui.com/). 22 | - Text: 23 | styles: 24 | names: heading-1 25 | text: Adding HTML Attributes 26 | htmlAttributes: 27 | value: testvalue 28 | - Markdown: 29 | text: More to come! In the meantime, checkout the Ensemble [documentation](https://docs.ensembleui.com/). 30 | -------------------------------------------------------------------------------- /apps/kitchen-sink/src/ensemble/screens/product.yaml: -------------------------------------------------------------------------------- 1 | View: 2 | header: 3 | title: 4 | Header: 5 | 6 | body: 7 | Column: 8 | styles: 9 | names: page 10 | children: 11 | - Text: 12 | styles: 13 | names: heading-1 14 | text: ${"Product Name = " + product_name} 15 | -------------------------------------------------------------------------------- /apps/kitchen-sink/src/ensemble/scripts/common.js: -------------------------------------------------------------------------------- 1 | const productTitleName = "SgrDvr"; 2 | 3 | const getDateLabel = (val) => { 4 | return `i am a date label ${val}` 5 | } 6 | const getHomeWidgetLabel = () => { 7 | return 'Home'; 8 | } -------------------------------------------------------------------------------- /apps/kitchen-sink/src/ensemble/scripts/test.js: -------------------------------------------------------------------------------- 1 | const sayHello = () => { 2 | window.alert("hello world!"); 3 | }; 4 | -------------------------------------------------------------------------------- /apps/kitchen-sink/src/ensemble/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.yaml" { 2 | const data: string; 3 | export default data; 4 | } 5 | -------------------------------------------------------------------------------- /apps/kitchen-sink/src/ensemble/widgets/Button.yaml: -------------------------------------------------------------------------------- 1 | Widget: 2 | events: 3 | onFormSubmit: 4 | data: 5 | name: 6 | email: 7 | 8 | body: 9 | Button: 10 | label: dispatch event 11 | onTap: 12 | invokeAPI: 13 | name: getDummyProducts2 14 | onResponse: 15 | dispatchEvent: 16 | onFormSubmit: 17 | data: 18 | name: Sagar 19 | email: sagardspeed2@gmail.com 20 | 21 | API: 22 | getDummyProducts2: 23 | method: GET 24 | uri: https://dummyjson.com/products 25 | -------------------------------------------------------------------------------- /apps/kitchen-sink/src/ensemble/widgets/StyledText.yaml: -------------------------------------------------------------------------------- 1 | Widget: 2 | onLoad: 3 | executeCode: | 4 | console.log('StyledText Widget loaded'); 5 | console.log(inputText); 6 | body: 7 | Column: 8 | onTap: 9 | executeCode: | 10 | console.log(inputText) 11 | children: 12 | - Text: 13 | text: Styled Text 14 | styles: 15 | fontSize: 24px 16 | fontWeight: ${typography.fontWeight['bold']} 17 | color: blue 18 | backgroundColor: ${colors.dark['300']} 19 | padding: 8px 20 | -------------------------------------------------------------------------------- /apps/kitchen-sink/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /apps/kitchen-sink/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 8 | const root = createRoot(document.getElementById("root")!); 9 | root.render( 10 | // 11 | , 12 | // , 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /apps/kitchen-sink/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/kitchen-sink/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import type { ReportHandler } from "web-vitals"; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler): void => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | void import("web-vitals").then( 6 | ({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 7 | getCLS(onPerfEntry); 8 | getFID(onPerfEntry); 9 | getFCP(onPerfEntry); 10 | getLCP(onPerfEntry); 11 | getTTFB(onPerfEntry); 12 | }, 13 | ); 14 | } 15 | }; 16 | 17 | export default reportWebVitals; 18 | -------------------------------------------------------------------------------- /apps/kitchen-sink/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /apps/kitchen-sink/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/react-app.json", 3 | "include": ["src"] 4 | } -------------------------------------------------------------------------------- /apps/preview/.env.development: -------------------------------------------------------------------------------- 1 | REACT_APP_apiKey="AIzaSyC3E_Y3y6ufwLNRe32PqmlFRXsiYEZ2-I4" 2 | REACT_APP_authDomain="ensemble-web-studio-dev.firebaseapp.com" 3 | REACT_APP_projectId="ensemble-web-studio-dev" 4 | REACT_APP_storageBucket="ensemble-web-studio-dev.appspot.com" 5 | REACT_APP_messagingSenderId="126811761383" 6 | REACT_APP_appId="1:126811761383:web:582bde07712c82bec7d042" 7 | REACT_APP_measurementId="G-95XC4X2T4S" -------------------------------------------------------------------------------- /apps/preview/.env.production: -------------------------------------------------------------------------------- 1 | REACT_APP_apiKey="AIzaSyCmBlQxl9q_aAy_tGxJs7iS9ZSwfJexUdI" 2 | REACT_APP_authDomain="ensemble-web-studio.firebaseapp.com" 3 | REACT_APP_projectId="ensemble-web-studio" 4 | REACT_APP_storageBucket="ensemble-web-studio.appspot.com" 5 | REACT_APP_messagingSenderId="326748243798" 6 | REACT_APP_appId="1:326748243798:web:73a816313c3e70fb94b8f7" 7 | REACT_APP_measurementId="G-CEZ4918M26" 8 | -------------------------------------------------------------------------------- /apps/preview/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["custom/react"], 3 | rules: { 4 | "unicorn/filename-case": "off", 5 | }, 6 | ignorePatterns: ["*.config.js", "src/ensemble/**"], 7 | }; 8 | -------------------------------------------------------------------------------- /apps/preview/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "ensemble-web-studio-dev", 4 | "dev": "ensemble-web-studio-dev", 5 | "prod": "ensemble-web-studio" 6 | }, 7 | "targets": { 8 | "ensemble-web-studio-dev": { 9 | "hosting": { 10 | "react-preview": [ 11 | "react-preview-dev" 12 | ] 13 | } 14 | }, 15 | "ensemble-web-studio": { 16 | "hosting": { 17 | "react-preview": [ 18 | "ensemble-react-preview" 19 | ] 20 | } 21 | } 22 | }, 23 | "etags": {} 24 | } 25 | -------------------------------------------------------------------------------- /apps/preview/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /apps/preview/craco.config.js: -------------------------------------------------------------------------------- 1 | const NodePolyfillPlugin = require("node-polyfill-webpack-plugin"); 2 | 3 | module.exports = { 4 | webpack: { 5 | configure: { 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.yaml$/i, 10 | type: "asset/source", 11 | }, 12 | { 13 | resourceQuery: /raw/, 14 | type: "asset/source", 15 | }, 16 | ], 17 | }, 18 | resolve: { 19 | fallback: { 20 | fs: false, 21 | }, 22 | }, 23 | }, 24 | plugins: { 25 | add: [ 26 | new NodePolyfillPlugin({ 27 | excludeAliases: ["console"], 28 | }), 29 | ], 30 | }, 31 | }, 32 | jest: { 33 | moduleNameMapper: { 34 | "^lodash-es$": "lodash", 35 | }, 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /apps/preview/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "target": "react-preview", 4 | "public": "build", 5 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 6 | "rewrites": [ 7 | { 8 | "source": "**", 9 | "destination": "/index.html" 10 | } 11 | ], 12 | "headers": [ 13 | { 14 | "source": "**/*.json", 15 | "headers": [ 16 | { 17 | "key": "Access-Control-Allow-Origin", 18 | "value": "*" 19 | } 20 | ] 21 | } 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/preview/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ensembleui/react-preview", 3 | "version": "0.0.123", 4 | "private": true, 5 | "dependencies": { 6 | "@ensembleui/react-framework": "workspace:*", 7 | "@ensembleui/react-runtime": "workspace:*", 8 | "antd": "^5.9.0", 9 | "firebase": "9.10.0", 10 | "lodash-es": "^4.17.21", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "react-router-dom": "^6.16.0", 14 | "web-vitals": "^2.1.4" 15 | }, 16 | "devDependencies": { 17 | "@craco/craco": "^7.1.0", 18 | "@testing-library/jest-dom": "^5.17.0", 19 | "@testing-library/react": "^13.4.0", 20 | "@testing-library/user-event": "^13.5.0", 21 | "@types/jest": "^27.5.2", 22 | "@types/lodash-es": "^4.17.8", 23 | "@types/node": "^16.18.45", 24 | "@types/react": "^18.2.21", 25 | "@types/react-dom": "^18.2.7", 26 | "dotenv": "^16.4.1", 27 | "dotenv-cli": "^7.3.0", 28 | "eslint-config-custom": "workspace:*", 29 | "https-browserify": "^1.0.0", 30 | "node-polyfill-webpack-plugin": "^2.0.1", 31 | "path-browserify": "^1.0.1", 32 | "react-scripts": "5.0.1", 33 | "stream-browserify": "^3.0.0", 34 | "stream-http": "^3.2.0", 35 | "tsconfig": "workspace:*", 36 | "typescript": "^4.9.5", 37 | "url": "^0.11.2", 38 | "util": "^0.12.5" 39 | }, 40 | "scripts": { 41 | "start": "craco start", 42 | "build": "craco build", 43 | "build:dev": "dotenv -e .env.development craco build", 44 | "test": "craco test --watchAll=false", 45 | "eject": "react-scripts eject" 46 | }, 47 | "lint-staged": { 48 | "*.{ts,tsx}": "eslint --fix" 49 | }, 50 | "eslintConfig": { 51 | "extends": [ 52 | "react-app", 53 | "react-app/jest" 54 | ] 55 | }, 56 | "browserslist": { 57 | "production": [ 58 | ">0.2%", 59 | "not dead", 60 | "not op_mini all" 61 | ], 62 | "development": [ 63 | "last 1 chrome version", 64 | "last 1 firefox version", 65 | "last 1 safari version" 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /apps/preview/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnsembleUI/ensemble-react/d11f8fe7c3bbb7dd9ec03f74e2c4be85845e125a/apps/preview/public/favicon.ico -------------------------------------------------------------------------------- /apps/preview/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 28 | 35 | React App 36 | 37 | 38 | 39 |
40 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /apps/preview/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnsembleUI/ensemble-react/d11f8fe7c3bbb7dd9ec03f74e2c4be85845e125a/apps/preview/public/logo192.png -------------------------------------------------------------------------------- /apps/preview/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnsembleUI/ensemble-react/d11f8fe7c3bbb7dd9ec03f74e2c4be85845e125a/apps/preview/public/logo512.png -------------------------------------------------------------------------------- /apps/preview/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /apps/preview/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /apps/preview/public/schema/react/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnsembleUI/ensemble-react/d11f8fe7c3bbb7dd9ec03f74e2c4be85845e125a/apps/preview/public/schema/react/.gitkeep -------------------------------------------------------------------------------- /apps/preview/src/App.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Roboto:400,400italic,500,500italic,700,700italic,900,900italic,300italic,300,100italic,100); 2 | 3 | body { 4 | font-family: 'Roboto', sans-serif; 5 | background: white; 6 | } 7 | 8 | .App-logo { 9 | height: 40vmin; 10 | pointer-events: none; 11 | } 12 | 13 | @media (prefers-reduced-motion: no-preference) { 14 | .App-logo { 15 | animation: App-logo-spin infinite 20s linear; 16 | } 17 | } 18 | 19 | .App-header { 20 | background-color: #282c34; 21 | min-height: 100vh; 22 | display: flex; 23 | flex-direction: column; 24 | align-items: center; 25 | justify-content: center; 26 | font-size: calc(10px + 2vmin); 27 | color: white; 28 | } 29 | 30 | .App-link { 31 | color: #61dafb; 32 | } 33 | 34 | @keyframes App-logo-spin { 35 | from { 36 | transform: rotate(0deg); 37 | } 38 | to { 39 | transform: rotate(360deg); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/preview/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import "react"; 2 | 3 | test.todo("Renders your Ensemble app"); 4 | -------------------------------------------------------------------------------- /apps/preview/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | import { initializeFirestore } from "firebase/firestore/lite"; 3 | import { createBrowserRouter, RouterProvider } from "react-router-dom"; 4 | import { AppPreview } from "./AppPreview"; 5 | import { AppSelector } from "./AppSelector"; 6 | import "./App.css"; 7 | 8 | export const firebaseApp = initializeApp({ 9 | apiKey: process.env.REACT_APP_apiKey, 10 | authDomain: process.env.REACT_APP_authDomain, 11 | projectId: process.env.REACT_APP_projectId, 12 | storageBucket: process.env.REACT_APP_storageBucket, 13 | messagingSenderId: process.env.REACT_APP_messagingSenderId, 14 | appId: process.env.REACT_APP_appId, 15 | measurementId: process.env.REACT_APP_measurementId, 16 | }); 17 | export const db = initializeFirestore(firebaseApp, {}); 18 | 19 | const router = createBrowserRouter([ 20 | { 21 | path: "/", 22 | element: , 23 | }, 24 | { 25 | path: "/preview/:previewId/*", 26 | element: , 27 | }, 28 | ]); 29 | const App: React.FC = () => { 30 | return ( 31 |
32 | 33 |
34 | ); 35 | }; 36 | 37 | export default App; 38 | -------------------------------------------------------------------------------- /apps/preview/src/AppSelector.tsx: -------------------------------------------------------------------------------- 1 | import { Input, Button, Alert, Typography } from "antd"; 2 | import { useCallback, useState } from "react"; 3 | import { useNavigate } from "react-router-dom"; 4 | 5 | export const AppSelector: React.FC = () => { 6 | const [appId, setAppId] = useState(); 7 | const [error, setError] = useState(); 8 | const navigate = useNavigate(); 9 | const onClick = useCallback(() => { 10 | if (!appId) { 11 | setError("Please enter an app id"); 12 | return; 13 | } 14 | navigate(`/preview/${appId}`); 15 | }, [appId, navigate]); 16 | 17 | return ( 18 |
19 | Ensemble React Preview 20 | setAppId(e.target.value)} 22 | placeholder="Enter app id" 23 | /> 24 | 27 | {error ? : null} 28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /apps/preview/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | 15 | .selector { 16 | display: flex; 17 | flex-direction: column; 18 | padding: 20px; 19 | gap: 8px; 20 | } -------------------------------------------------------------------------------- /apps/preview/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 8 | const root = createRoot(document.getElementById("root")!); 9 | root.render( 10 | 11 | 12 | , 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /apps/preview/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/preview/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import type { ReportHandler } from "web-vitals"; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler): void => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | void import("web-vitals").then( 6 | ({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 7 | getCLS(onPerfEntry); 8 | getFID(onPerfEntry); 9 | getFCP(onPerfEntry); 10 | getLCP(onPerfEntry); 11 | getTTFB(onPerfEntry); 12 | }, 13 | ); 14 | } 15 | }; 16 | 17 | export default reportWebVitals; 18 | -------------------------------------------------------------------------------- /apps/preview/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /apps/preview/src/useAppConsole.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | interface CustomConsole { 4 | _log: (...keys: unknown[]) => void; 5 | _warn: (...keys: unknown[]) => void; 6 | _error: (...keys: unknown[]) => void; 7 | } 8 | 9 | export const useAppConsole = (): void => { 10 | useEffect(() => { 11 | // eslint-disable-next-line eslint-comments/disable-enable-pair 12 | /* eslint-disable no-console */ 13 | const customConsole = {} as CustomConsole; 14 | 15 | const handleLog = (type: "log" | "warn" | "error") => { 16 | return (...message: unknown[]): void => { 17 | try { 18 | window.parent.postMessage( 19 | { 20 | type, 21 | message: message.map((msg) => JSON.stringify(msg)).join(", "), 22 | }, 23 | "*", 24 | ); 25 | } catch (error) { 26 | console.error("Error posting message to parent:", error); 27 | } 28 | customConsole[`_${type}`](...message); 29 | }; 30 | }; 31 | 32 | // Save the original console methods 33 | customConsole._log = console.log; 34 | customConsole._warn = console.warn; 35 | customConsole._error = console.error; 36 | 37 | // Override the console methods 38 | console.log = handleLog("log"); 39 | console.warn = handleLog("warn"); 40 | console.error = handleLog("error"); 41 | 42 | return (): void => { 43 | console.log = customConsole._log; 44 | console.warn = customConsole._warn; 45 | console.error = customConsole._error; 46 | }; 47 | }, []); 48 | }; 49 | -------------------------------------------------------------------------------- /apps/preview/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/react-app.json", 3 | "include": ["src"] 4 | } -------------------------------------------------------------------------------- /apps/starter/.env.development: -------------------------------------------------------------------------------- 1 | REACT_APP_apiKey="AIzaSyC3E_Y3y6ufwLNRe32PqmlFRXsiYEZ2-I4" 2 | REACT_APP_authDomain="ensemble-web-studio-dev.firebaseapp.com" 3 | REACT_APP_projectId="ensemble-web-studio-dev" 4 | REACT_APP_storageBucket="ensemble-web-studio-dev.appspot.com" 5 | REACT_APP_messagingSenderId="126811761383" 6 | REACT_APP_appId="1:126811761383:web:582bde07712c82bec7d042" 7 | REACT_APP_measurementId="G-95XC4X2T4S" -------------------------------------------------------------------------------- /apps/starter/.env.production: -------------------------------------------------------------------------------- 1 | REACT_APP_apiKey="AIzaSyCmBlQxl9q_aAy_tGxJs7iS9ZSwfJexUdI" 2 | REACT_APP_authDomain="ensemble-web-studio.firebaseapp.com" 3 | REACT_APP_projectId="ensemble-web-studio" 4 | REACT_APP_storageBucket="ensemble-web-studio.appspot.com" 5 | REACT_APP_messagingSenderId="326748243798" 6 | REACT_APP_appId="1:326748243798:web:73a816313c3e70fb94b8f7" 7 | REACT_APP_measurementId="G-CEZ4918M26" 8 | -------------------------------------------------------------------------------- /apps/starter/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["custom/react"], 3 | rules: { 4 | "unicorn/filename-case": "off", 5 | }, 6 | ignorePatterns: ["*.config.js", "src/ensemble/**"], 7 | }; 8 | -------------------------------------------------------------------------------- /apps/starter/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /apps/starter/craco.config.js: -------------------------------------------------------------------------------- 1 | const NodePolyfillPlugin = require("node-polyfill-webpack-plugin"); 2 | 3 | module.exports = { 4 | webpack: { 5 | configure: { 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.yaml$/i, 10 | type: "asset/source", 11 | }, 12 | { 13 | resourceQuery: /raw/, 14 | type: "asset/source", 15 | }, 16 | ], 17 | }, 18 | resolve: { 19 | fallback: { 20 | fs: false, 21 | }, 22 | }, 23 | }, 24 | plugins: { 25 | add: [ 26 | new NodePolyfillPlugin({ 27 | excludeAliases: ["console"], 28 | }), 29 | ], 30 | }, 31 | }, 32 | jest: { 33 | moduleNameMapper: { 34 | "^lodash-es$": "lodash", 35 | }, 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /apps/starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ensembleui/react-starter", 3 | "version": "0.4.70", 4 | "private": true, 5 | "dependencies": { 6 | "@ensembleui/react-framework": "workspace:*", 7 | "@ensembleui/react-runtime": "workspace:*", 8 | "firebase": "9.10.0", 9 | "react": "^18.2.0", 10 | "react-dom": "^18.2.0", 11 | "web-vitals": "^2.1.4" 12 | }, 13 | "devDependencies": { 14 | "@craco/craco": "^7.1.0", 15 | "@testing-library/jest-dom": "^5.17.0", 16 | "@testing-library/react": "^13.4.0", 17 | "@testing-library/user-event": "^13.5.0", 18 | "@types/jest": "^27.5.2", 19 | "@types/node": "^16.18.45", 20 | "@types/react": "^18.2.21", 21 | "@types/react-dom": "^18.2.7", 22 | "eslint-config-custom": "workspace:*", 23 | "https-browserify": "^1.0.0", 24 | "node-polyfill-webpack-plugin": "^2.0.1", 25 | "path-browserify": "^1.0.1", 26 | "react-scripts": "5.0.1", 27 | "stream-browserify": "^3.0.0", 28 | "stream-http": "^3.2.0", 29 | "tsconfig": "workspace:*", 30 | "typescript": "^4.9.5", 31 | "url": "^0.11.2", 32 | "util": "^0.12.5" 33 | }, 34 | "scripts": { 35 | "start": "craco start", 36 | "build": "craco build", 37 | "test": "craco test --watchAll=false", 38 | "eject": "react-scripts eject" 39 | }, 40 | "lint-staged": { 41 | "*.{ts,tsx}": "eslint --fix" 42 | }, 43 | "eslintConfig": { 44 | "extends": [ 45 | "react-app", 46 | "react-app/jest" 47 | ] 48 | }, 49 | "browserslist": { 50 | "production": [ 51 | ">0.2%", 52 | "not dead", 53 | "not op_mini all" 54 | ], 55 | "development": [ 56 | "last 1 chrome version", 57 | "last 1 firefox version", 58 | "last 1 safari version" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /apps/starter/public/ensemble.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": null 3 | } 4 | -------------------------------------------------------------------------------- /apps/starter/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnsembleUI/ensemble-react/d11f8fe7c3bbb7dd9ec03f74e2c4be85845e125a/apps/starter/public/favicon.ico -------------------------------------------------------------------------------- /apps/starter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 28 | 35 | React App 36 | 37 | 38 | 39 |
40 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /apps/starter/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnsembleUI/ensemble-react/d11f8fe7c3bbb7dd9ec03f74e2c4be85845e125a/apps/starter/public/logo192.png -------------------------------------------------------------------------------- /apps/starter/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnsembleUI/ensemble-react/d11f8fe7c3bbb7dd9ec03f74e2c4be85845e125a/apps/starter/public/logo512.png -------------------------------------------------------------------------------- /apps/starter/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /apps/starter/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /apps/starter/src/App.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Roboto:400,400italic,500,500italic,700,700italic,900,900italic,300italic,300,100italic,100); 2 | 3 | body { 4 | font-family: 'Roboto', sans-serif; 5 | } 6 | 7 | .App-logo { 8 | height: 40vmin; 9 | pointer-events: none; 10 | } 11 | 12 | @media (prefers-reduced-motion: no-preference) { 13 | .App-logo { 14 | animation: App-logo-spin infinite 20s linear; 15 | } 16 | } 17 | 18 | .App-header { 19 | background-color: #282c34; 20 | min-height: 100vh; 21 | display: flex; 22 | flex-direction: column; 23 | align-items: center; 24 | justify-content: center; 25 | font-size: calc(10px + 2vmin); 26 | color: white; 27 | } 28 | 29 | .App-link { 30 | color: #61dafb; 31 | } 32 | 33 | @keyframes App-logo-spin { 34 | from { 35 | transform: rotate(0deg); 36 | } 37 | to { 38 | transform: rotate(360deg); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/starter/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import "react"; 2 | 3 | test.todo("Renders your Ensemble app"); 4 | -------------------------------------------------------------------------------- /apps/starter/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { initializeApp } from "firebase/app"; 3 | import { initializeFirestore } from "firebase/firestore/lite"; 4 | import { EnsembleApp } from "@ensembleui/react-runtime"; 5 | import { getFirestoreApplicationLoader } from "@ensembleui/react-framework"; 6 | 7 | import "./App.css"; 8 | 9 | const firebaseApp = initializeApp({ 10 | apiKey: process.env.REACT_APP_apiKey, 11 | authDomain: process.env.REACT_APP_authDomain, 12 | projectId: process.env.REACT_APP_projectId, 13 | storageBucket: process.env.REACT_APP_storageBucket, 14 | messagingSenderId: process.env.REACT_APP_messagingSenderId, 15 | appId: process.env.REACT_APP_appId, 16 | measurementId: process.env.REACT_APP_measurementId, 17 | }); 18 | const db = initializeFirestore(firebaseApp, {}); 19 | const firestoreLoader = getFirestoreApplicationLoader(db); 20 | 21 | interface EnsembleConfig { 22 | appId?: string; 23 | } 24 | 25 | const App: React.FC = () => { 26 | const [appId, setAppId] = useState(); 27 | useEffect(() => { 28 | const getEnsembleConfig = async (): Promise => { 29 | const response = await fetch("/ensemble.config.json"); 30 | const config = (await response.json()) as EnsembleConfig; 31 | if (config.appId) { 32 | setAppId(config.appId); 33 | } else { 34 | throw Error("Please set your appId in your ensemble.config.json file"); 35 | } 36 | }; 37 | void getEnsembleConfig(); 38 | }, []); 39 | 40 | if (!appId) { 41 | return null; 42 | } 43 | 44 | return ( 45 |
46 | 47 |
48 | ); 49 | }; 50 | 51 | export default App; 52 | -------------------------------------------------------------------------------- /apps/starter/src/ensemble/config.yaml: -------------------------------------------------------------------------------- 1 | environmentVariables: 2 | mode: DEVELOPMENT 3 | -------------------------------------------------------------------------------- /apps/starter/src/ensemble/index.ts: -------------------------------------------------------------------------------- 1 | import { type ApplicationDTO } from "@ensembleui/react-framework"; 2 | // Screens 3 | import MenuYAML from "./screens/menu.yaml"; 4 | import HomeYAML from "./screens/home.yaml"; 5 | import HelpYAML from "./screens/help.yaml"; 6 | // Widgets 7 | import HeaderWidgetYAML from "./widgets/Header.yaml"; 8 | // Scripts 9 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 10 | // @ts-expect-error 11 | import HelloScriptJs from "./scripts/hello.js?raw"; 12 | // Config 13 | import ConfigYAML from "./config.yaml"; 14 | // Theme 15 | import ThemeYAML from "./theme.yaml"; 16 | 17 | // Construct our app programmatically. This can also be served remotely or 18 | // even generated server side. 19 | export const starterApp: ApplicationDTO = { 20 | id: "myStarterApp", 21 | name: "My App", 22 | theme: { 23 | id: "theme", 24 | content: String(ThemeYAML), 25 | }, 26 | config: ConfigYAML, 27 | scripts: [ 28 | { 29 | id: "hello", 30 | name: "hello.js", 31 | content: String(HelloScriptJs), 32 | }, 33 | ], 34 | widgets: [ 35 | { 36 | id: "Header", 37 | name: "Header", 38 | content: String(HeaderWidgetYAML), 39 | }, 40 | ], 41 | screens: [ 42 | { 43 | id: "menu", 44 | name: "Menu", 45 | content: String(MenuYAML), 46 | }, 47 | { 48 | id: "home", 49 | name: "Home", 50 | content: String(HomeYAML), 51 | }, 52 | { 53 | id: "help", 54 | name: "Help", 55 | content: String(HelpYAML), 56 | }, 57 | ], 58 | }; 59 | -------------------------------------------------------------------------------- /apps/starter/src/ensemble/screens/help.yaml: -------------------------------------------------------------------------------- 1 | View: 2 | header: 3 | title: 4 | Header: 5 | 6 | body: 7 | Column: 8 | styles: 9 | names: page 10 | children: 11 | - Text: 12 | styles: 13 | names: heading-1 14 | text: Help 15 | - Markdown: 16 | text: More to come! In the meantime, checkout the Ensemble [documentation](https://docs.ensembleui.com/). 17 | -------------------------------------------------------------------------------- /apps/starter/src/ensemble/screens/home.yaml: -------------------------------------------------------------------------------- 1 | View: 2 | onLoad: 3 | invokeAPI: 4 | name: getDummyProducts 5 | onResponse: 6 | executeCode: 7 | scriptName: hello.js 8 | header: 9 | title: 10 | Header: 11 | 12 | body: 13 | Column: 14 | styles: 15 | names: page 16 | children: 17 | - Text: 18 | styles: 19 | names: heading-1 20 | text: Hello, world! 21 | - Markdown: 22 | text: This screen demonstrates a few patterns to get you started with Ensemble. Here, we fetch data from an API and use control expressions to display a loading state and a list of products. Try making a few changes and see how the screen automatically updates! For more information, see [help](/help). 23 | - Text: 24 | styles: 25 | names: heading-3 26 | text: My Products 27 | - LoadingContainer: 28 | isLoading: ${getDummyProducts.isLoading} 29 | widget: 30 | GridView: 31 | item-template: 32 | data: ${ensemble.storage.get("products")} 33 | name: product 34 | template: 35 | Card: 36 | children: 37 | - Text: 38 | text: ${product.title} 39 | 40 | API: 41 | getDummyProducts: 42 | method: GET 43 | uri: https://dummyjson.com/products 44 | -------------------------------------------------------------------------------- /apps/starter/src/ensemble/screens/menu.yaml: -------------------------------------------------------------------------------- 1 | ViewGroup: 2 | SideBar: 3 | styles: 4 | width: 240px 5 | iconWidth: 20px 6 | iconHeight: 20px 7 | backgroundColor: '#f7fbfa' 8 | labelColor: "#171B2A" 9 | selectedColor: "#171B2A" 10 | labelFontSize: 1 11 | onSelectStyles: 12 | backgroundColor: "#BBDBD2" 13 | borderRadius: 10px 14 | header: 15 | Image: 16 | height: 50px 17 | source: https://docs.ensembleui.com/images/logo.svg 18 | items: 19 | - label: Home 20 | icon: HomeOutlined 21 | page: home 22 | selected: true 23 | - label: Help 24 | icon: HelpOutlineOutlined 25 | page: help 26 | footer: 27 | Text: 28 | text: Give us feedback 29 | -------------------------------------------------------------------------------- /apps/starter/src/ensemble/scripts/hello.js: -------------------------------------------------------------------------------- 1 | console.log("hello data manipulation! check out the response below:"); 2 | console.log(response); 3 | 4 | ensemble.storage.set("products", response.body.products); 5 | -------------------------------------------------------------------------------- /apps/starter/src/ensemble/theme.yaml: -------------------------------------------------------------------------------- 1 | Tokens: 2 | Colors: 3 | primary: 4 | '900': '#4c1e91' 5 | '800': '#5b22b2' 6 | '700': '#6d29d4' 7 | '600': '#7c3be7' 8 | '500': '#8b5df1' 9 | '400': '#a78bf6' 10 | '300': '#c4b5fa' 11 | '200': '#ddd6fc' 12 | '100': '#ede9fd' 13 | '50': '#f5f3f3' 14 | light: 15 | '300': '#f7fbfa' 16 | '200': '#fafafb' 17 | '100': '#ffffff' 18 | dark: 19 | '300': '#1F5348' 20 | '200': '#4E485D' 21 | '100': '#000000' 22 | Spacing: 23 | pagePadding: 20px 24 | Animation: 25 | Typography: 26 | fontFamily: "Roboto" 27 | fontSize: 28 | '54': 54px 29 | '48': 48px 30 | '42': 42px 31 | '36': 36px 32 | '32': 32px 33 | '28': 28px 34 | '26': 26px 35 | '24': 24px 36 | '22': 22px 37 | '20': 20px 38 | '18': 18px 39 | '16': 16px 40 | '14': 14px 41 | '12': 12px 42 | fontWeight: 43 | black: 900 44 | extraBold: 800 45 | bold: 700 46 | semiBold: 600 47 | medium: 500 48 | regular: 400 49 | light: 300 50 | extraLight: 200 51 | thin: 100 52 | Styles: 53 | page: 54 | padding: ${spacing.pagePadding} 55 | heading-1: 56 | fontSize: ${typography.fontSize['32']} 57 | fontWeight: ${typography.fontWeight.medium} 58 | heading-3: 59 | fontSize: ${typography.fontSize['24']} 60 | fontWeight: ${typography.fontWeight.bold} 61 | -------------------------------------------------------------------------------- /apps/starter/src/ensemble/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.yaml" { 2 | const data: string; 3 | export default data; 4 | } 5 | -------------------------------------------------------------------------------- /apps/starter/src/ensemble/widgets/Header.yaml: -------------------------------------------------------------------------------- 1 | Widget: 2 | body: 3 | Row: 4 | backgroundColor: white 5 | mainAxis: spaceBetween 6 | crossAxis: center 7 | padding: 8 8 | styles: 9 | borderWidth: "0 0 1px 0" 10 | borderColor: "#D5DAEA" 11 | width: "100%" 12 | children: 13 | - Search: 14 | id: mySearch 15 | styles: 16 | width: 320px 17 | height: 40px 18 | borderRadius: 12 19 | borderWidth: 2 20 | borderStyle: solid 21 | borderColor: "#B8BED6" 22 | placeholder: Search 23 | item-template: 24 | data: searchUsers.body.users 25 | name: user 26 | template: 27 | Text: 28 | text: ${user.firstName} 29 | searchKey: email 30 | onSearch: 31 | invokeAPI: 32 | name: findUsers 33 | inputs: 34 | search: ${search} 35 | - Row: 36 | styles: 37 | gap: 10 38 | children: 39 | - Avatar: 40 | styles: 41 | backgroundColor: "#171B2A" 42 | alt: abc 43 | name: Peter Parker 44 | -------------------------------------------------------------------------------- /apps/starter/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /apps/starter/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 8 | const root = createRoot(document.getElementById("root")!); 9 | root.render( 10 | 11 | 12 | , 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /apps/starter/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/starter/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import type { ReportHandler } from "web-vitals"; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler): void => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | void import("web-vitals").then( 6 | ({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 7 | getCLS(onPerfEntry); 8 | getFID(onPerfEntry); 9 | getFCP(onPerfEntry); 10 | getLCP(onPerfEntry); 11 | getTTFB(onPerfEntry); 12 | }, 13 | ); 14 | } 15 | }; 16 | 17 | export default reportWebVitals; 18 | -------------------------------------------------------------------------------- /apps/starter/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /apps/starter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/react-app.json", 3 | "include": ["src"] 4 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "private": true, 4 | "scripts": { 5 | "build": "turbo run build", 6 | "dev": "turbo run dev", 7 | "lint": "turbo run lint", 8 | "test": "turbo run test", 9 | "format": "prettier --write \"**/*.{ts,tsx,md,yaml,json}\"", 10 | "release": "turbo run build --filter='./packages/*' && changeset publish", 11 | "schema:generate": "ts-node --esm ./scripts/generateSchema.ts" 12 | }, 13 | "devDependencies": { 14 | "@changesets/cli": "^2.26.2", 15 | "jest": "^27.5.1", 16 | "lint-staged": "^14.0.1", 17 | "lodash-es": "^4.17.21", 18 | "prettier": "^3.0.3", 19 | "ts-jest": "27.1.5", 20 | "ts-json-schema-generator": "^1.5.0", 21 | "ts-node": "^10.9.2", 22 | "tsconfig": "workspace:*", 23 | "turbo": "1.9.9" 24 | }, 25 | "pnpm": { 26 | "overrides": { 27 | "typescript": "4.9.5" 28 | } 29 | }, 30 | "name": "ensemble-react", 31 | "packageManager": "pnpm@6.32.2", 32 | "workspaces": [ 33 | "apps/*", 34 | "packages/*" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /packages/cli/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["oclif", "oclif-typescript", "prettier"], 3 | "ignorePatterns": ["dist/"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/cli/.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | **/.DS_Store 4 | /.idea 5 | /dist 6 | /tmp 7 | /node_modules 8 | oclif.manifest.json 9 | 10 | tsconfig.tsbuildinfo 11 | yarn.lock 12 | package-lock.json 13 | 14 | -------------------------------------------------------------------------------- /packages/cli/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": [ 3 | "ts-node/register" 4 | ], 5 | "watch-extensions": [ 6 | "ts" 7 | ], 8 | "recursive": true, 9 | "reporter": "spec", 10 | "timeout": 60000, 11 | "node-option": [ 12 | "loader=ts-node/esm", 13 | "experimental-specifier-resolution=node" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/cli/.prettierrc.json: -------------------------------------------------------------------------------- 1 | "@oclif/prettier-config" 2 | -------------------------------------------------------------------------------- /packages/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @ensembleui/cli 2 | 3 | ## 0.0.6 4 | 5 | ### Patch Changes 6 | 7 | - c7575aa: Use regular firebase instead of firebase lite 8 | - Updated dependencies [c7575aa] 9 | - @ensembleui/js-commons@0.0.11 10 | 11 | ## 0.0.5 12 | 13 | ### Patch Changes 14 | 15 | - 6ce699c: Fix CLI build 16 | 17 | ## 0.0.4 18 | 19 | ### Patch Changes 20 | 21 | - 0a37d2b: Add apps:get command for copying yaml to local folder 22 | - Updated dependencies [0a37d2b] 23 | - @ensembleui/js-commons@0.0.5 24 | 25 | ## 0.0.3 26 | 27 | ### Patch Changes 28 | 29 | - 3cd6fad: Fix js-commons dep versioning in CLI 30 | 31 | ## 0.0.2 32 | 33 | ### Patch Changes 34 | 35 | - f1ee4bf: Fix CLI dependencies 36 | - Updated dependencies [f1ee4bf] 37 | - @ensembleui/js-commons@0.0.4 38 | 39 | ## 0.0.1 40 | 41 | ### Patch Changes 42 | 43 | - af59a60: Initial release for cli 44 | - Updated dependencies [af59a60] 45 | - @ensembleui/js-commons@0.0.3 46 | -------------------------------------------------------------------------------- /packages/cli/bin/dev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %* 4 | -------------------------------------------------------------------------------- /packages/cli/bin/dev.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning 2 | 3 | // eslint-disable-next-line n/shebang 4 | import {execute} from '@oclif/core' 5 | 6 | await execute({development: true, dir: import.meta.url}) 7 | -------------------------------------------------------------------------------- /packages/cli/bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /packages/cli/bin/run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import {execute} from '@oclif/core' 4 | 5 | await execute({dir: import.meta.url}) 6 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ensembleui/cli", 3 | "description": "Ensemble CLI for managing apps", 4 | "version": "0.0.6", 5 | "author": "Ensemble", 6 | "bin": { 7 | "ensemble": "./bin/run.js" 8 | }, 9 | "bugs": "https://github.com/@ensembleui/ensemble-react/issues", 10 | "dependencies": { 11 | "@oclif/core": "^4", 12 | "@oclif/plugin-help": "^6", 13 | "@oclif/plugin-plugins": "^5", 14 | "axios": "^1.7.7", 15 | "firebase": "9.10.0", 16 | "jwt-decode": "^4.0.0", 17 | "lodash-es": "^4.17.21", 18 | "@ensembleui/js-commons": "*" 19 | }, 20 | "devDependencies": { 21 | "@ensembleui/js-commons": "workspace:*", 22 | "@oclif/prettier-config": "^0.2.1", 23 | "@oclif/test": "^4", 24 | "@types/chai": "^4", 25 | "@types/mocha": "^10", 26 | "@types/node": "^16.18.45", 27 | "chai": "^4", 28 | "eslint-plugin-unicorn": "56.0.0", 29 | "eslint-config-oclif": "^5", 30 | "eslint-config-oclif-typescript": "^3", 31 | "mocha": "^10", 32 | "oclif": "^4", 33 | "shx": "^0.3.3", 34 | "ts-node": "^10" 35 | }, 36 | "engines": { 37 | "node": ">=16.0.0" 38 | }, 39 | "files": [ 40 | "/bin", 41 | "/dist", 42 | "/oclif.manifest.json" 43 | ], 44 | "homepage": "https://github.com/@ensembleui/ensemble-react", 45 | "keywords": [ 46 | "oclif" 47 | ], 48 | "license": "MIT", 49 | "main": "dist/index.js", 50 | "type": "module", 51 | "oclif": { 52 | "bin": "ensemble", 53 | "dirname": "ensemble", 54 | "commands": "./dist/commands", 55 | "plugins": [ 56 | "@oclif/plugin-help", 57 | "@oclif/plugin-plugins" 58 | ], 59 | "topicSeparator": " ", 60 | "topics": { 61 | "hello": { 62 | "description": "Say hello to the world and others" 63 | } 64 | } 65 | }, 66 | "repository": "https://github.com/@ensembleui/ensemble-react", 67 | "scripts": { 68 | "build": "shx rm -rf dist && tsc -b", 69 | "lint": "eslint . --ext .ts", 70 | "postpack": "shx rm -f oclif.manifest.json", 71 | "posttest": "pnpm run lint", 72 | "prepack": "oclif manifest && oclif readme", 73 | "test": "mocha --forbid-only \"test/**/*.test.ts\"", 74 | "version": "oclif readme && git add README.md" 75 | }, 76 | "types": "dist/index.d.ts" 77 | } 78 | -------------------------------------------------------------------------------- /packages/cli/src/commands/apps/get.ts: -------------------------------------------------------------------------------- 1 | import { getFirestoreApplicationTransporter, getLocalApplicationTransporter } from '@ensembleui/js-commons' 2 | import {Args, Command, Flags} from '@oclif/core' 3 | import { get } from 'lodash-es' 4 | import path from 'node:path' 5 | 6 | import { db } from '../../firebase.js' 7 | import { getStoredToken, signInWithEmailPassword } from '../../utils.js' 8 | 9 | export default class AppsGet extends Command { 10 | static override args = { 11 | id: Args.string({description: 'Ensemble App ID', required: true}), 12 | } 13 | 14 | static override description = 'Get a local copy of the app' 15 | 16 | static override examples = [ 17 | '<%= config.bin %> <%= command.id %>', 18 | ] 19 | 20 | static override flags = { 21 | dir: Flags.string({char: 'd', description: 'Where the app should be copied to'}), 22 | } 23 | 24 | public async run(): Promise { 25 | const { args, flags } = await this.parse(AppsGet); 26 | const cachedAuth = getStoredToken(); 27 | if (!cachedAuth) { 28 | this.error('Please login first.') 29 | } 30 | 31 | const { email, password } = cachedAuth; 32 | await signInWithEmailPassword(email, password); 33 | 34 | const appId = args.id; 35 | const dir = path.join(process.cwd(), flags.dir ?? appId); 36 | 37 | try { 38 | const firestoreAppTransporter = getFirestoreApplicationTransporter(db); 39 | const localAppTransporter = getLocalApplicationTransporter(this.config.dataDir); 40 | const app = await firestoreAppTransporter.get(appId); 41 | await localAppTransporter.put(app, dir); 42 | 43 | this.log(`Wrote app ${app.name} to ${dir}`) 44 | } catch (error) { 45 | this.error(get(error, "message") ?? "An error occurred") 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/cli/src/commands/apps/list.ts: -------------------------------------------------------------------------------- 1 | import { getCloudApps } from '@ensembleui/js-commons' 2 | import {Command} from '@oclif/core' 3 | import { jwtDecode } from "jwt-decode"; 4 | import { get } from 'lodash-es'; 5 | 6 | import { db } from '../../firebase.js' 7 | import { getStoredToken, signInWithEmailPassword } from '../../utils.js' 8 | 9 | export default class AppsList extends Command { 10 | static override description = 'List all apps you have access to' 11 | 12 | static override examples = [ 13 | '<%= config.bin %> <%= command.id %>', 14 | ] 15 | 16 | public async run(): Promise { 17 | const cachedAuth = getStoredToken(); 18 | if (!cachedAuth) { 19 | this.error('Please login first.') 20 | } 21 | 22 | const { email, password } = cachedAuth; 23 | const token = await signInWithEmailPassword(email, password); 24 | 25 | const decodedToken = jwtDecode(token); 26 | 27 | const userId = get(decodedToken, 'userId'); 28 | if (!userId) { 29 | this.error('Invalid token. Please try logging in again.') 30 | } 31 | 32 | const result = await getCloudApps(db, userId) 33 | const list = result.map((appData) => ({ 34 | description: appData.description, 35 | id: appData.id, 36 | name: appData.name, 37 | role: appData.collaborators?.[`users_${userId}`] 38 | })); 39 | 40 | // TODO: tabulate results 41 | this.log(JSON.stringify(list, null, 2)) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/cli/src/commands/hello/index.ts: -------------------------------------------------------------------------------- 1 | import {Args, Command, Flags} from '@oclif/core' 2 | 3 | export default class Hello extends Command { 4 | static args = { 5 | person: Args.string({description: 'Person to say hello to', required: true}), 6 | } 7 | 8 | static description = 'Say hello' 9 | 10 | static examples = [ 11 | `<%= config.bin %> <%= command.id %> friend --from oclif 12 | hello friend from oclif! (./src/commands/hello/index.ts) 13 | `, 14 | ] 15 | 16 | static flags = { 17 | from: Flags.string({char: 'f', description: 'Who is saying hello', required: true}), 18 | } 19 | 20 | async run(): Promise { 21 | const {args, flags} = await this.parse(Hello) 22 | 23 | this.log(`hello ${args.person} from ${flags.from}! (./src/commands/hello/index.ts)`) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/cli/src/commands/hello/world.ts: -------------------------------------------------------------------------------- 1 | import {Command} from '@oclif/core' 2 | 3 | export default class World extends Command { 4 | static args = {} 5 | 6 | static description = 'Say hello world' 7 | 8 | static examples = [ 9 | `<%= config.bin %> <%= command.id %> 10 | hello world! (./src/commands/hello/world.ts) 11 | `, 12 | ] 13 | 14 | static flags = {} 15 | 16 | async run(): Promise { 17 | this.log('hello world! (./src/commands/hello/world.ts)') 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/cli/src/commands/login.ts: -------------------------------------------------------------------------------- 1 | import { Command, Flags } from '@oclif/core'; 2 | 3 | import { signInWithEmailPassword } from '../utils.js'; 4 | 5 | class LoginCommand extends Command { 6 | static description = 'Sign in to Ensemble with email and password'; 7 | 8 | static flags = { 9 | email: Flags.string({ char: 'e', description: 'User email', required: true }), 10 | password: Flags.string({ char: 'p', description: 'User password', required: true }), 11 | }; 12 | 13 | async run() { 14 | const { flags } = await this.parse(LoginCommand); 15 | const { email, password } = flags; 16 | 17 | try { 18 | const idToken = await signInWithEmailPassword(email, password); 19 | this.log('User signed in successfully.'); 20 | this.log(`ID Token: ${idToken}`); 21 | } catch (error) { 22 | this.error(error as Error); 23 | } 24 | } 25 | } 26 | 27 | export default LoginCommand; 28 | -------------------------------------------------------------------------------- /packages/cli/src/commands/update-password.ts: -------------------------------------------------------------------------------- 1 | import { Command, Flags } from '@oclif/core'; 2 | import axios, { AxiosError } from 'axios'; 3 | import { get } from "lodash-es" 4 | 5 | import { WEB_API_KEY , getStoredToken, signInWithEmailPassword, storeTokenFile } from '../utils.js'; 6 | 7 | class UpdatePasswordCommand extends Command { 8 | static description = 'Update a user\'s password in Ensemble'; 9 | 10 | static flags = { 11 | email: Flags.string({ char: 'e', description: 'Old password', required: true }), 12 | newPassword: Flags.string({ char: 'n', description: 'New password', required: true }), 13 | oldPassword: Flags.string({ char: 'o', description: 'Old password', required: true }), 14 | }; 15 | 16 | async run() { 17 | const { flags } = await this.parse(UpdatePasswordCommand); 18 | const { email, newPassword, oldPassword } = flags; 19 | 20 | try { 21 | let idToken: string | undefined = get(getStoredToken(), 'auth'); 22 | if (!idToken) { 23 | idToken = await signInWithEmailPassword(email, oldPassword); 24 | } 25 | 26 | // Step 2: Use the ID token to update the password 27 | const updatePasswordUrl = `https://identitytoolkit.googleapis.com/v1/accounts:update?key=${WEB_API_KEY}`; 28 | const updateResponse = await axios.post(updatePasswordUrl, { 29 | idToken, 30 | password: newPassword, 31 | returnSecureToken: true, 32 | }); 33 | 34 | this.log('Password updated successfully!'); 35 | this.log(`New ID Token: ${updateResponse.data.idToken}`); 36 | storeTokenFile(updateResponse); 37 | } catch (error) { 38 | const axiosError = error as AxiosError; 39 | this.error(`Error updating password: ${get(axiosError.response?.data, "error.message") || get(error, "message")}`); 40 | } 41 | } 42 | } 43 | 44 | export default UpdatePasswordCommand; 45 | -------------------------------------------------------------------------------- /packages/cli/src/firebase.ts: -------------------------------------------------------------------------------- 1 | import { initializeApp } from 'firebase/app'; 2 | import { initializeAuth } from 'firebase/auth'; 3 | import { getFirestore } from 'firebase/firestore'; 4 | 5 | export const firebaseConfig = { 6 | apiKey: "AIzaSyCz2PtDtfSybjPB5y0eGw4-gh3Aic55NFk", 7 | appId: "1:326748243798:web:36c3865e4ffbcc1c94b8f7", 8 | authDomain: "ensemble-web-studio.firebaseapp.com", 9 | databaseURL: "https://ensemble-web-studio-default-rtdb.firebaseio.com", 10 | measurementId: "G-ZMG2LD1NYR", 11 | messagingSenderId: "326748243798", 12 | projectId: "ensemble-web-studio", 13 | storageBucket: "ensemble-web-studio.appspot.com" 14 | } 15 | 16 | export const firebaseApp = initializeApp(firebaseConfig); 17 | 18 | export const db = getFirestore(firebaseApp); 19 | 20 | export const auth = initializeAuth(firebaseApp); -------------------------------------------------------------------------------- /packages/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | import './firebase.js' 2 | 3 | export {run} from '@oclif/core' -------------------------------------------------------------------------------- /packages/cli/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from 'axios'; 2 | import { signInWithEmailAndPassword } from 'firebase/auth'; 3 | import { get } from 'lodash-es'; 4 | import fs from 'node:fs'; 5 | import os from 'node:os'; 6 | import path from 'node:path'; 7 | 8 | import { auth } from './firebase.js'; 9 | 10 | const TEMP_TOKEN_FILE = path.join(os.homedir(), '.ensemble_token.json'); 11 | export const WEB_API_KEY = 'AIzaSyC-YeNdc9IRMQpuxxVB27UkUIv9KcfpRVg'; 12 | 13 | export async function signInWithEmailPassword(email: string, password: string): Promise { 14 | // const signInUrl = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=${WEB_API_KEY}`; 15 | 16 | try { 17 | // const response = await axios.post(signInUrl, { 18 | // email, 19 | // password, 20 | // returnSecureToken: true, 21 | // }); 22 | 23 | // const { idToken, refreshToken } = response.data; 24 | 25 | // // Store the ID token and refresh token in a temporary file 26 | // const tokenData = { 27 | // idToken, 28 | // refreshToken, 29 | // timestamp: new Date().toISOString(), 30 | // }; 31 | 32 | const userCredential = await signInWithEmailAndPassword(auth, email, password); 33 | const token = await userCredential.user.getIdToken(); 34 | 35 | storeTokenFile({ email, password, token }); 36 | 37 | return token; 38 | } catch (error) { 39 | const axiosError = error as AxiosError; 40 | throw new Error(`Login failed: ${get(axiosError.response?.data, "error.message") || axiosError.message}`); 41 | } 42 | } 43 | 44 | export function getStoredToken(): { email: string, password: string, token: string } | null { 45 | if (fs.existsSync(TEMP_TOKEN_FILE)) { 46 | const tokenData = JSON.parse(fs.readFileSync(TEMP_TOKEN_FILE, 'utf8')); 47 | return tokenData; 48 | } 49 | 50 | return null; 51 | } 52 | 53 | export function storeTokenFile(tokenData: unknown): void { 54 | fs.writeFileSync(TEMP_TOKEN_FILE, JSON.stringify(tokenData)); 55 | } -------------------------------------------------------------------------------- /packages/cli/test/commands/apps/get.test.ts: -------------------------------------------------------------------------------- 1 | import {runCommand} from '@oclif/test' 2 | import {expect} from 'chai' 3 | 4 | describe.skip('apps:get', () => { 5 | it('runs apps:get cmd', async () => { 6 | const {stdout} = await runCommand('apps:get') 7 | expect(stdout).to.contain('hello world') 8 | }) 9 | 10 | it('runs apps:get --name oclif', async () => { 11 | const {stdout} = await runCommand('apps:get --name oclif') 12 | expect(stdout).to.contain('hello oclif') 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /packages/cli/test/commands/apps/list.test.ts: -------------------------------------------------------------------------------- 1 | import {runCommand} from '@oclif/test' 2 | import {expect} from 'chai' 3 | 4 | describe('apps:list', () => { 5 | it.skip('runs apps:list cmd', async () => { 6 | const {stdout} = await runCommand('apps:list') 7 | expect(stdout).to.contain('hello world') 8 | }) 9 | 10 | it.skip('runs apps:list --name oclif', async () => { 11 | const {stdout} = await runCommand('apps:list --name oclif') 12 | expect(stdout).to.contain('hello oclif') 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /packages/cli/test/commands/hello/index.test.ts: -------------------------------------------------------------------------------- 1 | import {runCommand} from '@oclif/test' 2 | import {expect} from 'chai' 3 | 4 | describe('hello', () => { 5 | it.skip('runs hello', async () => { 6 | const {stdout} = await runCommand('hello friend --from oclif') 7 | expect(stdout).to.contain('hello friend from oclif!') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /packages/cli/test/commands/hello/world.test.ts: -------------------------------------------------------------------------------- 1 | import {runCommand} from '@oclif/test' 2 | import {expect} from 'chai' 3 | 4 | describe('hello world', () => { 5 | it.skip('runs hello world cmd', async () => { 6 | const {stdout} = await runCommand('hello world') 7 | expect(stdout).to.contain('hello world!') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /packages/cli/test/commands/login.test.ts: -------------------------------------------------------------------------------- 1 | import {runCommand} from '@oclif/test' 2 | import {expect} from 'chai' 3 | 4 | describe('login', () => { 5 | it.skip('runs login cmd', async () => { 6 | const {stdout} = await runCommand('login') 7 | expect(stdout).to.contain('hello world') 8 | }) 9 | 10 | it.skip('runs login --name oclif', async () => { 11 | const {stdout} = await runCommand('login --name oclif') 12 | expect(stdout).to.contain('hello oclif') 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /packages/cli/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "references": [ 7 | {"path": ".."} 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "Node16", 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "strict": true, 8 | "target": "es2022", 9 | "moduleResolution": "node16", 10 | "skipLibCheck": true, 11 | }, 12 | "include": ["./src/**/*"], 13 | "ts-node": { 14 | "esm": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # eslint-config-custom 2 | 3 | ## 0.0.1 4 | 5 | ### Patch Changes 6 | 7 | - 59be854: Add js-commons package 8 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/README.md: -------------------------------------------------------------------------------- 1 | # `@turbo/eslint-config` 2 | 3 | Collection of internal eslint configurations. 4 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/library.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /* 6 | * This is a custom ESLint configuration for use with 7 | * typescript packages. 8 | * 9 | * This config extends the Vercel Engineering Style Guide. 10 | * For more information, see https://github.com/vercel/style-guide 11 | * 12 | */ 13 | 14 | module.exports = { 15 | extends: [ 16 | "@vercel/style-guide/eslint/node", 17 | "@vercel/style-guide/eslint/typescript", 18 | ].map(require.resolve).concat("plugin:prettier/recommended"), 19 | plugins: ["prettier"], 20 | parserOptions: { 21 | project, 22 | }, 23 | globals: { 24 | React: true, 25 | JSX: true, 26 | }, 27 | settings: { 28 | "import/resolver": { 29 | typescript: { 30 | project, 31 | }, 32 | }, 33 | }, 34 | ignorePatterns: ["node_modules/", "dist/"], 35 | rules: { 36 | "prettier/prettier": "error" 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/next.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /* 6 | * This is a custom ESLint configuration for use with 7 | * Next.js apps. 8 | * 9 | * This config extends the Vercel Engineering Style Guide. 10 | * For more information, see https://github.com/vercel/style-guide 11 | * 12 | */ 13 | 14 | module.exports = { 15 | extends: [ 16 | "@vercel/style-guide/eslint/typescript", 17 | "@vercel/style-guide/eslint/browser", 18 | "@vercel/style-guide/eslint/react", 19 | "@vercel/style-guide/eslint/next", 20 | "eslint-config-turbo", 21 | ].map(require.resolve), 22 | parserOptions: { 23 | project, 24 | }, 25 | globals: { 26 | React: true, 27 | JSX: true, 28 | }, 29 | settings: { 30 | "import/resolver": { 31 | typescript: { 32 | project, 33 | }, 34 | }, 35 | }, 36 | ignorePatterns: ["node_modules/", "dist/"], 37 | // add rules configurations here 38 | rules: { 39 | "import/no-default-export": "off", 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-custom", 3 | "license": "MIT", 4 | "version": "0.0.1", 5 | "private": true, 6 | "devDependencies": { 7 | "prettier": "^3.0.3", 8 | "eslint": "^8", 9 | "@vercel/style-guide": "^4.0.2", 10 | "eslint-config-prettier": "^9.0.0", 11 | "eslint-config-turbo": "^1.10.12", 12 | "eslint-plugin-prettier": "^5.0.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/react.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /* 6 | * This is a custom ESLint configuration for use with 7 | * internal (bundled by their consumer) libraries 8 | * that utilize React. 9 | * 10 | * This config extends the Vercel Engineering Style Guide. 11 | * For more information, see https://github.com/vercel/style-guide 12 | * 13 | */ 14 | 15 | module.exports = { 16 | extends: [ 17 | "@vercel/style-guide/eslint/browser", 18 | "@vercel/style-guide/eslint/typescript", 19 | "@vercel/style-guide/eslint/react", 20 | ].map(require.resolve) 21 | .concat(["plugin:prettier/recommended", "plugin:react-hooks/recommended"]), 22 | plugins: ["prettier"], 23 | parserOptions: { 24 | project, 25 | }, 26 | globals: { 27 | JSX: true, 28 | }, 29 | settings: { 30 | "import/resolver": { 31 | typescript: { 32 | project, 33 | }, 34 | }, 35 | }, 36 | ignorePatterns: ["node_modules/", "dist/", ".eslintrc.js"], 37 | // add rules configurations here 38 | rules: { 39 | "prettier/prettier": "error", 40 | "import/no-default-export": "off", 41 | "@typescript-eslint/consistent-indexed-object-style": [1, "index-signature"], 42 | "react/function-component-definition": [ 43 | 2, 44 | { 45 | namedComponents: "arrow-function", 46 | }, 47 | ] 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /packages/framework/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["custom/react"], 3 | rules: { 4 | "unicorn/filename-case": [ 5 | "error", 6 | { 7 | "cases": { 8 | "camelCase": true 9 | } 10 | } 11 | ], 12 | "@typescript-eslint/no-invalid-void-type": [ 13 | "warn", 14 | { 15 | allowInGenericTypeArguments: true 16 | } 17 | ] 18 | }, 19 | ignorePatterns: ["tsup.config.ts"] 20 | }; 21 | -------------------------------------------------------------------------------- /packages/framework/README.md: -------------------------------------------------------------------------------- 1 | # `@ensembleui/react-framework` 2 | 3 | Provides the state management and react hooks for EnsembleUI React components. -------------------------------------------------------------------------------- /packages/framework/jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 3 | module.exports = { 4 | preset: "ts-jest", 5 | testEnvironment: "jsdom", 6 | moduleNameMapper: { 7 | "^lodash-es$": "lodash", 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/framework/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ensembleui/react-framework", 3 | "version": "0.2.20", 4 | "main": "./dist/index.js", 5 | "types": "./dist/index.d.ts", 6 | "files": [ 7 | "./dist/**", 8 | "README.md" 9 | ], 10 | "publishConfig": { 11 | "access": "public" 12 | }, 13 | "repository": "https://github.com/EnsembleUI/ensemble-react", 14 | "scripts": { 15 | "dev": "tsup --watch", 16 | "build": "tsup", 17 | "test": "jest", 18 | "lint": "eslint ." 19 | }, 20 | "lint-staged": { 21 | "*.{ts,tsx}": "eslint --fix" 22 | }, 23 | "peerDependencies": { 24 | "firebase": "9.10.0", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0" 27 | }, 28 | "devDependencies": { 29 | "@tanstack/react-query": "^5.61.0", 30 | "@testing-library/react": "^13.4.0", 31 | "@types/lodash-es": "^4.17.8", 32 | "@types/react": "^18.2.21", 33 | "acorn": "^8.11.2", 34 | "axios": "0.27.2", 35 | "dayjs": "^1.11.10", 36 | "eslint-config-custom": "workspace:*", 37 | "firebase": "9.10.0", 38 | "i18next": "^23.11.5", 39 | "jotai": "^2.8.2", 40 | "jotai-location": "^0.5.2", 41 | "jotai-optics": "^0.3.1", 42 | "lodash-es": "^4.17.21", 43 | "optics-ts": "^2.4.1", 44 | "react-fast-compare": "^3.2.2", 45 | "react-i18next": "^14.1.2", 46 | "tsconfig": "workspace:*", 47 | "tsup": "^7.2.0", 48 | "yaml": "^2.3.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/framework/src/__tests__/__resources__/helloworld.yaml: -------------------------------------------------------------------------------- 1 | View: 2 | header: 3 | title: Welcome 4 | body: 5 | Column: 6 | children: 7 | - Text: 8 | text: Peter Parker 9 | 10 | API: 11 | getDummyProducts: 12 | method: GET 13 | cache: true 14 | cacheTime: 10 15 | uri: https://randomuser.me/api/?results=1 16 | -------------------------------------------------------------------------------- /packages/framework/src/__tests__/__resources__/mycustomwidget.yaml: -------------------------------------------------------------------------------- 1 | Widget: 2 | onLoad: 3 | executeCode: | 4 | console.log("foo") 5 | inputs: 6 | - name 7 | body: 8 | Text: 9 | text: "bar" 10 | 11 | API: 12 | getDummyProducts: 13 | method: GET 14 | uri: https://dummyjson.com/products 15 | -------------------------------------------------------------------------------- /packages/framework/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./data"; 2 | export * from "./navigate"; 3 | export * from "./modal"; 4 | export * from "./toast"; 5 | -------------------------------------------------------------------------------- /packages/framework/src/api/toast.ts: -------------------------------------------------------------------------------- 1 | import { get, kebabCase } from "lodash-es"; 2 | import type { ShowToastAction } from "../shared/actions"; 3 | 4 | export const showToast = ( 5 | toastAction: ShowToastAction | undefined, 6 | toaster?: (...args: unknown[]) => void, 7 | ): void => { 8 | if (!toastAction || !toaster) { 9 | return; 10 | } 11 | 12 | const position = kebabCase( 13 | get(toastAction.options, "position", "bottom-right"), 14 | ); 15 | const type = get(toastAction.options, "type", "success"); 16 | 17 | toaster(toastAction.message, { 18 | position, 19 | type, 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/framework/src/appConfig.ts: -------------------------------------------------------------------------------- 1 | const appConfigTable: { [key: string]: AppConfig } = {}; 2 | 3 | export interface AppConfig { 4 | useMockResponse: boolean; 5 | } 6 | 7 | export const isUsingMockResponse = (appId: string | undefined): boolean => { 8 | if (!appId || !(appId in appConfigTable)) return false; 9 | return appConfigTable[appId].useMockResponse; 10 | }; 11 | 12 | export const setUseMockResponse = ( 13 | appId: string | undefined, 14 | value: boolean, 15 | ): void => { 16 | if (typeof appId === "undefined") return; 17 | appConfigTable[appId] = { useMockResponse: value }; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/framework/src/data/index.ts: -------------------------------------------------------------------------------- 1 | import type * as fetcher from "./fetcher"; 2 | 3 | export * from "./fetcher"; 4 | 5 | export interface Response { 6 | body?: string | object; 7 | statusCode?: number; 8 | headers?: fetcher.Headers; 9 | reasonPhrase?: string; 10 | isLoading: boolean; 11 | isSuccess: boolean; 12 | isError: boolean; 13 | } 14 | 15 | export interface WebSocketConnection { 16 | socket?: WebSocket; 17 | isConnected: boolean; 18 | } 19 | -------------------------------------------------------------------------------- /packages/framework/src/date/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dateFormatter"; 2 | -------------------------------------------------------------------------------- /packages/framework/src/date/utils/getPrettyDate.ts: -------------------------------------------------------------------------------- 1 | export const getPrettyDate = (date: Date): string => 2 | date.toLocaleDateString("en-US", { 3 | month: "short", 4 | day: "numeric", 5 | year: "numeric", 6 | }); 7 | -------------------------------------------------------------------------------- /packages/framework/src/date/utils/getPrettyTime.ts: -------------------------------------------------------------------------------- 1 | export const getPrettyTime = (date: Date): string => 2 | date.toLocaleTimeString("en-US", { 3 | hour: "numeric", 4 | minute: "numeric", 5 | hour12: true, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/framework/src/evaluate/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./evaluate"; 2 | export * from "./binding"; 3 | export * from "./context"; 4 | export * from "./mock"; 5 | -------------------------------------------------------------------------------- /packages/framework/src/evaluate/mock.ts: -------------------------------------------------------------------------------- 1 | import { isObject, isNumber, isEmpty } from "lodash-es"; 2 | import type { EnsembleMockResponse } from "../shared"; 3 | 4 | export const mockResponse = ( 5 | evaluatedMockResponse: string | EnsembleMockResponse | undefined, 6 | isUsingMockResponse: boolean, 7 | ): { 8 | isLoading: boolean; 9 | isSuccess: boolean; 10 | isError: boolean; 11 | statusCode: number; 12 | body: object | string; 13 | headers?: { [key: string]: string }; 14 | reasonPhrase?: string; 15 | } => { 16 | if (!isUsingMockResponse) 17 | return { 18 | isLoading: false, 19 | isSuccess: false, 20 | isError: true, 21 | statusCode: 500, 22 | body: "Not using mock response", 23 | }; 24 | if (isEmpty(evaluatedMockResponse) || !isObject(evaluatedMockResponse)) 25 | throw new Error( 26 | "Improperly formatted mock response: Malformed mockResponse object", 27 | ); 28 | 29 | if (!isNumber(evaluatedMockResponse.statusCode)) 30 | throw new Error( 31 | "Improperly formatted mock response: Incorrect Status Code. Please check that you have included a status code and that it is a number", 32 | ); 33 | 34 | const isSuccess = 35 | evaluatedMockResponse.statusCode >= 200 && 36 | evaluatedMockResponse.statusCode <= 299; 37 | const mockRes = { 38 | ...evaluatedMockResponse, 39 | isLoading: false, 40 | isSuccess, 41 | isError: !isSuccess, 42 | }; 43 | return mockRes; 44 | }; 45 | -------------------------------------------------------------------------------- /packages/framework/src/hooks/__tests__/useEnsembleStorage.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from "@testing-library/react"; 2 | import { getDefaultStore } from "jotai"; 3 | import { screenStorageAtom, useEnsembleStorage } from "../useEnsembleStorage"; 4 | 5 | const store = getDefaultStore(); 6 | 7 | describe("useEnsembleStorage", () => { 8 | test("fetches variable from storage", () => { 9 | store.set(screenStorageAtom, { 10 | test: "value", 11 | }); 12 | const { result } = renderHook(() => useEnsembleStorage()); 13 | 14 | const storage = result.current; 15 | 16 | expect(storage.get("test")).toBe("value"); 17 | }); 18 | 19 | test("sets variable in storage", () => { 20 | const { result } = renderHook(() => useEnsembleStorage()); 21 | 22 | const storage = result.current; 23 | 24 | act(() => { 25 | storage.set("test", "bar"); 26 | }); 27 | 28 | expect(storage.get("test")).toBe("bar"); 29 | }); 30 | 31 | test("overwrites existing keys", () => { 32 | store.set(screenStorageAtom, { 33 | test: { 34 | foo: ["bar", "baz"], 35 | dead: { 36 | beef: "hello", 37 | }, 38 | }, 39 | }); 40 | 41 | const { result } = renderHook(() => useEnsembleStorage()); 42 | 43 | const storage = result.current; 44 | 45 | act(() => { 46 | storage.set("test", { 47 | foo: ["baz", "bar"], 48 | beef: { 49 | dead: "world", 50 | }, 51 | }); 52 | }); 53 | 54 | expect(storage.get("test")).toMatchObject({ 55 | foo: ["baz", "bar"], 56 | beef: { 57 | dead: "world", 58 | }, 59 | }); 60 | }); 61 | 62 | test("deletes keys", () => { 63 | store.set(screenStorageAtom, { 64 | test: { 65 | foo: ["bar", "baz"], 66 | dead: { 67 | beef: "hello", 68 | }, 69 | }, 70 | }); 71 | 72 | const { result } = renderHook(() => useEnsembleStorage()); 73 | 74 | const storage = result.current; 75 | 76 | act(() => { 77 | storage.delete("test"); 78 | }); 79 | 80 | expect(storage.get("test")).toBeUndefined(); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /packages/framework/src/hooks/__tests__/useEnsembleUser.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from "@testing-library/react"; 2 | import { getDefaultStore } from "jotai"; 3 | import { get } from "lodash-es"; 4 | import { userAtom } from "../../state"; 5 | import { useEnsembleUser } from "../useEnsembleUser"; 6 | 7 | const store = getDefaultStore(); 8 | 9 | describe("useEnsembleUser", () => { 10 | test("fetches variable from ensemble.user", () => { 11 | store.set(userAtom, { 12 | accessToken: "eyJabcd", 13 | }); 14 | const { result } = renderHook(() => useEnsembleUser()); 15 | 16 | const user = result.current; 17 | 18 | expect(get(user, "accessToken")).toBe("eyJabcd"); 19 | }); 20 | 21 | test("sets variable in ensemble.user", () => { 22 | const { result } = renderHook(() => useEnsembleUser()); 23 | 24 | const user = result.current; 25 | 26 | act(() => { 27 | user.set({ test: "bar" }); 28 | }); 29 | 30 | expect(get(store.get(userAtom), "test")).toBe("bar"); 31 | }); 32 | 33 | test("overwrites existing keys", () => { 34 | store.set(userAtom, { 35 | foo: ["bar", "baz"], 36 | dead: { 37 | beef: "hello", 38 | }, 39 | }); 40 | 41 | const { result } = renderHook(() => useEnsembleUser()); 42 | 43 | const user = result.current; 44 | 45 | act(() => { 46 | user.set({ 47 | foo: ["foo"], 48 | beef: { 49 | dead: "world", 50 | }, 51 | }); 52 | }); 53 | 54 | expect(store.get(userAtom)).toMatchObject({ 55 | foo: ["foo"], 56 | beef: { 57 | dead: "world", 58 | }, 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/framework/src/hooks/__tests__/useWidgetId.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from "@testing-library/react"; 2 | import { useWidgetId } from "../useWidgetId"; 3 | 4 | test("generates a valid ID when no ID is provided", () => { 5 | const { result } = renderHook(() => useWidgetId()); 6 | expect(result.current.resolvedWidgetId).toMatch(/^[a-zA-Z]+$/); 7 | expect(result.current.resolvedTestId).toBeUndefined(); 8 | }); 9 | 10 | test("uses the provided ID when valid", () => { 11 | const { result } = renderHook(() => useWidgetId("validId")); 12 | expect(result.current.resolvedWidgetId).toBe("validId"); 13 | }); 14 | 15 | test("generates a random ID when provided ID is invalid", () => { 16 | const { result } = renderHook(() => useWidgetId("123 invalid")); 17 | expect(result.current.resolvedWidgetId).not.toBe("123 invalid"); 18 | }); 19 | 20 | test("preserves test ID when provided", () => { 21 | const { result } = renderHook(() => useWidgetId("validId", "test-123")); 22 | expect(result.current.resolvedWidgetId).toBe("validId"); 23 | expect(result.current.resolvedTestId).toBe("test-123"); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/framework/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useRegisterBindings"; 2 | export * from "./useApplicationContext"; 3 | export * from "./useScreenContext"; 4 | export * from "./useCustomScope"; 5 | export * from "./useTemplateData"; 6 | export * from "./useWidgetId"; 7 | export * from "./useEnsembleStorage"; 8 | export * from "./useEvaluate"; 9 | export * from "./useStyles"; 10 | export * from "./useHtmlPassThrough"; 11 | export * from "./useEnsembleUser"; 12 | export * from "./useThemeContext"; 13 | export * from "./useEventContext"; 14 | export * from "./useLanguageScope"; 15 | export * from "./useScreenData"; 16 | export * from "./useDeviceObserver"; 17 | export * from "./useCommandCallback"; 18 | -------------------------------------------------------------------------------- /packages/framework/src/hooks/useCustomScope.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react"; 2 | 3 | export interface CustomScope { 4 | [key: string]: unknown; 5 | } 6 | type CustomScopeProps = { 7 | value?: CustomScope; 8 | } & React.PropsWithChildren; 9 | 10 | export const CustomScopeContext = createContext( 11 | undefined, 12 | ); 13 | 14 | export const CustomScopeProvider: React.FC = ({ 15 | children, 16 | value, 17 | }) => { 18 | const parentScope = useCustomScope(); 19 | return ( 20 | 21 | {children} 22 | 23 | ); 24 | }; 25 | 26 | export const useCustomScope = (): CustomScope | undefined => { 27 | const scope = useContext(CustomScopeContext); 28 | return scope; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/framework/src/hooks/useEnsembleUser.ts: -------------------------------------------------------------------------------- 1 | import { useAtom } from "jotai"; 2 | import { assign } from "lodash-es"; 3 | import { useMemo } from "react"; 4 | import { userAtom, type EnsembleUser } from "../state"; 5 | 6 | interface EnsembleUserBuffer { 7 | set: (items: { [key: string]: unknown }) => void; 8 | } 9 | 10 | export const useEnsembleUser = (): EnsembleUser & EnsembleUserBuffer => { 11 | const [user, setUser] = useAtom(userAtom); 12 | 13 | const storageBuffer = useMemo( 14 | () => ({ 15 | set: (items: { [key: string]: unknown }): void => { 16 | const updatedUser = assign({}, user, items); 17 | setUser(updatedUser); 18 | window.dispatchEvent( 19 | new StorageEvent("storage", { key: "ensemble.user" }), 20 | ); 21 | }, 22 | get: (key: string): unknown => { 23 | return user[key]; 24 | }, 25 | }), 26 | [setUser, user], 27 | ); 28 | 29 | return { ...storageBuffer, ...user }; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/framework/src/hooks/useEventContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react"; 2 | 3 | export interface CustomEventScope { 4 | [key: string]: unknown; 5 | } 6 | 7 | type CustomEventScopeProviderProps = { 8 | value?: CustomEventScope; 9 | } & React.PropsWithChildren; 10 | 11 | export const CustomEventScopeContext = createContext< 12 | CustomEventScope | undefined 13 | >(undefined); 14 | 15 | export const useCustomEventScope = (): CustomEventScope | undefined => { 16 | return useContext(CustomEventScopeContext); 17 | }; 18 | 19 | export const CustomEventScopeProvider: React.FC< 20 | CustomEventScopeProviderProps 21 | > = ({ children, value }) => { 22 | const parentScope = useCustomEventScope(); 23 | return ( 24 | 25 | {children} 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/framework/src/hooks/useFonts.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { error, type EnsembleFontModel } from "../shared"; 3 | 4 | export const useFonts = (fonts?: EnsembleFontModel[]): void => { 5 | useEffect(() => { 6 | if (fonts) { 7 | fonts.forEach((font: EnsembleFontModel) => { 8 | new FontFace(font.family, `url(${font.url})`, font.options) 9 | .load() 10 | .then((loadedFont) => document.fonts.add(loadedFont)) 11 | .catch((e) => error(e)); 12 | }); 13 | } 14 | }, [fonts]); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/framework/src/hooks/useHtmlPassThrough.tsx: -------------------------------------------------------------------------------- 1 | import type { RefCallback } from "react"; 2 | import { mapKeys, isObject, get } from "lodash-es"; 3 | import { useCallback } from "react"; 4 | import { error } from "../shared"; 5 | 6 | export const useHtmlPassThrough = ( 7 | htmlAttributes?: { [key: string]: string }, 8 | testId?: string, 9 | ): { rootRef: RefCallback } => { 10 | const rootRef = useCallback( 11 | (node: never) => { 12 | let element: any; 13 | if (node) { 14 | if ("setAttribute" in node) { 15 | element = node; 16 | } else { 17 | element = get(node, "nativeElement"); 18 | } 19 | } 20 | if (element && "setAttribute" in element) { 21 | if (isObject(htmlAttributes)) { 22 | const htmlAttributesObj = mapKeys(htmlAttributes, (_, key) => 23 | key.toLowerCase(), 24 | ); 25 | 26 | Object.keys(htmlAttributesObj).forEach((key: string) => { 27 | try { 28 | (element as HTMLElement).setAttribute( 29 | key, 30 | htmlAttributesObj[key], 31 | ); 32 | } catch (e) { 33 | error(e); 34 | } 35 | }); 36 | } 37 | if (testId) { 38 | try { 39 | (element as HTMLElement).setAttribute("data-testid", testId); 40 | } catch (e) { 41 | error(e); 42 | } 43 | } 44 | } 45 | }, 46 | [testId, htmlAttributes], 47 | ); 48 | 49 | return { rootRef }; 50 | }; 51 | -------------------------------------------------------------------------------- /packages/framework/src/hooks/useLanguageScope.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "react-i18next"; 2 | 3 | export interface CustomLanguageScope { 4 | [key: string]: unknown; 5 | } 6 | export const useLanguageScope = () => { 7 | return useTranslation(); 8 | }; 9 | -------------------------------------------------------------------------------- /packages/framework/src/hooks/useScreenData.ts: -------------------------------------------------------------------------------- 1 | import { useAtom, useAtomValue } from "jotai"; 2 | import { useCallback, useMemo } from "react"; 3 | import isEqual from "react-fast-compare"; 4 | import type { Response, WebSocketConnection } from "../data"; 5 | import type { 6 | EnsembleAPIModel, 7 | EnsembleMockResponse, 8 | EnsembleSocketModel, 9 | } from "../shared"; 10 | import type { ScreenContextData } from "../state"; 11 | import { screenApiAtom, screenSocketAtom, screenDataAtom } from "../state"; 12 | import { useEvaluate } from "./useEvaluate"; 13 | 14 | export const useScreenData = (): { 15 | apis?: EnsembleAPIModel[]; 16 | sockets?: EnsembleSocketModel[]; 17 | data: ScreenContextData; 18 | setData: ( 19 | name: string, 20 | response: Partial | WebSocketConnection, 21 | ) => void; 22 | mockResponses: { 23 | [apiName: string]: EnsembleMockResponse | string | undefined; 24 | }; 25 | } => { 26 | const apis = useAtomValue(screenApiAtom); 27 | const sockets = useAtomValue(screenSocketAtom); 28 | const [data, setDataAtom] = useAtom(screenDataAtom); 29 | 30 | const apiMockResponses = useMemo(() => { 31 | return apis?.reduce( 32 | ( 33 | acc: { [key: string]: EnsembleMockResponse | string | undefined }, 34 | api, 35 | ) => { 36 | acc[api.name] = api.mockResponse; 37 | return acc; 38 | }, 39 | {}, 40 | ); 41 | }, [apis]); 42 | 43 | const mockResponses = useEvaluate(apiMockResponses); 44 | 45 | const setData = useCallback( 46 | (name: string, response: Partial | WebSocketConnection) => { 47 | if (isEqual(data[name], response)) { 48 | return; 49 | } 50 | data[name] = response; 51 | setDataAtom({ [name]: response }); 52 | }, 53 | [data, setDataAtom], 54 | ); 55 | 56 | return { 57 | apis, 58 | sockets, 59 | data, 60 | setData, 61 | mockResponses, 62 | }; 63 | }; 64 | -------------------------------------------------------------------------------- /packages/framework/src/hooks/useThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import { useAtom } from "jotai"; 2 | import { createContext } from "react"; 3 | import { type EnsembleThemeModel } from "../shared"; 4 | import { defaultThemeDefinition, themeAtom } from "../state"; 5 | 6 | export type CustomTheme = { [key: string]: unknown } & { 7 | theme: EnsembleThemeModel; 8 | themeName?: string; 9 | setTheme?: (name: string) => void; 10 | }; 11 | 12 | export const CustomThemeContext = createContext({ 13 | theme: defaultThemeDefinition, 14 | }); 15 | 16 | export const useThemeScope = (): { 17 | theme: EnsembleThemeModel; 18 | themeName: string; 19 | setTheme: (name: string) => void; 20 | } => { 21 | const [theme, setTheme] = useAtom(themeAtom); 22 | 23 | return { 24 | theme, 25 | themeName: theme.name, 26 | setTheme, 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/framework/src/hooks/useWidgetState.ts: -------------------------------------------------------------------------------- 1 | import { useAtom } from "jotai"; 2 | import type { WidgetState } from "../state"; 3 | import { widgetFamilyAtom } from "../state"; 4 | 5 | export const useWidgetState = >( 6 | id: string, 7 | ): [WidgetState | undefined, (state: WidgetState) => void] => { 8 | const widgetState = useAtom(widgetFamilyAtom(id)); 9 | return widgetState as [ 10 | WidgetState | undefined, 11 | (state: WidgetState) => void, 12 | ]; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/framework/src/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from "i18next"; 2 | import { initReactI18next } from "react-i18next"; 3 | import { debug, error } from "./shared"; 4 | 5 | export const languageMap: { [key: string]: string } = { 6 | ar: "Arabic", 7 | bn: "Bengali", 8 | de: "German", 9 | en: "English", 10 | es: "Spanish", 11 | fr: "French", 12 | hi: "Hindi", 13 | id: "Indonesian", 14 | it: "Italian", 15 | ja: "Japanese", 16 | jv: "Javanese", 17 | ko: "Korean", 18 | ms: "Malay", 19 | nl: "Dutch", 20 | pa: "Punjabi", 21 | pl: "Polish", 22 | pt: "Portuguese", 23 | ro: "Romanian", 24 | ru: "Russian", 25 | sv: "Swedish", 26 | ta: "Tamil", 27 | te: "Telugu", 28 | th: "Thai", 29 | tr: "Turkish", 30 | uk: "Ukrainian", 31 | ur: "Urdu", 32 | vi: "Vietnamese", 33 | zh: "Chinese", 34 | el: "Greek", 35 | da: "Danish", 36 | }; 37 | 38 | i18n 39 | .use(initReactI18next) 40 | .init({ 41 | resources: {}, 42 | lng: "en", 43 | debug: process.env.NODE_ENV === "development", 44 | fallbackLng: "en", 45 | interpolation: { 46 | escapeValue: false, // not needed for react as it escapes by default 47 | }, 48 | react: { 49 | useSuspense: false, 50 | }, 51 | }) 52 | .then(() => { 53 | debug("i18next initialized successfully"); 54 | }) 55 | .catch((e) => { 56 | error(e); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/framework/src/index.ts: -------------------------------------------------------------------------------- 1 | import "./i18n"; 2 | 3 | export * from "./shared"; 4 | export * from "./loader"; 5 | export * from "./serializer"; 6 | export * from "./parser"; 7 | export * from "./hooks"; 8 | export * from "./data"; 9 | export * from "./date"; 10 | export * from "./state"; 11 | export * from "./evaluate"; 12 | export * from "./appConfig"; 13 | -------------------------------------------------------------------------------- /packages/framework/src/serializer.ts: -------------------------------------------------------------------------------- 1 | import type { Firestore } from "firebase/firestore/lite"; 2 | import { collection, doc, setDoc, writeBatch } from "firebase/firestore/lite"; 3 | import type { ApplicationDTO } from "./shared"; 4 | 5 | export const serializeApp = async ( 6 | db: Firestore, 7 | app: ApplicationDTO, 8 | userId: string, 9 | ) => { 10 | const appDocRef = doc(db, "apps", app.id); 11 | const { screens, widgets, theme, scripts, ...appData } = app; 12 | await setDoc(appDocRef, { 13 | ...appData, 14 | isReact: true, 15 | isPublic: false, 16 | isArchived: false, 17 | collaborators: { [`users_${userId}`]: "owner" }, 18 | }); 19 | 20 | const batch = writeBatch(db); 21 | screens.forEach((screen) => { 22 | const screenRef = doc(collection(appDocRef, "artifacts")); 23 | batch.set( 24 | screenRef, 25 | { 26 | ...screen, 27 | type: "screen", 28 | isPublic: false, 29 | isArchived: false, 30 | }, 31 | { 32 | merge: true, 33 | }, 34 | ); 35 | }); 36 | 37 | widgets.forEach((widget) => { 38 | const widgetRef = doc(collection(appDocRef, "internal_artifacts")); 39 | batch.set( 40 | widgetRef, 41 | { 42 | ...widget, 43 | type: "internal_widget", 44 | isPublic: false, 45 | isArchived: false, 46 | }, 47 | { 48 | merge: true, 49 | }, 50 | ); 51 | }); 52 | 53 | scripts.forEach((script) => { 54 | const scriptRef = doc(collection(appDocRef, "internal_artifacts")); 55 | batch.set( 56 | scriptRef, 57 | { 58 | ...script, 59 | type: "internal_script", 60 | isPublic: false, 61 | isArchived: false, 62 | }, 63 | { 64 | merge: true, 65 | }, 66 | ); 67 | }); 68 | 69 | const newThemeRef = doc(collection(appDocRef, "artifacts")); 70 | batch.set( 71 | newThemeRef, 72 | { 73 | ...theme, 74 | type: "theme", 75 | isPublic: false, 76 | isArchived: false, 77 | }, 78 | { 79 | merge: true, 80 | }, 81 | ); 82 | await batch.commit(); 83 | }; 84 | -------------------------------------------------------------------------------- /packages/framework/src/shared/api.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from "@tanstack/react-query"; 2 | 3 | export const queryClient = new QueryClient(); 4 | 5 | export const generateAPIHash = (props: { 6 | api: string | undefined; 7 | inputs: { [key: string]: unknown } | undefined; 8 | screen: string | undefined; 9 | }): string => { 10 | return JSON.stringify({ ...props }); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/framework/src/shared/dto.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * DTOs = Data Transfer Objects 3 | * 4 | * Mostly equivalent to raw JSON definition over wire 5 | */ 6 | export interface EnsembleDocument { 7 | readonly id: string; 8 | readonly name: string; 9 | readonly content: string; 10 | readonly path?: string; 11 | readonly isRoot?: boolean; 12 | readonly isDraft?: boolean; 13 | readonly category?: string; 14 | readonly isArchived?: boolean; 15 | } 16 | 17 | export interface ApplicationDTO extends Omit { 18 | readonly screens: ScreenDTO[]; 19 | readonly widgets: WidgetDTO[]; 20 | readonly scripts: ScriptDTO[]; 21 | readonly theme?: ThemeDTO; 22 | readonly themes?: ThemeDTO[]; 23 | readonly languages?: LanguageDTO[]; 24 | readonly config?: string | EnsembleConfigYAML; 25 | readonly fonts?: FontDTO[]; 26 | 27 | readonly description?: string; 28 | readonly isPublic?: boolean; 29 | readonly isAutoGenerated?: boolean; 30 | readonly status?: string; 31 | } 32 | 33 | export type ScreenDTO = EnsembleDocument; 34 | 35 | export type WidgetDTO = EnsembleDocument; 36 | 37 | export type ScriptDTO = EnsembleDocument; 38 | export interface ThemeDTO { 39 | readonly id: string; 40 | readonly name?: string; 41 | readonly content: string; 42 | } 43 | 44 | export interface LanguageDTO { 45 | readonly name: string; 46 | readonly nativeName: string; 47 | readonly languageCode: string; 48 | readonly content: string; 49 | } 50 | 51 | export interface EnsembleEnvironmentDTO { 52 | googleOAuthId?: string; 53 | } 54 | 55 | export interface EnsembleConfigYAML { 56 | environmentVariables?: { [key: string]: unknown }; 57 | secretVariables?: { [key: string]: unknown }; 58 | } 59 | 60 | export interface FontDTO { 61 | readonly fontFamily: string; 62 | readonly publicUrl: string; 63 | readonly fontWeight: string; 64 | readonly fontStyle: string; 65 | } 66 | -------------------------------------------------------------------------------- /packages/framework/src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./actions"; 2 | export * from "./models"; 3 | export * from "./dto"; 4 | export * from "./common"; 5 | export * from "./api"; 6 | -------------------------------------------------------------------------------- /packages/framework/src/state/__tests__/widget.test.ts: -------------------------------------------------------------------------------- 1 | import "../widget"; 2 | 3 | test.todo("only evaluates on dependency updates"); 4 | -------------------------------------------------------------------------------- /packages/framework/src/state/application.ts: -------------------------------------------------------------------------------- 1 | import { atom } from "jotai"; 2 | import { focusAtom } from "jotai-optics"; 3 | import type { EnsembleAppModel, EnsembleThemeModel } from "../shared"; 4 | import type { EnsembleUser } from "./user"; 5 | 6 | export const defaultThemeDefinition = { name: "default" }; 7 | 8 | export interface ApplicationContextDefinition { 9 | application: EnsembleAppModel | null; 10 | storage: unknown; 11 | secrets: { [key: string]: unknown }; 12 | env: { [key: string]: unknown }; 13 | auth: unknown; 14 | user: EnsembleUser | null; 15 | useMockResponse: boolean; 16 | } 17 | 18 | export interface ApplicationContextActions { 19 | setApplication: (app: EnsembleAppModel) => void; 20 | } 21 | 22 | export const defaultApplicationContext = { 23 | application: null, 24 | storage: null, 25 | env: {}, 26 | auth: null, 27 | user: null, 28 | secrets: {}, 29 | useMockResponse: false, 30 | }; 31 | 32 | export const appAtom = atom( 33 | defaultApplicationContext, 34 | ); 35 | 36 | // Store the theme model state 37 | export const themeModelAtom = atom(defaultThemeDefinition); 38 | 39 | export const themeAtom = atom( 40 | (get) => get(themeModelAtom), 41 | (get, set, name: string) => { 42 | const appContext = get(appAtom); 43 | if ( 44 | appContext.application?.themes && 45 | name in appContext.application.themes 46 | ) { 47 | const selectedTheme = appContext.application.themes[name]; 48 | if (selectedTheme) { 49 | set(themeModelAtom, selectedTheme); 50 | } 51 | } 52 | }, 53 | ); 54 | 55 | export const envAtom = focusAtom(appAtom, (optic) => optic.prop("env")); 56 | 57 | export const secretAtom = focusAtom(appAtom, (optic) => optic.prop("secrets")); 58 | -------------------------------------------------------------------------------- /packages/framework/src/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./widget"; 2 | export * from "./screen"; 3 | export * from "./application"; 4 | export * from "./platform"; 5 | export * from "./user"; 6 | -------------------------------------------------------------------------------- /packages/framework/src/state/platform.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultStore } from "jotai"; 2 | import { atomWithLocation } from "jotai-location"; 3 | 4 | export const locationAtom = atomWithLocation({ 5 | replace: true, 6 | }); 7 | 8 | /** 9 | * @deprecated DO NOT USE directly 10 | */ 11 | export const ensembleStore = getDefaultStore(); 12 | -------------------------------------------------------------------------------- /packages/framework/src/state/user.ts: -------------------------------------------------------------------------------- 1 | import type { WritableAtom } from "jotai"; 2 | import { atom } from "jotai"; 3 | 4 | export type EnsembleUser = { accessToken?: string } & { 5 | [key: string]: unknown; 6 | }; 7 | 8 | // Custom storage atom so it's writable and hydrate-able so values remain consistent 9 | const atomWithSessionStorage = ( 10 | key: string, 11 | initialValue: T, 12 | ): WritableAtom => { 13 | const getInitialValue = (): T => { 14 | const item = sessionStorage.getItem(key); 15 | if (item !== null) { 16 | return JSON.parse(item) as T; 17 | } 18 | return initialValue; 19 | }; 20 | const baseAtom = atom(getInitialValue()); 21 | const derivedAtom = atom( 22 | (get) => get(baseAtom), 23 | (get, set, update) => { 24 | const nextValue = ( 25 | typeof update === "function" ? update(get(baseAtom)) : update 26 | ) as T; 27 | set(baseAtom, nextValue); 28 | sessionStorage.setItem(key, JSON.stringify(nextValue)); 29 | }, 30 | ); 31 | return derivedAtom; 32 | }; 33 | 34 | export const userAtom = atomWithSessionStorage( 35 | "ensemble.user", 36 | {}, 37 | ); 38 | -------------------------------------------------------------------------------- /packages/framework/src/state/widget.ts: -------------------------------------------------------------------------------- 1 | import { focusAtom } from "jotai-optics"; 2 | import { atomFamily } from "jotai/utils"; 3 | import { screenAtom } from "./screen"; 4 | 5 | export interface WidgetState { 6 | values: T; 7 | invokable?: Invokable; 8 | } 9 | 10 | export interface InvokableMethods { 11 | // eslint-disable-next-line @typescript-eslint/ban-types 12 | [key: string]: Function; 13 | } 14 | export interface Invokable { 15 | id: string; 16 | methods?: InvokableMethods; 17 | } 18 | 19 | export const widgetFamilyAtom = atomFamily((id: string) => 20 | focusAtom(screenAtom, (optics) => optics.path("widgets", id)), 21 | ); 22 | -------------------------------------------------------------------------------- /packages/framework/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/react-library.json", 3 | "include": ["./src"], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/framework/tsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", 3 | "extends": ["tsconfig/tsdoc.json"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/framework/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | format: ["cjs"], 6 | external: ["react"], 7 | bundle: true, 8 | splitting: false, 9 | dts: true, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/js-commons/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["custom/library"], 3 | ignorePatterns: ["tsup.config.ts"], 4 | }; 5 | -------------------------------------------------------------------------------- /packages/js-commons/README.md: -------------------------------------------------------------------------------- 1 | # `js-commons` 2 | -------------------------------------------------------------------------------- /packages/js-commons/jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 3 | module.exports = { 4 | preset: "ts-jest", 5 | testEnvironment: "jsdom", 6 | moduleNameMapper: { 7 | "^lodash-es$": "lodash", 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/js-commons/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ensembleui/js-commons", 3 | "version": "0.0.22", 4 | "exports": { 5 | ".": { 6 | "import": "./dist/core.mjs", 7 | "require": "./dist/core.js", 8 | "types": "./dist/core.d.ts" 9 | }, 10 | "./core": { 11 | "import": "./dist/core.mjs", 12 | "require": "./dist/core.js", 13 | "types": "./dist/core.d.ts" 14 | }, 15 | "./browser": { 16 | "import": "./dist/browser.mjs", 17 | "require": "./dist/browser.js", 18 | "types": "./dist/browser.d.ts" 19 | } 20 | }, 21 | "main": "./dist/core.js", 22 | "module": "./dist/core.mjs", 23 | "types": "./dist/core.d.ts", 24 | "files": [ 25 | "./dist/**" 26 | ], 27 | "repository": "https://github.com/EnsembleUI/ensemble-react", 28 | "scripts": { 29 | "dev": "tsup --watch", 30 | "build": "tsup", 31 | "test": "jest --passWithNoTests", 32 | "lint": "eslint ." 33 | }, 34 | "lint-staged": { 35 | "*.{ts,tsx}": "eslint --fix" 36 | }, 37 | "peerDependencies": { 38 | "firebase": "9.10.0" 39 | }, 40 | "devDependencies": { 41 | "@types/lodash-es": "^4.17.8", 42 | "dayjs": "^1.11.10", 43 | "eslint-config-custom": "workspace:*", 44 | "firebase": "9.10.0", 45 | "json-stable-stringify": "^1.2.1", 46 | "lodash-es": "^4.17.21", 47 | "tsconfig": "workspace:*", 48 | "tsup": "^7.2.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/js-commons/src/browser/index.ts: -------------------------------------------------------------------------------- 1 | export * from "../core/firebase"; 2 | export * from "../core/service"; 3 | -------------------------------------------------------------------------------- /packages/js-commons/src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dto"; 2 | export * from "./service"; 3 | export * from "./firebase"; 4 | export * from "./transporter"; 5 | export * from "./local-files"; 6 | -------------------------------------------------------------------------------- /packages/js-commons/src/core/transporter.ts: -------------------------------------------------------------------------------- 1 | import type { ApplicationDTO } from "./dto"; 2 | 3 | export interface ApplicationTransporter { 4 | get: (appId: string) => Promise; 5 | put: (app: ApplicationDTO, userId: string) => Promise; 6 | } 7 | 8 | export interface LocalApplicationTransporter extends ApplicationTransporter { 9 | get: (appId: string) => Promise; 10 | put: (app: ApplicationDTO, path?: string) => Promise; 11 | delete: (appId: string) => Promise; 12 | } 13 | -------------------------------------------------------------------------------- /packages/js-commons/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "include": ["./src"], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/js-commons/tsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", 3 | "extends": ["tsconfig/tsdoc.json"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/js-commons/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: { 5 | core: "src/core/index.ts", 6 | browser: "src/browser/index.ts" 7 | }, 8 | format: ["cjs", "esm"], 9 | external: ["react"], 10 | bundle: true, 11 | clean: true, 12 | dts: true, 13 | }); 14 | -------------------------------------------------------------------------------- /packages/runtime/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["custom/react"], 3 | rules: { 4 | "unicorn/filename-case": [ 5 | "error", 6 | { 7 | "cases": { 8 | "camelCase": true, 9 | "pascalCase": true 10 | } 11 | } 12 | ] 13 | }, 14 | ignorePatterns: ["setupTests.ts"] 15 | }; 16 | -------------------------------------------------------------------------------- /packages/runtime/README.md: -------------------------------------------------------------------------------- 1 | # `@ensembleui/react-runtime` 2 | 3 | Contains the EnsembleUI React widgets and actions. Also contains the React container that bootstraps Ensemble. 4 | -------------------------------------------------------------------------------- /packages/runtime/jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 3 | module.exports = { 4 | preset: "ts-jest", 5 | testEnvironment: "jsdom", 6 | moduleNameMapper: { 7 | "^lodash-es$": "lodash", 8 | "react-markdown": 9 | "/node_modules/react-markdown/react-markdown.min.js", 10 | }, 11 | setupFilesAfterEnv: ["/setupTests.ts"], 12 | testPathIgnorePatterns: ["/__tests__/__shared__/*"], 13 | }; 14 | -------------------------------------------------------------------------------- /packages/runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ensembleui/react-runtime", 3 | "version": "0.3.30", 4 | "main": "./dist/index.js", 5 | "types": "./dist/index.d.ts", 6 | "files": [ 7 | "./dist/**", 8 | "README.md" 9 | ], 10 | "publishConfig": { 11 | "access": "public" 12 | }, 13 | "repository": "https://github.com/EnsembleUI/ensemble-react", 14 | "scripts": { 15 | "dev": "tsup src/index.tsx --format cjs --watch --dts --external react --sourcemap", 16 | "build": "tsup src/index.tsx --format cjs --dts --external react", 17 | "test": "jest --runInBand", 18 | "lint": "eslint ." 19 | }, 20 | "lint-staged": { 21 | "*.{ts,tsx}": "eslint --fix" 22 | }, 23 | "peerDependencies": { 24 | "@ensembleui/react-framework": "*", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0" 27 | }, 28 | "devDependencies": { 29 | "@ant-design/icons": "^5.2.6", 30 | "@emotion/react": "^11.11.1", 31 | "@emotion/styled": "^11.11.0", 32 | "@ensembleui/react-framework": "workspace:*", 33 | "@lottiefiles/react-lottie-player": "^3.5.3", 34 | "@mui/icons-material": "^5.14.9", 35 | "@mui/material": "^5.14.9", 36 | "@mui/x-date-pickers": "^6.18.3", 37 | "@react-oauth/google": "^0.12.1", 38 | "@tanstack/react-query": "^5.61.0", 39 | "@testing-library/react": "^13.4.0", 40 | "@testing-library/user-event": "^13.5.0", 41 | "@turbo/gen": "^1.10.12", 42 | "@types/lodash-es": "^4.17.8", 43 | "@types/react": "^18.2.21", 44 | "@types/react-dom": "^18.2.0", 45 | "@types/react-resizable": "3.0.7", 46 | "antd": "^5.21.0", 47 | "chart.js": "^4.4.2", 48 | "chartjs-plugin-datalabels": "^2.2.0", 49 | "dayjs": "^1.11.12", 50 | "eslint-config-custom": "workspace:*", 51 | "imask": "^7.6.1", 52 | "jest-canvas-mock": "^2.5.2", 53 | "jwt-decode": "^4.0.0", 54 | "lodash-es": "^4.17.21", 55 | "react-chartjs-2": "^5.2.0", 56 | "react-easy-crop": "^5.0.5", 57 | "react-fast-compare": "^3.2.2", 58 | "react-markdown": "^8.0.7", 59 | "react-resizable": "^3.0.5", 60 | "react-router-dom": "^6.16.0", 61 | "react-toastify": "^9.1.3", 62 | "react-use": "^17.4.2", 63 | "runes2": "^1.1.4", 64 | "tsconfig": "workspace:*", 65 | "tsup": "^7.2.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/runtime/setupTests.ts: -------------------------------------------------------------------------------- 1 | import "jest-canvas-mock"; 2 | 3 | global.ResizeObserver = require('resize-observer-polyfill') 4 | 5 | Object.defineProperty(window, "matchMedia", { 6 | writable: true, 7 | value: jest.fn().mockImplementation((query) => ({ 8 | matches: false, 9 | media: query, 10 | onchange: null, 11 | addListener: jest.fn(), // deprecated 12 | removeListener: jest.fn(), // deprecated 13 | addEventListener: jest.fn(), 14 | removeEventListener: jest.fn(), 15 | dispatchEvent: jest.fn(), 16 | })), 17 | }); -------------------------------------------------------------------------------- /packages/runtime/src/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import { CustomThemeContext, useThemeScope } from "@ensembleui/react-framework"; 2 | import { ConfigProvider } from "antd"; 3 | import { type PropsWithChildren } from "react"; 4 | 5 | const DEFAULT_FONT_FAMILY = "sans-serif"; 6 | 7 | export const ThemeProvider: React.FC = ({ children }) => { 8 | const themeScope = useThemeScope(); 9 | 10 | if (!themeScope.theme) { 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | } 17 | 18 | return ( 19 | 20 | 29 | {children} 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/runtime/src/__tests__/__resources__/helloworld.yaml: -------------------------------------------------------------------------------- 1 | View: 2 | header: 3 | title: Welcome 4 | body: 5 | Column: 6 | children: 7 | - Text: 8 | text: Peter Parker 9 | -------------------------------------------------------------------------------- /packages/runtime/src/__tests__/registry.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import type { ReactElement } from "react"; 3 | import { WidgetRegistry } from "../registry"; 4 | 5 | test.skip("throws error if a widget is already registered with same name", () => { 6 | const register = (): void => { 7 | WidgetRegistry.register("test", () => null); 8 | }; 9 | 10 | register(); 11 | expect(register).toThrow(); 12 | }); 13 | 14 | test("returns unknown widget if widget is not registered", () => { 15 | render(
{WidgetRegistry.find("test2") as ReactElement}
); 16 | 17 | const element = screen.getByText("Unknown widget: test2"); 18 | expect(element).not.toBeNull(); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/runtime/src/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./EnsembleApp"; 2 | export * from "./registry"; 3 | export * from "./runtime/hooks"; 4 | // component exports 5 | export * from "./widgets"; 6 | -------------------------------------------------------------------------------- /packages/runtime/src/registry.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from "antd"; 2 | import type { ReactElement } from "react"; 3 | 4 | export type WidgetComponent = React.FC; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | const registry: { [key: string]: WidgetComponent | undefined } = {}; 8 | 9 | export const WidgetRegistry = { 10 | register: (name: string, component: WidgetComponent): void => { 11 | registry[name] = component; 12 | }, 13 | find: (name: string): WidgetComponent | ReactElement => { 14 | const Widget = registry[name]; 15 | if (!Widget) { 16 | return ; 17 | } 18 | return Widget; 19 | }, 20 | findOrNull: (name: string): WidgetComponent | null => { 21 | return registry[name] || null; 22 | }, 23 | unregister: (name: string): void => { 24 | delete registry[name]; 25 | }, 26 | }; 27 | 28 | const UnknownWidget: React.FC<{ missingName: string }> = ({ missingName }) => { 29 | return ; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/__tests__/screen.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, waitFor } from "@testing-library/react"; 2 | import "@testing-library/jest-dom"; 3 | import { BrowserRouter } from "react-router-dom"; 4 | import "../../widgets"; 5 | import { EnsembleScreen } from "../screen"; 6 | 7 | describe("Ensemble Screen", () => { 8 | it("initializes inputs in context even if there is no value", async () => { 9 | render( 10 | , 41 | { 42 | wrapper: BrowserRouter, 43 | }, 44 | ); 45 | 46 | await waitFor(() => { 47 | expect(screen.getByText("works")).toBeInTheDocument(); 48 | expect(screen.getByText("baz")).toBeInTheDocument(); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/body.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useRegisterBindings, 3 | type EnsembleFooterModel, 4 | type EnsembleHeaderModel, 5 | type EnsembleWidget, 6 | } from "@ensembleui/react-framework"; 7 | import { ConfigProvider } from "antd"; 8 | import { WidgetRegistry } from "../registry"; 9 | // eslint-disable-next-line import/no-cycle 10 | import { Column } from "../widgets/Column"; 11 | import { isUndefined, omitBy } from "lodash-es"; 12 | 13 | interface EnsembleBodyProps { 14 | body: EnsembleWidget; 15 | header?: EnsembleHeaderModel; 16 | footer?: EnsembleFooterModel; 17 | isModal?: boolean; 18 | styles?: { [key: string]: unknown }; 19 | } 20 | 21 | export const EnsembleBody: React.FC = ({ body, styles }) => { 22 | const { values } = useRegisterBindings({ styles }); 23 | const BodyFn = WidgetRegistry.find(body.name); 24 | if (!(BodyFn instanceof Function)) 25 | throw new Error(`Unknown widget: ${body.name}`); 26 | 27 | // default body styles 28 | const defaultStyles = { 29 | styles: { 30 | flex: 1, 31 | overflow: !styles?.scrollableView ? "hidden" : "auto", 32 | ...styles, 33 | }, 34 | }; 35 | 36 | const configThemeObj = { 37 | colorText: values?.styles?.color as string, 38 | fontSize: values?.styles?.fontSize as number, 39 | fontFamily: values?.styles?.fontFamily as string, 40 | fontWeightStrong: values?.styles?.fontWeight as number, 41 | }; 42 | 43 | return ( 44 | 49 | {[body]} 50 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/error.tsx: -------------------------------------------------------------------------------- 1 | import { useRouteError } from "react-router-dom"; 2 | 3 | export const ErrorPage: React.FC = () => { 4 | const error = useRouteError(); 5 | 6 | return ( 7 |
8 |

Oops!

9 |

Sorry, an unexpected error has occurred.

10 |

11 | {(error as Error).message} 12 |

13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/footer.tsx: -------------------------------------------------------------------------------- 1 | import type { EnsembleFooterModel } from "@ensembleui/react-framework"; 2 | // eslint-disable-next-line import/no-cycle 3 | import { Column } from "../widgets/Column"; 4 | 5 | interface EnsembleFooterProps { 6 | footer?: EnsembleFooterModel; 7 | } 8 | 9 | export const EnsembleFooter: React.FC = ({ footer }) => { 10 | if (!footer) { 11 | return null; 12 | } 13 | 14 | return ( 15 | 27 | {footer.children} 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/header.tsx: -------------------------------------------------------------------------------- 1 | import type { EnsembleHeaderModel } from "@ensembleui/react-framework"; 2 | import { isObject } from "lodash-es"; 3 | // eslint-disable-next-line import/no-cycle 4 | import { Column } from "../widgets/Column"; 5 | import { WidgetRegistry } from "../registry"; 6 | 7 | interface EnsembleHeaderProps { 8 | header?: EnsembleHeaderModel; 9 | } 10 | 11 | export const EnsembleHeader: React.FC = ({ header }) => { 12 | if (!header?.title) { 13 | return null; 14 | } 15 | 16 | let titleWidget; 17 | if (isObject(header.title)) { 18 | titleWidget = header.title; 19 | } else if (WidgetRegistry.findOrNull(header.title) !== null) { 20 | titleWidget = { 21 | name: header.title, 22 | properties: {}, 23 | }; 24 | } else { 25 | titleWidget = { 26 | name: "Text", 27 | properties: { 28 | text: header.title, 29 | styles: { 30 | color: header.styles?.titleColor || "black", 31 | ...header.styles, 32 | }, 33 | }, 34 | }; 35 | } 36 | 37 | return ( 38 | 49 | {[titleWidget]} 50 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/hooks/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-cycle 2 | export { useEnsembleAction } from "./useEnsembleAction"; 3 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/hooks/useCloseAllDialogs.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo } from "react"; 2 | import { type EnsembleActionHookResult } from "@ensembleui/react-framework"; 3 | import { ModalContext } from "../modal"; 4 | import type { EnsembleActionHook } from "./useEnsembleAction"; 5 | 6 | export const useCloseAllDialogs: EnsembleActionHook< 7 | EnsembleActionHookResult 8 | > = () => { 9 | const { closeAllModals } = useContext(ModalContext) || {}; 10 | 11 | return useMemo( 12 | () => ({ 13 | callback: () => closeAllModals?.(), 14 | }), 15 | [], 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/hooks/useCloseAllModalScreens.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo } from "react"; 2 | import { type EnsembleActionHookResult } from "@ensembleui/react-framework"; 3 | import { ModalContext } from "../modal"; 4 | import type { EnsembleActionHook } from "./useEnsembleAction"; 5 | 6 | export const useCloseAllScreens: EnsembleActionHook< 7 | EnsembleActionHookResult 8 | > = () => { 9 | const { closeAllScreens } = useContext(ModalContext) || {}; 10 | 11 | return useMemo( 12 | () => ({ 13 | callback: () => closeAllScreens?.(), 14 | }), 15 | [], 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/hooks/useNavigateExternalScreen.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from "react"; 2 | import { isString } from "lodash-es"; 3 | import { 4 | useEvaluate, 5 | type NavigateExternalScreen, 6 | } from "@ensembleui/react-framework"; 7 | // eslint-disable-next-line import/no-cycle 8 | import { openExternalScreen } from "../navigation"; 9 | import { type EnsembleActionHook } from "./useEnsembleAction"; 10 | 11 | export const useNavigateExternalScreen: EnsembleActionHook< 12 | NavigateExternalScreen 13 | > = (action) => { 14 | const [screenNavigated, setScreenNavigated] = useState(); 15 | const [context, setContext] = useState<{ [key: string]: unknown }>(); 16 | const evaluatedInputs = useEvaluate( 17 | isString(action) ? { url: action } : { ...action }, 18 | { context }, 19 | ); 20 | 21 | const navigateScreen = useMemo(() => { 22 | if (!action) { 23 | return; 24 | } 25 | 26 | const callback = (args: unknown): void => { 27 | setScreenNavigated(false); 28 | setContext(args as { [key: string]: unknown }); 29 | }; 30 | 31 | return { callback }; 32 | }, [action]); 33 | 34 | useEffect(() => { 35 | if (!evaluatedInputs.url || screenNavigated !== false) { 36 | return; 37 | } 38 | 39 | setScreenNavigated(true); 40 | return openExternalScreen(evaluatedInputs as NavigateExternalScreen); 41 | }, [evaluatedInputs, screenNavigated]); 42 | 43 | return navigateScreen; 44 | }; 45 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/hooks/useNavigateModal.tsx: -------------------------------------------------------------------------------- 1 | import type { NavigateModalScreenAction } from "@ensembleui/react-framework"; 2 | import { 3 | unwrapWidget, 4 | useApplicationContext, 5 | useScreenModel, 6 | useCommandCallback, 7 | evaluateDeep, 8 | } from "@ensembleui/react-framework"; 9 | import { cloneDeep, isNil, isString, merge } from "lodash-es"; 10 | import { useContext, useMemo } from "react"; 11 | import { useNavigate } from "react-router-dom"; 12 | import { ModalContext } from "../modal"; 13 | import { EnsembleRuntime } from "../runtime"; 14 | // FIXME: refactor 15 | // eslint-disable-next-line import/no-cycle 16 | import { navigateModalScreen } from "../navigation"; 17 | import { 18 | useEnsembleAction, 19 | type EnsembleActionHook, 20 | } from "./useEnsembleAction"; 21 | 22 | export const useNavigateModalScreen: EnsembleActionHook< 23 | NavigateModalScreenAction 24 | > = (action) => { 25 | const navigate = useNavigate(); 26 | const applicationContext = useApplicationContext(); 27 | const modalContext = useContext(ModalContext); 28 | const screenModel = useScreenModel(); 29 | const ensembleAction = useEnsembleAction( 30 | !isString(action) && action ? action.onModalDismiss : undefined, 31 | ); 32 | 33 | const isStringAction = isString(action); 34 | 35 | const title = useMemo(() => { 36 | if (!isStringAction && action?.title && !isString(action.title)) { 37 | return EnsembleRuntime.render([unwrapWidget(action.title)]); 38 | } 39 | }, [isStringAction, action]); 40 | 41 | const navigateCommand = useCommandCallback( 42 | (evalContext, ...args) => { 43 | if (!action || !modalContext) return; 44 | 45 | const context = merge({}, evalContext, args[0]); 46 | 47 | const evaluatedInputs = 48 | !isString(action) && !isNil(action.inputs) 49 | ? evaluateDeep(cloneDeep(action.inputs), screenModel, context) 50 | : {}; 51 | 52 | navigateModalScreen( 53 | action, 54 | modalContext, 55 | applicationContext?.application, 56 | evaluatedInputs, 57 | title, 58 | ensembleAction?.callback, 59 | ); 60 | }, 61 | { navigate }, 62 | [action, applicationContext?.application, screenModel], 63 | ); 64 | return { callback: navigateCommand }; 65 | }; 66 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/hooks/useNavigateScreen.ts: -------------------------------------------------------------------------------- 1 | import type { NavigateScreenAction } from "@ensembleui/react-framework"; 2 | import { isNil, isString, merge, cloneDeep } from "lodash-es"; 3 | import { useNavigate } from "react-router-dom"; 4 | import { 5 | useCommandCallback, 6 | useScreenModel, 7 | useApplicationContext, 8 | evaluateDeep, 9 | } from "@ensembleui/react-framework"; 10 | import type { EnsembleActionHook } from "./useEnsembleAction"; 11 | 12 | type EvaluatedData = { 13 | screenName: string; 14 | inputs?: { [key: string]: unknown }; 15 | }; 16 | 17 | export const useNavigateScreen: EnsembleActionHook = ( 18 | action, 19 | ) => { 20 | const navigate = useNavigate(); 21 | const screenModel = useScreenModel(); 22 | const appContext = useApplicationContext(); 23 | 24 | const navigateCommand = useCommandCallback( 25 | (evalContext, ...args) => { 26 | if (!action) return; 27 | 28 | const context = merge({}, evalContext, args[0]); 29 | 30 | const { screenName, inputs } = evaluateDeep( 31 | { 32 | screenName: isString(action) ? action : action.name, 33 | inputs: 34 | !isString(action) && !isNil(action.inputs) 35 | ? cloneDeep(action.inputs) 36 | : undefined, 37 | }, 38 | screenModel, 39 | context, 40 | ) as EvaluatedData; 41 | 42 | const matchingScreen = appContext?.application?.screens.find( 43 | (s) => s.name?.toLowerCase() === screenName.toLowerCase(), 44 | ); 45 | 46 | if (!matchingScreen?.name) return; 47 | 48 | navigate(`/${matchingScreen.name.toLowerCase()}`, { 49 | state: inputs, 50 | }); 51 | }, 52 | { navigate }, 53 | [action, screenModel, appContext], 54 | ); 55 | 56 | return { callback: navigateCommand }; 57 | }; 58 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/hooks/useShowToast.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | evaluateDeep, 3 | type ShowToastAction, 4 | useCommandCallback, 5 | useScreenModel, 6 | } from "@ensembleui/react-framework"; 7 | import type { Id, ToastPosition } from "react-toastify"; 8 | import { toast } from "react-toastify"; 9 | import { useRef } from "react"; 10 | import { useNavigate } from "react-router-dom"; 11 | import { cloneDeep, merge } from "lodash-es"; 12 | import type { EnsembleActionHook } from "./useEnsembleAction"; 13 | 14 | const positionMapping: { [key: string]: ToastPosition } = { 15 | top: "top-center", 16 | topLeft: "top-left", 17 | topRight: "top-right", 18 | center: "bottom-center", 19 | centerLeft: "bottom-left", 20 | centerRight: "bottom-right", 21 | bottom: "bottom-center", 22 | bottomLeft: "bottom-left", 23 | bottomRight: "bottom-right", 24 | }; 25 | 26 | export const useShowToast: EnsembleActionHook = (action) => { 27 | const ref = useRef(null); 28 | const navigate = useNavigate(); 29 | const screenModel = useScreenModel(); 30 | 31 | const showToast = useCommandCallback( 32 | (evalContext, ...args) => { 33 | if (!action) return; 34 | 35 | const context = merge({}, evalContext, args[0]); 36 | 37 | const evaluatedInputs = evaluateDeep( 38 | cloneDeep({ ...action }), 39 | screenModel, 40 | context, 41 | ) as ShowToastAction & { [key: string]: unknown }; 42 | 43 | if (!ref.current || !toast.isActive(ref.current)) { 44 | ref.current = toast.success(evaluatedInputs.message, { 45 | position: 46 | positionMapping[ 47 | evaluatedInputs.options?.position || "bottom-right" 48 | ], 49 | type: evaluatedInputs?.options?.type, 50 | toastId: evaluatedInputs.message, 51 | }); 52 | } 53 | }, 54 | { navigate }, 55 | [action, screenModel], 56 | ); 57 | 58 | return { callback: showToast }; 59 | }; 60 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./runtime"; 2 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/invokeApi.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DataFetcher, 3 | isUsingMockResponse, 4 | type Response, 5 | type useScreenData, 6 | mockResponse, 7 | } from "@ensembleui/react-framework"; 8 | import { has } from "lodash-es"; 9 | 10 | export const invokeAPI = async ( 11 | screenData: ReturnType, 12 | apiName: string, 13 | appId: string | undefined, 14 | apiInputs?: { [key: string]: unknown }, 15 | context?: { [key: string]: unknown }, 16 | ): Promise => { 17 | const api = screenData.apis?.find((model) => model.name === apiName); 18 | 19 | if (!api) { 20 | return; 21 | } 22 | 23 | // Now, because the API exists, set its state to loading 24 | screenData.setData(api.name, { 25 | isLoading: true, 26 | isError: false, 27 | isSuccess: false, 28 | }); 29 | 30 | // If mock resposne does not exist, fetch the data directly from the API 31 | const useMockResponse = 32 | has(api, "mockResponse") && isUsingMockResponse(appId); 33 | const res = await DataFetcher.fetch( 34 | api, 35 | { ...apiInputs, ...context }, 36 | { 37 | mockResponse: mockResponse( 38 | screenData.mockResponses[api.name], 39 | useMockResponse, 40 | ), 41 | useMockResponse, 42 | }, 43 | ); 44 | 45 | screenData.setData(api.name, res); 46 | return res; 47 | }; 48 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/locationApi.tsx: -------------------------------------------------------------------------------- 1 | import type { Location } from "react-router-dom"; 2 | 3 | export interface EnsembleLocationInterface { 4 | pathname?: string; 5 | search?: string; 6 | } 7 | 8 | export interface EnsembleLocation { 9 | get: (key: keyof EnsembleLocationInterface) => string | undefined; 10 | } 11 | 12 | export const locationApi = (location: Location): EnsembleLocation => { 13 | return { 14 | get: (key: keyof EnsembleLocationInterface): string | undefined => { 15 | return location[key]; 16 | }, 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/mock.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnsembleUI/ensemble-react/d11f8fe7c3bbb7dd9ec03f74e2c4be85845e125a/packages/runtime/src/runtime/mock.tsx -------------------------------------------------------------------------------- /packages/runtime/src/runtime/modal/utils.ts: -------------------------------------------------------------------------------- 1 | import { omit } from "lodash-es"; 2 | import { getComponentStyles } from "../../shared/styles"; 3 | import type { ModalProps } from "."; 4 | 5 | export const getCustomStyles = (options: ModalProps, key: string): string => 6 | ` 7 | .ant-modal-root .ant-modal-centered .ant-modal { 8 | top: unset; 9 | } 10 | .ensemble-modal-${key} { 11 | max-height: 100vh; 12 | max-width: 100vw; 13 | } 14 | .ensemble-modal-${key} > div { 15 | height: ${options.height || "auto"}; 16 | } 17 | .ensemble-modal-${key} .ant-modal-content { 18 | display: flex; 19 | flex-direction: column; 20 | ${ 21 | getComponentStyles( 22 | "", 23 | omit(options, [ 24 | "width", 25 | "position", 26 | "top", 27 | "left", 28 | "bottom", 29 | "right", 30 | ]) as React.CSSProperties, 31 | ) as string 32 | } 33 | ${options.showShadow === false ? "box-shadow: none !important;" : ""} 34 | } 35 | .ensemble-modal-${key} .ant-modal-body { 36 | height: 100%; 37 | overflow-y: auto; 38 | } 39 | `; 40 | 41 | export const getFullScreenStyles = (key: string): string => ` 42 | .ensemble-modal-${key} .ant-modal-content { 43 | height: 100vh; 44 | width: 100vw; 45 | margin: 0; 46 | inset: 0; 47 | } 48 | `; 49 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/runtime.tsx: -------------------------------------------------------------------------------- 1 | import type { EnsembleWidget } from "@ensembleui/react-framework"; 2 | import type { ReactNode } from "react"; 3 | import { isValidElement } from "react"; 4 | import { isEmpty, isString } from "lodash-es"; 5 | import { WidgetRegistry } from "../registry"; 6 | 7 | export const EnsembleRuntime = { 8 | render: (widgets: (string | EnsembleWidget)[]): ReactNode[] => { 9 | if (isEmpty(widgets)) { 10 | return []; 11 | } 12 | return widgets.map((child, index) => { 13 | if (isString(child)) { 14 | const result = WidgetRegistry.find("Text"); 15 | const WidgetFn = result as React.FC<{ text: string }>; 16 | return ; 17 | } 18 | 19 | const result = WidgetRegistry.find(child.name); 20 | if (isValidElement(result)) { 21 | return result; 22 | } 23 | 24 | const WidgetFn = result as React.FC; 25 | return ; 26 | }); 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/showDialog.ts: -------------------------------------------------------------------------------- 1 | import { 2 | unwrapWidget, 3 | type EnsembleWidget, 4 | type ShowDialogAction, 5 | type ShowDialogOptions, 6 | } from "@ensembleui/react-framework"; 7 | import { EnsembleRuntime } from "./runtime"; 8 | import { cloneDeep, isObject } from "lodash-es"; 9 | 10 | export type ShowDialogApiProps = { 11 | action?: ShowDialogAction; 12 | openModal?: (...args: any[]) => void; 13 | }; 14 | 15 | export const showDialog = (props?: ShowDialogApiProps): void => { 16 | const { action, openModal } = props ?? {}; 17 | if (!action || !openModal || (!action?.widget && !action?.body)) { 18 | return; 19 | } 20 | 21 | const widget = action?.widget ?? action?.body; 22 | 23 | const content = widget?.name 24 | ? (cloneDeep(widget) as unknown as EnsembleWidget) 25 | : // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 26 | unwrapWidget(cloneDeep(widget!)); 27 | 28 | openModal?.( 29 | EnsembleRuntime.render([content]), 30 | getShowDialogOptions(action?.options), 31 | true, 32 | screen || undefined, 33 | ); 34 | }; 35 | 36 | export const getShowDialogOptions = ( 37 | options?: ShowDialogOptions, 38 | onClose?: () => void, 39 | ): ShowDialogOptions => { 40 | const noneStyleOption = { 41 | backgroundColor: "transparent", 42 | showShadow: false, 43 | }; 44 | 45 | const dialogOptions = { 46 | maskClosable: true, 47 | hideCloseIcon: true, 48 | hideFullScreenIcon: true, 49 | verticalOffset: options?.verticalOffset, 50 | horizontalOffset: options?.horizontalOffset, 51 | padding: "12px", 52 | onClose, 53 | ...(options?.style === "none" ? noneStyleOption : {}), 54 | ...(isObject(options) ? options : {}), 55 | }; 56 | 57 | return dialogOptions; 58 | }; 59 | -------------------------------------------------------------------------------- /packages/runtime/src/runtime/websocket.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | WebSocketConnection, 3 | EnsembleActionHookResult, 4 | ScreenContextData, 5 | EnsembleSocketModel, 6 | } from "@ensembleui/react-framework"; 7 | 8 | export const handleConnectSocket = ( 9 | screenData: ScreenContextData, 10 | screenDataSetter: (name: string, response: WebSocketConnection) => void, 11 | socket: EnsembleSocketModel, 12 | onOpen?: EnsembleActionHookResult, 13 | onMessage?: EnsembleActionHookResult, 14 | onClose?: EnsembleActionHookResult, 15 | ): WebSocketConnection | undefined => { 16 | // check the socket is already connected 17 | const prevSocketConnection = screenData[socket.name] as 18 | | WebSocketConnection 19 | | undefined; 20 | 21 | if (prevSocketConnection?.isConnected) { 22 | return prevSocketConnection; 23 | } 24 | 25 | const ws = new WebSocket(socket.uri); 26 | 27 | if (onOpen?.callback) { 28 | ws.onopen = () => onOpen.callback(); 29 | } 30 | 31 | if (onMessage?.callback) { 32 | ws.onmessage = (e: MessageEvent) => 33 | onMessage.callback({ data: e.data as unknown }); 34 | } 35 | 36 | if (onClose?.callback) { 37 | ws.onclose = () => onClose.callback(); 38 | } 39 | 40 | screenDataSetter(socket.name, { socket: ws, isConnected: true }); 41 | 42 | return { socket: ws, isConnected: true }; 43 | }; 44 | 45 | export const handleMessageSocket = ( 46 | screenData: ScreenContextData, 47 | socket: EnsembleSocketModel, 48 | message?: { [key: string]: unknown }, 49 | ): void => { 50 | const socketInstance = screenData[socket.name] as WebSocketConnection; 51 | if (socketInstance.isConnected) { 52 | socketInstance.socket?.send(JSON.stringify(message)); 53 | } 54 | }; 55 | 56 | export const handleDisconnectSocket = ( 57 | screenData: ScreenContextData, 58 | socket: EnsembleSocketModel, 59 | screenDataSetter: (name: string, response: WebSocketConnection) => void, 60 | ): void => { 61 | const socketInstance = screenData[socket.name] as WebSocketConnection; 62 | if (socketInstance.isConnected) { 63 | socketInstance.socket?.close(); 64 | screenDataSetter(socket.name, { isConnected: false }); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /packages/runtime/src/shared/icons.tsx: -------------------------------------------------------------------------------- 1 | import * as MuiIcons from "@mui/icons-material"; 2 | 3 | export const renderMuiIcon = ( 4 | iconName: keyof typeof MuiIcons, 5 | width?: string, 6 | height?: string, 7 | ) => { 8 | const MuiIconComponent = MuiIcons[iconName]; 9 | if (MuiIconComponent) { 10 | return ( 11 | 17 | ); 18 | } 19 | return null; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/runtime/src/shared/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | EnsembleAction, 3 | EnsembleWidget, 4 | Expression, 5 | TemplateData, 6 | } from "@ensembleui/react-framework"; 7 | import type { HasBorder } from "./hasSchema"; 8 | 9 | export type EnsembleWidgetStyles = Omit & { 10 | /** 11 | * @deprecated Use the `class` attribute for styling instead of `names`. 12 | */ 13 | names?: Expression; 14 | className?: Expression; 15 | visible?: boolean; 16 | }; 17 | 18 | export interface EnsembleWidgetProps< 19 | T extends Partial = EnsembleWidgetStyles, 20 | > { 21 | id?: string; 22 | styles?: T; 23 | htmlAttributes?: { [key: string]: Expression }; 24 | } 25 | 26 | export interface HasItemTemplate { 27 | "item-template"?: { 28 | data: Expression; 29 | name: string; 30 | template: EnsembleWidget; 31 | }; 32 | } 33 | 34 | export type BaseTextProps = { 35 | text?: Expression; 36 | } & EnsembleWidgetProps; 37 | 38 | export interface FlexboxStyles { 39 | mainAxis?: string; 40 | crossAxis?: string; 41 | gap?: number; 42 | margin?: number | string; 43 | padding?: number | string; 44 | maxWidth?: string; 45 | minWidth?: string; 46 | visible?: boolean; 47 | } 48 | 49 | export type FlexboxProps = { 50 | "item-template"?: { 51 | data: Expression; 52 | name: string; 53 | template: EnsembleWidget; 54 | }; 55 | onTap?: EnsembleAction; 56 | children?: EnsembleWidget[]; 57 | } & FlexboxStyles & 58 | HasBorder & 59 | EnsembleWidgetProps; 60 | 61 | export type IconProps = { 62 | name: Expression; 63 | size?: number; 64 | color?: string; 65 | styles?: { 66 | backgroundColor?: string; 67 | padding?: number | string; 68 | margin?: number | string; 69 | } & HasBorder; 70 | onTap?: EnsembleAction; 71 | onMouseEnter?: EnsembleAction; 72 | onMouseLeave?: EnsembleAction; 73 | } & EnsembleWidgetProps; 74 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Avatar/utils/generateInitials.ts: -------------------------------------------------------------------------------- 1 | import { filter, isEmpty } from "lodash-es"; 2 | 3 | export const generateInitials = (name?: string): string => 4 | filter(name?.split(/\s+/), (word) => !isEmpty(word)) 5 | .map((word) => word[0].toUpperCase()) 6 | .join("") 7 | .slice(0, 2); 8 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Avatar/utils/stringToColors.ts: -------------------------------------------------------------------------------- 1 | import type { Expression } from "@ensembleui/react-framework"; 2 | 3 | export function stringToColor(string: Expression): string { 4 | let hash = 0; 5 | let i; 6 | 7 | /* eslint-disable no-bitwise */ 8 | for (i = 0; i < string.length; i += 1) { 9 | hash = string.charCodeAt(i) + ((hash << 5) - hash); 10 | } 11 | 12 | let color = "#"; 13 | 14 | for (i = 0; i < 3; i += 1) { 15 | const value = (hash >> (i * 8)) & 0xff; 16 | color += `00${value.toString(16)}`.slice(-2); 17 | } 18 | /* eslint-enable no-bitwise */ 19 | 20 | return color; 21 | } 22 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Card.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useRegisterBindings, 3 | type EnsembleWidget, 4 | } from "@ensembleui/react-framework"; 5 | import { useMemo } from "react"; 6 | import { WidgetRegistry } from "../registry"; 7 | import type { 8 | EnsembleWidgetProps, 9 | EnsembleWidgetStyles, 10 | } from "../shared/types"; 11 | import { EnsembleRuntime } from "../runtime"; 12 | 13 | const widgetName = "Card"; 14 | 15 | interface CardStyles { 16 | width?: string; 17 | height?: string; 18 | /** @uiType color */ 19 | backgroundColor?: string; 20 | border?: string; 21 | borderRadius?: string; 22 | shadowColor?: string; 23 | shadowOffset?: string; 24 | shadowBlur?: string; 25 | shadowSpread?: string; 26 | padding?: string; 27 | maxWidth?: string; 28 | minWidth?: string; 29 | gap?: string; 30 | } 31 | 32 | export type CardProps = { 33 | children: EnsembleWidget[]; 34 | } & EnsembleWidgetProps< 35 | Omit & CardStyles 36 | >; 37 | 38 | const defaultStyles: CardStyles = { 39 | backgroundColor: "none", 40 | border: "1px solid lightgrey", 41 | width: "100%", 42 | height: "100%", 43 | padding: "20px", 44 | borderRadius: "10px", 45 | shadowColor: "lightgrey", 46 | shadowOffset: "0", 47 | shadowBlur: "0", 48 | shadowSpread: "0", 49 | maxWidth: "250px", 50 | minWidth: "250px", 51 | }; 52 | 53 | export const Card: React.FC = ({ children, styles }) => { 54 | const { values } = useRegisterBindings({ styles, widgetName }); 55 | 56 | const renderedChildren = useMemo(() => { 57 | return EnsembleRuntime.render(children); 58 | }, [children]); 59 | 60 | const mergedStyles = { ...defaultStyles, ...values?.styles }; 61 | const { shadowOffset, shadowBlur, shadowSpread, shadowColor } = mergedStyles; 62 | 63 | return ( 64 |
74 | {renderedChildren} 75 |
76 | ); 77 | }; 78 | 79 | WidgetRegistry.register(widgetName, Card); 80 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Charts/BarChart.tsx: -------------------------------------------------------------------------------- 1 | import { Bar } from "react-chartjs-2"; 2 | import type { ChartOptions } from "chart.js"; 3 | import { useRegisterBindings } from "@ensembleui/react-framework"; 4 | import { useState } from "react"; 5 | import { type ChartProps } from ".."; 6 | import { getMergedOptions } from "./utils/getMergedOptions"; 7 | 8 | const options: ChartOptions<"bar"> = { 9 | maintainAspectRatio: false, 10 | scales: { 11 | x: { 12 | border: { 13 | display: true, 14 | }, 15 | grid: { 16 | lineWidth: 0, 17 | }, 18 | }, 19 | y: { 20 | border: { 21 | display: false, 22 | dash: [2, 2], 23 | }, 24 | grid: { 25 | lineWidth: 1, 26 | tickBorderDash: [1], 27 | }, 28 | }, 29 | }, 30 | }; 31 | 32 | export const BarChart: React.FC = (props) => { 33 | const { id, styles, config } = props; 34 | 35 | const [title, setTitle] = useState(config?.title); 36 | const [labels, setLabels] = useState(config?.data.labels || []); 37 | 38 | const { values } = useRegisterBindings({ labels, title }, id, { 39 | setLabels, 40 | setTitle, 41 | }); 42 | 43 | return ( 44 |
50 | 58 |
59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Charts/DoughnutChart.tsx: -------------------------------------------------------------------------------- 1 | import { Doughnut } from "react-chartjs-2"; 2 | import type { ChartOptions, Plugin } from "chart.js"; 3 | import { get } from "lodash-es"; 4 | import { useState } from "react"; 5 | import { useRegisterBindings } from "@ensembleui/react-framework"; 6 | import { type ChartProps } from ".."; 7 | import { getMergedOptions } from "./utils/getMergedOptions"; 8 | 9 | const options: ChartOptions<"doughnut"> = { 10 | cutout: "90%", 11 | maintainAspectRatio: false, 12 | }; 13 | 14 | export const DoughnutChart: React.FC = (props) => { 15 | const { id, config } = props; 16 | 17 | const [title, setTitle] = useState(config?.title); 18 | const [labels, setLabels] = useState(config?.data.labels || []); 19 | 20 | const { values } = useRegisterBindings({ labels, title }, id, { 21 | setLabels, 22 | setTitle, 23 | }); 24 | 25 | return ( 26 | []} 33 | style={{ 34 | ...(get(props, "styles") as object), 35 | }} 36 | /> 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Charts/LineChart.tsx: -------------------------------------------------------------------------------- 1 | import { Line } from "react-chartjs-2"; 2 | import type { ChartOptions } from "chart.js"; 3 | import { useState } from "react"; 4 | import { useRegisterBindings } from "@ensembleui/react-framework"; 5 | import { get } from "lodash-es"; 6 | import { type ChartProps } from ".."; 7 | import { getMergedOptions } from "./utils/getMergedOptions"; 8 | 9 | const options: ChartOptions<"line"> = { 10 | maintainAspectRatio: false, 11 | plugins: { 12 | legend: { 13 | display: false, 14 | }, 15 | filler: { 16 | propagate: false, 17 | }, 18 | }, 19 | scales: { 20 | x: { 21 | border: { 22 | display: true, 23 | }, 24 | grid: { 25 | lineWidth: 0, 26 | }, 27 | }, 28 | y: { 29 | border: { 30 | display: false, 31 | dash: [2, 2], 32 | }, 33 | grid: { 34 | lineWidth: 1, 35 | tickBorderDash: [1], 36 | }, 37 | }, 38 | }, 39 | }; 40 | 41 | export const LineChart: React.FC = (props) => { 42 | const { id, config } = props; 43 | 44 | const [title, setTitle] = useState(config?.title); 45 | const [labels, setLabels] = useState(config?.data.labels || []); 46 | 47 | const { values } = useRegisterBindings({ labels, title }, id, { 48 | setLabels, 49 | setTitle, 50 | }); 51 | 52 | return ( 53 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Charts/StackBarChart.tsx: -------------------------------------------------------------------------------- 1 | import { Bar } from "react-chartjs-2"; 2 | import type { ChartOptions } from "chart.js"; 3 | import { useRegisterBindings } from "@ensembleui/react-framework"; 4 | import { useState } from "react"; 5 | import { get } from "lodash-es"; 6 | import { type ChartProps } from ".."; 7 | import { getMergedOptions } from "./utils/getMergedOptions"; 8 | 9 | const options: ChartOptions<"bar"> = { 10 | maintainAspectRatio: false, 11 | indexAxis: "y", 12 | plugins: { 13 | legend: { 14 | display: false, 15 | position: "top", 16 | }, 17 | tooltip: { 18 | enabled: false, 19 | }, 20 | }, 21 | scales: { 22 | x: { 23 | display: false, 24 | stacked: true, 25 | ticks: { 26 | display: false, 27 | }, 28 | border: { 29 | display: true, 30 | }, 31 | grid: { 32 | lineWidth: 0, 33 | }, 34 | }, 35 | y: { 36 | display: false, 37 | ticks: { 38 | display: false, 39 | }, 40 | stacked: true, 41 | border: { 42 | display: false, 43 | }, 44 | grid: { 45 | lineWidth: 0, 46 | }, 47 | }, 48 | }, 49 | }; 50 | 51 | export const StackBarChart: React.FC = (props) => { 52 | const { id, config } = props; 53 | 54 | const [title, setTitle] = useState(config?.title); 55 | const [labels, setLabels] = useState(config?.data.labels || []); 56 | 57 | const { values } = useRegisterBindings({ labels, title }, id, { 58 | setLabels, 59 | setTitle, 60 | }); 61 | 62 | return ( 63 | 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Charts/utils/getMergedOptions.ts: -------------------------------------------------------------------------------- 1 | import { type ChartOptions } from "chart.js"; 2 | import { merge } from "lodash-es"; 3 | import { type ChartConfigs } from ".."; 4 | 5 | export const getMergedOptions = ( 6 | defaultOptions: ChartOptions>, 7 | title?: string, 8 | configOptions?: ChartOptions, 9 | ): typeof defaultOptions & ChartOptions => 10 | merge( 11 | {}, 12 | defaultOptions, 13 | { 14 | plugins: { 15 | title: { 16 | display: Boolean(title), 17 | text: title, 18 | }, 19 | }, 20 | }, 21 | configOptions, 22 | ); 23 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/DataGrid/DataCell.tsx: -------------------------------------------------------------------------------- 1 | import { isEmpty, isObject } from "lodash-es"; 2 | import type { CustomScope } from "@ensembleui/react-framework"; 3 | import { 4 | CustomScopeProvider, 5 | useTemplateData, 6 | } from "@ensembleui/react-framework"; 7 | import { memo } from "react"; 8 | import { EnsembleRuntime } from "../../runtime"; 9 | import type { DataGridRowTemplate } from "./DataGrid"; 10 | 11 | export interface DataCellProps { 12 | scopeName: string; 13 | data: unknown; 14 | template: DataGridRowTemplate; 15 | columnIndex: number; 16 | rowIndex: number; 17 | } 18 | 19 | export const DataCell: React.FC = memo( 20 | ({ template, columnIndex, rowIndex, data }) => { 21 | const { "item-template": itemTemplate, children } = template.properties; 22 | const { namedData } = useTemplateData({ 23 | ...itemTemplate, 24 | context: data, 25 | }); 26 | 27 | if (children) { 28 | return ( 29 | 32 | {EnsembleRuntime.render([children[columnIndex]])} 33 | 34 | ); 35 | } 36 | 37 | if (isObject(itemTemplate) && !isEmpty(namedData)) { 38 | return ( 39 | 47 | {EnsembleRuntime.render([itemTemplate.template])} 48 | 49 | ); 50 | } 51 | 52 | return null; 53 | }, 54 | ); 55 | 56 | DataCell.displayName = "DataCell"; 57 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/DataGrid/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./DataGrid"; 2 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Divider.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useRegisterBindings, 3 | type Expression, 4 | } from "@ensembleui/react-framework"; 5 | import { WidgetRegistry } from "../registry"; 6 | import type { 7 | EnsembleWidgetProps, 8 | EnsembleWidgetStyles, 9 | } from "../shared/types"; 10 | 11 | const widgetName = "Divider"; 12 | 13 | export interface DividerStyles extends EnsembleWidgetStyles { 14 | direction?: "horizontal" | "vertical"; 15 | thickness?: number; 16 | endIndent?: string | number; 17 | indent?: string | number; 18 | color?: Expression; 19 | width?: string; 20 | } 21 | 22 | export type DividerProps = EnsembleWidgetProps; 23 | 24 | export const DividerWidget: React.FC = (props) => { 25 | const { values } = useRegisterBindings({ ...props, widgetName }, props.id); 26 | const { direction, ...restStyles } = values?.styles || {}; 27 | 28 | let width; 29 | if (direction === "vertical") { 30 | width = "0px"; 31 | } else if (values?.styles?.width) { 32 | width = values.styles.width; 33 | } else { 34 | width = "100%"; 35 | } 36 | 37 | return ( 38 |
59 | ); 60 | }; 61 | 62 | WidgetRegistry.register(widgetName, DividerWidget); 63 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/FittedColumn.tsx: -------------------------------------------------------------------------------- 1 | import { type Expression, useEvaluate } from "@ensembleui/react-framework"; 2 | import { isArray } from "lodash-es"; 3 | import { WidgetRegistry } from "../registry"; 4 | import type { 5 | EnsembleWidgetProps, 6 | FlexboxProps, 7 | FlexboxStyles, 8 | } from "../shared/types"; 9 | import { Column } from "./Column"; 10 | 11 | interface FittedColumnStyles extends FlexboxStyles { 12 | childrenFits?: Expression; 13 | } 14 | 15 | export type FittedColumnProps = { 16 | childrenFits?: Expression; 17 | } & FlexboxProps & 18 | EnsembleWidgetProps; 19 | 20 | export const FittedColumn: React.FC = (props) => { 21 | const values = useEvaluate({ 22 | childrenFits: props.childrenFits || props.styles?.childrenFits, 23 | }); 24 | 25 | return ( 26 | (fit === "auto" ? fit : `${fit}fr`)) 34 | ?.join(" ") 35 | : "auto", 36 | ...props?.styles, 37 | }} 38 | /> 39 | ); 40 | }; 41 | 42 | WidgetRegistry.register("FittedColumn", FittedColumn); 43 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/FittedRow.tsx: -------------------------------------------------------------------------------- 1 | import { type Expression, useEvaluate } from "@ensembleui/react-framework"; 2 | import { isArray } from "lodash-es"; 3 | import { WidgetRegistry } from "../registry"; 4 | import type { 5 | EnsembleWidgetProps, 6 | FlexboxProps, 7 | FlexboxStyles, 8 | } from "../shared/types"; 9 | import { Row } from "./Row"; 10 | 11 | interface FittedRowStyles extends FlexboxStyles { 12 | childrenFits?: Expression; 13 | } 14 | 15 | export type FittedRowProps = { 16 | childrenFits?: Expression; 17 | } & FlexboxProps & 18 | EnsembleWidgetProps; 19 | 20 | export const FittedRow: React.FC = (props) => { 21 | const values = useEvaluate({ 22 | childrenFits: props.childrenFits || props.styles?.childrenFits, 23 | }); 24 | 25 | return ( 26 | (fit === "auto" ? fit : `${fit}fr`)) 34 | ?.join(" ") 35 | : "auto", 36 | ...props?.styles, 37 | }} 38 | /> 39 | ); 40 | }; 41 | 42 | WidgetRegistry.register("FittedRow", FittedRow); 43 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Flex.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useRegisterBindings, 3 | type EnsembleWidget, 4 | } from "@ensembleui/react-framework"; 5 | import { omit } from "lodash-es"; 6 | import { WidgetRegistry } from "../registry"; 7 | import type { 8 | EnsembleWidgetStyles, 9 | EnsembleWidgetProps, 10 | HasItemTemplate, 11 | FlexboxStyles, 12 | } from "../shared/types"; 13 | import { Column } from "./Column"; 14 | import { Row } from "./Row"; 15 | 16 | interface FlexStyles extends EnsembleWidgetStyles { 17 | direction?: "horizontal" | "vertical" | undefined; 18 | gap?: number; 19 | } 20 | 21 | export type FlexProps = { 22 | children?: EnsembleWidget[]; 23 | } & HasItemTemplate & 24 | EnsembleWidgetProps; 25 | 26 | export const FlexWidget: React.FC = (props) => { 27 | const { values } = useRegisterBindings( 28 | { styles: { direction: props.styles?.direction } }, 29 | props.id, 30 | ); 31 | 32 | return ( 33 | <> 34 | {values?.styles.direction === "vertical" ? ( 35 | 36 | ) : ( 37 | 38 | )} 39 | 40 | ); 41 | }; 42 | 43 | WidgetRegistry.register("Flex", FlexWidget); 44 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Form/Date/utils/DateConstants.ts: -------------------------------------------------------------------------------- 1 | export const DateHeaderFormat = "ddd, MMM DD"; 2 | export const DateDisplayFormat = "MM/DD/YYYY"; 3 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Form/Date/utils/DatePickerContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | import type dayjs from "dayjs"; 3 | 4 | export interface DatePickerProps { 5 | firstDate?: string; 6 | lastDate?: string; 7 | value?: dayjs.Dayjs | undefined; 8 | setValue?: (value: dayjs.Dayjs | undefined) => void; 9 | isCalendarOpen?: boolean; 10 | setIsCalendarOpen?: (value: boolean) => void; 11 | enteredDate?: string; 12 | setEnteredDate?: (value: string) => void; 13 | errorText?: string; 14 | setErrorText?: (value: string) => void; 15 | onChangeCallback?: (date?: string) => void; 16 | } 17 | 18 | export const DatePickerContext = createContext( 19 | undefined, 20 | ); 21 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Form/Date/utils/isDateValid.ts: -------------------------------------------------------------------------------- 1 | // returns true if date is in mm/dd/yyyy format 2 | export const isDateValid = (date?: string): boolean => 3 | Boolean(date) && 4 | /^(?:0[1-9]|1[0-2])\/(?:0[1-9]|[12][0-9]|3[01])\/\d{4}$/.test(date!); 5 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Form/__tests__/__shared__/fixtures.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from "react"; 2 | import { BrowserRouter } from "react-router-dom"; 3 | import { ScreenContextProvider } from "@ensembleui/react-framework"; 4 | 5 | export const FormTestWrapper: React.FC = ({ children }) => { 6 | return ( 7 | 8 | 15 | {children} 16 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Form/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Form"; 2 | export * from "./TextInput"; 3 | export * from "./Checkbox"; 4 | export * from "./Radio"; 5 | export * from "./Dropdown"; 6 | export * from "./MultiSelect"; 7 | export * from "./Date/Date"; 8 | export * from "./Date/DateRange"; 9 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Form/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { Expression } from "@ensembleui/react-framework"; 2 | import type { 3 | EnsembleWidgetProps, 4 | EnsembleWidgetStyles, 5 | } from "../../shared/types"; 6 | 7 | export type FormInputProps = EnsembleWidgetProps & { 8 | value?: Expression; 9 | initialValue?: Expression; 10 | label: Expression; 11 | hintText?: Expression; 12 | required?: Expression; 13 | enabled?: Expression; 14 | labelStyle?: EnsembleWidgetStyles; 15 | validateOnUserInteraction?: Expression; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Html.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { 3 | useRegisterBindings, 4 | type Expression, 5 | } from "@ensembleui/react-framework"; 6 | import { isEmpty } from "lodash-es"; 7 | import { WidgetRegistry } from "../registry"; 8 | import type { EnsembleWidgetProps } from "../shared/types"; 9 | 10 | const widgetName = "Html"; 11 | 12 | interface HtmlCssSelectorStyle { 13 | selector: Expression; 14 | properties: React.CSSProperties; 15 | } 16 | 17 | export type HtmlProps = { 18 | text: Expression; 19 | cssStyles?: HtmlCssSelectorStyle[]; 20 | } & EnsembleWidgetProps; 21 | 22 | export const Html: React.FC = (props) => { 23 | const { values, rootRef } = useRegisterBindings( 24 | { ...props, widgetName }, 25 | props.id, 26 | {}, 27 | ); 28 | 29 | const cssStyles = useMemo(() => { 30 | return (values?.cssStyles || []).reduce((styles, item) => { 31 | const properties = Object.entries(item.properties) 32 | .map(([key, value]) => { 33 | // Convert camelCase to kebab-case for CSS properties 34 | const cssKey = key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`); 35 | return `${cssKey}: ${String(value)}`; 36 | }) 37 | .join(";"); 38 | 39 | return `${styles} 40 | ${item.selector} { 41 | ${properties} 42 | } 43 | `; 44 | }, ""); 45 | }, [values?.cssStyles]); 46 | 47 | return ( 48 | <> 49 | {!isEmpty(cssStyles) && } 50 |
55 | 56 | ); 57 | }; 58 | 59 | WidgetRegistry.register(widgetName, Html); 60 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/LoadingContainer.tsx: -------------------------------------------------------------------------------- 1 | import type { Expression } from "@ensembleui/react-framework"; 2 | import { unwrapWidget, useRegisterBindings } from "@ensembleui/react-framework"; 3 | import { Skeleton } from "@mui/material"; 4 | import { cloneDeep } from "lodash-es"; 5 | import type { EnsembleWidgetProps } from "../shared/types"; 6 | import { EnsembleRuntime } from "../runtime"; 7 | import { WidgetRegistry } from "../registry"; 8 | import { type Widget } from "../shared/coreSchema"; 9 | 10 | const widgetName = "LoadingContainer"; 11 | 12 | export interface LoadingContainerProps extends EnsembleWidgetProps { 13 | isLoading: Expression; 14 | useShimmer?: Expression; 15 | baseColor?: Expression; 16 | highlightColor?: Expression; 17 | width?: Expression; 18 | height?: Expression; 19 | /** 20 | * The widget to render as the content of this container. 21 | * @treeItemWidgetLabel Set Content Widget 22 | */ 23 | widget: Widget; 24 | loadingWidget?: Widget; 25 | } 26 | 27 | export const LoadingContainer: React.FC = (props) => { 28 | const { widget, loadingWidget, ...rest } = props; 29 | const unwrappedWidget = unwrapWidget(cloneDeep(widget)); 30 | const unwrappedLoadingWidget = loadingWidget 31 | ? unwrapWidget(cloneDeep(loadingWidget)) 32 | : undefined; 33 | const { values } = useRegisterBindings({ ...rest, widgetName }, props.id); 34 | 35 | if (values?.isLoading) { 36 | if (unwrappedLoadingWidget) { 37 | return <>{EnsembleRuntime.render([unwrappedLoadingWidget])}; 38 | } 39 | return ( 40 | 47 | ); 48 | } 49 | 50 | return <>{EnsembleRuntime.render([unwrappedWidget])}; 51 | }; 52 | 53 | WidgetRegistry.register(widgetName, LoadingContainer); 54 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Markdown.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactElement } from "react"; 2 | import React, { useState } from "react"; 3 | import ReactMarkdown from "react-markdown"; 4 | import { useRegisterBindings } from "@ensembleui/react-framework"; 5 | import { WidgetRegistry } from "../registry"; 6 | import type { BaseTextProps } from "../shared/types"; 7 | import { getTextAlign } from "../shared/styles"; 8 | 9 | const widgetName = "Markdown"; 10 | 11 | export type MarkdownProps = { 12 | // to be added more 13 | } & BaseTextProps; 14 | // TODO: customize in theme 15 | const components = { 16 | h1: ({ ...props }): ReactElement => ( 17 | // eslint-disable-next-line jsx-a11y/heading-has-content 18 |

19 | ), 20 | }; 21 | 22 | export const Markdown: React.FC = (props) => { 23 | const [text, setText] = useState(props.text); 24 | const { values } = useRegisterBindings( 25 | { ...props, text, widgetName }, 26 | props.id, 27 | { 28 | setText, 29 | }, 30 | ); 31 | return ( 32 |
42 | 43 | {values?.text ?? ""} 44 | 45 |
46 | ); 47 | }; 48 | 49 | WidgetRegistry.register(widgetName, Markdown); 50 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/QrCode.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from "react"; 2 | import { QRCode as AntQRCode } from "antd"; 3 | import { 4 | type EnsembleAction, 5 | useRegisterBindings, 6 | } from "@ensembleui/react-framework"; 7 | import { WidgetRegistry } from "../registry"; 8 | import type { EnsembleWidgetProps } from "../shared/types"; 9 | import { useEnsembleAction } from "../runtime/hooks"; 10 | 11 | const widgetName = "QRCode"; 12 | 13 | type QRCodeProps = { 14 | value: string; 15 | size?: number; 16 | icon?: string; 17 | iconSize?: number; 18 | status?: "active" | "expired" | "loading" | "scanned"; 19 | onRefresh?: EnsembleAction; 20 | } & EnsembleWidgetProps; 21 | 22 | export const QRCode: React.FC = (props) => { 23 | const [qrValue, setQrValue] = useState(props.value); 24 | const { onRefresh, ...rest } = props; 25 | 26 | const { values, rootRef } = useRegisterBindings( 27 | { ...rest, qrValue, widgetName }, 28 | rest.id, 29 | { 30 | setValue: setQrValue, 31 | }, 32 | ); 33 | 34 | const onRefreshAction = useEnsembleAction(onRefresh); 35 | 36 | // trigger on signin action 37 | const onRefreshActionCallback = useCallback(() => { 38 | return onRefreshAction?.callback(); 39 | }, [onRefreshAction?.callback]); 40 | 41 | return ( 42 |
43 | 53 |
54 | ); 55 | }; 56 | 57 | WidgetRegistry.register(widgetName, QRCode); 58 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import type { Expression } from "@ensembleui/react-framework"; 2 | import { useRegisterBindings } from "@ensembleui/react-framework"; 3 | import { Skeleton as MuiSkeleton } from "@mui/material"; 4 | import type { EnsembleWidgetProps } from "../shared/types"; 5 | import { WidgetRegistry } from "../registry"; 6 | 7 | const widgetName = "Skeleton"; 8 | 9 | export interface SkeletonProps extends EnsembleWidgetProps { 10 | useShimmer?: Expression; 11 | variant?: "circular" | "rectangular" | "rounded" | "text"; 12 | } 13 | 14 | export const Skeleton: React.FC = (props) => { 15 | const { values } = useRegisterBindings({ ...props, widgetName }, props.id); 16 | 17 | return ( 18 | 25 | ); 26 | }; 27 | 28 | WidgetRegistry.register(widgetName, Skeleton); 29 | WidgetRegistry.register("Shape", Skeleton); 30 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Spacer.tsx: -------------------------------------------------------------------------------- 1 | import { useRegisterBindings } from "@ensembleui/react-framework"; 2 | import { type EnsembleWidgetProps } from "../shared/types"; 3 | import { WidgetRegistry } from "../registry"; 4 | 5 | const widgetName = "Spacer"; 6 | 7 | export type SpacerProps = { 8 | styles: { 9 | size?: number; 10 | }; 11 | } & EnsembleWidgetProps; 12 | 13 | export const Spacer: React.FC = (props) => { 14 | const { values } = useRegisterBindings({ ...props, widgetName }, props.id); 15 | return ( 16 |
22 | ); 23 | }; 24 | 25 | WidgetRegistry.register(widgetName, Spacer); 26 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Stepper/StepType.tsx: -------------------------------------------------------------------------------- 1 | import type { EnsembleWidget } from "@ensembleui/react-framework"; 2 | import { CustomScopeProvider } from "@ensembleui/react-framework"; 3 | import { EnsembleRuntime } from "../../runtime"; 4 | import React from "react"; 5 | 6 | export interface StepTypeProps { 7 | scopeName?: string; 8 | data: unknown; 9 | template: EnsembleWidget; 10 | name: string; 11 | stateData: { 12 | active?: boolean; 13 | completed?: boolean; 14 | stepsLength?: number; 15 | index?: number; 16 | }; 17 | } 18 | export const StepType: React.FC = ({ 19 | data, 20 | template, 21 | stateData, 22 | name, 23 | }) => { 24 | const newData = data as Record; // Assuming data is an object 25 | const newStateData = { 26 | [name]: { ...stateData, ...(newData[name] as object) }, 27 | }; 28 | return ( 29 | 30 | {EnsembleRuntime.render([template])} 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Stepper/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Stepper"; 2 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/Text.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useRegisterBindings, 3 | type Expression, 4 | } from "@ensembleui/react-framework"; 5 | import { useEffect, useState } from "react"; 6 | import { Typography } from "antd"; 7 | import { toString } from "lodash-es"; 8 | import { WidgetRegistry } from "../registry"; 9 | import type { BaseTextProps } from "../shared/types"; 10 | import type { TextAlignment } from "../shared/styleSchema"; 11 | 12 | const widgetName = "Text"; 13 | 14 | export interface TextStyles { 15 | fontSize?: string | number; 16 | fontWeight?: string | number; 17 | /** @uiType color */ 18 | color?: Expression; 19 | fontFamily?: string; 20 | /** @uiType color */ 21 | backgroundColor?: string; 22 | textAlign?: TextAlignment; 23 | } 24 | 25 | export type TextProps = { 26 | styles?: TextStyles; 27 | } & BaseTextProps; 28 | 29 | export const Text: React.FC = (props) => { 30 | const [text, setText] = useState(props.text ?? ""); 31 | const { values, rootRef } = useRegisterBindings( 32 | { ...props, textBinding: props.text, text, widgetName }, 33 | props.id, 34 | { 35 | setText, 36 | }, 37 | ); 38 | 39 | // Update text if the binding changes 40 | useEffect(() => { 41 | if (props.text) { 42 | setText(values?.textBinding ?? ""); 43 | } 44 | }, [props.text, values?.textBinding]); 45 | 46 | return ( 47 | 57 | {`${toString(values?.text)}`} 58 | 59 | ); 60 | }; 61 | 62 | WidgetRegistry.register(widgetName, Text); 63 | -------------------------------------------------------------------------------- /packages/runtime/src/widgets/__tests__/Text.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-template-curly-in-string */ 2 | import { fireEvent, render, screen, waitFor } from "@testing-library/react"; 3 | import "@testing-library/jest-dom"; 4 | import { BrowserRouter } from "react-router-dom"; 5 | import { Text } from "../Text"; 6 | import { Button } from "../Button"; 7 | 8 | describe("Text", () => { 9 | test("initializes value from binding", () => { 10 | render(); 11 | 12 | expect(screen.getByText("my first widget")).toBeInTheDocument(); 13 | }); 14 | test("can set value with setText", async () => { 15 | render( 16 | <> 17 | 18 |