├── .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 |
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 |
22 | >,
23 | { wrapper: BrowserRouter },
24 | );
25 |
26 | const button = screen.getByText("Update");
27 | fireEvent.click(button);
28 |
29 | await waitFor(() => {
30 | expect(screen.getByText("my second widget")).toBeInTheDocument();
31 | });
32 | });
33 |
34 | test("updates value from binding", async () => {
35 | render(
36 | <>
37 |
38 |
42 |
48 | >,
49 | { wrapper: BrowserRouter },
50 | );
51 |
52 | const button = screen.getByText("Update");
53 | fireEvent.click(button);
54 | const button2 = screen.getByText("Update Storage");
55 | fireEvent.click(button2);
56 |
57 | await waitFor(() => {
58 | expect(screen.getByText("my third widget")).toBeInTheDocument();
59 | });
60 | });
61 | });
62 |
63 | /* eslint-enable no-template-curly-in-string */
64 |
--------------------------------------------------------------------------------
/packages/runtime/src/widgets/index.ts:
--------------------------------------------------------------------------------
1 | // component exports
2 | export * from "./Button";
3 | export * from "./Column";
4 | export * from "./Text";
5 | export * from "./Markdown";
6 | export * from "./Card";
7 | export * from "./Image";
8 | export * from "./Icon";
9 | export * from "./Row";
10 | export * from "./Charts";
11 | export * from "./DataGrid";
12 | export * from "./Conditional";
13 | export * from "./TabBar";
14 | export * from "./Search";
15 | export * from "./Progress";
16 | export * from "./GridView";
17 | export * from "./Avatar/Avatar";
18 | export * from "./Tag";
19 | export * from "./Form";
20 | export * from "./Divider";
21 | export * from "./Tag";
22 | export * from "./Carousel";
23 | export * from "./PopupMenu";
24 | export * from "./Stepper";
25 | export * from "./ToggleButton";
26 | export * from "./SignInWithGoogle";
27 | export * from "./LoadingContainer";
28 | export * from "./Skeleton";
29 | export * from "./Collapsible";
30 | export * from "./FittedRow";
31 | export * from "./FittedColumn";
32 | export * from "./Flow";
33 | export * from "./Lottie";
34 | export * from "./Slider";
35 | export * from "./Flex";
36 | export * from "./Switch";
37 | export * from "./ImageCropper";
38 | export * from "./Spacer";
39 | export * from "./Stack";
40 | export * from "./QrCode";
41 | export * from "./ToolTip";
42 | export * from "./Html";
43 |
--------------------------------------------------------------------------------
/packages/runtime/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/react-library.json",
3 | "include": ["./src", "./turbo"],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/runtime/tsdoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
3 | "extends": ["tsconfig/tsdoc.json"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/runtime/turbo/generators/config.ts:
--------------------------------------------------------------------------------
1 | import type { PlopTypes } from "@turbo/gen";
2 |
3 | // Learn more about Turborepo Generators at https://turbo.build/repo/docs/core-concepts/monorepos/code-generation
4 |
5 | export default function generator(plop: PlopTypes.NodePlopAPI): void {
6 | // A simple generator to add a new React widget to the internal UI library
7 | plop.setGenerator("react-widget", {
8 | description: "Adds a new react widget",
9 | prompts: [
10 | {
11 | type: "input",
12 | name: "name",
13 | message: "What is the name of the widget?",
14 | },
15 | ],
16 | actions: [
17 | {
18 | type: "add",
19 | path: "src/widgets/{{pascalCase name}}.tsx",
20 | templateFile: "templates/component.hbs",
21 | },
22 | {
23 | type: "append",
24 | path: "src/widgets/index.tsx",
25 | pattern: /(?\/\/ component exports)/g,
26 | template: 'export * from "./{{pascalCase name}}";',
27 | },
28 | ],
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/packages/runtime/turbo/generators/templates/component.hbs:
--------------------------------------------------------------------------------
1 | interface Props {
2 | children?: React.ReactNode;
3 | }
4 |
5 | export const {{ pascalCase name }} = ({ children }: Props) => {
6 | return (
7 |
8 |
{{ name }}
9 | {children}
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/packages/tsconfig/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # tsconfig
2 |
3 | ## 0.0.1
4 |
5 | ### Patch Changes
6 |
7 | - e0b96f5: adopted the dot notation CSS class-based styling while introducing the className attribute to the styles. also, deprecated the names attribute os styles.
8 |
--------------------------------------------------------------------------------
/packages/tsconfig/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "composite": false,
6 | "declaration": true,
7 | "declarationMap": true,
8 | "esModuleInterop": true,
9 | "experimentalDecorators": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "inlineSources": false,
12 | "isolatedModules": true,
13 | "moduleResolution": "node",
14 | "noUnusedLocals": false,
15 | "noUnusedParameters": false,
16 | "preserveWatchOutput": true,
17 | "skipLibCheck": true,
18 | "strict": true,
19 | "strictNullChecks": true,
20 | "module": "ESNext",
21 | "jsx": "react-jsx"
22 | },
23 | "exclude": ["node_modules"]
24 | }
25 |
--------------------------------------------------------------------------------
/packages/tsconfig/nextjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Next.js",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "plugins": [{ "name": "next" }],
7 | "allowJs": true,
8 | "declaration": false,
9 | "declarationMap": false,
10 | "incremental": true,
11 | "jsx": "preserve",
12 | "lib": ["dom", "dom.iterable", "esnext"],
13 | "module": "esnext",
14 | "noEmit": true,
15 | "resolveJsonModule": true,
16 | "strict": false,
17 | "target": "es5"
18 | },
19 | "include": ["src", "next-env.d.ts"],
20 | "exclude": ["node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/packages/tsconfig/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tsconfig",
3 | "version": "0.0.1",
4 | "private": true,
5 | "license": "MIT",
6 | "publishConfig": {
7 | "access": "public"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/tsconfig/react-app.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Next.js",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "allowJs": true,
7 | "declaration": false,
8 | "declarationMap": false,
9 | "incremental": true,
10 | "jsx": "preserve",
11 | "lib": ["dom", "dom.iterable", "esnext"],
12 | "module": "esnext",
13 | "noEmit": true,
14 | "resolveJsonModule": true,
15 | "target": "es5"
16 | }
17 | }
--------------------------------------------------------------------------------
/packages/tsconfig/react-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "React Library",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "jsx": "react-jsx",
7 | "lib": ["dom", "dom.iterable", "es2019", "DOM"],
8 | "module": "ESNext",
9 | "target": "es6"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/tsconfig/tsdoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
3 | "tagDefinitions": [
4 | {
5 | "tagName": "@uiType",
6 | "syntaxKind": "block"
7 | },
8 | {
9 | "tagName": "@treeItemWidgetLabel",
10 | "syntaxKind": "block"
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "apps/*"
3 | - "packages/*"
4 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/base.json"
3 | }
4 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "globalDependencies": ["**/.env.*local"],
4 | "pipeline": {
5 | "build": {
6 | "dependsOn": ["^build"],
7 | "outputs": ["dist/**", "build/**"]
8 | },
9 | "lint": {},
10 | "lint-staged": {},
11 | "dev": {
12 | "cache": false,
13 | "persistent": true
14 | },
15 | "test": {}
16 | }
17 | }
18 |
--------------------------------------------------------------------------------