├── .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 |
4 | 5 |
6 | 7 | 8 |
9 |
10 | Blog Image 11 |
12 | 13 |
14 |

15 | Studio by Preline 16 |

17 |

18 | Produce professional, reliable streams easily leveraging Preline's innovative broadcast studio 19 |

20 |

21 | Read more 22 | 23 |

24 |
25 |
26 |
27 | 28 |
29 | 30 |
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 | Image 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 |
9 | 10 | Chai Builder 11 | Chai Builder 12 | 13 | 14 | 15 | Chai Builder 16 | 17 |
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 |
  1. 17 | 20 | 21 |
  2. 22 | {reverse(hierarchy).map((block, index) => ( 23 |
  3. 24 | 34 | {index !== hierarchy.length - 1 && } 35 |
  4. 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 | 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 | 39 | 48 | 60 | 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 |
38 |
39 |
40 | 41 |
42 |
43 |
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 |
16 |

Screen: 

17 | 18 |
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 | setOpen(_open)}> 28 | {children} 29 | 30 |
31 | {MediaManagerComponent ? ( 32 | setOpen(false)} onSelect={handleSelect} mode={mode} assetId={assetId} /> 33 | ) : null} 34 |
35 |
36 |
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 |
20 |
21 |
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 = /]+src=['"]([^'">]+)['"]/; 17 | const iframeTagRegex = /]+src=['"]([^'">]+)['"]/; 18 | 19 | // Extract the URL from the video or iframe tag 20 | const videoTagMatch = html.match(videoTagRegex); 21 | const iframeTagMatch = html.match(iframeTagRegex); 22 | 23 | const videoUrl = videoTagMatch ? videoTagMatch[1] : iframeTagMatch ? iframeTagMatch[1] : null; 24 | 25 | // Check if the URL is a YouTube URL or vimeo URL 26 | const youtubeRegex = 27 | /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/; 28 | const vimeoRegex = /(?:https?:\/\/)?(?:www\.)?player.vimeo\.com/; 29 | if (videoUrl && (youtubeRegex.test(videoUrl) || vimeoRegex.test(videoUrl))) { 30 | return videoUrl; 31 | } 32 | 33 | // If it's not a YouTube URL, return the HTML 34 | return html; 35 | }; 36 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | import "@/index.css"; 2 | -------------------------------------------------------------------------------- /src/core/lib.ts: -------------------------------------------------------------------------------- 1 | export { generateUUID, getBreakpointValue } from "@/core/functions/common-functions"; 2 | export { getBlocksFromHTML } from "@/core/import-html/html-to-json"; 3 | export * from "@/render/functions"; 4 | export { getStylesForBlocks } from "@/render/get-tailwind-css"; 5 | -------------------------------------------------------------------------------- /src/core/locales/load.ts: -------------------------------------------------------------------------------- 1 | import lngEn from "@/core/locales/en.json"; 2 | import i18n from "i18next"; 3 | import { initReactI18next } from "react-i18next"; 4 | 5 | i18n 6 | .use(initReactI18next) // passes i18n down to react-i18next 7 | .init({ 8 | // the translations 9 | // (tip move them in a JSON file and import them, 10 | // or even better, manage them via a UI: https://react.i18next.com/guides/multiple-translation-files#manage-your-translations-with-a-management-gui) 11 | resources: { 12 | en: { 13 | translation: lngEn, 14 | }, 15 | }, 16 | lng: "en", // if you're using a language detector, do not define the lng option 17 | fallbackLng: "en", 18 | interpolation: { 19 | escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape 20 | }, 21 | }); 22 | 23 | export default i18n; 24 | -------------------------------------------------------------------------------- /src/core/miscellaneous/index.ts: -------------------------------------------------------------------------------- 1 | import i18n from "@/core/locales/load"; 2 | 3 | export { CHAI_BUILDER_EVENTS } from "@/core/events"; 4 | export { generateUUID as generateBlockId, cn as mergeClasses } from "@/core/functions/common-functions"; 5 | export { usePubSub } from "@/core/hooks/use-pub-sub"; 6 | export { getBlocksFromHTML } from "@/core/import-html/html-to-json"; 7 | export { pubsub } from "@/core/pubsub"; 8 | 9 | export { i18n }; 10 | -------------------------------------------------------------------------------- /src/core/pubsub.ts: -------------------------------------------------------------------------------- 1 | type EventCallback = (data?: T) => void; 2 | 3 | class PubSub { 4 | private subscribers: Map> = new Map(); 5 | 6 | subscribe(eventName: string, callback: EventCallback): () => void { 7 | if (!this.subscribers.has(eventName)) { 8 | this.subscribers.set(eventName, new Set()); 9 | } 10 | 11 | this.subscribers.get(eventName)!.add(callback); 12 | 13 | // Return unsubscribe function 14 | return () => { 15 | const subs = this.subscribers.get(eventName); 16 | if (subs) { 17 | subs.delete(callback); 18 | if (subs.size === 0) { 19 | this.subscribers.delete(eventName); 20 | } 21 | } 22 | }; 23 | } 24 | 25 | publish(eventName: string, data?: T): void { 26 | const subs = this.subscribers.get(eventName); 27 | if (subs) { 28 | subs.forEach((callback) => callback(data)); 29 | } 30 | } 31 | } 32 | 33 | // Create a single instance for global use 34 | export const pubsub = new PubSub(); 35 | -------------------------------------------------------------------------------- /src/core/rjsf-widgets/binding.tsx: -------------------------------------------------------------------------------- 1 | export const BindingWidget = () => { 2 | return ( 3 |
4 | Data binding is set for this field 5 |
6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /src/core/rjsf-widgets/code-widget.tsx: -------------------------------------------------------------------------------- 1 | import { useCodeEditor, useSelectedBlock } from "@/core/hooks"; 2 | import { ChaiBlock } from "@/types/chai-block"; 3 | import { Button } from "@/ui/shadcn/components/ui/button"; 4 | import { WidgetProps } from "@rjsf/utils"; 5 | import { get } from "lodash-es"; 6 | import { useTranslation } from "react-i18next"; 7 | 8 | const CodeEditor = ({ id, placeholder }: WidgetProps) => { 9 | const { t } = useTranslation(); 10 | const [, setCodeEditor] = useCodeEditor(); 11 | const selectedBlock = useSelectedBlock() as ChaiBlock; 12 | if (typeof window === "undefined") return null; 13 | const blockProp = id.replace("root.", ""); 14 | const value = get(selectedBlock, blockProp, ""); 15 | const openCodeEditor = () => { 16 | const blockId = selectedBlock?._id; 17 | 18 | // @ts-ignore 19 | setCodeEditor({ blockId, blockProp, placeholder, initialCode: get(selectedBlock, blockProp, value) }); 20 | }; 21 | 22 | return ( 23 |
24 | 31 | 34 |
35 | ); 36 | }; 37 | 38 | export { CodeEditor }; 39 | -------------------------------------------------------------------------------- /src/core/rjsf-widgets/collection-select.tsx: -------------------------------------------------------------------------------- 1 | import { WidgetProps } from "@rjsf/utils"; 2 | import { find, get } from "lodash-es"; 3 | import { COLLECTION_PREFIX } from "../constants/STRINGS"; 4 | import { useBuilderProp, useSelectedBlock } from "../hooks"; 5 | 6 | const CollectionFilterSortField = ({ id, value, onChange, onBlur }: WidgetProps) => { 7 | const collections = useBuilderProp("collections", []); 8 | const selectedBlock = useSelectedBlock(); 9 | const repeaterItem = get(selectedBlock, "repeaterItems", "") 10 | .replace(/\{\{(.*)\}\}/g, "$1") 11 | .replace(COLLECTION_PREFIX, ""); 12 | const collection = find(collections, { id: repeaterItem }); 13 | 14 | const key = "root.filter" === id ? "filters" : "sorts"; 15 | const options = get(collection, key, []); 16 | 17 | return ( 18 |
19 | 27 |
28 | ); 29 | }; 30 | 31 | export { CollectionFilterSortField }; 32 | -------------------------------------------------------------------------------- /src/core/rjsf-widgets/color.tsx: -------------------------------------------------------------------------------- 1 | import { WidgetProps } from "@rjsf/utils"; 2 | import { debounce } from "lodash-es"; 3 | 4 | const ColorField = ({ value, onChange, id, onBlur }: WidgetProps) => { 5 | const throttledChange = debounce(onChange, 200); 6 | const onChangeCb = (e) => throttledChange(e.target.value); 7 | return ( 8 |
9 |
10 | onBlur(id, url)} 15 | onChange={onChangeCb} 16 | /> 17 |
18 |
19 | ); 20 | }; 21 | export { ColorField }; 22 | -------------------------------------------------------------------------------- /src/core/rjsf-widgets/index.ts: -------------------------------------------------------------------------------- 1 | export * from "@/core/rjsf-widgets/Icon"; 2 | export * from "@/core/rjsf-widgets/image"; 3 | export * from "@/core/rjsf-widgets/link"; 4 | export * from "@/core/rjsf-widgets/row-col"; 5 | export * from "@/core/rjsf-widgets/rte-widget"; 6 | export * from "@/core/rjsf-widgets/slider"; 7 | export * from "@/core/rjsf-widgets/sources"; 8 | -------------------------------------------------------------------------------- /src/core/rjsf-widgets/repeater-binding.tsx: -------------------------------------------------------------------------------- 1 | import { WidgetProps } from "@rjsf/utils"; 2 | import { Database, File, X } from "lucide-react"; 3 | import { Button } from "@/ui/shadcn/components/ui/button"; 4 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/ui/shadcn/components/ui/tooltip"; 5 | import { COLLECTION_PREFIX } from "@/core/constants/STRINGS"; 6 | 7 | export const RepeaterBindingWidget = ({ value, onChange }: WidgetProps) => { 8 | if (!value) { 9 | return ( 10 |
11 | Choose a collection 12 |
13 | ); 14 | } 15 | 16 | const prefixWithBracket = `{{${COLLECTION_PREFIX}`; 17 | const isCollection = value?.startsWith(prefixWithBracket); 18 | let displayValue = value; 19 | if (isCollection) { 20 | displayValue = value?.replace(prefixWithBracket, "")?.replace("}}", ""); 21 | } 22 | 23 | return ( 24 |
25 |
26 | 27 | {" "} 28 | {isCollection ? : null} 29 | {displayValue} 30 | 31 | 32 | 33 | 40 | 41 | Remove binding 42 | 43 |
44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/core/rjsf-widgets/row-col.tsx: -------------------------------------------------------------------------------- 1 | import { useAddBlock, useSelectedBlock, useWrapperBlock } from "@/core/hooks"; 2 | import { Plus } from "lucide-react"; 3 | 4 | const RowColField = () => { 5 | const selectedBlock = useSelectedBlock(); 6 | const wrapperBlock = useWrapperBlock(); 7 | const { addCoreBlock } = useAddBlock(); 8 | 9 | if (!selectedBlock && !wrapperBlock) return null; 10 | 11 | const rowBlock = selectedBlock?._type === "Row" ? selectedBlock : wrapperBlock; 12 | 13 | return ( 14 |
15 | 21 |
22 | ); 23 | }; 24 | 25 | export { RowColField }; 26 | -------------------------------------------------------------------------------- /src/core/screen-too-small.tsx: -------------------------------------------------------------------------------- 1 | export const ScreenTooSmall = () => { 2 | return ( 3 |
4 |
5 |
6 | Chai Builder 11 |
12 |

Screen too small

13 |

14 | Please view this page on greater than 1280px screen width for the 15 | best experience. 16 |

17 |
18 |
19 |
20 | 21 | 27 | 28 | Minimum width: 1280px 29 |
30 |
31 |
32 |
33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/core/utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | /** 5 | * Combines multiple class names and merges Tailwind CSS classes efficiently 6 | */ 7 | export function cn(...inputs: ClassValue[]) { 8 | return twMerge(clsx(inputs)); 9 | } 10 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 0 0% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 0 0% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 0 0% 3.9%; 13 | --primary: 0 0% 9%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 0 0% 96.1%; 16 | --secondary-foreground: 0 0% 9%; 17 | --muted: 0 0% 96.1%; 18 | --muted-foreground: 0 0% 45.1%; 19 | --accent: 0 0% 96.1%; 20 | --accent-foreground: 0 0% 9%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 0 0% 89.8%; 24 | --input: 0 0% 89.8%; 25 | --ring: 0 0% 3.9%; 26 | --chart-1: 12 76% 61%; 27 | --chart-2: 173 58% 39%; 28 | --chart-3: 197 37% 24%; 29 | --chart-4: 43 74% 66%; 30 | --chart-5: 27 87% 67%; 31 | --radius: 0.5rem; 32 | } 33 | .dark { 34 | --background: 0 0% 3.9%; 35 | --foreground: 0 0% 98%; 36 | --card: 0 0% 3.9%; 37 | --card-foreground: 0 0% 98%; 38 | --popover: 0 0% 3.9%; 39 | --popover-foreground: 0 0% 98%; 40 | --primary: 0 0% 98%; 41 | --primary-foreground: 0 0% 9%; 42 | --secondary: 0 0% 14.9%; 43 | --secondary-foreground: 0 0% 98%; 44 | --muted: 0 0% 14.9%; 45 | --muted-foreground: 0 0% 63.9%; 46 | --accent: 0 0% 14.9%; 47 | --accent-foreground: 0 0% 98%; 48 | --destructive: 0 62.8% 30.6%; 49 | --destructive-foreground: 0 0% 98%; 50 | --border: 0 0% 14.9%; 51 | --input: 0 0% 14.9%; 52 | --ring: 0 0% 83.1%; 53 | --chart-1: 220 70% 50%; 54 | --chart-2: 160 60% 45%; 55 | --chart-3: 30 80% 55%; 56 | --chart-4: 280 65% 60%; 57 | --chart-5: 340 75% 55%; 58 | } 59 | } 60 | 61 | @layer base { 62 | * { 63 | @apply border-border; 64 | } 65 | body { 66 | @apply bg-background text-foreground; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { MicrosoftClarity } from "@/_demo/microsoft-clarity"; 2 | import "@/index.css"; 3 | import { DevTools } from "jotai-devtools"; 4 | import "jotai-devtools/styles.css"; 5 | import React, { lazy } from "react"; 6 | import ReactDOM from "react-dom/client"; 7 | import { createBrowserRouter, RouterProvider } from "react-router-dom"; 8 | 9 | async function enableMocking() { 10 | if (import.meta.env.MODE !== "development") { 11 | return; 12 | } 13 | return true; 14 | } 15 | 16 | const ChaiBuilderDefault = lazy(() => import("@/Editor")); 17 | const Preview = lazy(() => import("@/Preview")); 18 | 19 | const router = createBrowserRouter([ 20 | { 21 | path: "/", 22 | element: , 23 | }, 24 | { 25 | path: "/preview", 26 | element: , 27 | }, 28 | ]); 29 | 30 | enableMocking().then(() => { 31 | ReactDOM.createRoot(document.getElementById("root")!).render( 32 | 33 | 34 | 35 | {import.meta.env.VITE_CLARITY_ID && } 36 | , 37 | ); 38 | }); 39 | -------------------------------------------------------------------------------- /src/render/async-props-block.tsx: -------------------------------------------------------------------------------- 1 | import { ChaiBlock, ChaiPageProps } from "@chaibuilder/runtime"; 2 | import { has, isFunction, omit } from "lodash-es"; 3 | import React from "react"; 4 | 5 | type DataProvider = (props: { 6 | draft: boolean; 7 | inBuilder: boolean; 8 | lang: string; 9 | block: ChaiBlock; 10 | pageProps: ChaiPageProps; 11 | }) => Record | Promise>; 12 | 13 | export default async function DataProviderPropsBlock(props: { 14 | lang: string; 15 | pageProps: ChaiPageProps; 16 | block: ChaiBlock; 17 | dataProvider: DataProvider; 18 | dataProviderMetadataCallback?: (block: ChaiBlock, meta: Record) => void; 19 | draft: boolean; 20 | children: (dataProviderProps: Record) => React.ReactNode; 21 | }) { 22 | const dataProps = await props.dataProvider({ 23 | pageProps: props.pageProps, 24 | block: props.block, 25 | lang: props.lang, 26 | draft: props.draft, 27 | inBuilder: false, 28 | }); 29 | 30 | if (has(dataProps, "$metadata") && isFunction(props.dataProviderMetadataCallback)) { 31 | props.dataProviderMetadataCallback(props.block, dataProps.$metadata); 32 | } 33 | 34 | return props.children({ 35 | ...omit(dataProps, "$metadata"), 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/render/blocks-renderer.tsx: -------------------------------------------------------------------------------- 1 | import { filter, get, has, isArray, isEmpty, map, uniqBy } from "lodash-es"; 2 | import { RenderBlock } from "./block-renderer"; 3 | import { RenderChaiBlocksProps } from "./render-chai-blocks"; 4 | 5 | export const RenderBlocks = (props: RenderChaiBlocksProps & { repeaterData?: { index: number; dataKey: string } }) => { 6 | const { blocks, parent, repeaterData } = props; 7 | let filteredBlocks = uniqBy( 8 | filter(blocks, (block) => has(block, "_id") && (!isEmpty(parent) ? block._parent === parent : !block._parent)), 9 | "_id", 10 | ); 11 | const hasChildren = (blockId: string) => filter(blocks, (b) => b._parent === blockId).length > 0; 12 | 13 | return map(filteredBlocks, (block) => { 14 | if (!block) return null; 15 | return ( 16 | 17 | {({ _id, _type, repeaterItems, $repeaterItemsKey }) => { 18 | return _type === "Repeater" ? ( 19 | isArray(repeaterItems) && 20 | repeaterItems.map((_, index) => ( 21 | 27 | )) 28 | ) : hasChildren(_id) ? ( 29 | 35 | ) : null; 36 | }} 37 | 38 | ); 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /src/render/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | getChaiThemeCssVariables, 3 | getThemeFontsCSSImport, 4 | getThemeFontsLinkMarkup, 5 | } from "@/core/components/canvas/static/chai-theme-helpers"; 6 | export { applyChaiDataBinding } from "@/core/components/canvas/static/new-blocks-render-helpers"; 7 | export { convertToBlocks, getMergedPartialBlocks } from "@/render/functions"; 8 | export { getStylesForBlocks } from "@/render/get-tailwind-css"; 9 | export { RenderChaiBlocks } from "@/render/render-chai-blocks"; 10 | -------------------------------------------------------------------------------- /src/runtime.ts: -------------------------------------------------------------------------------- 1 | export * from "@chaibuilder/runtime"; 2 | -------------------------------------------------------------------------------- /src/tailwind/get-chai-builder-theme.ts: -------------------------------------------------------------------------------- 1 | import { getChaiThemeOptions } from "@/core/components/canvas/static/chai-theme-helpers"; 2 | import { defaultThemeOptions } from "@/core/hooks/default-theme-options"; 3 | import { ChaiBuilderThemeOptions } from "@/types/chaibuilder-editor-props"; 4 | 5 | export const getChaiBuilderTheme = (themeOptions: ChaiBuilderThemeOptions = defaultThemeOptions) => { 6 | return { 7 | container: { 8 | center: true, 9 | padding: "1rem", 10 | screens: { "2xl": "1400px" }, 11 | }, 12 | ...getChaiThemeOptions(themeOptions), 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/tailwind/index.ts: -------------------------------------------------------------------------------- 1 | import chaiBuilderPlugin from "@/tailwind/plugin"; 2 | 3 | export { getChaiBuilderTailwindConfig } from "@/tailwind/get-chai-builder-tailwind-config"; 4 | export { getChaiBuilderTheme } from "@/tailwind/get-chai-builder-theme"; 5 | 6 | export { chaiBuilderPlugin }; 7 | -------------------------------------------------------------------------------- /src/tailwind/plugin.ts: -------------------------------------------------------------------------------- 1 | import plugin from "tailwindcss/plugin"; 2 | 3 | /** 4 | * This is the tailwind plugin for chai builder 5 | * @param {*} theme 6 | * @returns typeof plugin 7 | */ 8 | export default plugin(function ({ addBase, theme }) { 9 | addBase({ 10 | "h1,h2,h3,h4,h5,h6": { 11 | fontFamily: theme("fontFamily.heading"), 12 | }, 13 | body: { 14 | fontFamily: theme("fontFamily.body"), 15 | color: theme("colors.foreground"), 16 | backgroundColor: theme("colors.background"), 17 | }, 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/types/chai-block.ts: -------------------------------------------------------------------------------- 1 | type ChaiBlock> = { 2 | _id: string; 3 | _name?: string; 4 | _parent?: string | null | undefined; 5 | _bindings?: Record; 6 | _type: string; 7 | _libBlock?: string; 8 | } & T; 9 | 10 | export type { ChaiBlock }; 11 | -------------------------------------------------------------------------------- /src/types/collections.ts: -------------------------------------------------------------------------------- 1 | type FilterOptions = { 2 | id: string; 3 | name: string; 4 | description?: string; 5 | }; 6 | 7 | type SortOptions = { 8 | id: string; 9 | name: string; 10 | description?: string; 11 | }; 12 | 13 | export type ChaiCollectoin = { 14 | id: string; 15 | name: string; 16 | description?: string; 17 | filters?: FilterOptions[]; 18 | sorts?: SortOptions[]; 19 | }; 20 | -------------------------------------------------------------------------------- /src/types/common.ts: -------------------------------------------------------------------------------- 1 | type ChaiBlock> = { 2 | _id: string; 3 | _name?: string; 4 | _parent?: string | null | undefined; 5 | _bindings?: Record; 6 | _libBlock?: string; 7 | _type: string; 8 | _partialBlockId?: string; 9 | } & T; 10 | 11 | export type { ChaiBlock }; 12 | -------------------------------------------------------------------------------- /src/types/core-block.ts: -------------------------------------------------------------------------------- 1 | import { ChaiBlock } from "@/types/chai-block"; 2 | 3 | export interface CoreBlock { 4 | blocks?: ChaiBlock[]; 5 | data: any; 6 | props: { [key: string]: any }; 7 | type: string; 8 | _name?: string; 9 | partialBlockId?: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { ChaiBuilderEditorProps } from "@/types/chaibuilder-editor-props"; 2 | import { ChaiPageProps } from "@chaibuilder/runtime"; 3 | 4 | export type { ChaiBuilderEditorProps }; 5 | 6 | export type ChaiPage = { 7 | slug: string; 8 | uuid?: string; 9 | name?: string; 10 | }; 11 | 12 | export type ChaiAsset = { 13 | url: string; 14 | id?: string; 15 | thumbnailUrl?: string; 16 | description?: string; 17 | width?: number; 18 | height?: number; 19 | }; 20 | 21 | export type { ChaiPageProps }; 22 | -------------------------------------------------------------------------------- /src/types/types.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export * from "@/types/chai-block"; 4 | 5 | export type ChaiRenderBlockProps = { 6 | blockProps: Record; 7 | children?: React.ReactNode; 8 | inBuilder: boolean; 9 | } & T; 10 | 11 | export type ChaiBlockStyles = Record; 12 | 13 | export type { ChaiThemeValues as ChaiBuilderThemeValues, SavePageData } from "@/types/chaibuilder-editor-props"; 14 | -------------------------------------------------------------------------------- /src/ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from "@/ui/shadcn/components/ui/accordion"; 2 | export * from "@/ui/shadcn/components/ui/alert"; 3 | export * from "@/ui/shadcn/components/ui/alert-dialog"; 4 | export * from "@/ui/shadcn/components/ui/avatar"; 5 | export * from "@/ui/shadcn/components/ui/badge"; 6 | export * from "@/ui/shadcn/components/ui/button"; 7 | export * from "@/ui/shadcn/components/ui/card"; 8 | export * from "@/ui/shadcn/components/ui/command"; 9 | export * from "@/ui/shadcn/components/ui/context-menu"; 10 | export * from "@/ui/shadcn/components/ui/dialog"; 11 | export * from "@/ui/shadcn/components/ui/dropdown-menu"; 12 | export * from "@/ui/shadcn/components/ui/hover-card"; 13 | export * from "@/ui/shadcn/components/ui/input"; 14 | export * from "@/ui/shadcn/components/ui/label"; 15 | export * from "@/ui/shadcn/components/ui/popover"; 16 | export * from "@/ui/shadcn/components/ui/scroll-area"; 17 | export * from "@/ui/shadcn/components/ui/select"; 18 | export * from "@/ui/shadcn/components/ui/separator"; 19 | export * from "@/ui/shadcn/components/ui/sheet"; 20 | export * from "@/ui/shadcn/components/ui/skeleton"; 21 | export * from "@/ui/shadcn/components/ui/slider"; 22 | export * from "@/ui/shadcn/components/ui/sooner"; 23 | export * from "@/ui/shadcn/components/ui/switch"; 24 | export * from "@/ui/shadcn/components/ui/tabs"; 25 | export * from "@/ui/shadcn/components/ui/textarea"; 26 | export * from "@/ui/shadcn/components/ui/toggle"; 27 | export * from "@/ui/shadcn/components/ui/tooltip"; 28 | -------------------------------------------------------------------------------- /src/ui/shadcn/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from "class-variance-authority"; 2 | import * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 13 | }, 14 | }, 15 | defaultVariants: { 16 | variant: "default", 17 | }, 18 | }, 19 | ); 20 | 21 | const Alert = React.forwardRef< 22 | HTMLDivElement, 23 | React.HTMLAttributes & VariantProps 24 | >(({ className, variant, ...props }, ref) => ( 25 |
26 | )); 27 | Alert.displayName = "Alert"; 28 | 29 | const AlertTitle = React.forwardRef>( 30 | ({ className, ...props }, ref) => ( 31 |
32 | ), 33 | ); 34 | AlertTitle.displayName = "AlertTitle"; 35 | 36 | const AlertDescription = React.forwardRef>( 37 | ({ className, ...props }, ref) => ( 38 |
39 | ), 40 | ); 41 | AlertDescription.displayName = "AlertDescription"; 42 | 43 | export { Alert, AlertDescription, AlertTitle }; 44 | -------------------------------------------------------------------------------- /src/ui/shadcn/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Avatar = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | )) 19 | Avatar.displayName = AvatarPrimitive.Root.displayName 20 | 21 | const AvatarImage = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => ( 25 | 30 | )) 31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 32 | 33 | const AvatarFallback = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, ...props }, ref) => ( 37 | 45 | )) 46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 47 | 48 | export { Avatar, AvatarImage, AvatarFallback } 49 | -------------------------------------------------------------------------------- /src/ui/shadcn/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /src/ui/shadcn/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = React.forwardRef>(({ className, ...props }, ref) => ( 6 |
7 | )); 8 | Card.displayName = "Card"; 9 | 10 | const CardHeader = React.forwardRef>( 11 | ({ className, ...props }, ref) => ( 12 |
13 | ), 14 | ); 15 | CardHeader.displayName = "CardHeader"; 16 | 17 | const CardTitle = React.forwardRef>( 18 | ({ className, ...props }, ref) => ( 19 |
20 | ), 21 | ); 22 | CardTitle.displayName = "CardTitle"; 23 | 24 | const CardDescription = React.forwardRef>( 25 | ({ className, ...props }, ref) => ( 26 |
27 | ), 28 | ); 29 | CardDescription.displayName = "CardDescription"; 30 | 31 | const CardContent = React.forwardRef>( 32 | ({ className, ...props }, ref) =>
, 33 | ); 34 | CardContent.displayName = "CardContent"; 35 | 36 | const CardFooter = React.forwardRef>( 37 | ({ className, ...props }, ref) => ( 38 |
39 | ), 40 | ); 41 | CardFooter.displayName = "CardFooter"; 42 | 43 | export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }; 44 | -------------------------------------------------------------------------------- /src/ui/shadcn/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 2 | import { Check } from "lucide-react"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const Checkbox = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 18 | 19 | 20 | 21 | 22 | )); 23 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 24 | 25 | export { Checkbox }; 26 | -------------------------------------------------------------------------------- /src/ui/shadcn/components/ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"; 2 | import * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const HoverCard = HoverCardPrimitive.Root; 7 | 8 | const HoverCardTrigger = HoverCardPrimitive.Trigger; 9 | 10 | const HoverCardContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 14 | 24 | )); 25 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName; 26 | 27 | export { HoverCard, HoverCardContent, HoverCardTrigger }; 28 | -------------------------------------------------------------------------------- /src/ui/shadcn/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ); 18 | }, 19 | ); 20 | Input.displayName = "Input"; 21 | 22 | export { Input }; 23 | -------------------------------------------------------------------------------- /src/ui/shadcn/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /src/ui/shadcn/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 2 | import * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Popover = PopoverPrimitive.Root; 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger; 9 | 10 | const PopoverAnchor = PopoverPrimitive.Anchor; 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )); 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 30 | 31 | export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }; 32 | -------------------------------------------------------------------------------- /src/ui/shadcn/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 2 | import * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const ScrollArea = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, children, ...props }, ref) => ( 10 | 11 | {children} 12 | 13 | 14 | 15 | )); 16 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 17 | 18 | const ScrollBar = React.forwardRef< 19 | React.ElementRef, 20 | React.ComponentPropsWithoutRef 21 | >(({ className, orientation = "vertical", ...props }, ref) => ( 22 | 32 | 33 | 34 | )); 35 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 36 | 37 | export { ScrollArea, ScrollBar }; 38 | -------------------------------------------------------------------------------- /src/ui/shadcn/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 2 | import * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => ( 10 | 17 | )); 18 | Separator.displayName = SeparatorPrimitive.Root.displayName; 19 | 20 | export { Separator }; 21 | -------------------------------------------------------------------------------- /src/ui/shadcn/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /src/ui/shadcn/components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SliderPrimitive from "@radix-ui/react-slider" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Slider = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 19 | 20 | 21 | 22 | 23 | )) 24 | Slider.displayName = SliderPrimitive.Root.displayName 25 | 26 | export { Slider } 27 | -------------------------------------------------------------------------------- /src/ui/shadcn/components/ui/sooner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | import { Toaster as Sonner } from "sonner"; 5 | 6 | type ToasterProps = React.ComponentProps; 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme(); 10 | 11 | return ( 12 | 26 | ); 27 | }; 28 | 29 | export { Toaster }; 30 | -------------------------------------------------------------------------------- /src/ui/shadcn/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 2 | import * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Switch = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 17 | 22 | 23 | )); 24 | Switch.displayName = SwitchPrimitives.Root.displayName; 25 | 26 | export { Switch }; 27 | -------------------------------------------------------------------------------- /src/ui/shadcn/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Textarea = React.forwardRef< 6 | HTMLTextAreaElement, 7 | React.ComponentProps<"textarea"> 8 | >(({ className, ...props }, ref) => { 9 | return ( 10 |