├── .cursor
└── rules
│ ├── use-shadcn-for-ui.mdc
│ └── use-vitest-for-unit-testing.mdc
├── .eslintrc.cjs
├── .github
├── FUNDING.yml
└── workflows
│ ├── release.yml
│ └── tests.yml
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .prettierignore
├── .windsurf
├── component-rules.yaml
├── hooks-rules.yaml
├── rules.yaml
└── web-blocks-rules.yaml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── commitlint.config.cjs
├── components.json
├── declaration.d.ts
├── e2e_tests
└── example.spec.ts
├── index.html
├── package.json
├── playwright.config.ts
├── plugin.js
├── pnpm-lock.yaml
├── poc
├── convert.ts
└── html.ts
├── postcss.config.js
├── prettier.config.cjs
├── public
├── chaibuilder-logo.png
├── fonts
│ ├── GeistMonoVF.woff
│ └── GeistVF.woff
├── mockServiceWorker.js
└── vite.svg
├── publish.js
├── src
├── App.css
├── Editor.tsx
├── EditorCustom.tsx
├── FEATURE_TOGGLES.tsx
├── Preview.tsx
├── _demo
│ ├── EXTERNAL_DATA.ts
│ ├── PARTIALS.ts
│ ├── THEME_PRESETS.ts
│ ├── add-block-ai.tsx
│ ├── atoms-dev.ts
│ ├── blocks
│ │ ├── collection-list.tsx
│ │ ├── index.tsx
│ │ └── modal.tsx
│ ├── custom-layout.tsx
│ ├── custom-widget.tsx
│ ├── data-providers
│ │ └── data.ts
│ ├── export-button.tsx
│ ├── lang-button.tsx
│ ├── microsoft-clarity.tsx
│ ├── mock
│ │ ├── browser.ts
│ │ ├── data.ts
│ │ ├── handlers.ts
│ │ └── template.html
│ ├── page.ts
│ ├── panels
│ │ └── panel.tsx
│ ├── preview
│ │ ├── preview-settings.tsx
│ │ └── web-preview.tsx
│ ├── ptBR.json
│ ├── right-top.tsx
│ └── top-bar.tsx
├── core
│ ├── async-props
│ │ └── use-async-props.ts
│ ├── atoms
│ │ ├── blocks.ts
│ │ ├── builder.ts
│ │ ├── store.ts
│ │ └── ui.ts
│ ├── components
│ │ ├── PreviewScreen.tsx
│ │ ├── QuickPrompts.tsx
│ │ ├── ai
│ │ │ └── ai-chat-panel.tsx
│ │ ├── ask-ai-panel.tsx
│ │ ├── canvas
│ │ │ ├── IframeInitialContent.ts
│ │ │ ├── add-block-placements.tsx
│ │ │ ├── block-floating-actions.tsx
│ │ │ ├── block-style-highlight.tsx
│ │ │ ├── bread-crumb.tsx
│ │ │ ├── canvas-area.tsx
│ │ │ ├── dnd
│ │ │ │ ├── atoms.ts
│ │ │ │ ├── getOrientation.ts
│ │ │ │ └── useDnd.ts
│ │ │ ├── empty-canvas.tsx
│ │ │ ├── keyboar-handler.tsx
│ │ │ ├── static
│ │ │ │ ├── add-block-at-bottom.tsx
│ │ │ │ ├── async-props-wrapper.tsx
│ │ │ │ ├── bubble-menu.tsx
│ │ │ │ ├── canvas-events-watcher.tsx
│ │ │ │ ├── chai-canvas.tsx
│ │ │ │ ├── chai-theme-helpers.ts
│ │ │ │ ├── code-editor.tsx
│ │ │ │ ├── error-fallback.tsx
│ │ │ │ ├── get-theme-custom-font-face.test.ts
│ │ │ │ ├── head-tags.tsx
│ │ │ │ ├── new-blocks-render-helpers.ts
│ │ │ │ ├── new-blocks-renderer.tsx
│ │ │ │ ├── resizable-canvas-wrapper.tsx
│ │ │ │ ├── static-blocks-renderer.tsx
│ │ │ │ ├── static-canvas.tsx
│ │ │ │ ├── use-block-runtime-props.ts
│ │ │ │ ├── use-canvas-scale.ts
│ │ │ │ ├── use-chai-external-data.ts
│ │ │ │ └── with-block-text-editor.tsx
│ │ │ └── topbar
│ │ │ │ ├── ai-assistant.tsx
│ │ │ │ ├── canvas-breakpoints.tsx
│ │ │ │ ├── canvas-top-bar.tsx
│ │ │ │ ├── clear-canvas.tsx
│ │ │ │ ├── dark-mode.tsx
│ │ │ │ ├── data-binding.tsx
│ │ │ │ └── undo-redo.tsx
│ │ ├── chai-select.tsx
│ │ ├── chaibuilder-editor.tsx
│ │ ├── count-down.tsx
│ │ ├── css-theme-var.tsx
│ │ ├── fallback-error.tsx
│ │ ├── hot-keys.tsx
│ │ ├── index.ts
│ │ ├── layout
│ │ │ ├── add-blocks-dialog.tsx
│ │ │ └── root-layout.tsx
│ │ ├── nested-path-selector.tsx
│ │ ├── noop-component.tsx
│ │ ├── settings
│ │ │ ├── ask-ai-style.tsx
│ │ │ ├── block-settings.tsx
│ │ │ ├── block-styling-props.tsx
│ │ │ ├── block-styling.tsx
│ │ │ ├── choices
│ │ │ │ ├── advance-choices.tsx
│ │ │ │ ├── block-style.tsx
│ │ │ │ ├── color-choice.tsx
│ │ │ │ ├── dropdown-choices.tsx
│ │ │ │ ├── icon-choice.tsx
│ │ │ │ ├── multiple-choices.tsx
│ │ │ │ ├── range-choices.tsx
│ │ │ │ └── style-context.tsx
│ │ │ ├── json-form.tsx
│ │ │ ├── new-panel
│ │ │ │ ├── attributes-editor.tsx
│ │ │ │ ├── block-attributes-editor.tsx
│ │ │ │ ├── breakpoint-selector.tsx
│ │ │ │ ├── manual-classes.tsx
│ │ │ │ └── setting-section.tsx
│ │ │ ├── settings-context.tsx
│ │ │ └── settings-panel.tsx
│ │ ├── sidepanels
│ │ │ └── panels
│ │ │ │ ├── add-blocks
│ │ │ │ ├── add-blocks.tsx
│ │ │ │ ├── block-controller.tsx
│ │ │ │ ├── core-block.tsx
│ │ │ │ ├── default-blocks.tsx
│ │ │ │ ├── import-html.tsx
│ │ │ │ ├── libraries-panel.tsx
│ │ │ │ ├── libraries-select.tsx
│ │ │ │ └── partial-blocks.tsx
│ │ │ │ ├── images
│ │ │ │ └── media-manager-modal.tsx
│ │ │ │ ├── outline
│ │ │ │ ├── block-more-options.tsx
│ │ │ │ ├── block-type-icon.tsx
│ │ │ │ ├── default-cursor.tsx
│ │ │ │ ├── default-drag-preview.tsx
│ │ │ │ ├── default-shortcuts.tsx
│ │ │ │ ├── list-tree.tsx
│ │ │ │ ├── node.tsx
│ │ │ │ ├── paste-into-root.tsx
│ │ │ │ ├── save-to-library.tsx
│ │ │ │ ├── tree-action-tooltips.tsx
│ │ │ │ ├── unlink-library-block.tsx
│ │ │ │ └── upsert-library-block-modal.tsx
│ │ │ │ └── theme-configuration
│ │ │ │ ├── BorderRadiusInput.tsx
│ │ │ │ ├── ColorPickerInput.tsx
│ │ │ │ ├── ThemeConfigPanel.tsx
│ │ │ │ ├── font-selector.tsx
│ │ │ │ └── index.ts
│ │ └── topbar
│ │ │ ├── Preview.tsx
│ │ │ └── save-button.tsx
│ ├── constants
│ │ ├── CLASSES_LIST.ts
│ │ ├── CLASS_VALUES.ts
│ │ ├── FONTS.ts
│ │ ├── ICONS.tsx
│ │ ├── LANGUAGES.ts
│ │ ├── LAYOUT_MODE.ts
│ │ ├── MODIFIERS.ts
│ │ ├── PERMISSIONS.ts
│ │ ├── STRINGS.ts
│ │ ├── STYLING_GROUPS.ts
│ │ ├── TWCLASS_VALUES.ts
│ │ └── TW_COREPLUGIN_PREFIX.ts
│ ├── events.ts
│ ├── extensions
│ │ ├── __tests__
│ │ │ └── save-to-library.test.tsx
│ │ ├── add-block-tabs.test.tsx
│ │ ├── add-block-tabs.tsx
│ │ ├── blocks-settings.test.tsx
│ │ ├── blocks-settings.tsx
│ │ ├── libraries.ts
│ │ ├── media-manager.test.tsx
│ │ ├── media-manager.tsx
│ │ ├── save-to-library.tsx
│ │ ├── sidebar-panels.test.tsx
│ │ ├── sidebar-panels.tsx
│ │ ├── top-bar.test.tsx
│ │ └── top-bar.tsx
│ ├── frame
│ │ ├── Frame.tsx
│ │ ├── frame-content.tsx
│ │ ├── frame-context.tsx
│ │ └── index.ts
│ ├── functions
│ │ ├── SanitizeClasses.ts
│ │ ├── block-helpers.ts
│ │ ├── blocks-fn.ts
│ │ ├── class-fn.test.ts
│ │ ├── class-fn.ts
│ │ ├── common-functions.test.ts
│ │ ├── common-functions.ts
│ │ ├── convert-brbitrary-to-tailwind-class-data.ts
│ │ ├── convert-brbitrary-to-tailwind-class.test.ts
│ │ ├── convert-brbitrary-to-tailwind-class.ts
│ │ ├── get-new-classes.ts
│ │ ├── get-user-input-values.test.ts
│ │ ├── get-user-input-values.ts
│ │ ├── helper-fn.test.ts
│ │ ├── helper-fn.ts
│ │ ├── is-visible-at-breakpoint.test.ts
│ │ ├── is-visible-at-breakpoint.ts
│ │ ├── logging.ts
│ │ ├── order-classes-by-breakpoint.ts
│ │ ├── remove-duplicate-classes.ts
│ │ ├── sanitize-classes.test.ts
│ │ ├── split-blocks.ts
│ │ └── wrapInsideContainer.ts
│ ├── history
│ │ ├── insert-block-at-position.test.ts
│ │ ├── insert-block-at-position.ts
│ │ ├── move-blocks-with-children.test.ts
│ │ ├── move-blocks-with-children.ts
│ │ ├── use-blocks-store-manager.ts
│ │ ├── use-blocks-store-undoable-actions.ts
│ │ └── use-undo-manager.ts
│ ├── hooks
│ │ ├── default-theme-options.ts
│ │ ├── get-split-classes.ts
│ │ ├── hooks.ts
│ │ ├── index.ts
│ │ ├── use-add-block.ts
│ │ ├── use-add-classes-to-blocks.ts
│ │ ├── use-all-data-providers.ts
│ │ ├── use-ask-ai.ts
│ │ ├── use-block-highlight.ts
│ │ ├── use-branding-options.ts
│ │ ├── use-broadcast-channel.ts
│ │ ├── use-builder-prop.ts
│ │ ├── use-builder-reset.ts
│ │ ├── use-canvas-settings.ts
│ │ ├── use-canvas-zoom.ts
│ │ ├── use-code-editor.ts
│ │ ├── use-copy-blockIds.ts
│ │ ├── use-copy-to-clipboard.ts
│ │ ├── use-current-page.ts
│ │ ├── use-cut-blockIds.ts
│ │ ├── use-dark-mode.ts
│ │ ├── use-duplicate-blocks.ts
│ │ ├── use-expand-tree.ts
│ │ ├── use-get-page-data.ts
│ │ ├── use-hidden-blocks.ts
│ │ ├── use-highlight-blockId.ts
│ │ ├── use-inline-editing.ts
│ │ ├── use-key-event-watcher.ts
│ │ ├── use-languages.ts
│ │ ├── use-library-blocks.tsx
│ │ ├── use-partial-blocks-store.ts
│ │ ├── use-paste-blocks.ts
│ │ ├── use-permissions.ts
│ │ ├── use-preview-mode.ts
│ │ ├── use-pub-sub.ts
│ │ ├── use-remove-blocks.ts
│ │ ├── use-remove-classes-from-blocks.ts
│ │ ├── use-save-page.ts
│ │ ├── use-screen-size-width.ts
│ │ ├── use-select-block-classes.ts
│ │ ├── use-selected-blockIds.ts
│ │ ├── use-selected-breakpoints.ts
│ │ ├── use-selected-library.ts
│ │ ├── use-selected-styling-blocks.ts
│ │ ├── use-sidebar-active-panel.ts
│ │ ├── use-styling-breakpoint.ts
│ │ ├── use-styling-state.ts
│ │ ├── use-theme.ts
│ │ ├── use-update-block-atom.ts
│ │ ├── use-update-blocks-props.ts
│ │ └── use-wrapper-block.ts
│ ├── import-html
│ │ ├── general.ts
│ │ ├── html-to-json.test.ts
│ │ ├── html-to-json.ts
│ │ ├── import-video.test.ts
│ │ └── import-video.ts
│ ├── index.css
│ ├── index.ts
│ ├── lib.ts
│ ├── locales
│ │ ├── en.json
│ │ └── load.ts
│ ├── main
│ │ └── index.ts
│ ├── miscellaneous
│ │ └── index.ts
│ ├── pubsub.ts
│ ├── rjsf-widgets
│ │ ├── Icon.tsx
│ │ ├── binding.tsx
│ │ ├── code-widget.tsx
│ │ ├── collection-select.tsx
│ │ ├── color.tsx
│ │ ├── data-binding-selector.tsx
│ │ ├── image.tsx
│ │ ├── index.ts
│ │ ├── json-form-field-template.tsx
│ │ ├── link.tsx
│ │ ├── repeater-binding.tsx
│ │ ├── row-col.tsx
│ │ ├── rte-widget.tsx
│ │ ├── slider.tsx
│ │ └── sources.tsx
│ ├── screen-too-small.tsx
│ └── utils
│ │ └── cn.ts
├── extentions.tsx
├── hooks
│ └── use-toast.ts
├── index.css
├── lib
│ └── utils.ts
├── main.tsx
├── render
│ ├── apply-binding.ts
│ ├── async-props-block.tsx
│ ├── block-renderer.tsx
│ ├── blocks-renderer.tsx
│ ├── functions.test.ts
│ ├── functions.ts
│ ├── get-tailwind-css.ts
│ ├── index.ts
│ └── render-chai-blocks.tsx
├── runtime.ts
├── tailwind
│ ├── get-chai-builder-tailwind-config.ts
│ ├── get-chai-builder-theme.ts
│ ├── index.ts
│ └── plugin.ts
├── types
│ ├── chai-block.ts
│ ├── chaibuilder-editor-props.ts
│ ├── collections.ts
│ ├── common.ts
│ ├── core-block.ts
│ ├── index.ts
│ └── types.ts
├── ui
│ ├── index.ts
│ └── shadcn
│ │ ├── components
│ │ └── ui
│ │ │ ├── accordion.tsx
│ │ │ ├── alert-dialog.tsx
│ │ │ ├── alert.tsx
│ │ │ ├── avatar.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── checkbox.tsx
│ │ │ ├── command.tsx
│ │ │ ├── context-menu.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ ├── hover-card.tsx
│ │ │ ├── input.tsx
│ │ │ ├── label.tsx
│ │ │ ├── navigation-menu.tsx
│ │ │ ├── popover.tsx
│ │ │ ├── scroll-area.tsx
│ │ │ ├── select.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── sheet.tsx
│ │ │ ├── skeleton.tsx
│ │ │ ├── slider.tsx
│ │ │ ├── sooner.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── textarea.tsx
│ │ │ ├── toast.tsx
│ │ │ ├── toaster.tsx
│ │ │ ├── toggle.tsx
│ │ │ ├── tooltip.tsx
│ │ │ └── use-toast.ts
│ │ └── lib
│ │ └── utils.ts
├── vite-env.d.ts
├── vitest-setup.ts
└── web-blocks
│ ├── README.md
│ ├── box.tsx
│ ├── button.tsx
│ ├── chaibuilder-link.tsx
│ ├── copy-button.tsx
│ ├── custom-html.tsx
│ ├── custom-script.tsx
│ ├── dark-mode.tsx
│ ├── divider.tsx
│ ├── empty-box.tsx
│ ├── empty-slot.tsx
│ ├── form
│ ├── checkbox.tsx
│ ├── form-button.tsx
│ ├── form.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── radio.tsx
│ ├── select.tsx
│ └── textarea.tsx
│ ├── global-block.tsx
│ ├── heading.tsx
│ ├── helper.ts
│ ├── hidden
│ ├── body.tsx
│ ├── line-break.tsx
│ └── table.tsx
│ ├── icon.tsx
│ ├── image.tsx
│ ├── index.ts
│ ├── lightbox-link.tsx
│ ├── link.tsx
│ ├── list.tsx
│ ├── listitem.tsx
│ ├── paragraph.tsx
│ ├── partial-block.tsx
│ ├── repeater.tsx
│ ├── row-col.tsx
│ ├── rte.tsx
│ ├── span.tsx
│ ├── text.tsx
│ └── video.tsx
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.node.json
├── vercel.json
├── vite.config.live.ts
├── vite.config.ts
└── vitest.config.ts
/.cursor/rules/use-shadcn-for-ui.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs:
4 | alwaysApply: true
5 | ---
6 |
--------------------------------------------------------------------------------
/.cursor/rules/use-vitest-for-unit-testing.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description: write unit tests for functions, components and hooks
3 | globs:
4 | alwaysApply: false
5 | ---
6 |
7 | # Vitest for unit testing
8 |
9 | - If you need to test a pure function and its the only function in file, use vitest inline mode
10 | - Wrap tests in describe blocks based on the exported functions and components
11 | - Do not use vitest imports as they are globally available
12 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:react-hooks/recommended',
8 | ],
9 | ignorePatterns: ['dist', 'src/core/ui/**', '.eslintrc.cjs'],
10 | parser: '@typescript-eslint/parser',
11 | plugins: ['react-refresh'],
12 | rules: {
13 | '@typescript-eslint/no-explicit-any': 'off',
14 | '@typescript-eslint/ban-ts-comment': 'off',
15 | 'react/prop-type': 'off',
16 | 'react-refresh/only-export-components': [
17 | 'warn',
18 | { allowConstantExport: true },
19 | ],
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | #github: [surajair]
4 | open_collective: chaibuilder
5 | buy_me_a_coffee: chaibuilder
6 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Unit & Integration Tests
2 | on:
3 | push:
4 | branches: [main]
5 | pull_request:
6 | branches: [main]
7 | jobs:
8 | test:
9 | timeout-minutes: 60
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | - uses: actions/setup-node@v3
14 | with:
15 | node-version: 18
16 | - name: Install dependencies
17 | run: npm install -g pnpm && pnpm install --no-frozen-lockfile
18 | - name: Run Unit & Integration tests
19 | run: pnpm run test
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | .env
15 | live
16 |
17 | # Editor directories and files
18 | .fleet
19 | .vscode/*
20 | !.vscode/extensions.json
21 | .idea
22 | .DS_Store
23 | *.suo
24 | *.ntvs*
25 | *.njsproj
26 | *.sln
27 | *.sw?
28 | /test-results/
29 | /playwright-report/
30 | /blob-report/
31 | /playwright/.cache/
32 | docs/
33 | scratch/
34 | tasks/
35 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | npx --no -- commitlint --edit $1
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | npm test
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | pnpm-lock.yaml
2 | node_modules
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2024, Suraj Air
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 "Chai Builder" 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 |
--------------------------------------------------------------------------------
/commitlint.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ['@commitlint/config-conventional'] };
2 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/index.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/ui/shadcn/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/ui/shadcn/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
22 |
--------------------------------------------------------------------------------
/declaration.d.ts:
--------------------------------------------------------------------------------
1 | declare module "undo-manager";
2 |
--------------------------------------------------------------------------------
/e2e_tests/example.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("get started link", async ({ page }) => {
4 | await page.goto("https://playwright.dev/");
5 |
6 | // Click the get started link.
7 | await page.getByRole("link", { name: "Get started" }).click();
8 |
9 | // Expects page to have a heading with the name of Installation.
10 | await expect(page.getByRole("heading", { name: "Installation" })).toBeVisible();
11 | });
12 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 | Chai Builder
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/plugin.js:
--------------------------------------------------------------------------------
1 | const plugin = require("tailwindcss/plugin");
2 |
3 | module.exports = plugin(function ({ addVariant, e }) {
4 | addVariant("cb-dropdown-open", [
5 | // Match: .cb-dropdown.cb-dropdown-open > .cb-dropdown-toggle .cb-dropdown-open:rotate-180
6 | ({ modifySelectors, separator }) => {
7 | modifySelectors(({ className }) => {
8 | return `.cb-dropdown.cb-dropdown-open > .cb-dropdown-toggle .${e(`cb-dropdown-open${separator}${className}`)}`;
9 | });
10 | },
11 | // Match: .cb-dropdown.cb-dropdown-open > .cb-dropdown-menu > .cb-dropdown-open:bg-gray-100
12 | ({ modifySelectors, separator }) => {
13 | modifySelectors(({ className }) => {
14 | return `.cb-dropdown.cb-dropdown-open > .cb-dropdown-menu > .${e(`cb-dropdown-open${separator}${className}`)}`;
15 | });
16 | },
17 | // Match: .cb-dropdown-menu.cb-dropdown-open:block
18 | ({ modifySelectors, separator }) => {
19 | modifySelectors(({ className }) => {
20 | return `.cb-dropdown-menu.cb-dropdown-open.${e(`cb-dropdown-open${separator}${className}`)}`;
21 | });
22 | },
23 | ]);
24 | });
25 |
--------------------------------------------------------------------------------
/poc/html.ts:
--------------------------------------------------------------------------------
1 | export const html = `
2 |
3 |
31 |
32 |
33 | `;
34 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prettier.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | bracketSpacing: true,
3 | bracketSameLine: true,
4 | semi: true,
5 | trailingComma: "all",
6 | printWidth: 120,
7 | tabWidth: 2,
8 | plugins: ["prettier-plugin-tailwindcss"]
9 | };
10 |
--------------------------------------------------------------------------------
/public/chaibuilder-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chaibuilder/sdk/55f61ceee8ed90f8705cfac327b79a594b7322c0/public/chaibuilder-logo.png
--------------------------------------------------------------------------------
/public/fonts/GeistMonoVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chaibuilder/sdk/55f61ceee8ed90f8705cfac327b79a594b7322c0/public/fonts/GeistMonoVF.woff
--------------------------------------------------------------------------------
/public/fonts/GeistVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chaibuilder/sdk/55f61ceee8ed90f8705cfac327b79a594b7322c0/public/fonts/GeistVF.woff
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chaibuilder/sdk/55f61ceee8ed90f8705cfac327b79a594b7322c0/src/App.css
--------------------------------------------------------------------------------
/src/EditorCustom.tsx:
--------------------------------------------------------------------------------
1 | import { lsAiContextAtom, lsBlocksAtom } from "@/_demo/atoms-dev";
2 | import CustomLayout from "@/_demo/custom-layout";
3 | import PreviewWeb from "@/_demo/preview/web-preview";
4 | import { ChaiBlock, ChaiBuilderEditor } from "@/core/main";
5 | import { loadWebBlocks } from "@/web-blocks";
6 | import { useAtom } from "jotai";
7 | loadWebBlocks();
8 |
9 | function ChaiBuilderCustom() {
10 | const [blocks] = useAtom(lsBlocksAtom);
11 | const [aiContext, setAiContext] = useAtom(lsAiContextAtom);
12 | return (
13 | {
22 | localStorage.setItem("chai-builder-blocks", JSON.stringify(blocks));
23 | localStorage.setItem("chai-builder-providers", JSON.stringify(providers));
24 | localStorage.setItem("chai-builder-branding-options", JSON.stringify(brandingOptions));
25 | await new Promise((resolve) => setTimeout(resolve, 2000));
26 | return true;
27 | }}
28 | saveAiContextCallback={async (aiContext: string) => {
29 | setAiContext(aiContext);
30 | return true;
31 | }}
32 | aiContext={aiContext}
33 | askAiCallBack={async (type: "styles" | "content", prompt: string, blocks: ChaiBlock[]) => {
34 | console.log("askAiCallBack", type, prompt, blocks);
35 | return new Promise((resolve) => resolve({ error: new Error("Not implemented") }));
36 | }}
37 | />
38 | );
39 | }
40 |
41 | export default ChaiBuilderCustom;
42 |
--------------------------------------------------------------------------------
/src/FEATURE_TOGGLES.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * A map of feature toggle flags.
3 | * @type {FeatureToggles}
4 | */
5 | // get value from query string
6 | function getFromQueryParams(key: string) {
7 | const urlParams = new URLSearchParams(window.location.search);
8 | return urlParams.get("flags")?.includes(key);
9 | }
10 |
11 | export const FEATURE_TOGGLES: { [key: string]: boolean } = {
12 | dnd: getFromQueryParams("dnd"),
13 | aiChat: getFromQueryParams("ai-chat"),
14 | };
15 |
--------------------------------------------------------------------------------
/src/_demo/EXTERNAL_DATA.ts:
--------------------------------------------------------------------------------
1 | export const EXTERNAL_DATA = {
2 | vehicle: {
3 | title: "Chai Builder",
4 | description: "Chai Builder is a tool that allows you to build your own website visually.",
5 | price: "$2000",
6 | image: "https://picsum.photos/400/200",
7 | link: "https://www.google.com",
8 | features: [
9 | {
10 | name: "Visual Editor",
11 | description: "Build your website visually with a drag and drop interface.",
12 | },
13 | {
14 | name: "Responsive Design",
15 | description: "Your website will look great on all devices.",
16 | },
17 | {
18 | name: "SEO Optimized",
19 | description: "Your website will be optimized for search engines.",
20 | },
21 | ],
22 | reviews: [
23 | {
24 | name: "Lorem Ipsum is a good astrologer",
25 | image: "https://picsum.photos/400/200",
26 | rating: 4.5,
27 | comment: "This is a comment",
28 | },
29 | {
30 | name: "Pippo is a good driver",
31 | image: "https://picsum.photos/500/300",
32 | rating: 4,
33 | comment: "This is an another comment",
34 | },
35 | ],
36 | },
37 | global: {
38 | siteName: "My Site",
39 | twitterHandle: "@my-twitter-handle",
40 | description: "This is a description of my page",
41 | },
42 | };
43 |
--------------------------------------------------------------------------------
/src/_demo/PARTIALS.ts:
--------------------------------------------------------------------------------
1 | export const PARTIALS = {
2 | partial: [
3 | {
4 | _type: "Box",
5 | _id: "header",
6 | tag: "div",
7 | styles: "#styles:,flex flex-col items-center justify-center h-96",
8 | },
9 | {
10 | _type: "Span",
11 | content: "Span 2",
12 | _id: "span",
13 | _parent: "header",
14 | styles: "#styles:,text-center text-3xl font-bold p-4 bg-gray-100",
15 | },
16 | {
17 | _type: "Heading",
18 | content: "Heading 1",
19 | _id: "heading_1",
20 | _parent: "header",
21 | styles: "#styles:,text-center text-3xl font-bold p-4 bg-gray-100",
22 | },
23 | ],
24 | header: [
25 | {
26 | styles: "#styles:,flex w-full max-w-full overflow-hidden rounded-lg bg-white shadow-md dark:bg-gray-800",
27 | tag: "div",
28 | backgroundImage: "",
29 | _type: "Box",
30 | _id: "header_1",
31 | _name: "Box",
32 | },
33 | {
34 | _type: "Heading",
35 | content: "Heading",
36 | _id: "heading_2",
37 | _parent: "header_1",
38 | styles: "#styles:,text-center text-3xl font-bold p-4 bg-gray-100",
39 | },
40 | ],
41 | footer: [
42 | {
43 | styles: "#styles:,flex w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-md dark:bg-gray-800",
44 | tag: "div",
45 | backgroundImage: "",
46 | _type: "Box",
47 | _id: "footer_1",
48 | _name: "Box",
49 | },
50 | {
51 | _type: "Heading",
52 | content: "Footer",
53 | _id: "footer_heading_1",
54 | _parent: "footer_1",
55 | styles: "#styles:,text-center text-3xl font-bold p-4 bg-gray-100",
56 | },
57 | ],
58 | };
59 |
--------------------------------------------------------------------------------
/src/_demo/add-block-ai.tsx:
--------------------------------------------------------------------------------
1 | export default function AddBlockAi() {
2 | return AddBlockAi
;
3 | }
4 |
--------------------------------------------------------------------------------
/src/_demo/atoms-dev.ts:
--------------------------------------------------------------------------------
1 | import { atomWithStorage } from "jotai/utils";
2 |
3 | export const lsBlocksAtom = atomWithStorage("chai-builder-blocks", []);
4 | export const lsThemeAtom = atomWithStorage("chai-builder-theme", {});
5 | export const lsAiContextAtom = atomWithStorage("chai-builder-ai-context", "");
6 |
--------------------------------------------------------------------------------
/src/_demo/blocks/index.tsx:
--------------------------------------------------------------------------------
1 | import { Component as CollectionListComponent, Config as CollectionListConfig } from "@/_demo/blocks/collection-list";
2 | import { Component as ModalComponent, Config as ModalConfig } from "@/_demo/blocks/modal";
3 | import { registerChaiBlock } from "@chaibuilder/runtime";
4 |
5 | export default function registerCustomBlocks() {
6 | registerChaiBlock(CollectionListComponent, CollectionListConfig);
7 | registerChaiBlock(ModalComponent, ModalConfig);
8 | }
9 |
--------------------------------------------------------------------------------
/src/_demo/custom-widget.tsx:
--------------------------------------------------------------------------------
1 | const GalleryWidget = () => {
2 | return GalleryWidget
;
3 | };
4 |
5 | export default GalleryWidget;
6 |
--------------------------------------------------------------------------------
/src/_demo/data-providers/data.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chaibuilder/sdk/55f61ceee8ed90f8705cfac327b79a594b7322c0/src/_demo/data-providers/data.ts
--------------------------------------------------------------------------------
/src/_demo/microsoft-clarity.tsx:
--------------------------------------------------------------------------------
1 | import clarity from "@microsoft/clarity";
2 | import { useEffect } from "react";
3 |
4 | declare global {
5 | interface Window {
6 | clarity: any;
7 | }
8 | }
9 |
10 | interface MicrosoftClarityProps {
11 | clarityId: string;
12 | }
13 |
14 | export function MicrosoftClarity({ clarityId }: MicrosoftClarityProps) {
15 | useEffect(() => {
16 | // Skip if no Clarity ID is provided
17 | if (!clarityId) {
18 | console.warn("Microsoft Clarity ID is not provided");
19 | return;
20 | }
21 |
22 | // Initialize Clarity
23 | clarity.init(clarityId);
24 | }, [clarityId]);
25 |
26 | return null;
27 | }
28 |
--------------------------------------------------------------------------------
/src/_demo/mock/browser.ts:
--------------------------------------------------------------------------------
1 | import { handlers } from "@/_demo/mock/handlers";
2 | import { setupWorker } from "msw/browser";
3 |
4 | export const worker = setupWorker(...handlers);
5 |
--------------------------------------------------------------------------------
/src/_demo/mock/template.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
Hello World
15 |
Hello World
16 |
17 |
--------------------------------------------------------------------------------
/src/_demo/right-top.tsx:
--------------------------------------------------------------------------------
1 | import { useRightPanel, useSavePage } from "@/core/hooks";
2 | import { Button } from "@/ui/shadcn/components/ui/button";
3 | import { Eye, Paintbrush, Save } from "lucide-react";
4 |
5 | export default function RightTop() {
6 | const [panel, setRightPanel] = useRightPanel();
7 | const { savePage, saveState } = useSavePage();
8 | return (
9 |
10 |
18 |
19 |
23 |
24 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/_demo/top-bar.tsx:
--------------------------------------------------------------------------------
1 | import { LanguageButton } from "@/_demo/lang-button";
2 | import RightTop from "@/_demo/right-top";
3 | import { Alert, AlertDescription } from "@/ui/shadcn/components/ui/alert";
4 | import { Info } from "lucide-react";
5 |
6 | const Logo = () => {
7 | return (
8 |
18 | );
19 | };
20 |
21 | const DemoAlert = () => {
22 | return (
23 |
24 |
25 |
26 | Demo mode - Changes are saved in your browser local storage. AI actions are
27 | mocked.
28 |
29 |
30 | );
31 | };
32 |
33 | export default function Topbar() {
34 | return (
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/core/atoms/blocks.ts:
--------------------------------------------------------------------------------
1 | import { convertToBlocksTree } from "@/core/functions/blocks-fn";
2 | import { atom } from "jotai";
3 | import { splitAtom } from "jotai/utils";
4 | import { filter, has } from "lodash-es";
5 |
6 | // derived atoms
7 | // @ts-ignore
8 | export const presentBlocksAtom = atom([]);
9 | presentBlocksAtom.debugLabel = "presentBlocksAtom";
10 |
11 | //TODO: Need a better name for this atom. Also should be a custom hook
12 | export const treeDSBlocks = atom((get) => {
13 | const presentBlocks = get(presentBlocksAtom);
14 | return convertToBlocksTree([...presentBlocks]);
15 | });
16 | treeDSBlocks.debugLabel = "treeDSBlocks";
17 |
18 | export const pageBlocksAtomsAtom = splitAtom(presentBlocksAtom);
19 | pageBlocksAtomsAtom.debugLabel = "pageBlocksAtomsAtom";
20 |
21 | export const builderActivePageAtom = atom("");
22 | builderActivePageAtom.debugLabel = "builderActivePageAtom";
23 |
24 | export const destinationDropIndexAtom = atom(-1);
25 | destinationDropIndexAtom.debugLabel = "destinationDropIndexAtom";
26 |
27 | export const buildingBlocksAtom: any = atom>([]);
28 | buildingBlocksAtom.debugLabel = "buildingBlocksAtom";
29 |
30 | export const globalBlocksAtom = atom>((get) => {
31 | const globalBlocks = get(buildingBlocksAtom) as Array;
32 | return filter(globalBlocks, (block) => has(block, "blockId"));
33 | });
34 | globalBlocksAtom.debugLabel = "globalBlocksAtom";
35 |
--------------------------------------------------------------------------------
/src/core/atoms/builder.ts:
--------------------------------------------------------------------------------
1 | import { useBlockRepeaterDataAtom } from "@/core/async-props/use-async-props";
2 | import { ChaiBuilderEditorProps } from "@/types";
3 | import { atom, useAtomValue } from "jotai";
4 | import { useMemo } from "react";
5 |
6 | export const chaiBuilderPropsAtom = atom | null>(null);
10 | chaiBuilderPropsAtom.debugLabel = "chaiBuilderPropsAtom";
11 |
12 | export const chaiExternalDataAtom = atom({});
13 | chaiExternalDataAtom.debugLabel = "chaiExternalDataAtom";
14 |
15 | export const chaiRjsfFieldsAtom = atom>>({});
16 | chaiRjsfFieldsAtom.debugLabel = "chaiRjsfFieldsAtom";
17 |
18 | export const chaiRjsfWidgetsAtom = atom>>({});
19 | chaiRjsfWidgetsAtom.debugLabel = "chaiRjsfWidgetsAtom";
20 |
21 | export const chaiRjsfTemplatesAtom = atom>>({});
22 | chaiRjsfTemplatesAtom.debugLabel = "chaiRjsfTemplatesAtom";
23 |
24 | export const chaiPageExternalDataAtom = atom>({});
25 | chaiPageExternalDataAtom.debugLabel = "chaiPageExternalDataAtom";
26 |
27 | export const usePageExternalData = () => {
28 | const [blockRepeaterData] = useBlockRepeaterDataAtom();
29 | const repeaterItems = useMemo(() => {
30 | const result = {};
31 | Object.entries(blockRepeaterData).forEach(([key, value]) => {
32 | if (value.status === "loaded")
33 | result[value.repeaterItems.replace("}}", `/${key}`).replace("{{", "")] = value.props;
34 | });
35 | return result;
36 | }, [blockRepeaterData]);
37 | const pageExternalData = useAtomValue(chaiPageExternalDataAtom);
38 | return { ...pageExternalData, ...repeaterItems };
39 | };
40 |
--------------------------------------------------------------------------------
/src/core/atoms/store.ts:
--------------------------------------------------------------------------------
1 | import { getDefaultStore } from "jotai";
2 |
3 | /**
4 | * Jotai store for global state management
5 | */
6 | export const builderStore: any = getDefaultStore();
7 |
--------------------------------------------------------------------------------
/src/core/components/PreviewScreen.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/core/functions/common-functions";
2 | import { useBuilderProp, usePreviewMode } from "@/core/hooks";
3 | import { Button } from "@/ui/shadcn/components/ui/button";
4 | import { Skeleton } from "@/ui/shadcn/components/ui/skeleton";
5 | import { EyeClosedIcon } from "@radix-ui/react-icons";
6 | import React, { Suspense } from "react";
7 | import { useTranslation } from "react-i18next";
8 |
9 | export const PreviewScreen = () => {
10 | const [isPreviewOn, setPreviewMode] = usePreviewMode();
11 | const { t } = useTranslation();
12 | const previewComponent = useBuilderProp("previewComponent", null);
13 | if (!isPreviewOn) return null;
14 | return (
15 |
16 |
20 |
21 | {previewComponent ? (
22 | }>{React.createElement(previewComponent)}
23 | ) : null}
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/core/components/canvas/block-style-highlight.tsx:
--------------------------------------------------------------------------------
1 | import { useInlineEditing } from "@/core/hooks/hooks";
2 | import { ChaiBlock } from "@/types/chai-block";
3 | import { flip } from "@floating-ui/dom";
4 | import { shift, useFloating } from "@floating-ui/react-dom";
5 | import { useResizeObserver } from "@react-hookz/web";
6 | import { Paintbrush } from "lucide-react";
7 |
8 | // NOTE: this component is not used anymore, but keeping it for now. Might remove it later.
9 | // Author: surajair
10 | export const BlockStyleHighlight = ({
11 | block,
12 | selectedStyleElement,
13 | }: {
14 | block: ChaiBlock;
15 | selectedStyleElement: HTMLElement | null;
16 | }) => {
17 | const { editingBlockId } = useInlineEditing();
18 | const { floatingStyles, refs, update } = useFloating({
19 | placement: "top-start",
20 | middleware: [shift(), flip()],
21 | elements: { reference: selectedStyleElement },
22 | });
23 |
24 | useResizeObserver(selectedStyleElement as HTMLElement, () => update(), selectedStyleElement !== null);
25 | if (!selectedStyleElement || editingBlockId) return null;
26 | const sameBlock = selectedStyleElement.getAttribute("data-block-id") === block?._id;
27 | if (sameBlock) return null;
28 |
29 | return (
30 | <>
31 | {
37 | e.stopPropagation();
38 | e.preventDefault();
39 | }}
40 | onMouseEnter={(e) => {
41 | e.stopPropagation();
42 | }}
43 | onKeyDown={(e) => e.stopPropagation()}
44 | className="isolate z-[999] flex items-center rounded-t bg-orange-500 p-px text-xs text-white">
45 |
46 |
47 | >
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/src/core/components/canvas/bread-crumb.tsx:
--------------------------------------------------------------------------------
1 | import { TypeIcon } from "@/core/components/sidepanels/panels/outline/block-type-icon";
2 | import { useBlockHighlight } from "@/core/hooks/use-block-highlight";
3 | import { useSelectedBlockHierarchy, useSelectedBlockIds } from "@/core/hooks/use-selected-blockIds";
4 | import { Button } from "@/ui/shadcn/components/ui/button";
5 | import { reverse } from "lodash-es";
6 | import { ChevronRight } from "lucide-react";
7 |
8 | export const Breadcrumb = () => {
9 | const hierarchy = useSelectedBlockHierarchy();
10 | const [, setSelected] = useSelectedBlockIds();
11 | const { highlightBlock } = useBlockHighlight();
12 |
13 | return (
14 |
15 |
16 | -
17 |
20 |
21 |
22 | {reverse(hierarchy).map((block, index) => (
23 | -
24 |
34 | {index !== hierarchy.length - 1 && }
35 |
36 | ))}
37 |
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/src/core/components/canvas/canvas-area.tsx:
--------------------------------------------------------------------------------
1 | import { Breadcrumb } from "@/core/components/canvas/bread-crumb";
2 | import StaticCanvas from "@/core/components/canvas/static/static-canvas";
3 | import { FallbackError } from "@/core/components/fallback-error";
4 | import { useBuilderProp, useCodeEditor } from "@/core/hooks";
5 | import { Skeleton } from "@/ui/shadcn/components/ui/skeleton";
6 | import { noop } from "lodash-es";
7 | import { Resizable } from "re-resizable";
8 | import React, { Suspense } from "react";
9 | import { ErrorBoundary } from "react-error-boundary";
10 |
11 | const CodeEditor = React.lazy(() => import("@/core/components/canvas/static/code-editor"));
12 |
13 | const CanvasArea: React.FC = () => {
14 | const [codeEditor] = useCodeEditor();
15 | const onErrorFn = useBuilderProp("onError", noop);
16 | return (
17 |
18 |
19 | }>
20 | } onError={onErrorFn}>
21 |
22 |
23 |
24 | {codeEditor ? (
25 | }>
26 |
27 |
28 |
29 |
30 | ) : null}
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | export default CanvasArea;
38 |
--------------------------------------------------------------------------------
/src/core/components/canvas/dnd/atoms.ts:
--------------------------------------------------------------------------------
1 | import { ChaiBlock } from "@/types/chai-block";
2 | import { atom } from "jotai";
3 |
4 | export const draggedBlockAtom = atom(null);
5 |
6 | export const dropTargetBlockIdAtom = atom(null);
7 |
--------------------------------------------------------------------------------
/src/core/components/canvas/dnd/getOrientation.ts:
--------------------------------------------------------------------------------
1 | export function getOrientation(
2 | parentElement: HTMLElement,
3 | blockElement: HTMLElement = null,
4 | ): "vertical" | "horizontal" {
5 | const computedStyle = window.getComputedStyle(parentElement);
6 | const blockComputedStyle = blockElement ? window.getComputedStyle(blockElement) : null;
7 | const display = computedStyle.display;
8 | const blockDisplay = blockComputedStyle ? blockComputedStyle.display : null;
9 |
10 | if (display === "flex" || display === "inline-flex") {
11 | const flexDirection = computedStyle.flexDirection;
12 |
13 | return flexDirection === "column" || flexDirection === "column-reverse" ? "vertical" : "horizontal";
14 | } else if (display === "grid") {
15 | const gridAutoFlow = computedStyle.gridAutoFlow;
16 | const gridTemplateColumns = computedStyle.gridTemplateColumns;
17 |
18 | // If the grid auto flow is set to column, it's vertical
19 | if (gridAutoFlow.includes("column")) {
20 | return "vertical";
21 | }
22 |
23 | // Check if gridTemplateColumns is explicitly set to a single column
24 | // In JSDOM environment, gridTemplateColumns might be empty or "none" for default grid
25 | if (
26 | gridTemplateColumns &&
27 | gridTemplateColumns !== "none" &&
28 | gridTemplateColumns !== "" &&
29 | !gridTemplateColumns.includes("calc") && // Handle calc expressions
30 | gridTemplateColumns.split(" ").length <= 1
31 | ) {
32 | return "vertical";
33 | }
34 |
35 | return "horizontal";
36 | } else if (blockDisplay === "inline-block" || blockDisplay === "inline") {
37 | return "horizontal";
38 | }
39 |
40 | return "vertical";
41 | }
42 |
--------------------------------------------------------------------------------
/src/core/components/canvas/empty-canvas.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from "react-i18next";
2 |
3 | export const EmptyCanvas = () => {
4 | const { t } = useTranslation();
5 | return (
6 |
7 |
{t("canvas_empty")}
8 |
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/src/core/components/canvas/keyboar-handler.tsx:
--------------------------------------------------------------------------------
1 | import { useFrame } from "@/core/frame";
2 | import { useKeyEventWatcher } from "@/core/hooks/use-key-event-watcher";
3 |
4 | export const KeyboardHandler = () => {
5 | const { document: iframeDoc } = useFrame();
6 | useKeyEventWatcher(iframeDoc);
7 | return null;
8 | };
9 |
--------------------------------------------------------------------------------
/src/core/components/canvas/static/add-block-at-bottom.tsx:
--------------------------------------------------------------------------------
1 | import { CHAI_BUILDER_EVENTS } from "@/core/events";
2 | import { usePermissions } from "@/core/hooks";
3 | import { PERMISSIONS } from "@/core/main";
4 | import { pubsub } from "@/core/pubsub";
5 | import { PlusIcon } from "lucide-react";
6 | import { useTranslation } from "react-i18next";
7 |
8 | export const AddBlockAtBottom = () => {
9 | const { t } = useTranslation();
10 | const { hasPermission } = usePermissions();
11 | const canAddBlock = hasPermission(PERMISSIONS.ADD_BLOCK);
12 |
13 | if (!canAddBlock) return null;
14 |
15 | return (
16 |
17 |
18 |
pubsub.publish(CHAI_BUILDER_EVENTS.OPEN_ADD_BLOCK)}
21 | className="block h-1 rounded bg-primary opacity-0 duration-200 group-hover:opacity-100">
22 |
23 |
{t("Add block")}
24 |
25 |
26 |
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/src/core/components/canvas/static/async-props-wrapper.tsx:
--------------------------------------------------------------------------------
1 | import { useAsyncProps } from "@/core/async-props/use-async-props";
2 | import { ChaiBlock, getRegisteredChaiBlock } from "@chaibuilder/runtime";
3 | import { get } from "lodash-es";
4 | import { useMemo } from "react";
5 |
6 | type AsyncPropsWrapperProps = {
7 | children: (asyncProps: Record) => React.ReactNode;
8 | block: ChaiBlock;
9 | };
10 |
11 | export const MayBeAsyncPropsWrapper = ({ children, block }: AsyncPropsWrapperProps) => {
12 | const registeredChaiBlock = useMemo(() => getRegisteredChaiBlock(block._type) as any, [block._type]);
13 | const dependencies = get(registeredChaiBlock, "dataProviderDependencies");
14 | const dataProviderFn = get(registeredChaiBlock, "dataProvider");
15 | const dataProviderMode = get(registeredChaiBlock, "dataProviderMode", "mock");
16 | const asyncPropsByBlockId = useAsyncProps(block, dataProviderMode, dependencies, dataProviderFn);
17 | return children(asyncPropsByBlockId);
18 | };
19 |
--------------------------------------------------------------------------------
/src/core/components/canvas/static/error-fallback.tsx:
--------------------------------------------------------------------------------
1 | export const ErrorFallback = () => {
2 | return (
3 |
4 | Something went wrong.
5 |
6 | );
7 | };
8 |
--------------------------------------------------------------------------------
/src/core/components/canvas/static/resizable-canvas-wrapper.tsx:
--------------------------------------------------------------------------------
1 | import { useSelectedBlockIds, useSelectedStylingBlocks } from "@/core/hooks";
2 | import { useDebouncedCallback, useResizeObserver } from "@react-hookz/web";
3 | import { useCallback, useEffect, useRef } from "react";
4 |
5 | export const ResizableCanvasWrapper = ({ children, onMount, onResize }: any) => {
6 | const [, setSelected] = useSelectedBlockIds();
7 | const [, setSelectedStyles] = useSelectedStylingBlocks();
8 | const mainContentRef = useRef(null);
9 | const db = useDebouncedCallback(
10 | () => {
11 | const { clientWidth } = mainContentRef.current as HTMLDivElement;
12 | onResize(clientWidth);
13 | },
14 | [mainContentRef.current],
15 | 100,
16 | );
17 | useResizeObserver(mainContentRef.current as HTMLElement, db, mainContentRef.current !== null);
18 | useEffect(() => {
19 | const { clientWidth } = mainContentRef.current as HTMLDivElement;
20 | onMount(clientWidth);
21 | // eslint-disable-next-line react-hooks/exhaustive-deps
22 | }, []);
23 |
24 | const deselectSelected = useCallback(() => {
25 | setSelected([]);
26 | setSelectedStyles([]);
27 | }, [setSelected, setSelectedStyles]);
28 |
29 | return (
30 |
31 | {children}
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/core/components/canvas/static/static-blocks-renderer.tsx:
--------------------------------------------------------------------------------
1 | import { PageBlocksRenderer } from "@/core/components/canvas/static/new-blocks-renderer";
2 | import { useBlocksStore } from "@/core/history/use-blocks-store-undoable-actions";
3 | import { isEmpty } from "lodash-es";
4 |
5 | export const StaticBlocksRenderer = () => {
6 | const [blocks] = useBlocksStore();
7 | const blocksHtml = isEmpty(blocks) ? null : ;
8 | return <>{blocksHtml}>;
9 | };
10 |
--------------------------------------------------------------------------------
/src/core/components/canvas/static/use-block-runtime-props.ts:
--------------------------------------------------------------------------------
1 | import { useBlocksStore } from "@/core/hooks/hooks";
2 | import { find, get, isEmpty } from "lodash-es";
3 | import { useCallback } from "react";
4 |
5 | export const useBlockRuntimeProps = () => {
6 | const [allBlocks] = useBlocksStore();
7 | return useCallback(
8 | (blockId: string, runtimeProps: Record) => {
9 | if (isEmpty(runtimeProps)) return {};
10 | return Object.entries(runtimeProps).reduce((acc, [key, schema]) => {
11 | const hierarchy = [];
12 | let block = find(allBlocks, { _id: blockId });
13 | while (block) {
14 | hierarchy.push(block);
15 | block = find(allBlocks, { _id: block._parent });
16 | }
17 | const matchingBlock = find(hierarchy, { _type: schema.block });
18 | if (matchingBlock) {
19 | acc[key] = get(matchingBlock, get(schema, "prop"), null);
20 | }
21 | return acc;
22 | }, {});
23 | },
24 | [allBlocks],
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/core/components/canvas/static/use-canvas-scale.ts:
--------------------------------------------------------------------------------
1 | import { useBuilderProp, useCanvasDisplayWidth, useCanvasZoom } from "@/core/hooks";
2 | import { useCallback, useEffect, useState } from "react";
3 |
4 | export const useCanvasScale = (dimension: { height: number; width: number }) => {
5 | const [canvasWidth] = useCanvasDisplayWidth();
6 | const [, setZoom] = useCanvasZoom();
7 | const htmlDir = useBuilderProp("htmlDir", "ltr") as "ltr" | "rtl";
8 | const [scale, setScale] = useState({});
9 | const updateScale = useCallback(() => {
10 | const { width, height } = dimension;
11 | if (width < canvasWidth) {
12 | const newScale: number = parseFloat((width / canvasWidth).toFixed(2).toString());
13 | let heightObj = {};
14 | const scaledHeight = height * newScale;
15 | const scaledWidth = width * newScale;
16 | if (height) {
17 | heightObj = {
18 | // Eureka! This is the formula to calculate the height of the scaled element. Thank you ChatGPT 4
19 | height: 100 + ((height - scaledHeight) / scaledHeight) * 100 + "%",
20 | width: 100 + ((width - scaledWidth) / scaledWidth) * 100 + "%",
21 | };
22 | }
23 | setScale({
24 | position: "relative",
25 | top: 0,
26 | transform: `scale(${newScale})`,
27 | transformOrigin: htmlDir === "rtl" ? "top right" : "top left",
28 | ...heightObj,
29 | maxWidth: "none", // TODO: Add max-width to the wrapper
30 | });
31 |
32 | setZoom(newScale * 100);
33 | } else {
34 | setScale({});
35 | setZoom(100);
36 | }
37 | }, [canvasWidth, dimension, htmlDir, setZoom]);
38 |
39 | useEffect(() => {
40 | updateScale();
41 | }, [canvasWidth, dimension, setZoom, updateScale]);
42 |
43 | return scale;
44 | };
45 |
--------------------------------------------------------------------------------
/src/core/components/canvas/static/use-chai-external-data.ts:
--------------------------------------------------------------------------------
1 | import { chaiExternalDataAtom } from "@/core/atoms/builder";
2 | import { useAtom } from "jotai";
3 |
4 | export const useChaiExternalData = () => useAtom(chaiExternalDataAtom);
5 |
--------------------------------------------------------------------------------
/src/core/components/canvas/topbar/ai-assistant.tsx:
--------------------------------------------------------------------------------
1 | import { useBuilderProp } from "@/core/hooks";
2 | import { useAiAssistant } from "@/core/hooks/use-ask-ai";
3 | import { useRightPanel } from "@/core/hooks/use-theme";
4 | import { PERMISSIONS, usePermissions } from "@/core/main";
5 | import { Label } from "@/ui/shadcn/components/ui/label";
6 | import { Switch } from "@/ui/shadcn/components/ui/switch";
7 | import { SparklesIcon } from "lucide-react";
8 | import { useTranslation } from "react-i18next";
9 |
10 | export const AiAssistant = () => {
11 | const setAiAssistantActive = useAiAssistant();
12 | const [panel] = useRightPanel();
13 | const askAiCallBack = useBuilderProp("askAiCallBack", null);
14 | const { t } = useTranslation();
15 | const { hasPermission } = usePermissions();
16 | if (!askAiCallBack || !hasPermission(PERMISSIONS.EDIT_BLOCK)) return null;
17 | return (
18 |
19 |
23 | {
27 | setAiAssistantActive(state);
28 | }}
29 | id="ai-assistant"
30 | />
31 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/src/core/components/canvas/topbar/dark-mode.tsx:
--------------------------------------------------------------------------------
1 | import { useDarkMode } from "@/core/hooks";
2 | import { Switch } from "@/ui/shadcn/components/ui/switch";
3 | import { Moon, SunIcon } from "lucide-react";
4 |
5 | export function DarkMode() {
6 | const [darkMode, setDarkMode] = useDarkMode();
7 | return (
8 |
9 |
10 | {
14 | setDarkMode(!darkMode);
15 | }}
16 | className={`${darkMode ? "bg-violet-600" : "bg-violet-300"} relative ml-2 inline-flex h-[20px] w-[40px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}>
17 |
21 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/core/components/canvas/topbar/data-binding.tsx:
--------------------------------------------------------------------------------
1 | import { usePageExternalData } from "@/core/atoms/builder";
2 | import { dataBindingActiveAtom } from "@/core/atoms/ui";
3 | import { cn } from "@/core/functions/common-functions";
4 | import { Button } from "@/ui/shadcn/components/ui/button";
5 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/ui/shadcn/components/ui/tooltip";
6 | import { useAtom } from "jotai";
7 | import { isEmpty } from "lodash-es";
8 | import { DatabaseZapIcon } from "lucide-react";
9 | import { useTranslation } from "react-i18next";
10 |
11 | export const DataBinding = () => {
12 | const pageExternalData = usePageExternalData();
13 | const [dataBindingActive, setDataBindingActive] = useAtom(dataBindingActiveAtom);
14 | const { t } = useTranslation();
15 | if (isEmpty(pageExternalData)) return null;
16 | return (
17 |
18 |
19 |
20 |
23 |
24 |
25 | {t("Toggle Data Binding")}
26 |
27 |
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/src/core/components/canvas/topbar/undo-redo.tsx:
--------------------------------------------------------------------------------
1 | import { useUndoManager } from "@/core/history/use-undo-manager";
2 | import { Button } from "@/ui/shadcn/components/ui/button";
3 | import { ResetIcon } from "@radix-ui/react-icons";
4 |
5 | export const UndoRedo = () => {
6 | const { hasUndo, hasRedo, undo, redo } = useUndoManager();
7 | return (
8 |
9 |
12 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/core/components/chai-select.tsx:
--------------------------------------------------------------------------------
1 | import { mergeClasses } from "@/core/main";
2 | import React, { ChangeEvent, useState } from "react";
3 |
4 | interface Option {
5 | value: string;
6 | label: string;
7 | }
8 |
9 | interface ChaiSelectProps {
10 | defaultValue?: string;
11 | onValueChange: (value: string) => void;
12 | options: Option[];
13 | placeholder?: string;
14 | className?: string;
15 | height?: string;
16 | }
17 |
18 | const ChaiSelect: React.FC = ({
19 | defaultValue = "",
20 | onValueChange,
21 | options,
22 | placeholder = "Select",
23 | className = "",
24 | height = "",
25 | }) => {
26 | const [selectedValue, setSelectedValue] = useState(defaultValue);
27 |
28 | const handleChange = (event: ChangeEvent) => {
29 | const value = event.target.value;
30 | setSelectedValue(value);
31 | onValueChange(value);
32 | };
33 |
34 | return (
35 |
36 |
52 |
53 | );
54 | };
55 |
56 | export default ChaiSelect;
57 |
--------------------------------------------------------------------------------
/src/core/components/count-down.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | const SECONDS = 10;
4 |
5 | export default function Countdown() {
6 | const [timeLeft, setTimeLeft] = useState(SECONDS);
7 | const [isActive, setIsActive] = useState(false);
8 |
9 | useEffect(() => {
10 | if (isActive && timeLeft > 0) {
11 | const timer = setTimeout(() => {
12 | setTimeLeft(timeLeft - 0.1);
13 | }, 100);
14 | return () => clearTimeout(timer);
15 | } else if (timeLeft <= 0) {
16 | setIsActive(false);
17 | setTimeLeft(SECONDS);
18 | }
19 | }, [isActive, timeLeft]);
20 |
21 | const startTimer = () => {
22 | setIsActive(true);
23 | setTimeLeft(SECONDS);
24 | };
25 |
26 | useEffect(() => {
27 | if (timeLeft === SECONDS) {
28 | startTimer();
29 | }
30 | }, [timeLeft]);
31 |
32 | const radius = 18;
33 | const circumference = 2 * Math.PI * radius;
34 | const strokeDashoffset = circumference * (1 - (SECONDS - timeLeft) / SECONDS);
35 |
36 | return (
37 |
38 |
61 |
62 |
63 |
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/src/core/components/css-theme-var.tsx:
--------------------------------------------------------------------------------
1 | import { getChaiThemeCssVariables } from "@/render";
2 | import { ChaiBuilderThemeValues } from "@/types/types";
3 | import { useMemo } from "react";
4 |
5 | export const CssThemeVariables = ({ theme }: { theme: ChaiBuilderThemeValues }) => {
6 | const themeVariables = useMemo(() => {
7 | return getChaiThemeCssVariables(theme);
8 | }, [theme]);
9 | return ;
10 | };
11 |
--------------------------------------------------------------------------------
/src/core/components/fallback-error.tsx:
--------------------------------------------------------------------------------
1 | export const FallbackError = () => {
2 | return (
3 |
4 |
5 |
Oops! Something went wrong.
6 |
Please try again.
7 |
8 |
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/src/core/components/index.ts:
--------------------------------------------------------------------------------
1 | import ChaiBuilderCanvas from "@/core/components/canvas/canvas-area";
2 | import BlockPropsEditor from "@/core/components/settings/block-settings";
3 | import BlockStyleEditor from "@/core/components/settings/block-styling";
4 | import AddBlocksPanel from "@/core/components/sidepanels/panels/add-blocks/add-blocks";
5 | import ImportHTML from "@/core/components/sidepanels/panels/add-blocks/import-html";
6 | import UILibrariesPanel from "@/core/components/sidepanels/panels/add-blocks/libraries-panel";
7 | import Outline from "@/core/components/sidepanels/panels/outline/list-tree";
8 | import ThemeOptions from "@/core/components/sidepanels/panels/theme-configuration/ThemeConfigPanel";
9 | import i18n from "@/core/locales/load";
10 |
11 | export { AISetContext, AIUserPrompt } from "@/core/components/ask-ai-panel";
12 | export { Breakpoints as ScreenSizes } from "@/core/components/canvas/topbar/canvas-breakpoints";
13 | export { DarkMode as DarkModeSwitcher } from "@/core/components/canvas/topbar/dark-mode";
14 | export { UndoRedo } from "@/core/components/canvas/topbar/undo-redo";
15 | export { ChaiBuilderEditor } from "@/core/components/chaibuilder-editor";
16 | export { AddBlocksDialog } from "@/core/components/layout/add-blocks-dialog";
17 | export { BlockAttributesEditor } from "@/core/components/settings/new-panel/block-attributes-editor";
18 | export { DefaultChaiBlocks } from "@/core/components/sidepanels/panels/add-blocks/default-blocks";
19 | export { default as JSONFormFieldTemplate } from "@/core/rjsf-widgets/json-form-field-template";
20 | export {
21 | AddBlocksPanel,
22 | BlockPropsEditor,
23 | BlockStyleEditor,
24 | ChaiBuilderCanvas,
25 | i18n,
26 | ImportHTML,
27 | Outline,
28 | ThemeOptions,
29 | UILibrariesPanel as UILibraries,
30 | };
31 |
32 | export * from "@/types/index";
33 |
--------------------------------------------------------------------------------
/src/core/components/noop-component.tsx:
--------------------------------------------------------------------------------
1 | export const NoopComponent = () => {
2 | return null;
3 | };
4 |
--------------------------------------------------------------------------------
/src/core/components/settings/block-styling-props.tsx:
--------------------------------------------------------------------------------
1 | import { useSelectedBlock, useSelectedStylingBlocks, useTranslation } from "@/core/hooks";
2 | import { Badge } from "@/ui/shadcn/components/ui/badge";
3 | import { find, isEmpty, map, startCase } from "lodash-es";
4 |
5 | export const BlockStylingProps = () => {
6 | const selectedBlock = useSelectedBlock();
7 | const [stylingBlocks, setStylingBlocks] = useSelectedStylingBlocks();
8 | const { t } = useTranslation();
9 | if (!selectedBlock) return null;
10 | // find all styles props of selected block by checking for value of each prop as string and starts with #styles:
11 | const stylesProps = Object.keys(selectedBlock).filter(
12 | (prop) => typeof selectedBlock[prop] === "string" && selectedBlock[prop].startsWith("#styles:"),
13 | );
14 | if (isEmpty(stylesProps) || stylesProps.length <= 1) return null;
15 |
16 | const isSelected = (prop: string) => {
17 | return find(stylingBlocks, (block) => block.prop === prop);
18 | };
19 | return (
20 |
21 |
24 |
25 | {map(stylesProps, (prop) => {
26 | return (
27 | {
30 | setStylingBlocks([{ id: `${prop}-${selectedBlock._id}`, blockId: selectedBlock._id, prop }]);
31 | }}
32 | variant={isSelected(prop) ? "default" : "secondary"}
33 | key={prop}>
34 | {startCase(prop)}
35 |
36 | );
37 | })}
38 |
39 |
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/src/core/components/settings/choices/icon-choice.tsx:
--------------------------------------------------------------------------------
1 | import { useCurrentClassByProperty } from "@/core/components/settings/choices/block-style";
2 | import { StyleContext } from "@/core/components/settings/choices/style-context";
3 | import { useTailwindClassList } from "@/core/constants/CLASSES_LIST";
4 | import { EDITOR_ICONS } from "@/core/constants/ICONS";
5 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/ui/shadcn/components/ui/tooltip";
6 | import { BoxIcon } from "@radix-ui/react-icons";
7 | import { get, map, startCase, toLower } from "lodash-es";
8 | import React, { useContext, useMemo } from "react";
9 |
10 | export const IconChoices = ({ property, onChange }: any) => {
11 | const { getClasses } = useTailwindClassList();
12 | const classes = getClasses(property);
13 | const { canChange } = useContext(StyleContext);
14 | const currentClass = useCurrentClassByProperty(property);
15 | const pureClsName = useMemo(() => get(currentClass, "cls", ""), [currentClass]);
16 |
17 | return (
18 |
19 | {map(classes, (cls) => (
20 |
21 |
22 |
31 |
32 | {startCase(toLower(cls))}
33 |
34 | ))}
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/src/core/components/settings/choices/style-context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | export const StyleContext = createContext({ canReset: false, canChange: true });
4 |
5 | export const BlockStyleProvider = ({ children, canReset = false, canChange = true }: any) => (
6 | // eslint-disable-next-line react/jsx-no-constructed-context-values
7 | {children}
8 | );
9 |
--------------------------------------------------------------------------------
/src/core/components/settings/new-panel/block-attributes-editor.tsx:
--------------------------------------------------------------------------------
1 | import AttrsEditor from "@/core/components/settings/new-panel/attributes-editor";
2 | import { useSelectedBlock, useSelectedStylingBlocks, useUpdateBlocksProps } from "@/core/hooks";
3 | import { forEach, get, isEmpty, map, set } from "lodash-es";
4 | import * as React from "react";
5 | import { useState } from "react";
6 |
7 | export const BlockAttributesEditor = React.memo(() => {
8 | const block = useSelectedBlock();
9 | const [attributes, setAttributes] = useState([] as Array<{ key: string; value: string }>);
10 | const [selectedStylingBlock] = useSelectedStylingBlocks();
11 | const updateBlockProps = useUpdateBlocksProps();
12 |
13 | const attrKey = `${get(selectedStylingBlock, "0.prop")}_attrs`;
14 |
15 | React.useEffect(() => {
16 | const _attributes = map(get(block, attrKey), (value, key) => ({ key, value }));
17 | if (!isEmpty(_attributes)) setAttributes(_attributes as any);
18 | else setAttributes([]);
19 |
20 | // eslint-disable-next-line react-hooks/exhaustive-deps
21 | }, [get(block, attrKey)]);
22 |
23 | const updateAttributes = React.useCallback(
24 | (updatedAttributes: any = []) => {
25 | const _attrs = {};
26 | forEach(updatedAttributes, (item) => {
27 | if (!isEmpty(item.key)) {
28 | set(_attrs, item.key, item.value);
29 | }
30 | });
31 | updateBlockProps([get(block, "_id")], { [attrKey]: _attrs });
32 | },
33 | [block, updateBlockProps, attrKey],
34 | );
35 |
36 | return (
37 |
44 | );
45 | });
46 |
--------------------------------------------------------------------------------
/src/core/components/settings/new-panel/breakpoint-selector.tsx:
--------------------------------------------------------------------------------
1 | import { Breakpoints, WEB_BREAKPOINTS } from "@/core/components/canvas/topbar/canvas-breakpoints";
2 | import { useScreenSizeWidth } from "@/core/hooks";
3 | import { useMemo } from "react";
4 |
5 | export function BreakpointSelector() {
6 | const [, breakpoint] = useScreenSizeWidth();
7 |
8 | const message = useMemo(() => {
9 | const currentBreakpoint = WEB_BREAKPOINTS.find((bp) => bp.breakpoint === breakpoint);
10 | return currentBreakpoint?.content;
11 | }, [breakpoint, WEB_BREAKPOINTS]);
12 |
13 | return (
14 | <>
15 |
19 |
20 |
21 |
22 |
23 | {breakpoint === "xs" ? "Base" : breakpoint}
24 |
25 | {message}
26 |
27 |
28 |
29 | >
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/core/components/settings/settings-context.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const BlockSettingsContext = React.createContext<{ setDragData: Function }>({
4 | setDragData: () => {},
5 | });
6 |
--------------------------------------------------------------------------------
/src/core/components/sidepanels/panels/add-blocks/default-blocks.tsx:
--------------------------------------------------------------------------------
1 | import { ChaiBuilderBlocks } from "@/core/components/sidepanels/panels/add-blocks/add-blocks";
2 | import { useRegisteredChaiBlocks } from "@chaibuilder/runtime";
3 | import { groupBy, map, uniq } from "lodash-es";
4 |
5 | export const DefaultChaiBlocks = ({
6 | parentId,
7 | position,
8 | gridCols = "grid-cols-2",
9 | }: {
10 | parentId?: string;
11 | position?: number;
12 | gridCols?: string;
13 | }) => {
14 | const chaiBlocks = useRegisteredChaiBlocks();
15 |
16 | const groupedBlocks = groupBy(chaiBlocks, "category") as Record;
17 | const uniqueTypeGroup = uniq(map(groupedBlocks.core, "group"));
18 |
19 | return (
20 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/core/components/sidepanels/panels/add-blocks/libraries-select.tsx:
--------------------------------------------------------------------------------
1 | import ChaiSelect from "@/core/components/chai-select";
2 | import { ChaiLibrary } from "@/types/chaibuilder-editor-props";
3 | import { useTranslation } from "react-i18next";
4 |
5 | export function UILibrariesSelect({
6 | uiLibraries,
7 | library,
8 | setLibrary,
9 | }: {
10 | library?: string;
11 | uiLibraries: (ChaiLibrary & { id: string })[];
12 | setLibrary: (library: string) => void;
13 | }) {
14 | const { t } = useTranslation();
15 | if (!library) return null;
16 | return (
17 |
18 |
{t("Choose library")}
19 |
({
22 | value: uiLibrary.id,
23 | label: uiLibrary.name,
24 | }))}
25 | defaultValue={library}
26 | onValueChange={(v) => setLibrary(v as any)}
27 | />
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/core/components/sidepanels/panels/images/media-manager-modal.tsx:
--------------------------------------------------------------------------------
1 | import { useMediaManagerComponent } from "@/core/extensions/media-manager";
2 | import { ChaiAsset } from "@/types";
3 | import { Dialog, DialogContent, DialogTrigger } from "@/ui/shadcn/components/ui/dialog";
4 | import React, { useState } from "react";
5 |
6 | const MediaManagerModal = ({
7 | assetId,
8 | children,
9 | onSelect,
10 | mode = "image",
11 | }: {
12 | assetId?: string;
13 | children: React.JSX.Element;
14 | onSelect: (assets: ChaiAsset[] | ChaiAsset) => void;
15 | mode?: "image" | "video" | "audio";
16 | }) => {
17 | const [open, setOpen] = useState(false);
18 | const MediaManagerComponent = useMediaManagerComponent();
19 |
20 | const handleSelect = (...arg: any) => {
21 | //@ts-ignore
22 | onSelect.call(this, ...arg);
23 | setOpen(false);
24 | };
25 |
26 | return (
27 |
37 | );
38 | };
39 |
40 | MediaManagerModal.displayName = "MediaManagerModal";
41 |
42 | export default MediaManagerModal;
43 |
--------------------------------------------------------------------------------
/src/core/components/sidepanels/panels/outline/block-type-icon.tsx:
--------------------------------------------------------------------------------
1 | import { useRegisteredChaiBlocks } from "@chaibuilder/runtime";
2 | import { BoxModelIcon } from "@radix-ui/react-icons";
3 | import { get } from "lodash-es";
4 | import React from "react";
5 |
6 | type Props = {
7 | type?: string;
8 | };
9 |
10 | const ICON_CLASS = "h-3 w-3 stroke-[2]";
11 |
12 | export const TypeIcon: React.FC = (props) => {
13 | const allChaiBlocks = useRegisteredChaiBlocks();
14 | const blockIcon: any = get(allChaiBlocks, [props?.type, "icon"]);
15 |
16 | if (blockIcon) {
17 | return React.createElement(blockIcon, { className: ICON_CLASS });
18 | }
19 |
20 | // * Fallback Icon
21 | return ;
22 | };
23 |
--------------------------------------------------------------------------------
/src/core/components/sidepanels/panels/outline/default-cursor.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from "react";
2 | import { CursorProps } from "react-arborist";
3 |
4 | const placeholderStyle = {
5 | display: "flex",
6 | alignItems: "center",
7 | zIndex: 1,
8 | };
9 |
10 | export const DefaultCursor = React.memo(function DefaultCursor({ top, left }: CursorProps) {
11 | const style: CSSProperties = {
12 | position: "absolute",
13 | pointerEvents: "none",
14 | top: top + "px",
15 | left: left + "px",
16 | right: 0,
17 | };
18 | return (
19 |
22 | );
23 | });
24 |
--------------------------------------------------------------------------------
/src/core/components/sidepanels/panels/outline/default-drag-preview.tsx:
--------------------------------------------------------------------------------
1 | import { TypeIcon } from "@/core/components/sidepanels/panels/outline/block-type-icon";
2 | import { useBlocksStore } from "@/core/hooks";
3 | import { ChaiBlock } from "@/types/types";
4 | import { memo, useMemo } from "react";
5 | import { DragPreviewProps } from "react-arborist";
6 |
7 | const Overlay = memo(function Overlay({ children, isDragging }: { children: React.ReactNode; isDragging: boolean }) {
8 | if (!isDragging) return null;
9 |
10 | return {children}
;
11 | });
12 |
13 | export const DefaultDragPreview = memo(({ id, isDragging, mouse }: Omit) => {
14 | const [allBlocks] = useBlocksStore();
15 |
16 | const block: ChaiBlock | undefined = useMemo(() => {
17 | return allBlocks.find((block) => block._id === id);
18 | }, [allBlocks, id]);
19 |
20 | const style = useMemo(
21 | () => ({
22 | transform: `translate(${mouse?.x - 10}px, ${mouse?.y - 10}px)`,
23 | }),
24 | [mouse],
25 | );
26 |
27 | if (!mouse) {
28 | return ;
29 | }
30 |
31 | return (
32 |
33 |
34 |
37 |
43 |
44 |
45 |
46 | );
47 | });
48 |
--------------------------------------------------------------------------------
/src/core/components/sidepanels/panels/outline/default-shortcuts.tsx:
--------------------------------------------------------------------------------
1 | import { TreeApi } from "react-arborist";
2 |
3 | export const defaultShortcuts = [
4 | { key: "ArrowDown", command: "selectNext" },
5 | { key: "ArrowUp", command: "selectPrev" },
6 | { key: "ArrowLeft", command: "selectParent", when: "isLeaf || isClosed" },
7 | { key: "ArrowLeft", command: "close", when: "isOpen" },
8 | { key: "ArrowRight", command: "open", when: "isClosed" },
9 | { key: "ArrowRight", command: "selectNext", when: "isOpen" },
10 | { key: "Home", command: "selectFirst" },
11 | { key: "End", command: "selectLast" },
12 | ];
13 |
14 | export function selectFirst(tree: TreeApi) {
15 | if (tree.firstNode) tree.select(tree.firstNode.id);
16 | }
17 |
18 | export function selectLast(tree: TreeApi) {
19 | if (tree.lastNode) tree.select(tree.lastNode.id);
20 | }
21 |
22 | export function selectNext(tree: TreeApi) {
23 | const next = tree.selectedNodes[0].next || tree.firstNode;
24 | tree.select(next.id);
25 | }
26 |
27 | export function selectPrev(tree: TreeApi) {
28 | const prev = tree.selectedNodes[0].prev || tree.lastNode;
29 | tree.select(prev.id);
30 | }
31 |
32 | export const selectParent = (tree: TreeApi, when: boolean) => {
33 | const parent = tree.selectedIds[0]?.parent || null;
34 |
35 | if (parent && when) tree.select(parent.id);
36 | };
37 |
38 | export const open = (tree: TreeApi, when: boolean) => {
39 | const selectedNode = tree.selectedNodes[0];
40 |
41 | if (selectedNode.isInternal && when) selectedNode.open();
42 | };
43 |
44 | export const close = (tree: TreeApi, when: boolean) => {
45 | const selectedNode = tree.selectedNodes[0];
46 |
47 | if (selectedNode.isInternal && when) selectedNode.close();
48 | };
49 |
--------------------------------------------------------------------------------
/src/core/components/sidepanels/panels/outline/paste-into-root.tsx:
--------------------------------------------------------------------------------
1 | import { DropdownMenuItem } from "@/ui/shadcn/components/ui/dropdown-menu";
2 |
3 | import { usePasteBlocks } from "@/core/hooks/use-paste-blocks";
4 | import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/ui/shadcn/components/ui/dropdown-menu";
5 | import { CardStackIcon } from "@radix-ui/react-icons";
6 | import { useEffect } from "react";
7 | import { useTranslation } from "react-i18next";
8 |
9 | export const PasteAtRootContextMenu = ({ parentContext, setParentContext }) => {
10 | const { t } = useTranslation();
11 | const { canPaste, pasteBlocks } = usePasteBlocks();
12 |
13 | useEffect(() => {
14 | if (!canPaste("root")) setParentContext(null);
15 | }, [canPaste("root")]);
16 |
17 | if (!parentContext || !canPaste("root")) return null;
18 |
19 | return (
20 |
21 | setParentContext(null)}>
22 |
23 |
26 | {
29 | pasteBlocks("root");
30 | setParentContext(null);
31 | }}>
32 | {t("Paste")}
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/core/components/sidepanels/panels/outline/save-to-library.tsx:
--------------------------------------------------------------------------------
1 | import { saveToLibraryModalAtom } from "@/core/components/sidepanels/panels/outline/upsert-library-block-modal";
2 | import { useSaveToLibraryComponent } from "@/core/extensions/save-to-library";
3 | import { useSelectedBlock } from "@/core/hooks";
4 | import { DropdownMenuItem } from "@/ui/shadcn/components/ui/dropdown-menu";
5 | import { useAtom } from "jotai";
6 | import { SaveIcon } from "lucide-react";
7 | import { useTranslation } from "react-i18next";
8 |
9 | export const SaveToLibrary = () => {
10 | const selectedBlock = useSelectedBlock();
11 | const { t } = useTranslation();
12 | const [, setModalState] = useAtom(saveToLibraryModalAtom);
13 | const SaveToLibraryComponent = useSaveToLibraryComponent();
14 |
15 | const handleSaveToLibrary = () => {
16 | if (selectedBlock) {
17 | setModalState({
18 | isOpen: true,
19 | blockId: selectedBlock._id,
20 | });
21 | }
22 | };
23 |
24 | if (!SaveToLibraryComponent) return null;
25 |
26 | return (
27 |
28 | {t(selectedBlock?._libBlockId ? "Update library block" : "Save to library")}
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/core/components/sidepanels/panels/outline/unlink-library-block.tsx:
--------------------------------------------------------------------------------
1 | import { useSelectedBlock, useUpdateBlocksProps } from "@/core/hooks";
2 | import { DropdownMenuItem } from "@/ui/shadcn/components/ui/dropdown-menu";
3 | import { UnlinkIcon } from "lucide-react";
4 | import { useTranslation } from "react-i18next";
5 |
6 | export const UnlinkLibraryBlock = () => {
7 | const { t } = useTranslation();
8 | const selectedBlock = useSelectedBlock();
9 | const updateBlocksProps = useUpdateBlocksProps();
10 |
11 | const handleUnlink = () => {
12 | updateBlocksProps([selectedBlock._id], {
13 | _libBlockId: null,
14 | });
15 | };
16 |
17 | return (
18 |
19 | {t("Unlink from library")}
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/core/components/sidepanels/panels/theme-configuration/BorderRadiusInput.tsx:
--------------------------------------------------------------------------------
1 | import { debounce } from "lodash-es";
2 |
3 | type BorderRadiusInputProps = {
4 | value: string;
5 | onChange: (value: string) => void;
6 | disabled?: boolean;
7 | };
8 |
9 | const BorderRadiusInput = ({ value, onChange, disabled }: BorderRadiusInputProps) => {
10 | const throttledChange = debounce((value: string) => onChange(value), 200);
11 |
12 | return (
13 | throttledChange(e.target.value)}
21 | className="flex-1 cursor-pointer"
22 | />
23 | );
24 | };
25 |
26 | export default BorderRadiusInput;
27 |
--------------------------------------------------------------------------------
/src/core/components/sidepanels/panels/theme-configuration/ColorPickerInput.tsx:
--------------------------------------------------------------------------------
1 | import { debounce } from "lodash-es";
2 |
3 | const ColorPickerInput = ({ value, onChange }: { value: string; onChange: (value: string) => void }) => {
4 | const handleColorChange = debounce((value: string) => onChange(value), 200);
5 |
6 | return (
7 |
10 | {
14 | const hexValue = e.target.value;
15 | if (/^#[0-9A-F]{6}$/i.test(hexValue)) {
16 | handleColorChange(hexValue);
17 | }
18 | }}
19 | className="absolute inset-0 h-full w-full cursor-pointer rounded-lg border-0 opacity-0"
20 | />
21 |
22 | );
23 | };
24 |
25 | export default ColorPickerInput;
26 |
--------------------------------------------------------------------------------
/src/core/components/sidepanels/panels/theme-configuration/font-selector.tsx:
--------------------------------------------------------------------------------
1 | import { Label } from "@/ui/shadcn/components/ui/label";
2 | import { useRegisteredFonts } from "@chaibuilder/runtime";
3 | import { startCase } from "lodash-es";
4 |
5 | const FontSelector = ({
6 | label,
7 | value,
8 | onChange,
9 | }: {
10 | label: string;
11 | value: string;
12 | onChange: (value: string) => void;
13 | }) => {
14 | const availableFonts = useRegisteredFonts();
15 | return (
16 |
17 |
18 |
28 |
29 | );
30 | };
31 |
32 | export default FontSelector;
33 |
--------------------------------------------------------------------------------
/src/core/components/sidepanels/panels/theme-configuration/index.ts:
--------------------------------------------------------------------------------
1 | import BorderRadiusInput from "@/core/components/sidepanels/panels/theme-configuration/BorderRadiusInput";
2 | import ColorPickerInput from "@/core/components/sidepanels/panels/theme-configuration/ColorPickerInput";
3 | import FontSelector from "@/core/components/sidepanels/panels/theme-configuration/font-selector";
4 |
5 | export { BorderRadiusInput, ColorPickerInput, FontSelector };
6 |
--------------------------------------------------------------------------------
/src/core/components/topbar/Preview.tsx:
--------------------------------------------------------------------------------
1 | import { useBuilderProp, usePreviewMode } from "@/core/hooks";
2 | import { Button } from "@/ui/shadcn/components/ui/button";
3 | import { EyeOpenIcon } from "@radix-ui/react-icons";
4 | import { useTranslation } from "react-i18next";
5 |
6 | export const Preview = function Preview() {
7 | const preview = useBuilderProp("previewComponent");
8 | const [, setPreviewMode] = usePreviewMode();
9 | const { t } = useTranslation();
10 | if (!preview) return null;
11 | return (
12 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/core/components/topbar/save-button.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/core/functions/common-functions";
2 | import { useSavePage } from "@/core/hooks";
3 | import { Button } from "@/ui/shadcn/components/ui/button";
4 | import { Check } from "lucide-react";
5 | import { useTranslation } from "react-i18next";
6 |
7 | export const SaveButton = () => {
8 | const { savePage, saveState } = useSavePage();
9 | const { t } = useTranslation();
10 |
11 | const button = (
12 |
35 | );
36 | return {button}
;
37 | };
38 |
--------------------------------------------------------------------------------
/src/core/constants/LAYOUT_MODE.ts:
--------------------------------------------------------------------------------
1 |
2 | export type LayoutVariant = "SINGLE_SIDE_PANEL" | "DUAL_SIDE_PANEL" | "DUAL_SIDE_PANEL_ADVANCED";
3 |
--------------------------------------------------------------------------------
/src/core/constants/MODIFIERS.ts:
--------------------------------------------------------------------------------
1 | export const MODIFIERS: Array = [
2 | "hover",
3 | "focus",
4 | "focus-within",
5 | "focus-visible",
6 | "active",
7 | "visited",
8 | "target",
9 | "first",
10 | "last",
11 | "only",
12 | "odd",
13 | "even",
14 | "first-of-type",
15 | "last-of-type",
16 | "only-of-type",
17 | "empty",
18 | "disabled",
19 | "checked",
20 | "indeterminate",
21 | "default",
22 | "required",
23 | "valid",
24 | "invalid",
25 | "in-range",
26 | "out-of-range",
27 | "placeholder-shown",
28 | "autofill",
29 | "read-only",
30 | "open",
31 | "before",
32 | "after",
33 | "first-letter",
34 | "first-line",
35 | "marker",
36 | "selection",
37 | "file",
38 | "placeholder",
39 | // preline
40 | "hs-collapse-open",
41 | "hs-accordion-active",
42 | ];
43 |
44 | export const BRANDING_OPTIONS_DEFAULTS = {
45 | bodyFont: "Inter",
46 | headingFont: "Inter",
47 | roundedCorners: 5,
48 | primaryColor: "#570df8",
49 | secondaryColor: "#f002b8",
50 | bodyBgDarkColor: "#031022",
51 | bodyBgLightColor: "#fcfcfc",
52 | bodyTextDarkColor: "#ffffff",
53 | bodyTextLightColor: "#000000",
54 | };
55 |
--------------------------------------------------------------------------------
/src/core/constants/PERMISSIONS.ts:
--------------------------------------------------------------------------------
1 | export const PERMISSIONS = {
2 | ADD_BLOCK: "add_block",
3 | DELETE_BLOCK: "delete_block",
4 | EDIT_BLOCK: "edit_block",
5 | MOVE_BLOCK: "move_block",
6 | EDIT_THEME: "edit_theme",
7 | SAVE_PAGE: "save_page",
8 | EDIT_STYLES: "edit_styles",
9 | IMPORT_HTML: "import_html",
10 |
11 | //LIbrary permissions
12 | CREATE_LIBRARY_BLOCK: "create_library_block",
13 | CREATE_LIBRARY_GROUP: "create_library_group",
14 | EDIT_LIBRARY_BLOCK: "edit_library_block",
15 | DELETE_LIBRARY_BLOCK: "delete_library_block",
16 | };
17 |
18 | export const PERMISSIONS_LIST = Object.values(PERMISSIONS);
19 |
--------------------------------------------------------------------------------
/src/core/constants/STRINGS.ts:
--------------------------------------------------------------------------------
1 | export const STYLES_KEY = "#styles:";
2 | export const SLOT_KEY = "#slots:";
3 | export const I18N_KEY = "#i18n";
4 | export const OUTLINE_KEY = "outline";
5 | export const ROOT_TEMP_KEY = "__ADD_BLOCK_INTERNAL_ROOT";
6 |
7 | export const REPEATER_PREFIX = "@";
8 | export const COLLECTION_PREFIX = "#";
9 | export const STATE_CONTEXT_PREFIX = "$";
10 |
--------------------------------------------------------------------------------
/src/core/events.ts:
--------------------------------------------------------------------------------
1 | export const CHAI_BUILDER_EVENTS = {
2 | OPEN_ADD_BLOCK: "OPEN_ADD_BLOCK",
3 | CLOSE_ADD_BLOCK: "CLOSE_ADD_BLOCK",
4 | SHOW_BLOCK_SETTINGS: "SHOW_BLOCK_SETTINGS",
5 |
6 | //CANVAS Events
7 | CLEAR_CANVAS_SELECTION: "CLEAR_CANVAS_SELECTION",
8 | CANVAS_BLOCK_SELECTED: "CANVAS_BLOCK_SELECTED",
9 | CANVAS_BLOCK_STYLE_SELECTED: "CANVAS_BLOCK_STYLE_SELECTED",
10 | };
11 |
--------------------------------------------------------------------------------
/src/core/extensions/__tests__/save-to-library.test.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | registerChaiSaveToLibrary,
3 | resetSaveToLibrary,
4 | SaveToLibraryProps,
5 | useSaveToLibraryComponent,
6 | } from "@/core/extensions/save-to-library";
7 | import { renderHook } from "@testing-library/react";
8 | import { beforeEach, describe, expect, it, vi } from "vitest";
9 |
10 | describe("save-to-library", () => {
11 | beforeEach(() => {
12 | resetSaveToLibrary();
13 | });
14 |
15 | describe("registerSaveToLibrary", () => {
16 | it("should register a component", () => {
17 | const MockComponent = vi.fn((_props: SaveToLibraryProps) => null);
18 | registerChaiSaveToLibrary(MockComponent);
19 | const { result } = renderHook(() => useSaveToLibraryComponent());
20 | expect(result.current).toBe(MockComponent);
21 | });
22 | });
23 |
24 | describe("useSaveToLibraryComponent", () => {
25 | it("should return null when no component is registered", () => {
26 | const { result } = renderHook(() => useSaveToLibraryComponent());
27 | expect(result.current).toBeNull();
28 | });
29 |
30 | it("should return registered component", () => {
31 | const MockComponent = vi.fn((_props: SaveToLibraryProps) => null);
32 | registerChaiSaveToLibrary(MockComponent);
33 | const { result } = renderHook(() => useSaveToLibraryComponent());
34 | expect(result.current).toBe(MockComponent);
35 | });
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/src/core/extensions/add-block-tabs.tsx:
--------------------------------------------------------------------------------
1 | import { has, set, values } from "lodash-es";
2 | import { useMemo } from "react";
3 |
4 | export type AddBlockTab = {
5 | id: string;
6 | tab: React.ComponentType;
7 | tabContent: React.ComponentType;
8 | };
9 |
10 | // Export for testing purposes
11 | export const ADD_BLOCK_TABS: Record = {};
12 |
13 | export const registerChaiAddBlockTab = (id: string, tab: Omit) => {
14 | if (has(ADD_BLOCK_TABS, id)) {
15 | console.warn(`Add block tab with id ${id} already registered`);
16 | }
17 | set(ADD_BLOCK_TABS, id, { id, ...tab });
18 | };
19 |
20 | export const useChaiAddBlockTabs = () => {
21 | return useMemo(() => {
22 | return values(ADD_BLOCK_TABS);
23 | }, []);
24 | };
25 |
--------------------------------------------------------------------------------
/src/core/extensions/blocks-settings.tsx:
--------------------------------------------------------------------------------
1 | export const RJSF_EXTENSIONS: Record; type: string }> = {};
2 |
3 | export const registerBlockSettingWidget = (id: string, component: React.ComponentType) => {
4 | RJSF_EXTENSIONS[id] = {
5 | id,
6 | component,
7 | type: "widget",
8 | };
9 | };
10 |
11 | export const registerBlockSettingField = (id: string, component: React.ComponentType) => {
12 | RJSF_EXTENSIONS[id] = {
13 | id,
14 | component,
15 | type: "field",
16 | };
17 | };
18 |
19 | export const registerBlockSettingTemplate = (id: string, component: React.ComponentType) => {
20 | RJSF_EXTENSIONS[id] = {
21 | id,
22 | component,
23 | type: "template",
24 | };
25 | };
26 |
27 | export const useBlockSettingComponents = (
28 | type: "widget" | "field" | "template",
29 | ): Record> => {
30 | return Object.values(RJSF_EXTENSIONS)
31 | .filter((component) => component.type === type)
32 | .reduce(
33 | (acc, component) => {
34 | acc[component.id] = component.component;
35 | return acc;
36 | },
37 | {} as Record>,
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/src/core/extensions/libraries.ts:
--------------------------------------------------------------------------------
1 | import { ChaiLibrary, ChaiLibraryBlock } from "@/types/chaibuilder-editor-props";
2 | import { ChaiBlock } from "@/types/common";
3 | import { values } from "lodash-es";
4 |
5 | type HTMLString = string;
6 |
7 | type ChaiLibraryConfig = {
8 | id: string;
9 | name: string;
10 | description: string;
11 | getBlocksList: (library: ChaiLibrary) => Promise[]>;
12 | getBlock: ({
13 | library,
14 | block,
15 | }: {
16 | library: ChaiLibrary;
17 | block: ChaiLibraryBlock;
18 | }) => Promise;
19 | };
20 |
21 | let LIBRARIES_REGISTRY: Record> = {};
22 |
23 | export const registerChaiLibrary = = Record>(
24 | id: string,
25 | library: Omit, "id">,
26 | ) => {
27 | LIBRARIES_REGISTRY[id] = { ...library, id };
28 | };
29 |
30 | export const getChaiLibrary = (id: string) => {
31 | return LIBRARIES_REGISTRY[id];
32 | };
33 |
34 | export const useChaiLibraries = () => {
35 | return values(LIBRARIES_REGISTRY);
36 | };
37 |
--------------------------------------------------------------------------------
/src/core/extensions/save-to-library.tsx:
--------------------------------------------------------------------------------
1 | import { ChaiBlock } from "@/types/chai-block";
2 | import { ComponentType, useMemo } from "react";
3 |
4 | export type SaveToLibraryProps = {
5 | blockId: string;
6 | blocks: ChaiBlock[];
7 | close: () => void;
8 | };
9 |
10 | let SAVE_TO_LIB_COMPONENT: ComponentType | null = null;
11 |
12 | export const registerChaiSaveToLibrary = (component: ComponentType) => {
13 | SAVE_TO_LIB_COMPONENT = component;
14 | };
15 |
16 | export const useSaveToLibraryComponent = () => {
17 | return useMemo(() => SAVE_TO_LIB_COMPONENT, []);
18 | };
19 |
20 | // For testing purposes
21 | export const resetSaveToLibrary = () => {
22 | SAVE_TO_LIB_COMPONENT = null;
23 | };
24 |
--------------------------------------------------------------------------------
/src/core/extensions/sidebar-panels.tsx:
--------------------------------------------------------------------------------
1 | import { filter, has, set, values } from "lodash-es";
2 | import { ComponentType, useMemo } from "react";
3 |
4 | export interface ChaiSidebarPanel {
5 | id: string;
6 | position: "top" | "bottom";
7 | view?: "standard" | "modal" | "overlay" | "drawer";
8 | button: React.ComponentType<{
9 | isActive: boolean;
10 | show: () => void;
11 | panelId: string;
12 | position: "top" | "bottom";
13 | }>;
14 | label: string;
15 | panel?: ComponentType;
16 | width?: number;
17 | isInternal?: boolean;
18 | icon?: React.ReactNode;
19 | }
20 |
21 | // Export for testing purposes
22 | export const CHAI_BUILDER_PANELS: Record = {};
23 |
24 | export const registerChaiSidebarPanel = (panelId: string, panelOptions: Omit) => {
25 | if (has(CHAI_BUILDER_PANELS, panelId)) {
26 | console.warn(`Panel ${panelId} already registered. Overriding...`);
27 | }
28 | set(CHAI_BUILDER_PANELS, panelId, { id: panelId, ...panelOptions });
29 | };
30 |
31 | export const useChaiSidebarPanels = (position: "top" | "bottom") => {
32 | return useMemo(
33 | () =>
34 | filter(values(CHAI_BUILDER_PANELS), (panel) => {
35 | return panel.position === position;
36 | }),
37 | [position],
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/src/core/extensions/top-bar.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 |
3 | const DefaultTopBar = () => {
4 | return ;
5 | };
6 |
7 | const TOP_BAR: { component: React.ComponentType } = {
8 | component: DefaultTopBar,
9 | };
10 |
11 | export const registerChaiTopBar = (component: React.ComponentType) => {
12 | TOP_BAR.component = component;
13 | };
14 |
15 | export const useTopBarComponent = () => {
16 | return useMemo(() => TOP_BAR.component, []);
17 | };
18 |
--------------------------------------------------------------------------------
/src/core/frame/frame-content.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { Children, Component } from "react"; // eslint-disable-line no-unused-vars
3 |
4 | interface ContentProps {
5 | children: React.ReactElement;
6 | contentDidMount(...args: unknown[]): unknown;
7 | contentDidUpdate(...args: unknown[]): unknown;
8 | }
9 |
10 | export default class Content extends Component {
11 | componentDidMount() {
12 | this.props.contentDidMount();
13 | }
14 |
15 | componentDidUpdate() {
16 | this.props.contentDidUpdate();
17 | }
18 |
19 | render() {
20 | return Children.only(this.props.children);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/core/frame/frame-context.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import React from "react";
3 |
4 | let doc;
5 | let win;
6 | if (typeof document !== "undefined") {
7 | doc = document;
8 | }
9 | if (typeof window !== "undefined") {
10 | win = window;
11 | }
12 |
13 | export const FrameContext = React.createContext({ document: doc, window: win });
14 |
15 | export const useFrame = () => React.useContext(FrameContext);
16 |
17 | export const { Provider: FrameContextProvider, Consumer: FrameContextConsumer } = FrameContext;
18 |
--------------------------------------------------------------------------------
/src/core/frame/index.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | export { ChaiFrame } from "@/core/frame/Frame";
3 | export { FrameContext, FrameContextConsumer, useFrame } from "@/core/frame/frame-context";
4 |
--------------------------------------------------------------------------------
/src/core/functions/common-functions.ts:
--------------------------------------------------------------------------------
1 | import { isEmpty, startsWith } from "lodash-es";
2 | import { ClassValue, clsx } from "clsx";
3 | import { twMerge } from "tailwind-merge";
4 |
5 | /**
6 | * Check the passed value and converts it to valid css property value
7 | * @param value
8 | */
9 | export function getBgImageValue(value: string) {
10 | if (isEmpty(value)) return "";
11 | return startsWith(value, "http") ? `url('${value}')` : value.replace(";", "");
12 | }
13 |
14 | /**
15 | * Get the unique uuid
16 | */
17 | export function generateUUID(length: number = 6, chars = "abcdefghijklmnopqrstuvwxyzABCD"): string {
18 | let result = "";
19 | for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)];
20 | return result;
21 | }
22 |
23 | export const getBreakpointValue = (width: number) =>
24 | width >= 1536
25 | ? "2XL"
26 | : width >= 1280
27 | ? "XL"
28 | : width >= 1024
29 | ? "LG"
30 | : width >= 768
31 | ? "MD"
32 | : width >= 640
33 | ? "SM"
34 | : "XS";
35 |
36 | export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
37 |
--------------------------------------------------------------------------------
/src/core/functions/convert-brbitrary-to-tailwind-class-data.ts:
--------------------------------------------------------------------------------
1 | export const nonArbitraryClasses = {
2 | "": "",
3 | "z-50": "z-50",
4 | "z-0": "z-0",
5 | "top-0": "top-0",
6 | "-top-0": "-top-0",
7 | };
8 | export const zIndex = {
9 | "z-[10]": "z-10",
10 | "z-[150]": "z-[150]",
11 | "-z-[200]": "-z-[200]",
12 | "-z-[40]": "-z-40",
13 | };
14 | export const position = {
15 | "top-[0px]": "top-0",
16 | "top-[4px]": "top-1",
17 | "top-[40px]": "top-10",
18 | "top-[160px]": "top-40",
19 | "top-[2rem]": "top-8",
20 | "top-[1.5rem]": "top-6",
21 | "top-[2px]": "top-0.5",
22 | "-top-[16px]": "-top-4",
23 | "top-[1.75rem]": "top-7",
24 | "left-[20px]": "left-5",
25 | "right-[4rem]": "right-16",
26 | "-right-[3rem]": "-right-12",
27 | "inset-[0px]": "inset-0",
28 | "inset-[1px]": "inset-px",
29 | "inset-x-[12px]": "inset-x-3",
30 | "inset-y-[12px]": "inset-y-3",
31 | "-top-[1px]": "-top-px",
32 | "top-[33px]": "top-[33px]",
33 | "top-[3.8rem]": "top-[3.8rem]",
34 | "top-[50%]": "top-1/2",
35 | "top-[80%]": "top-4/5",
36 | "inset-[60%]": "inset-3/5",
37 | "left-[100%]": "left-full",
38 | };
39 | export const opacity = {
40 | "opacity-[0.5]": "opacity-50",
41 | "opacity-[0.75]": "opacity-75",
42 | "opacity-[1]": "opacity-100",
43 | "opacity-[0.35]": "opacity-[0.35]",
44 | "opacity-[0.05]": "opacity-5",
45 | };
46 | export const gap = {
47 | "gap-[1px]": "gap-px",
48 | "gap-[2rem]": "gap-8",
49 | "gap-x-[3rem]": "gap-x-12",
50 | "gap-y-[24px]": "gap-y-6",
51 | };
52 | export const padding = {
53 | "p-[44px]": "p-11",
54 | "p-[100%]": "p-full",
55 | "px-[32px]": "px-8",
56 | "px-[64px]": "px-16",
57 | "py-[40px]": "py-10",
58 | };
59 |
--------------------------------------------------------------------------------
/src/core/functions/get-user-input-values.ts:
--------------------------------------------------------------------------------
1 | import { isEmpty } from "lodash-es";
2 |
3 | export const getUserInputValues = (
4 | input: string,
5 | allowedUnits: string[],
6 | ): { error: string } | { unit: string; value: string } => {
7 | // eslint-disable-next-line no-param-reassign
8 | input = input.toLowerCase();
9 | let sanitizedInput = input.trim().replace(/ |\+/g, "");
10 |
11 | if ((sanitizedInput === "auto" || sanitizedInput === "none") && allowedUnits.includes(sanitizedInput)) {
12 | return { value: "", unit: sanitizedInput };
13 | }
14 |
15 | const expression = allowedUnits.length ? new RegExp(allowedUnits.join("|"), "g") : /XXXXXX/g;
16 | sanitizedInput = sanitizedInput.replace(expression, "");
17 |
18 | const unit = input.match(expression);
19 |
20 | const hasMoreUnits = unit && unit.length > 1;
21 | const isNotNumber = !isEmpty(sanitizedInput) && Number.isNaN(Number(sanitizedInput));
22 |
23 | if (hasMoreUnits || isNotNumber) {
24 | return { error: "Invalid value" };
25 | }
26 |
27 | if (unit && (unit[0] === "auto" || unit[0] === "none")) {
28 | return { value: unit[0], unit: "" };
29 | }
30 |
31 | return { value: sanitizedInput, unit: unit ? unit[0] : "" };
32 | };
33 |
--------------------------------------------------------------------------------
/src/core/functions/is-visible-at-breakpoint.ts:
--------------------------------------------------------------------------------
1 | type Breakpoint = "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
2 |
3 | /**
4 | * Checks if a given class string is visible at a given breakpoint
5 | * important: The default assumption is that the block is hidden
6 | * @param classes
7 | * @param breakpoint
8 | */
9 | const isVisibleAtBreakpoint = (classes: string, breakpoint: Breakpoint): boolean => {
10 | const breakpoints: Breakpoint[] = ["xs", "sm", "md", "lg", "xl", "2xl"];
11 | const breakpointIndex = breakpoints.indexOf(breakpoint);
12 |
13 | const classArray = classes.split(" ");
14 |
15 | // Initialize all breakpoints as hidden
16 | let visibilityState = new Array(breakpoints.length).fill(false);
17 |
18 | for (const cls of classArray) {
19 | let [prefix, baseClass] = cls.split(":");
20 |
21 | if (!baseClass) {
22 | baseClass = prefix;
23 | prefix = "xs"; // No prefix means it applies from the smallest breakpoint
24 | }
25 |
26 | const classBreakpointIndex = breakpoints.indexOf(prefix as Breakpoint);
27 |
28 | if (classBreakpointIndex <= breakpointIndex) {
29 | const visibilityClasses = ["block", "flex", "inline", "inline-block", "inline-flex", "grid", "table"];
30 | const hiddenClasses = ["hidden"];
31 |
32 | if (visibilityClasses.includes(baseClass)) {
33 | for (let i = classBreakpointIndex; i < breakpoints.length; i++) {
34 | visibilityState[i] = true;
35 | }
36 | } else if (hiddenClasses.includes(baseClass)) {
37 | for (let i = classBreakpointIndex; i < breakpoints.length; i++) {
38 | visibilityState[i] = false;
39 | }
40 | }
41 | // Classes that don't affect visibility are ignored
42 | }
43 | }
44 |
45 | // Return the visibility state for the current breakpoint
46 | return visibilityState[breakpointIndex];
47 | };
48 |
49 | export { isVisibleAtBreakpoint };
50 |
--------------------------------------------------------------------------------
/src/core/functions/logging.ts:
--------------------------------------------------------------------------------
1 | let DEBUG_LOGS: boolean = false;
2 |
3 | export const setDebugLogs = (value: boolean) => {
4 | DEBUG_LOGS = value;
5 | };
6 |
7 | export const debugLog = (...args: any) => {
8 | if (DEBUG_LOGS) {
9 | console.log(...args);
10 | }
11 | };
12 | export const debugWarn = (...args: any) => {
13 | if (DEBUG_LOGS) {
14 | console.warn(...args);
15 | }
16 | };
17 |
18 | export const debugInfo = (...args: any) => {
19 | if (DEBUG_LOGS) {
20 | console.info(...args);
21 | }
22 | };
23 |
24 | export const debugError = (...args: any) => {
25 | if (DEBUG_LOGS) {
26 | console.error(...args);
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/src/core/functions/order-classes-by-breakpoint.ts:
--------------------------------------------------------------------------------
1 | import { constructClassObject } from "@/core/functions/class-fn";
2 |
3 | export function orderClassesByBreakpoint(classes: string): string {
4 | //sanitize the classes
5 | classes = classes.replace(/\s+/g, " ");
6 | const classesArray = classes.split(" ").map(constructClassObject);
7 | const breakpointOrder = ["xs", "sm", "md", "lg", "xl", "2xl"];
8 | return classesArray
9 | .sort((a, b) => {
10 | return breakpointOrder.indexOf(a.mq) - breakpointOrder.indexOf(b.mq);
11 | })
12 | .map((cls) => cls.fullCls)
13 | .join(" ");
14 | }
15 |
16 | if (import.meta.vitest) {
17 | test("orderClassesByBreakpoint", () => {
18 | expect(orderClassesByBreakpoint("bg-red-400 sm:bg-red-500")).toBe("bg-red-400 sm:bg-red-500");
19 | expect(orderClassesByBreakpoint("bg-red-400 sm:bg-red-500 md:bg-red-600")).toBe(
20 | "bg-red-400 sm:bg-red-500 md:bg-red-600",
21 | );
22 | expect(orderClassesByBreakpoint("xl:sticky block sm:absolute")).toBe("block sm:absolute xl:sticky");
23 | expect(orderClassesByBreakpoint("sm:bg-red-500 bg-red-400")).toBe("bg-red-400 sm:bg-red-500");
24 | expect(orderClassesByBreakpoint("sm:w-[30%] w-[30%]")).toBe("w-[30%] sm:w-[30%]");
25 | expect(orderClassesByBreakpoint("text-[30px] sm:text-[20px]")).toBe("text-[30px] sm:text-[20px]");
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/src/core/functions/sanitize-classes.test.ts:
--------------------------------------------------------------------------------
1 | import { sanitizeClasses } from "@/core/functions/SanitizeClasses";
2 |
3 | describe("Sanitize classes string", () => {
4 | test("sanitizeClasses", () => {
5 | expect(sanitizeClasses("")).toBe("");
6 | expect(sanitizeClasses("flex")).toBe("flex");
7 | expect(sanitizeClasses("flex xl:block")).toBe("flex xl:block");
8 | expect(sanitizeClasses("flex sm:block")).toBe("flex sm:block");
9 | expect(sanitizeClasses("flex sm:flex")).toBe("flex");
10 | expect(sanitizeClasses("flex sm:flex md:flex")).toBe("flex");
11 | expect(sanitizeClasses("flex sm:block md:flex")).toBe("flex sm:block md:flex");
12 | expect(sanitizeClasses("flex sm:block md:flex 2xl:flex")).toBe("flex sm:block md:flex");
13 | expect(sanitizeClasses("flex 2xl:flex")).toBe("flex");
14 | expect(sanitizeClasses("w-1/2 xl:w-full")).toBe("w-1/2 xl:w-full");
15 | expect(sanitizeClasses("w-1/2 lg:w-full xl:w-full")).toBe("w-1/2 lg:w-full");
16 | expect(sanitizeClasses("w-1/2 lg:flex xl:w-1/2")).toBe("w-1/2 lg:flex");
17 | // extra spaces
18 | expect(sanitizeClasses("w-1/2 lg:flex xl:w-1/2")).toBe("w-1/2 lg:flex");
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/core/functions/split-blocks.ts:
--------------------------------------------------------------------------------
1 | import { convertToBlocksTree } from "@/core/functions/blocks-fn";
2 | import { ChaiBlock } from "@/types/chai-block";
3 |
4 | export function getBlocksTree(blocks: Partial[]) {
5 | return convertToBlocksTree(blocks);
6 | }
7 |
--------------------------------------------------------------------------------
/src/core/functions/wrapInsideContainer.ts:
--------------------------------------------------------------------------------
1 | import { ChaiBlock } from "@/types/chai-block";
2 | import { getDefaultBlockProps } from "@chaibuilder/runtime";
3 |
4 | export const wrapInsideContainer = (container: ChaiBlock | "Body" | "Html") => {
5 | return container ? container : { ...getDefaultBlockProps(container as string), _id: "container", _type: container };
6 | };
7 |
--------------------------------------------------------------------------------
/src/core/hooks/hooks.ts:
--------------------------------------------------------------------------------
1 | export * from "@/core/hooks";
2 |
--------------------------------------------------------------------------------
/src/core/hooks/use-all-data-providers.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 |
3 | export const useAllDataProviders = () => {
4 | return useMemo(() => [], []);
5 | };
6 |
--------------------------------------------------------------------------------
/src/core/hooks/use-block-highlight.ts:
--------------------------------------------------------------------------------
1 | import { canvasIframeAtom } from "@/core/atoms/ui";
2 | import { useAtom } from "jotai";
3 | import { useCallback, useMemo } from "react";
4 |
5 | //module-level variable to track the last highlighted block
6 | let lastHighlighted: HTMLElement | null = null;
7 |
8 | export const useBlockHighlight = () => {
9 | const [iframe] = useAtom(canvasIframeAtom);
10 | const innerDoc = useMemo(() => iframe?.contentDocument || iframe?.contentWindow?.document, [iframe]);
11 | const highlightBlock = useCallback(
12 | (elementOrID: HTMLElement | string) => {
13 | if (!innerDoc) return;
14 | if (lastHighlighted) {
15 | // Remove highlight from previous block
16 | lastHighlighted.removeAttribute("data-highlighted");
17 | }
18 | // Find and highlight new bloc
19 | if (typeof elementOrID !== "string") {
20 | elementOrID.setAttribute("data-highlighted", "true");
21 | lastHighlighted = elementOrID;
22 | } else if (typeof elementOrID === "string") {
23 | const element = innerDoc.querySelector(`[data-block-id="${elementOrID}"]`) as HTMLElement;
24 | if (element) {
25 | element.setAttribute("data-highlighted", "true");
26 | lastHighlighted = element;
27 | }
28 | } else {
29 | lastHighlighted = null;
30 | }
31 | },
32 | [innerDoc],
33 | );
34 |
35 | const clearHighlight = useCallback(() => {
36 | if (lastHighlighted) {
37 | lastHighlighted.removeAttribute("data-highlighted");
38 | lastHighlighted = null;
39 | }
40 | }, []);
41 |
42 | return { highlightBlock, clearHighlight, lastHighlighted };
43 | };
44 |
--------------------------------------------------------------------------------
/src/core/hooks/use-branding-options.ts:
--------------------------------------------------------------------------------
1 | import { BRANDING_OPTIONS_DEFAULTS } from "@/core/constants/MODIFIERS";
2 | import { ChaiBlock } from "@/types/chai-block";
3 | import { atom, useAtom } from "jotai";
4 | import { isObject } from "lodash-es";
5 |
6 | type BrandingOptions = {
7 | bodyBgDarkColor: string;
8 | bodyBgLightColor: string;
9 | bodyFont: string;
10 | bodyTextDarkColor: string;
11 | bodyTextLightColor: string;
12 | headingFont: string;
13 | primaryColor: string;
14 | roundedCorners: number;
15 | secondaryColor: string;
16 | } & Record;
17 |
18 | export const brandingOptionsAtom = atom(BRANDING_OPTIONS_DEFAULTS as BrandingOptions);
19 | export const blocksContainerAtom = atom(null);
20 | /**
21 | * Wrapper around useAtom
22 | */
23 | export const useBrandingOptions = () => {
24 | const [brandingOptions, setBrandingOptions] = useAtom(brandingOptionsAtom);
25 | return [
26 | isObject(brandingOptions) ? { ...BRANDING_OPTIONS_DEFAULTS, ...brandingOptions } : BRANDING_OPTIONS_DEFAULTS,
27 | setBrandingOptions,
28 | ] as const;
29 | };
30 |
31 | export const useBlocksContainer = () => {
32 | return useAtom(blocksContainerAtom);
33 | };
34 |
--------------------------------------------------------------------------------
/src/core/hooks/use-broadcast-channel.ts:
--------------------------------------------------------------------------------
1 | import { useBlocksStoreManager } from "@/core/history/use-blocks-store-manager";
2 | import { useBlocksStore, useBuilderProp } from "@/core/hooks";
3 | import { useDebouncedCallback } from "@react-hookz/web";
4 | import { useEffect } from "react";
5 |
6 | const broadcastChannel = new BroadcastChannel("chaibuilder");
7 | export const useBroadcastChannel = () => {
8 | const pageId = useBuilderProp("pageId", "chaibuilder_page");
9 | const postMessage = useDebouncedCallback(
10 | (message: any) => broadcastChannel.postMessage({ ...message, pageId }),
11 | [pageId],
12 | 200,
13 | );
14 |
15 | return { postMessage };
16 | };
17 |
18 | export const useUnmountBroadcastChannel = () => {
19 | const [, setBlocks] = useBlocksStore();
20 | const pageId = useBuilderProp("pageId", "chaibuilder_page");
21 | const { updateBlocksProps } = useBlocksStoreManager();
22 | useEffect(() => {
23 | broadcastChannel.onmessageerror = (event) => {
24 | console.log("error", event);
25 | };
26 | broadcastChannel.onmessage = (event) => {
27 | if (event.data.type === "blocks-updated" && event.data.pageId === pageId) {
28 | setBlocks(event.data.blocks);
29 | }
30 | if (event.data.type === "blocks-props-updated" && event.data.pageId === pageId) {
31 | updateBlocksProps(event.data.blocks);
32 | }
33 | };
34 | return () => {
35 | broadcastChannel.onmessage = null;
36 | broadcastChannel.onmessageerror = null;
37 | //broadcastChannel.close();
38 | };
39 | }, [setBlocks, pageId]);
40 | };
41 |
--------------------------------------------------------------------------------
/src/core/hooks/use-builder-prop.ts:
--------------------------------------------------------------------------------
1 | import { chaiBuilderPropsAtom } from "@/core/atoms/builder";
2 | import { ChaiBuilderEditorProps } from "@/types/chaibuilder-editor-props";
3 | import { useAtomValue } from "jotai";
4 | import { get } from "lodash-es";
5 | import { useMemo } from "react";
6 |
7 | type ExcludedBuilderProps = "blocks" | "subPages" | "brandingOptions" | "dataProviders";
8 |
9 | export const useBuilderProp = (
10 | propKey: keyof Omit | "languages",
11 | defaultValue: T = undefined,
12 | ): T => {
13 | const builderProps = useAtomValue(chaiBuilderPropsAtom);
14 | return useMemo(() => get(builderProps, propKey, defaultValue), [builderProps, propKey, defaultValue]);
15 | };
16 |
--------------------------------------------------------------------------------
/src/core/hooks/use-builder-reset.ts:
--------------------------------------------------------------------------------
1 | import { useBlockRepeaterDataAtom } from "@/core/async-props/use-async-props";
2 | import { aiAssistantActiveAtom } from "@/core/atoms/ui";
3 | import { useUndoManager } from "@/core/history/use-undo-manager";
4 | import { useBlockHighlight } from "@/core/hooks/use-block-highlight";
5 | import { usePartialBlocksStore } from "@/core/hooks/use-partial-blocks-store";
6 | import { useSavePage } from "@/core/hooks/use-save-page";
7 | import { useSelectedBlockIds } from "@/core/hooks/use-selected-blockIds";
8 | import { useSelectedStylingBlocks } from "@/core/hooks/use-selected-styling-blocks";
9 | import { useAtom } from "jotai";
10 |
11 | export const useBuilderReset = () => {
12 | const { clear } = useUndoManager();
13 | const [, setSelectedIds] = useSelectedBlockIds();
14 | const { clearHighlight } = useBlockHighlight();
15 | const [, setStylingHighlighted] = useSelectedStylingBlocks();
16 | const [, setAiAssistantActive] = useAtom(aiAssistantActiveAtom);
17 | const { reset: resetPartialBlocks } = usePartialBlocksStore();
18 | const { setSaveState } = useSavePage();
19 | const [, setBlockRepeaterDataAtom] = useBlockRepeaterDataAtom();
20 |
21 | return () => {
22 | setBlockRepeaterDataAtom({});
23 | setSelectedIds([]);
24 | setStylingHighlighted([]);
25 | clearHighlight();
26 | clear();
27 | setAiAssistantActive(false);
28 | resetPartialBlocks();
29 | setSaveState("SAVED");
30 | };
31 | };
32 |
--------------------------------------------------------------------------------
/src/core/hooks/use-canvas-settings.ts:
--------------------------------------------------------------------------------
1 | import { canvasSettingsAtom } from "@/core/atoms/ui";
2 | import { useAtom } from "jotai";
3 |
4 | export const useCanvasSettings = () => {
5 | return useAtom(canvasSettingsAtom);
6 | };
7 |
--------------------------------------------------------------------------------
/src/core/hooks/use-canvas-zoom.ts:
--------------------------------------------------------------------------------
1 | import { atomWithStorage } from "jotai/utils";
2 | import { useAtom } from "jotai";
3 |
4 | export const canvasZoomAtom = atomWithStorage("canvasZoom", 100);
5 |
6 | /**
7 | * Wrapper hook around useAtom
8 | */
9 | export const useCanvasZoom = () => useAtom(canvasZoomAtom);
10 |
--------------------------------------------------------------------------------
/src/core/hooks/use-code-editor.ts:
--------------------------------------------------------------------------------
1 | import { atom, useAtom } from "jotai";
2 |
3 | type CodeEditorProps = {
4 | blockId: string;
5 | blockProp: string;
6 | initialCode: string;
7 | placeholder?: string;
8 | };
9 |
10 | const codeEditorAtom = atom(null);
11 |
12 | /**
13 | * Custom hook to access the current state of the code editor.
14 | * @category Hooks
15 | * @returns The current state of the code editor from the `codeEditorAtom`.
16 | */
17 | export const useCodeEditor = () => {
18 | return useAtom(codeEditorAtom);
19 | };
20 |
--------------------------------------------------------------------------------
/src/core/hooks/use-copy-to-clipboard.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 |
3 | type CopiedValue = string | null;
4 |
5 | type CopyFn = (text: string) => Promise;
6 |
7 | export const useCopyToClipboard = (): [CopiedValue, CopyFn] => {
8 | const [copiedText, setCopiedText] = useState(null);
9 |
10 | const copy: CopyFn = useCallback(async (text) => {
11 | if (!navigator?.clipboard) {
12 | console.warn("Clipboard not supported");
13 | return false;
14 | }
15 |
16 | // Try to save to clipboard then save it in the state if worked
17 | try {
18 | await navigator.clipboard.writeText(text);
19 | setCopiedText(text);
20 | return true;
21 | } catch (error) {
22 | console.warn("Copy failed", error);
23 | setCopiedText(null);
24 | return false;
25 | }
26 | }, []);
27 |
28 | return [copiedText, copy];
29 | };
30 |
--------------------------------------------------------------------------------
/src/core/hooks/use-current-page.ts:
--------------------------------------------------------------------------------
1 | import { atom, useAtomValue } from "jotai";
2 |
3 | export const currentPageAtom: any = atom(null);
4 |
5 | export const useCurrentPage = () => {
6 | const currentPage = useAtomValue(currentPageAtom);
7 | return { currentPage };
8 | };
9 |
--------------------------------------------------------------------------------
/src/core/hooks/use-cut-blockIds.ts:
--------------------------------------------------------------------------------
1 | import { copiedBlockIdsAtom } from "@/core/hooks/use-copy-blockIds";
2 | import { atom, useAtom, useSetAtom } from "jotai";
3 | import { useCallback } from "react";
4 |
5 | export const cutBlockIdsAtom: any = atom>([]);
6 |
7 | export const useCutBlockIds = (): [Array, Function] => {
8 | const [ids, setIds] = useAtom(cutBlockIdsAtom);
9 | const resetCopyIds = useSetAtom(copiedBlockIdsAtom);
10 |
11 | const setCutBlockIds = useCallback(
12 | (blockIds: Array) => {
13 | setIds(blockIds);
14 | resetCopyIds([]);
15 | },
16 | [setIds, resetCopyIds],
17 | );
18 |
19 | return [ids as string[], setCutBlockIds];
20 | };
21 |
--------------------------------------------------------------------------------
/src/core/hooks/use-dark-mode.ts:
--------------------------------------------------------------------------------
1 | import { useAtom } from "jotai";
2 | import { atomWithStorage } from "jotai/utils";
3 |
4 | export const darkModeAtom = atomWithStorage("darkMode", false);
5 |
6 | /**
7 | * Wrapper hook around useAtom
8 | */
9 | export const useDarkMode = (): [boolean, Function] => {
10 | const [darkMode, setDarkMode] = useAtom(darkModeAtom);
11 | return [darkMode, setDarkMode];
12 | };
13 |
--------------------------------------------------------------------------------
/src/core/hooks/use-duplicate-blocks.ts:
--------------------------------------------------------------------------------
1 | import { getDuplicatedBlocks } from "@/core/functions/blocks-fn";
2 | import { useBlocksStore, useBlocksStoreUndoableActions } from "@/core/history/use-blocks-store-undoable-actions";
3 | import { useSelectedBlockIds } from "@/core/hooks/use-selected-blockIds";
4 | import { ChaiBlock } from "@/types/chai-block";
5 | import { each, filter, get, isString } from "lodash-es";
6 | import { useCallback } from "react";
7 |
8 | /**
9 | * useDuplicateBlock
10 | */
11 | export const useDuplicateBlocks = (): Function => {
12 | const [presentBlocks] = useBlocksStore();
13 | const [, setSelected] = useSelectedBlockIds();
14 | const { addBlocks } = useBlocksStoreUndoableActions();
15 |
16 | return useCallback(
17 | (blockIds: string[], parentId: string | null = null) => {
18 | const newBlockIds: string[] = [];
19 | each(blockIds, (blockId: string) => {
20 | const block = presentBlocks.find((block) => block._id === blockId);
21 | if (!parentId) {
22 | // use the parent of the same block. Can be a falsy value. null undefined etc.
23 | parentId = block._parent;
24 | } else if (parentId === "root") {
25 | parentId = null;
26 | }
27 | // get sibling blocks
28 | const siblingBlocks = filter(presentBlocks, (_block: ChaiBlock) =>
29 | isString(parentId) ? _block._parent === parentId : !_block._parent,
30 | );
31 | const blockPosition = siblingBlocks.indexOf(block);
32 | const newBlockPosition = blockPosition + 1;
33 | const newBlocks = getDuplicatedBlocks(presentBlocks, blockId, parentId);
34 | addBlocks(newBlocks, parentId, newBlockPosition);
35 | newBlockIds.push(get(newBlocks, "0._id", ""));
36 | });
37 | setSelected(newBlockIds);
38 | },
39 | [presentBlocks, setSelected],
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/src/core/hooks/use-expand-tree.ts:
--------------------------------------------------------------------------------
1 | import { presentBlocksAtom } from "@/core/atoms/blocks";
2 | import { useSelectedBlockIds } from "@/core/hooks/use-selected-blockIds";
3 | import { ChaiBlock } from "@/types/chai-block";
4 | import { atom, useAtom, useAtomValue } from "jotai";
5 | import { find, first, flatten, get, isEmpty, isString } from "lodash-es";
6 | import { useEffect } from "react";
7 |
8 | /**
9 | * Traverse the components array to find all the parent nodes
10 | * of the given component
11 | * @param blocks
12 | * @param id
13 | * @returns {unknown[]}
14 | */
15 | const getParentNodeIds = (blocks: ChaiBlock[], id: string | null | undefined): string[] => {
16 | const ids: string[] = [];
17 | let block = find(blocks, { _id: id }) as ChaiBlock;
18 | let parent: string | undefined | null = get(block, "_parent", "");
19 | while (isString(parent) && !isEmpty(parent)) {
20 | ids.push(block?._parent as string);
21 | block = find(blocks, { _id: parent }) as ChaiBlock;
22 | parent = block?._parent;
23 | }
24 | return flatten(ids);
25 | };
26 | export const expandedIdsAtom = atom([]);
27 |
28 | export const useExpandTree = () => {
29 | const [ids] = useSelectedBlockIds();
30 | const pageBlocks = useAtomValue(presentBlocksAtom);
31 | const [, setExpandedIds] = useAtom(expandedIdsAtom);
32 | useEffect(() => {
33 | let expandedIds: string[] = [];
34 | const id = first(ids);
35 | if (isString(id)) {
36 | expandedIds = [id, ...getParentNodeIds(pageBlocks, id)];
37 | }
38 | setExpandedIds(expandedIds);
39 | }, [ids, pageBlocks, setExpandedIds]);
40 | };
41 |
--------------------------------------------------------------------------------
/src/core/hooks/use-get-page-data.ts:
--------------------------------------------------------------------------------
1 | import { useBlocksStore } from "@/core/history/use-blocks-store-undoable-actions";
2 | import { useBrandingOptions } from "@/core/hooks/use-branding-options";
3 | import { useCurrentPage } from "@/core/hooks/use-current-page";
4 | import { ChaiBlock } from "@/types/chai-block";
5 | import { getRegisteredChaiBlock } from "@chaibuilder/runtime";
6 | import { compact, get, map, memoize, omit } from "lodash-es";
7 | import { useCallback } from "react";
8 |
9 | /**
10 | * Get the builder props for a block
11 | * @param type - The type of the block
12 | * @returns The builder props for the block
13 | */
14 | const getBlockBuilderProps = memoize((type: string) => {
15 | const registeredBlock = getRegisteredChaiBlock(type);
16 | const props = get(registeredBlock, "schema.properties", {});
17 | return compact(
18 | Object.keys(props).map((key) => {
19 | return get(props[key], "builderProp", false) || get(props[key], "runtime", false) ? key : null;
20 | }),
21 | );
22 | });
23 |
24 | export const useGetPageData = () => {
25 | const [projectOptions] = useBrandingOptions();
26 | const { currentPage } = useCurrentPage();
27 | const [presentBlocks] = useBlocksStore();
28 |
29 | return useCallback(() => {
30 | // omit the builder props from the blocks as they are not needed for the page data
31 | // and only used inside the builder
32 | const blocks = map(presentBlocks, (block: ChaiBlock) => {
33 | return omit(block, getBlockBuilderProps(block._type));
34 | });
35 | return {
36 | currentPage,
37 | blocks,
38 | };
39 | }, [projectOptions, currentPage, presentBlocks]);
40 | };
41 |
--------------------------------------------------------------------------------
/src/core/hooks/use-hidden-blocks.ts:
--------------------------------------------------------------------------------
1 | import { atom, useAtom } from "jotai";
2 | import { includes, without } from "lodash-es";
3 | import { useCallback } from "react";
4 |
5 | const hiddenBlockIdsAtom = atom>([]);
6 |
7 | export const useHiddenBlockIds = () => {
8 | const [blockIds, setBlockIds] = useAtom(hiddenBlockIdsAtom);
9 |
10 | const toggleHidden = useCallback(
11 | (blockId: string) => {
12 | setBlockIds((prevIds) => (includes(prevIds, blockId) ? without(prevIds, blockId) : [...prevIds, blockId]));
13 | },
14 | [setBlockIds],
15 | );
16 |
17 | return [blockIds, setBlockIds, toggleHidden] as const;
18 | };
19 |
--------------------------------------------------------------------------------
/src/core/hooks/use-highlight-blockId.ts:
--------------------------------------------------------------------------------
1 | import { atom, useAtom } from "jotai";
2 |
3 | const highlightBlockIdAtom = atom("");
4 |
5 | /**
6 | *
7 | */
8 | export const useHighlightBlockId = (): [string, Function] => useAtom(highlightBlockIdAtom);
9 |
--------------------------------------------------------------------------------
/src/core/hooks/use-inline-editing.ts:
--------------------------------------------------------------------------------
1 | import { atom } from "jotai";
2 | import { useAtom } from "jotai";
3 |
4 | const inlineEditingActiveAtom = atom("");
5 | inlineEditingActiveAtom.debugLabel = "inlineEditingActiveAtom";
6 |
7 | const inlineEditingItemIndexAtom = atom(0);
8 | inlineEditingItemIndexAtom.debugLabel = "inlineEditingItemIndexAtom";
9 |
10 | export const useInlineEditing = () => {
11 | const [editingBlockId, setEditingBlockId] = useAtom(inlineEditingActiveAtom);
12 | const [editingItemIndex, setEditingItemIndex] = useAtom(inlineEditingItemIndexAtom);
13 |
14 | return {
15 | editingBlockId,
16 | editingItemIndex,
17 | setEditingBlockId,
18 | setEditingItemIndex,
19 | };
20 | };
21 |
--------------------------------------------------------------------------------
/src/core/hooks/use-languages.ts:
--------------------------------------------------------------------------------
1 | import { useBuilderProp } from "@/core/hooks/use-builder-prop";
2 | import { atom, useAtom } from "jotai";
3 |
4 | const languageAtom = atom("");
5 | languageAtom.debugLabel = "selectedLanguageAtom";
6 |
7 | export const useLanguages = () => {
8 | const languages = useBuilderProp("languages", []);
9 | const fallbackLang = useBuilderProp("fallbackLang", "en");
10 | const [selectedLang, _setSelectedLang] = useAtom(languageAtom);
11 |
12 | const setSelectedLang = (lang: string) => {
13 | _setSelectedLang(fallbackLang === lang ? "" : lang);
14 | };
15 |
16 | return {
17 | languages: languages?.filter((_lang) => _lang !== fallbackLang),
18 | fallbackLang,
19 | selectedLang,
20 | setSelectedLang,
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/src/core/hooks/use-library-blocks.tsx:
--------------------------------------------------------------------------------
1 | import { ChaiLibrary, ChaiLibraryBlock } from "@/types/chaibuilder-editor-props";
2 | import { atom, useAtom } from "jotai";
3 | import { get } from "lodash-es";
4 | import { useCallback, useEffect, useMemo, useRef } from "react";
5 |
6 | const libraryBlocksAtom = atom<{ [uuid: string]: { loading: "idle" | "loading" | "complete"; blocks: any[] | null } }>(
7 | {},
8 | );
9 | export const useLibraryBlocks = (library?: Partial & { id: string }) => {
10 | const [libraryBlocks, setLibraryBlocks] = useAtom(libraryBlocksAtom);
11 | const getBlocks = useMemo(() => library?.getBlocksList || (() => []), [library]);
12 | const blocks = get(libraryBlocks, `${library?.id}.blocks`, null);
13 | const state = get(libraryBlocks, `${library?.id}.loading`, "idle");
14 | const loadingRef = useRef("idle");
15 |
16 | useEffect(() => {
17 | (async () => {
18 | if (state === "complete" || loadingRef.current === "loading") return;
19 | loadingRef.current = "loading";
20 | setLibraryBlocks((prev) => ({ ...prev, [library?.id]: { loading: "loading", blocks: [] } }));
21 | try {
22 | const libraryBlocks: ChaiLibraryBlock[] = await getBlocks(library);
23 | loadingRef.current = "idle";
24 | setLibraryBlocks((prev) => ({ ...prev, [library?.id]: { loading: "complete", blocks: libraryBlocks || [] } }));
25 | } catch (error) {
26 | loadingRef.current = "idle";
27 | setLibraryBlocks((prev) => ({ ...prev, [library?.id]: { loading: "complete", blocks: [] } }));
28 | }
29 | })();
30 | }, [library, blocks, state, loadingRef, setLibraryBlocks, getBlocks]);
31 |
32 | const resetLibrary = useCallback(
33 | (libraryId: string) => {
34 | setLibraryBlocks((prev) => ({ ...prev, [libraryId]: { loading: "idle", blocks: [] } }));
35 | },
36 | [setLibraryBlocks],
37 | );
38 |
39 | return { data: blocks || [], isLoading: state === "loading", resetLibrary };
40 | };
41 |
--------------------------------------------------------------------------------
/src/core/hooks/use-permissions.ts:
--------------------------------------------------------------------------------
1 | import { useBuilderProp } from "@/core/hooks/use-builder-prop";
2 | import { useCallback } from "react";
3 |
4 | export const usePermissions = () => {
5 | const permissions = useBuilderProp("permissions", undefined);
6 | const hasPermission = useCallback(
7 | (permission: string) => {
8 | if (!permissions) return true;
9 | return permissions.includes(permission);
10 | },
11 | [permissions],
12 | );
13 |
14 | return { hasPermission };
15 | };
16 |
--------------------------------------------------------------------------------
/src/core/hooks/use-preview-mode.ts:
--------------------------------------------------------------------------------
1 | import { atom, useAtom } from "jotai";
2 |
3 | const previewModeAtom = atom(false);
4 |
5 | /**
6 | *
7 | */
8 | export const usePreviewMode = (): [boolean, Function] => {
9 | const [previewMode, setPreviewMode] = useAtom(previewModeAtom);
10 | return [previewMode, setPreviewMode];
11 | };
12 |
--------------------------------------------------------------------------------
/src/core/hooks/use-pub-sub.ts:
--------------------------------------------------------------------------------
1 | import { pubsub } from "@/core/pubsub";
2 | import { useEffect } from "react";
3 |
4 | export function usePubSub(eventName: string, callback: (data?: T) => void) {
5 | useEffect(() => {
6 | const unsubscribe = pubsub.subscribe(eventName, callback);
7 | return () => unsubscribe();
8 | }, [eventName, callback]);
9 | }
10 |
--------------------------------------------------------------------------------
/src/core/hooks/use-save-page.ts:
--------------------------------------------------------------------------------
1 | import { useBuilderProp } from "@/core/hooks/use-builder-prop";
2 | import { useGetPageData } from "@/core/hooks/use-get-page-data";
3 | import { usePermissions } from "@/core/hooks/use-permissions";
4 | import { useTheme } from "@/core/hooks/use-theme";
5 | import { useThrottledCallback } from "@react-hookz/web";
6 | import { atom, useAtom } from "jotai";
7 | import { noop } from "lodash-es";
8 | export const builderSaveStateAtom = atom<"SAVED" | "SAVING" | "UNSAVED">("SAVED"); // SAVING
9 | builderSaveStateAtom.debugLabel = "builderSaveStateAtom";
10 |
11 | export const useSavePage = () => {
12 | const [saveState, setSaveState] = useAtom(builderSaveStateAtom);
13 | const onSave = useBuilderProp("onSave", async (_args) => {});
14 | const onSaveStateChange = useBuilderProp("onSaveStateChange", noop);
15 | const getPageData = useGetPageData();
16 | const [theme] = useTheme();
17 | const { hasPermission } = usePermissions();
18 |
19 | const savePage = useThrottledCallback(
20 | async (autoSave: boolean = false) => {
21 | if (!hasPermission("save_page")) {
22 | return;
23 | }
24 | setSaveState("SAVING");
25 | onSaveStateChange("SAVING");
26 | const pageData = getPageData();
27 | await onSave({
28 | autoSave,
29 | blocks: pageData.blocks,
30 | theme,
31 | });
32 | setTimeout(() => {
33 | setSaveState("SAVED");
34 | onSaveStateChange("SAVED");
35 | }, 100);
36 | return true;
37 | },
38 | [getPageData, setSaveState, theme, onSave, onSaveStateChange],
39 | 3000, // save only every 5 seconds
40 | );
41 |
42 | return { savePage, saveState, setSaveState };
43 | };
44 |
--------------------------------------------------------------------------------
/src/core/hooks/use-screen-size-width.ts:
--------------------------------------------------------------------------------
1 | import { getBreakpointValue } from "@/core/functions/common-functions";
2 | import { useStylingBreakpoint } from "@/core/hooks/use-styling-breakpoint";
3 | import { atom, useAtom, useAtomValue } from "jotai";
4 | import { atomWithStorage } from "jotai/utils";
5 | import { useEffect } from "react";
6 |
7 | export const canvasWidthAtom = atomWithStorage("canvasWidth", 800);
8 | export const canvasDisplayWidthAtom = atomWithStorage("canvasDisplayWidth", 800);
9 |
10 | export const canvasBreakpointAtom = atom((get) => {
11 | const width: number = get(canvasWidthAtom);
12 | return getBreakpointValue(width).toLowerCase();
13 | });
14 |
15 | /**
16 | *
17 | */
18 | export const useScreenSizeWidth = () => {
19 | const [currentWidth, setCanvasWidth] = useAtom(canvasWidthAtom);
20 | const breakpoint = useAtomValue(canvasBreakpointAtom);
21 | const [stylingBreakpoint, setStylingBreakpoint] = useStylingBreakpoint();
22 |
23 | useEffect(() => {
24 | if (stylingBreakpoint !== "xs") {
25 | setStylingBreakpoint(breakpoint);
26 | }
27 | }, [breakpoint, stylingBreakpoint, setStylingBreakpoint]);
28 |
29 | return [currentWidth, breakpoint, setCanvasWidth] as const;
30 | };
31 |
32 | export const useCanvasDisplayWidth = () => {
33 | const [canvasDisplayWidth, setCanvasDisplayWidth] = useAtom(canvasDisplayWidthAtom);
34 | return [canvasDisplayWidth, setCanvasDisplayWidth] as const;
35 | };
36 |
--------------------------------------------------------------------------------
/src/core/hooks/use-selected-breakpoints.ts:
--------------------------------------------------------------------------------
1 | import { useAtom } from "jotai";
2 | import { atomWithStorage } from "jotai/utils";
3 |
4 | export const selectedBreakpointsAtom = atomWithStorage("selectedBreakpoints", ["XS", "MD", "XL"]);
5 |
6 | export const useSelectedBreakpoints = (): [string[], Function] => {
7 | const [selectedBreakpoints, setSelectedBreakpoints] = useAtom(selectedBreakpointsAtom);
8 | return [selectedBreakpoints, setSelectedBreakpoints];
9 | };
10 |
--------------------------------------------------------------------------------
/src/core/hooks/use-selected-library.ts:
--------------------------------------------------------------------------------
1 | import { selectedLibraryAtom } from "@/core/atoms/ui";
2 | import { useAtom } from "jotai";
3 |
4 | /**
5 | * Hook to get and set the selected UI library
6 | * @returns {[string, (library: string) => void]} A tuple containing the selected library ID and a function to set the selected library
7 | */
8 |
9 | export const useSelectedLibrary = () => {
10 | return useAtom(selectedLibraryAtom);
11 | };
12 |
--------------------------------------------------------------------------------
/src/core/hooks/use-selected-styling-blocks.ts:
--------------------------------------------------------------------------------
1 | import { atom, useAtom } from "jotai";
2 |
3 | export type TStyleBlock = {
4 | blockId: string;
5 | id: string;
6 | prop: string;
7 | };
8 |
9 | /**
10 | * Core selected ids atom
11 | */
12 | export const selectedStylingBlocksAtom = atom([]);
13 | selectedStylingBlocksAtom.debugLabel = "selectedStylingBlocksAtom";
14 |
15 | /**
16 | * @group Hooks
17 | * @returns {TStyleBlock[]} selected styling blocks
18 | */
19 | export const useSelectedStylingBlocks = () => useAtom(selectedStylingBlocksAtom);
20 |
--------------------------------------------------------------------------------
/src/core/hooks/use-sidebar-active-panel.ts:
--------------------------------------------------------------------------------
1 | import { atom, useAtom } from "jotai";
2 |
3 | export const sidebarActivePanelAtom = atom("outline");
4 | sidebarActivePanelAtom.debugLabel = "sidebarActivePanelAtom";
5 |
6 | export const useSidebarActivePanel = () => {
7 | return useAtom(sidebarActivePanelAtom);
8 | };
9 |
--------------------------------------------------------------------------------
/src/core/hooks/use-styling-breakpoint.ts:
--------------------------------------------------------------------------------
1 | import { styleBreakpointAtom } from "@/core/hooks/use-selected-blockIds";
2 | import { useAtom } from "jotai";
3 |
4 | export const useStylingBreakpoint = () => useAtom(styleBreakpointAtom);
5 |
--------------------------------------------------------------------------------
/src/core/hooks/use-styling-state.ts:
--------------------------------------------------------------------------------
1 | import { styleStateAtom } from "@/core/hooks/use-selected-blockIds";
2 | import { useAtom } from "jotai";
3 |
4 | export const useStylingState = () => useAtom(styleStateAtom);
5 |
--------------------------------------------------------------------------------
/src/core/hooks/use-wrapper-block.ts:
--------------------------------------------------------------------------------
1 | import { presentBlocksAtom } from "@/core/atoms/blocks";
2 | import { selectedBlockIdsAtom } from "@/core/hooks/use-selected-blockIds";
3 | import { ChaiBlock } from "@/types/chai-block";
4 | import { getRegisteredChaiBlock } from "@chaibuilder/runtime";
5 | import { atom, useAtomValue } from "jotai";
6 | import { find } from "lodash-es";
7 |
8 | // * This atom computes the wrapper block for the currently selected block.
9 | // * It iterates through the block's ancestors to find the first block that is marked as a wrapper.
10 | const wrapperBlockAtom = atom((get) => {
11 | const blocks = get(presentBlocksAtom);
12 | const blockIds = get(selectedBlockIdsAtom);
13 |
14 | // If only one block is selected, use its ID; otherwise, return null
15 | const blockId = blockIds.length === 1 ? blockIds[0] : null;
16 | if (!blockId) return null;
17 |
18 | // Find the block with the selected ID
19 | const block = find(blocks, { _id: blockId });
20 | if (!block) return null; // If the block is not found, return null
21 |
22 | // Start from the selected block and move up the parent chain
23 | let parentId = block._parent;
24 | while (parentId) {
25 | const parentBlock = find(blocks, { _id: parentId });
26 | if (!parentBlock) return null; // If the parent block is not found, return null
27 |
28 | // Check if the parent block is a wrapper block
29 | if (getRegisteredChaiBlock(parentBlock._type)?.wrapper) {
30 | return parentBlock; // If it's a wrapper, return it
31 | }
32 |
33 | parentId = parentBlock._parent;
34 | }
35 |
36 | // If no wrapper block is found, return null
37 | return null;
38 | });
39 | wrapperBlockAtom.debugLabel = "wrapperBlockAtom";
40 |
41 | export const useWrapperBlock = () => useAtomValue(wrapperBlockAtom);
42 |
--------------------------------------------------------------------------------
/src/core/import-html/general.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns a boolean indicating whether the current environment is development
3 | * @returns {boolean} A boolean indicating whether the current environment is development
4 | */
5 | export const isDevelopment = () => import.meta.env.DEV;
6 |
--------------------------------------------------------------------------------
/src/core/import-html/import-video.ts:
--------------------------------------------------------------------------------
1 | import { isEmpty } from "lodash-es";
2 |
3 | export const hasVideoEmbed = (html: string): boolean => {
4 | // Regular expressions for YouTube and Vimeo URLs
5 | const youtubeRegex =
6 | /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
7 | const vimeoRegex = /(?:https?:\/\/)?(?:www\.)?(player)?.vimeo\.com/;
8 |
9 | // Check if the HTML contains a YouTube or Vimeo URL
10 | return youtubeRegex.test(html) || vimeoRegex.test(html);
11 | };
12 |
13 | export const getVideoURLFromHTML = (html: string): string => {
14 | if (isEmpty(html)) return html;
15 | // Regular expressions for video or iframe tags
16 | const videoTagRegex = /