├── .changeset ├── README.md └── config.json ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── CONTRIBUTING.md ├── License ├── README.md ├── apps └── web │ ├── CHANGELOG.md │ ├── components.json │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── favicon.ico │ ├── images │ │ ├── builder.png │ │ ├── index.png │ │ └── templates.png │ ├── logo.svg │ ├── next.svg │ ├── templates │ │ ├── customer-feedback.png │ │ ├── job-application.png │ │ └── product-registration.png │ ├── thirteen.svg │ └── vercel.svg │ ├── src │ ├── app │ │ ├── IndexPage.tsx │ │ ├── NewVersionDialog.tsx │ │ ├── builder │ │ │ ├── BuilderPage.tsx │ │ │ ├── _components │ │ │ │ ├── AddField │ │ │ │ │ ├── AddFieldAccordion.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── Code │ │ │ │ │ ├── Code.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── CustomSensor.ts │ │ │ │ ├── FieldSettings │ │ │ │ │ └── index.tsx │ │ │ │ ├── FormList │ │ │ │ │ ├── FormList.tsx │ │ │ │ │ ├── ImportExport.tsx │ │ │ │ │ ├── ImportFormModal.tsx │ │ │ │ │ ├── NewFormModal.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── FormSettings │ │ │ │ │ ├── FrameworkCombobox.tsx │ │ │ │ │ ├── SettingsToggle.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── Preview │ │ │ │ │ ├── Preview.tsx │ │ │ │ │ ├── RenderField.tsx │ │ │ │ │ ├── component-variants │ │ │ │ │ │ ├── boolean │ │ │ │ │ │ │ ├── checkbox.tsx │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── switch.tsx │ │ │ │ │ │ ├── date │ │ │ │ │ │ │ ├── date.tsx │ │ │ │ │ │ │ ├── daterange.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── enum │ │ │ │ │ │ │ ├── button.tsx │ │ │ │ │ │ │ ├── combobox.tsx │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ ├── radio.tsx │ │ │ │ │ │ │ └── select.tsx │ │ │ │ │ │ ├── file │ │ │ │ │ │ │ ├── file.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── heading │ │ │ │ │ │ │ ├── Heading.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── number │ │ │ │ │ │ │ ├── dual-slider.tsx │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ ├── number.tsx │ │ │ │ │ │ │ ├── phone-number.tsx │ │ │ │ │ │ │ └── slider.tsx │ │ │ │ │ │ └── text │ │ │ │ │ │ │ ├── autoresize-textarea.tsx │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ ├── input-otp.tsx │ │ │ │ │ │ │ ├── input-tag.tsx │ │ │ │ │ │ │ ├── input.tsx │ │ │ │ │ │ │ ├── password-strength-indicator.tsx │ │ │ │ │ │ │ ├── password.tsx │ │ │ │ │ │ │ └── textarea.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── SettingInputs.tsx │ │ │ │ ├── SortableGrid.tsx │ │ │ │ └── SortableItem │ │ │ │ │ ├── AddNewFieldArrows.tsx │ │ │ │ │ ├── FormFieldContent.tsx │ │ │ │ │ ├── SortableItem.tsx │ │ │ │ │ └── index.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── templates │ │ │ ├── TemplatesPage.tsx │ │ │ ├── page.tsx │ │ │ └── templates.ts │ ├── components │ │ ├── Logo.tsx │ │ ├── code-block-command.tsx │ │ ├── code-highlight.tsx │ │ ├── icons.tsx │ │ ├── main-nav.tsx │ │ ├── shared │ │ │ ├── CopyTextBtn.tsx │ │ │ ├── FormName.tsx │ │ │ ├── FrameworkCombobox.tsx │ │ │ ├── TemplateCard.tsx │ │ │ └── Toaster.tsx │ │ ├── site-header.tsx │ │ ├── tailwind-indicator.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ └── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert.tsx │ │ │ ├── autosize-textarea.tsx │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── datetime-picker.tsx │ │ │ ├── dialog.tsx │ │ │ ├── dual-range-slider.tsx │ │ │ ├── form.tsx │ │ │ ├── heading-with-anchor.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── phone-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── tooltip.tsx │ │ │ └── use-toast.ts │ ├── config │ │ └── site.ts │ ├── constants │ │ └── index.ts │ ├── hooks │ │ └── initializeAppState.ts │ ├── lib │ │ ├── fonts.ts │ │ └── utils.ts │ ├── mock │ │ ├── mockFields.ts │ │ └── mockForm.ts │ ├── state │ │ └── state.ts │ ├── styles │ │ └── globals.css │ ├── types │ │ └── nav.ts │ └── utils │ │ ├── checkDuplicates.ts │ │ └── findFieldIndex.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── wrangler.toml ├── biome.json ├── builder.png ├── bun.lockb ├── package.json └── packages ├── cli ├── README.md ├── index.ts ├── package.json └── tsconfig.json └── core ├── CHANGELOG.md ├── README.md ├── package.json ├── src ├── codegen │ ├── imports │ │ ├── generateImports.ts │ │ ├── next │ │ │ ├── boolean.ts │ │ │ ├── date.ts │ │ │ ├── enum.ts │ │ │ ├── heading.ts │ │ │ ├── initialImports.ts │ │ │ ├── number.ts │ │ │ └── text.ts │ │ ├── svelte │ │ │ ├── boolean.ts │ │ │ ├── date.ts │ │ │ ├── enum.ts │ │ │ ├── heading.ts │ │ │ ├── initialImports.ts │ │ │ ├── number.ts │ │ │ └── text.ts │ │ └── vue │ │ │ ├── boolean.ts │ │ │ ├── date.ts │ │ │ ├── enum.ts │ │ │ ├── heading.ts │ │ │ ├── initialImports.ts │ │ │ ├── number.ts │ │ │ └── text.ts │ ├── index.ts │ ├── templates │ │ ├── next │ │ │ ├── boolean │ │ │ │ ├── checkbox.ts │ │ │ │ ├── index.ts │ │ │ │ └── switch.ts │ │ │ ├── date │ │ │ │ ├── date.ts │ │ │ │ ├── daterange.ts │ │ │ │ └── index.ts │ │ │ ├── enum │ │ │ │ ├── button.ts │ │ │ │ ├── combobox.ts │ │ │ │ ├── index.ts │ │ │ │ ├── radio.ts │ │ │ │ └── select.ts │ │ │ ├── heading │ │ │ │ ├── heading-with-anchor.tsx │ │ │ │ ├── heading-without-anchor.tsx │ │ │ │ └── index.ts │ │ │ ├── main.ts │ │ │ ├── number │ │ │ │ ├── dual-slider.ts │ │ │ │ ├── index.ts │ │ │ │ ├── number.ts │ │ │ │ ├── phone-number.ts │ │ │ │ └── slider.ts │ │ │ └── text │ │ │ │ ├── autoresize-textarea.ts │ │ │ │ ├── index.ts │ │ │ │ ├── input-otp.ts │ │ │ │ ├── input-tag.ts │ │ │ │ ├── input.ts │ │ │ │ ├── password-strength-indicator.ts │ │ │ │ ├── password.ts │ │ │ │ └── textarea.ts │ │ ├── svelte │ │ │ ├── boolean │ │ │ │ ├── checkbox.ts │ │ │ │ ├── index.ts │ │ │ │ └── switch.ts │ │ │ ├── date │ │ │ │ ├── date.ts │ │ │ │ ├── daterange.ts │ │ │ │ └── index.ts │ │ │ ├── enum │ │ │ │ ├── button.ts │ │ │ │ ├── combobox.ts │ │ │ │ ├── index.ts │ │ │ │ ├── radio.ts │ │ │ │ └── select.ts │ │ │ ├── heading │ │ │ │ ├── heading-with-anchor.ts │ │ │ │ ├── heading-without-anchor.ts │ │ │ │ └── index.ts │ │ │ ├── main.ts │ │ │ ├── number │ │ │ │ ├── dual-slider.ts │ │ │ │ ├── index.ts │ │ │ │ ├── number.ts │ │ │ │ ├── phone-number.ts │ │ │ │ └── slider.ts │ │ │ └── text │ │ │ │ ├── index.ts │ │ │ │ ├── input-otp.ts │ │ │ │ ├── input.ts │ │ │ │ ├── password.ts │ │ │ │ └── textarea.ts │ │ └── vue │ │ │ ├── boolean │ │ │ ├── checkbox.ts │ │ │ ├── index.ts │ │ │ └── switch.ts │ │ │ ├── date │ │ │ ├── date.ts │ │ │ ├── daterange.ts │ │ │ └── index.ts │ │ │ ├── enum │ │ │ ├── button.ts │ │ │ ├── combobox.ts │ │ │ ├── index.ts │ │ │ ├── radio.ts │ │ │ └── select.ts │ │ │ ├── heading │ │ │ └── index.ts │ │ │ ├── main.ts │ │ │ ├── number │ │ │ ├── index.ts │ │ │ ├── number.ts │ │ │ ├── phone-number.ts │ │ │ └── slider.ts │ │ │ └── text │ │ │ ├── index.ts │ │ │ ├── input-otp.ts │ │ │ ├── input-tag.ts │ │ │ ├── input.ts │ │ │ ├── password-strength-indicator.ts │ │ │ └── textarea.ts │ └── utils.ts ├── components │ ├── components.ts │ ├── index.ts │ ├── next-components.ts │ ├── svelte-components.ts │ └── vue-components.ts ├── index.ts ├── mock │ └── mockFields.ts ├── types │ ├── field.ts │ ├── fieldVariants │ │ ├── fieldVariants.ts │ │ ├── index.ts │ │ ├── nextVariant.ts │ │ ├── svelteVariant.ts │ │ └── vueVariant.ts │ ├── index.ts │ └── prettify.ts └── utils │ ├── checkDuplicates.ts │ ├── getRequiredComponents.ts │ ├── index.ts │ ├── newField.ts │ └── randID.ts ├── test ├── codegen.test.ts ├── forms │ └── form1.ts └── imports.test.ts └── tsconfig.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "privatePackages": { 10 | "version": true, 11 | "tag": true 12 | }, 13 | "updateInternalDependencies": "patch", 14 | "ignore": [] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup bun 19 | uses: oven-sh/setup-bun@v1 20 | 21 | - name: Install Dependencies 22 | run: bun install 23 | 24 | - name: Run tests 25 | run: bun test 26 | 27 | - name: Create Release Pull Request 28 | uses: changesets/action@v1 29 | with: 30 | publish: bun run release 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 33 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | .vercel 16 | 17 | # test dir 18 | /apps/web/src/app/test 19 | /apps/web/src/app/test2 20 | 21 | # misc 22 | .DS_Store 23 | *.pem 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | .pnpm-debug.log* 30 | 31 | # local env files 32 | .env.local 33 | .env.development.local 34 | .env.test.local 35 | .env.production.local 36 | 37 | # turbo 38 | .turbo 39 | 40 | .contentlayer 41 | .env -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "biomejs.biome", 4 | "tailwindcss.tailwind-css", 5 | "YoavBls.pretty-ts-errors" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "biomejs.biome", 3 | "editor.formatOnSave": true, 4 | "[typescriptreact]": { 5 | "editor.defaultFormatter": "biomejs.biome" 6 | }, 7 | "[css]": { 8 | "editor.defaultFormatter": "vscode.css-language-features" 9 | }, 10 | "workbench.editor.enablePreview": false, 11 | "workbench.editor.wrapTabs": true, 12 | "workbench.editor.tabSizing": "fit", 13 | "[typescript]": { 14 | "editor.defaultFormatter": "vscode.typescript-language-features" 15 | }, 16 | "editor.wordWrap": "on" 17 | } 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Form Builder 2 | 3 | Thank you for considering contributing to FormBuilder! We appreciate your help in making this project better. 4 | 5 | *Note: This contribution guide may need improving, and we welcome any suggestions for enhancements.* 6 | 7 | ## Development workflow 8 | 9 | We use [bun](https://bun.sh) as our package manager, but you can use any other package manager of your choice. Make sure to [install](https://bun.sh/docs/installation) it first if you choose to use bun. 10 | 11 | ```bash 12 | git clone https://github.com/kryptxbsa/FormBuilder.git 13 | cd FormBuilder 14 | bun install 15 | ``` 16 | After installation, you can start the development server: 17 | 18 | ```bash 19 | bun dev-web 20 | ``` 21 | 22 | ## Project overview 23 | 24 | This project is a monorepo. There is one main package located in the `packages/core` directory. 25 | 26 | The `apps/web` directory contains the frontend application. 27 | 28 | ### core package `packages/core` 29 | 30 | The `src/types` directory includes the primary types and component variants. 31 | 32 | The `src/components` directory contains code for component variant definitions. 33 | 34 | The `src/codegen` directory contains the main code for code generation, utilizing Handlebars. All logic for code generation and code templates are located there. 35 | 36 | ### Adding a New Component (Next.js) 37 | 38 | Let's say we wanted to add a new component in Next.js, follow these steps: 39 | 40 | 1. **Add the Variant**: 41 | - Update the variant in `src/types/nextVariant.ts`. 42 | 43 | 2. **Add the Component Code and Imports**: 44 | - Include the component code and necessary imports in `src/codegen/imports` and `src/codegen/templates`. 45 | 46 | 3. **Add the Component Config**: 47 | - Configure the component in `src/components/next-components`. 48 | 49 | 50 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Aland Sleman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Form Builder for @shadcn/ui 2 | 3 | UI-based code generation tool to easily create @shadcn/ui forms (Next.js, Vue, Svelte). 4 | 5 | Try it out [here](https://formbuilder.kryptxbsa.com). 6 | 7 | ![FormBuilder demo](./builder.png) 8 | 9 | ## Table of Contents 10 | - [Field Types](#field-types) 11 | - [Installation & Usage](#installation-usage) 12 | - [Contributing](#contributing) 13 | - [License](#license) 14 | 15 | ## Field Types 16 | 17 | Currently, these field types are implemented: 18 | 19 | - **Heading** 20 | - **Text** (Input, Textarea, Password) 21 | - **Number** (Input, Slider) 22 | - **Boolean** (Checkbox, Switch) 23 | - **Enum** (Select, Radio, Combobox) 24 | - **Date** (Date picker) 25 | 26 | More field types per framework. 27 | 28 | ## Installation & Usage 29 | 30 | To install the Form Builder and run locally, clone the repository and install the dependencies: 31 | 32 | ```bash 33 | git clone https://github.com/kryptxbsa/FormBuilder.git 34 | cd FormBuilder 35 | bun install 36 | ``` 37 | 38 | After installation, you can start the development server: 39 | 40 | ```bash 41 | bun dev-web 42 | ``` 43 | 44 | Visit `http://localhost:7017` to see the application. 45 | 46 | ## Contributing 47 | 48 | Contributions are welcome! Please open an issue or submit a pull request. For more details, check the [CONTRIBUTING.md](CONTRIBUTING.md). 49 | 50 | ## License 51 | 52 | This project is licensed under the MIT License. 53 | -------------------------------------------------------------------------------- /apps/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "tailwind": { 5 | "config": "tailwind.config.js", 6 | "css": "src/app/globals.css", 7 | "baseColor": "slate", 8 | "cssVariables": true 9 | }, 10 | "rsc": false, 11 | "aliases": { 12 | "utils": "@/lib/utils", 13 | "components": "@/components" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/web/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { setupDevPlatform } from '@cloudflare/next-on-pages/next-dev'; 2 | 3 | // if (process.env.NODE_ENV === 'development') { 4 | // await setupDevPlatform(); 5 | // } 6 | 7 | /** @type {import('next').NextConfig} */ 8 | const nextConfig = { 9 | reactStrictMode: true, 10 | typescript: { 11 | ignoreBuildErrors: true, 12 | }, 13 | }; 14 | 15 | export default nextConfig; 16 | -------------------------------------------------------------------------------- /apps/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/FormBuilder/7b3a243f4b8078ce4f2044fb7e1c9332baf21248/apps/web/public/favicon.ico -------------------------------------------------------------------------------- /apps/web/public/images/builder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/FormBuilder/7b3a243f4b8078ce4f2044fb7e1c9332baf21248/apps/web/public/images/builder.png -------------------------------------------------------------------------------- /apps/web/public/images/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/FormBuilder/7b3a243f4b8078ce4f2044fb7e1c9332baf21248/apps/web/public/images/index.png -------------------------------------------------------------------------------- /apps/web/public/images/templates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/FormBuilder/7b3a243f4b8078ce4f2044fb7e1c9332baf21248/apps/web/public/images/templates.png -------------------------------------------------------------------------------- /apps/web/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/public/templates/customer-feedback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/FormBuilder/7b3a243f4b8078ce4f2044fb7e1c9332baf21248/apps/web/public/templates/customer-feedback.png -------------------------------------------------------------------------------- /apps/web/public/templates/job-application.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/FormBuilder/7b3a243f4b8078ce4f2044fb7e1c9332baf21248/apps/web/public/templates/job-application.png -------------------------------------------------------------------------------- /apps/web/public/templates/product-registration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/FormBuilder/7b3a243f4b8078ce4f2044fb7e1c9332baf21248/apps/web/public/templates/product-registration.png -------------------------------------------------------------------------------- /apps/web/public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/src/app/NewVersionDialog.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogFooter, 7 | DialogHeader, 8 | DialogTitle, 9 | } from "@/components/ui/dialog"; 10 | 11 | export default function NewVersionDialog({ 12 | open, 13 | setOpen, 14 | }: { 15 | open: boolean; 16 | setOpen: (v: boolean) => void; 17 | }) { 18 | return ( 19 | 20 | 21 | 22 | New Version Coming Soon 23 | 24 | 25 | Version 1 of FormBuilder is on its way! Stay tuned for exciting new 26 | features and improvements. 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/AddField/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import * as React from "react"; 3 | import { Accordion } from "@/components/ui/accordion"; 4 | import type { FormFramework, FrameworkFieldKinds } from "formbuilder-core"; 5 | import { AddFieldAccordion } from "./AddFieldAccordion"; 6 | 7 | export function AddField({ 8 | fields, 9 | }: { 10 | fields: { 11 | label: string; 12 | kind: FrameworkFieldKinds[F]; 13 | }[]; 14 | }) { 15 | return ( 16 |
17 |

18 | Add Field 19 |

20 | 21 |
22 | {fields.map((f) => ( 23 | 24 | ))} 25 |
26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Code/index.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "./Code"; 2 | 3 | export { Code }; 4 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/CustomSensor.ts: -------------------------------------------------------------------------------- 1 | import type { MouseEvent, KeyboardEvent } from "react"; 2 | import { 3 | MouseSensor as LibMouseSensor, 4 | KeyboardSensor as LibKeyboardSensor, 5 | } from "@dnd-kit/core"; 6 | 7 | export class MouseSensor extends LibMouseSensor { 8 | static activators = [ 9 | { 10 | eventName: "onMouseDown" as const, 11 | handler: ({ nativeEvent: event }: MouseEvent) => { 12 | return shouldHandleEvent(event.target as HTMLElement); 13 | }, 14 | }, 15 | ]; 16 | } 17 | 18 | // export class KeyboardSensor extends LibKeyboardSensor { 19 | // static activators = [ 20 | // { 21 | // eventName: 'onKeyDown' as const, 22 | // handler: ({ nativeEvent: event }: KeyboardEvent) => { 23 | // return shouldHandleEvent(event.target as HTMLElement) 24 | // } 25 | // } 26 | // ] 27 | // } 28 | 29 | function shouldHandleEvent(element: HTMLElement | null) { 30 | let cur = element; 31 | 32 | while (cur) { 33 | if (cur.dataset?.noDnd) { 34 | return false; 35 | } 36 | cur = cur.parentElement; 37 | } 38 | 39 | return true; 40 | } 41 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/FormList/FormList.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { useAppState } from "@/state/state"; 4 | import { FiPlus, FiTrash, FiFile } from "react-icons/fi"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | import { NewFormModal } from "./NewFormModal"; 8 | import { Icons } from "@/components/icons"; 9 | import { ImportExport } from "./ImportExport"; 10 | 11 | export function FormList() { 12 | const { forms, selectForm, deleteForm, selectedForm } = useAppState(); 13 | 14 | return ( 15 |
16 |
17 |

18 | My Forms 19 |

20 |

21 | {forms?.length || 0} forms 22 |

23 |
24 | 25 |
    26 | {forms?.map((f, idx) => { 27 | const isSelected = idx === selectedForm; 28 | return ( 29 |
  • 30 | 52 | 62 |
  • 63 | ); 64 | })} 65 |
66 | 67 |
68 | 69 | 70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/FormList/ImportExport.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { useAppState } from "@/state/state"; 3 | import { Upload, Download } from "lucide-react"; 4 | import { ImportFormModal } from "./ImportFormModal"; 5 | 6 | export function ImportExport() { 7 | const state = useAppState(); 8 | 9 | function handleExport() { 10 | const jsonString = JSON.stringify(state.forms, null, 2); 11 | const blob = new Blob([jsonString], { type: "application/json" }); 12 | const url = URL.createObjectURL(blob); 13 | const a = document.createElement("a"); 14 | a.href = url; 15 | a.download = `FormBuilder-forms-${new Date().toISOString().split("T")[0]}.json`; 16 | document.body.appendChild(a); 17 | a.click(); 18 | document.body.removeChild(a); 19 | URL.revokeObjectURL(url); 20 | } 21 | 22 | return ( 23 |
24 | 32 | 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/FormList/ImportFormModal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Download, Upload } from "lucide-react"; 3 | 4 | import { Button } from "@/components/ui/button"; 5 | import { 6 | Dialog, 7 | DialogClose, 8 | DialogContent, 9 | DialogFooter, 10 | DialogHeader, 11 | DialogTitle, 12 | DialogTrigger, 13 | } from "@/components/ui/dialog"; 14 | import { useAppState } from "@/state/state"; 15 | import type { FormSchema } from "formbuilder-core"; 16 | import * as React from "react"; 17 | import { Label } from "@/components/ui/label"; 18 | import { Input } from "@/components/ui/input"; 19 | import { toast } from "sonner"; 20 | 21 | export function ImportFormModal() { 22 | const state = useAppState(); 23 | 24 | const [fileData, setFileData] = React.useState(""); 25 | 26 | function handleImport() { 27 | if (fileData) { 28 | const forms: FormSchema[] = JSON.parse(fileData); 29 | toast(`Successfully imported ${forms.length} forms`); 30 | state.setAppState({ 31 | ...state, 32 | forms: [...state.forms, ...forms], 33 | }); 34 | } 35 | } 36 | 37 | function handleFileChange(event: React.ChangeEvent) { 38 | const file = event.target.files![0]; 39 | if (file) { 40 | const reader = new FileReader(); 41 | // @ts-ignore 42 | reader.onload = (e) => setFileData(e.target.result); 43 | reader.readAsText(file); 44 | } 45 | } 46 | 47 | return ( 48 | 49 | 50 | 57 | 58 | 59 | 60 | Import JSON 61 | 62 |
63 | 64 | 70 |
71 | 72 | 73 | 76 | 77 | 78 |
79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/FormList/index.tsx: -------------------------------------------------------------------------------- 1 | import { FormList } from "./FormList"; 2 | export { FormList }; 3 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/FormSettings/SettingsToggle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Settings } from "lucide-react"; 4 | import { useAppState } from "@/state/state"; 5 | 6 | interface SettingsToggleProps { 7 | onClick: any; 8 | } 9 | 10 | export function SettingsToggle({ onClick }: SettingsToggleProps) { 11 | const state = useAppState(); 12 | return ( 13 |
14 |
15 |

16 | {state.currentForm.name} 17 |

18 |

19 | {state.currentForm.fields.reduce( 20 | (acc, row) => 21 | acc + row.filter((field) => field.kind !== "heading").length, 22 | 0, 23 | )} 24 |  Fields 25 |

26 |
27 | 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/FormSettings/index.tsx: -------------------------------------------------------------------------------- 1 | import { FormSwitch, FormInput } from "../SettingInputs"; 2 | 3 | import { useAppState } from "@/state/state"; 4 | 5 | export default function FormSettings() { 6 | const state = useAppState(); 7 | return ( 8 |
9 |
10 | state.updateFormName(newValue)} 14 | /> 15 | 16 | 20 | state.updateFormSettings({ importAliasUtils: newValue }) 21 | } 22 | /> 23 | 27 | state.updateFormSettings({ importAliasComponents: newValue }) 28 | } 29 | /> 30 |
31 |
32 | 37 | state.updateFormSettings({ 38 | noPlaceholder: !state.currentForm.settings.noPlaceholder, 39 | }) 40 | } 41 | /> 42 | 43 | 48 | state.updateFormSettings({ 49 | noDescription: !state.currentForm.settings.noDescription, 50 | }) 51 | } 52 | /> 53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/boolean/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import type { BooleanField, FormFramework } from "formbuilder-core"; 2 | import { 3 | FormControl, 4 | FormDescription, 5 | FormField, 6 | FormItem, 7 | FormLabel, 8 | FormMessage, 9 | } from "@/components/ui/form"; 10 | import { useFormContext } from "react-hook-form"; 11 | import { Checkbox as ShadcnCheckbox } from "@/components/ui/checkbox"; 12 | 13 | export function Checkbox({ f }: { f: BooleanField }) { 14 | const form = useFormContext(); 15 | return ( 16 | ( 20 | 21 | 22 | 27 | 28 |
29 | {f.label} 30 | {f.description} 31 | 32 |
33 |
34 | )} 35 | /> 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/boolean/index.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox } from "./checkbox"; 2 | import { Switch } from "./switch"; 3 | export { Checkbox, Switch }; 4 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/boolean/switch.tsx: -------------------------------------------------------------------------------- 1 | import type { BooleanField, FormFramework } from "formbuilder-core"; 2 | import { 3 | FormControl, 4 | FormDescription, 5 | FormField, 6 | FormItem, 7 | FormLabel, 8 | FormMessage, 9 | } from "@/components/ui/form"; 10 | import { useFormContext } from "react-hook-form"; 11 | import { Switch as ShadcnSwitch } from "@/components/ui/switch"; 12 | 13 | export function Switch({ f }: { f: BooleanField }) { 14 | const form = useFormContext(); 15 | return ( 16 | ( 20 | 21 |
22 | {f.label} 23 | {f.description} 24 |
25 | 26 | 27 | 28 | 29 |
30 | )} 31 | /> 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/date/date.tsx: -------------------------------------------------------------------------------- 1 | import type { DateField, FormFramework } from "formbuilder-core"; 2 | import { 3 | FormControl, 4 | FormDescription, 5 | FormField, 6 | FormItem, 7 | FormLabel, 8 | FormMessage, 9 | } from "@/components/ui/form"; 10 | import { useFormContext } from "react-hook-form"; 11 | import { 12 | Popover, 13 | PopoverContent, 14 | PopoverTrigger, 15 | } from "@/components/ui/popover"; 16 | import { Button } from "@/components/ui/button"; 17 | import { cn } from "@/lib/utils"; 18 | import { format } from "date-fns"; 19 | import { CalendarIcon } from "lucide-react"; 20 | import { Calendar } from "@/components/ui/calendar"; 21 | 22 | export function DatePicker({ f }: { f: DateField }) { 23 | const form = useFormContext(); 24 | return ( 25 | ( 29 | 30 | {f.label} 31 | 32 | 33 | 34 | 48 | 49 | 50 | 51 | 57 | 58 | 59 | {f.description} 60 | 61 | 62 | )} 63 | /> 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/date/index.tsx: -------------------------------------------------------------------------------- 1 | import { DatePicker } from "./date"; 2 | import { DateRangePicker } from "./daterange"; 3 | 4 | export { DatePicker, DateRangePicker }; 5 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/enum/button.tsx: -------------------------------------------------------------------------------- 1 | import type { EnumField, FormFramework } from "formbuilder-core"; 2 | import { 3 | FormControl, 4 | FormDescription, 5 | FormField, 6 | FormItem, 7 | FormLabel, 8 | FormMessage, 9 | } from "@/components/ui/form"; 10 | import { useFormContext } from "react-hook-form"; 11 | import { Button } from "@/components/ui/button"; 12 | 13 | export function ButtonGroup({ f }: { f: EnumField }) { 14 | const form = useFormContext(); 15 | return ( 16 | ( 20 | 21 | {f.label} 22 | 23 |
24 | {f.enumValues?.map((item) => ( 25 | 37 | ))} 38 |
39 |
40 | 41 |
42 | )} 43 | /> 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/enum/index.tsx: -------------------------------------------------------------------------------- 1 | import { RadioGroup } from "./radio"; 2 | import { Select } from "./select"; 3 | import { Combobox } from "./combobox"; 4 | import { ButtonGroup } from "./button"; 5 | 6 | export { RadioGroup, Select, Combobox, ButtonGroup }; 7 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/enum/radio.tsx: -------------------------------------------------------------------------------- 1 | import type { EnumField, FormFramework } from "formbuilder-core"; 2 | import { 3 | RadioGroup as ShadcnRadioGroup, 4 | RadioGroupItem, 5 | } from "@/components/ui/radio-group"; 6 | 7 | import { 8 | FormControl, 9 | FormDescription, 10 | FormField, 11 | FormItem, 12 | FormLabel, 13 | FormMessage, 14 | } from "@/components/ui/form"; 15 | import { useFormContext } from "react-hook-form"; 16 | 17 | export function RadioGroup({ f }: { f: EnumField }) { 18 | const form = useFormContext(); 19 | return ( 20 | ( 24 | 25 | {f.label} 26 | 27 | 32 | {f.enumValues?.map((item) => ( 33 | 37 | 38 | 39 | 40 | {item.label} 41 | 42 | ))} 43 | 44 | 45 | {f.description} 46 | 47 | 48 | )} 49 | /> 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/enum/select.tsx: -------------------------------------------------------------------------------- 1 | import type { EnumField, FormFramework } from "formbuilder-core"; 2 | import { 3 | FormControl, 4 | FormDescription, 5 | FormField, 6 | FormItem, 7 | FormLabel, 8 | FormMessage, 9 | } from "@/components/ui/form"; 10 | import { useFormContext } from "react-hook-form"; 11 | import { 12 | Select as ShadcnSelect, 13 | SelectContent, 14 | SelectItem, 15 | SelectTrigger, 16 | SelectValue, 17 | } from "@/components/ui/select"; 18 | 19 | export function Select({ f }: { f: EnumField }) { 20 | const form = useFormContext(); 21 | return ( 22 | ( 26 | 27 | {f.label} 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {f.enumValues?.map((item) => ( 39 | 40 | {item.label} 41 | 42 | ))} 43 | 44 | 45 | {f.description} 46 | 47 | 48 | )} 49 | /> 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/file/file.tsx: -------------------------------------------------------------------------------- 1 | // import { Input } from "@/components/ui/input"; 2 | // import type { FormFramework } from "formbuilder-core"; 3 | // import { useFormContext } from "react-hook-form"; 4 | // import { 5 | // FormControl, 6 | // FormDescription, 7 | // FormField, 8 | // FormItem, 9 | // FormLabel, 10 | // FormMessage, 11 | // } from "@/components/ui/form"; 12 | 13 | // export function FileInput({ f }: { f: FileField }) { 14 | // const form = useFormContext(); 15 | // return ( 16 | // ( 20 | // 21 | // {f.label} 22 | // 23 | //
24 | // 30 | //
31 | //
32 | // {f.description} 33 | // 34 | //
35 | // )} 36 | // /> 37 | // ); 38 | // } 39 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/file/index.tsx: -------------------------------------------------------------------------------- 1 | // import { FileInput } from "./file"; 2 | // export { FileInput }; 3 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/heading/Heading.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { H1, H2, H3, H4, H5, H6 } from "@/components/ui/heading-with-anchor"; 3 | 4 | interface HeadingProps { 5 | useAnchor: boolean; 6 | headingLevel: "H1" | "H2" | "H3" | "H4" | "H5" | "H6"; 7 | anchorValue?: string; 8 | label: string; 9 | } 10 | 11 | // TODO: Heading not done 12 | export function Heading({ 13 | useAnchor, 14 | headingLevel, 15 | label, 16 | anchorValue, 17 | }: HeadingProps) { 18 | const HeadingComponent = { 19 | H1: H1, 20 | H2: H2, 21 | H3: H3, 22 | H4: H4, 23 | H5: H5, 24 | H6: H6, 25 | }[headingLevel || "H1"]; 26 | 27 | return ( 28 | <> 29 | {useAnchor ? ( 30 | 31 | {label} 32 | 33 | ) : ( 34 | {label} 35 | )} 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/heading/index.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/FormBuilder/7b3a243f4b8078ce4f2044fb7e1c9332baf21248/apps/web/src/app/builder/_components/Preview/component-variants/heading/index.tsx -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/number/dual-slider.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | FormControl, 4 | FormDescription, 5 | FormField, 6 | FormItem, 7 | FormLabel, 8 | FormMessage, 9 | } from "@/components/ui/form"; 10 | import { DualRangeSlider } from "@/components/ui/dual-range-slider"; 11 | import type { FormFramework, NumberField } from "formbuilder-core"; 12 | import { useFormContext } from "react-hook-form"; 13 | 14 | export const DualSlider = ({ f }: { f: NumberField }) => { 15 | const form = useFormContext(); 16 | 17 | return ( 18 | ( 22 | 23 | {f.label} 24 | 25 | value} 28 | value={field.value || [0, 100]} 29 | onValueChange={(e: any) => field.onChange(e)} 30 | min={f.validation?.min} 31 | max={f.validation?.max} 32 | step={f.validation?.step} 33 | /> 34 | 35 | {f.description} 36 | 37 | 38 | )} 39 | /> 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/number/index.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/FormBuilder/7b3a243f4b8078ce4f2044fb7e1c9332baf21248/apps/web/src/app/builder/_components/Preview/component-variants/number/index.tsx -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/number/number.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | FormControl, 4 | FormDescription, 5 | FormField, 6 | FormItem, 7 | FormLabel, 8 | FormMessage, 9 | } from "@/components/ui/form"; 10 | import { Input } from "@/components/ui/input"; 11 | import * as React from "react"; 12 | import type { FormFramework, NumberField } from "formbuilder-core"; 13 | import { useFormContext } from "react-hook-form"; 14 | 15 | export function NumberInput({ f }: { f: NumberField }) { 16 | const form = useFormContext(); 17 | 18 | return ( 19 | ( 23 | 24 | {f.label} 25 | 26 | 27 | 28 | {f.description} 29 | 30 | 31 | )} 32 | /> 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/number/phone-number.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | FormControl, 4 | FormDescription, 5 | FormField, 6 | FormItem, 7 | FormLabel, 8 | FormMessage, 9 | } from "@/components/ui/form"; 10 | import { PhoneInput } from "@/components/ui/phone-input"; 11 | 12 | import * as React from "react"; 13 | import type { FormFramework, NumberField } from "formbuilder-core"; 14 | import { useFormContext } from "react-hook-form"; 15 | 16 | export function PhoneNumber({ f }: { f: NumberField }) { 17 | const form = useFormContext(); 18 | 19 | return ( 20 | ( 24 | 25 | {f.label} 26 | 27 | 33 | 34 | {f.description} 35 | 36 | 37 | )} 38 | /> 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/number/slider.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | FormControl, 4 | FormDescription, 5 | FormField, 6 | FormItem, 7 | FormLabel, 8 | FormMessage, 9 | } from "@/components/ui/form"; 10 | import type { FormFramework, NumberField } from "formbuilder-core"; 11 | import { useFormContext } from "react-hook-form"; 12 | import { Slider as ShadcnSlider } from "@/components/ui/slider"; 13 | 14 | export function Slider({ f }: { f: NumberField }) { 15 | const form = useFormContext(); 16 | return ( 17 | ( 21 | 22 | {f.label} 23 | 24 | field.onChange(e)} 28 | min={f.validation?.min} 29 | max={f.validation?.max} 30 | step={f.validation?.step} 31 | /> 32 | 33 | {f.description} 34 | 35 | 36 | )} 37 | /> 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/text/autoresize-textarea.tsx: -------------------------------------------------------------------------------- 1 | import type { TextField, FormFramework } from "formbuilder-core"; 2 | import { 3 | FormControl, 4 | FormDescription, 5 | FormField, 6 | FormItem, 7 | FormLabel, 8 | FormMessage, 9 | } from "@/components/ui/form"; 10 | import { useFormContext } from "react-hook-form"; 11 | import { AutosizeTextarea } from "@/components/ui/autosize-textarea"; 12 | 13 | export function AutoResizeTextarea({ f }: { f: TextField }) { 14 | const form = useFormContext(); 15 | return ( 16 | ( 20 | 21 | {f.label} 22 | 23 | 28 | 29 | {f.description} 30 | 31 | 32 | )} 33 | /> 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/text/index.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from "./input"; 2 | import { Textarea } from "./textarea"; 3 | import { AutoResizeTextarea } from "./autoresize-textarea"; 4 | import { InputOTP } from "./input-otp"; 5 | import { InputTag } from "./input-tag"; 6 | import { PasswordStrengthIndicator } from "./password-strength-indicator"; 7 | import { Password } from "./password"; 8 | 9 | export { 10 | Input, 11 | Textarea, 12 | AutoResizeTextarea, 13 | InputOTP, 14 | InputTag, 15 | PasswordStrengthIndicator, 16 | Password 17 | }; 18 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/text/input-otp.tsx: -------------------------------------------------------------------------------- 1 | import type { TextField, FormFramework } from "formbuilder-core"; 2 | import { 3 | FormControl, 4 | FormDescription, 5 | FormField, 6 | FormItem, 7 | FormLabel, 8 | FormMessage, 9 | } from "@/components/ui/form"; 10 | import { useFormContext } from "react-hook-form"; 11 | import { 12 | InputOTP as ShadcnInputOTP, 13 | InputOTPGroup, 14 | InputOTPSlot, 15 | } from "@/components/ui/input-otp"; 16 | 17 | export function InputOTP({ f }: { f: TextField }) { 18 | const form = useFormContext(); 19 | return ( 20 | ( 24 | 25 | {f.label} 26 | 27 | 28 | 29 | {Array.from({ length: f.digits! }, (_, i) => ( 30 | 31 | ))} 32 | 33 | 34 | 35 | z{f.description} 36 | 37 | 38 | )} 39 | /> 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/text/input-tag.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FormControl, 3 | FormDescription, 4 | FormField, 5 | FormItem, 6 | FormLabel, 7 | FormMessage, 8 | } from "@/components/ui/form"; 9 | import { type Tag, TagInput } from "emblor"; 10 | import type { FormFramework, TextField } from "formbuilder-core"; 11 | import { useState } from "react"; 12 | import { useFormContext } from "react-hook-form"; 13 | 14 | const tags = [ 15 | { 16 | id: "1", 17 | text: "Sport", 18 | }, 19 | { 20 | id: "2", 21 | text: "Coding", 22 | }, 23 | { 24 | id: "3", 25 | text: "Travel", 26 | }, 27 | ]; 28 | 29 | export function InputTag({ f }: { f: TextField }) { 30 | const form = useFormContext(); 31 | const [exampleTags, setExampleTags] = useState(tags); 32 | const [activeTagIndex, setActiveTagIndex] = useState(null); 33 | return ( 34 | ( 38 | 39 | {f.label} 40 | 41 |
42 | { 47 | field.onChange(newTags); 48 | setExampleTags(newTags); 49 | }} 50 | placeholder="Add a tag" 51 | styleClasses={{ 52 | tagList: { 53 | container: "gap-1", 54 | }, 55 | input: 56 | "rounded-lg transition-shadow placeholder:text-muted-foreground/70 focus-visible:border-ring focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/20", 57 | tag: { 58 | body: "relative h-7 bg-background border border-input hover:bg-background rounded-md font-medium text-xs ps-2 pe-7", 59 | closeButton: 60 | "absolute -inset-y-px -end-px p-0 rounded-s-none rounded-e-lg flex size-7 transition-colors outline-0 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring/70 text-muted-foreground/80 hover:text-foreground", 61 | }, 62 | }} 63 | activeTagIndex={activeTagIndex} 64 | setActiveTagIndex={setActiveTagIndex} 65 | inputFieldPosition="bottom" 66 | /> 67 |
68 |
69 | {f.description} 70 | 71 |
72 | )} 73 | /> 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/text/input.tsx: -------------------------------------------------------------------------------- 1 | import type { TextField, FormFramework } from "formbuilder-core"; 2 | import { 3 | FormControl, 4 | FormDescription, 5 | FormField, 6 | FormItem, 7 | FormLabel, 8 | FormMessage, 9 | } from "@/components/ui/form"; 10 | import { useFormContext } from "react-hook-form"; 11 | import { Input as ShadcnInput } from "@/components/ui/input"; 12 | 13 | export function Input({ f }: { f: TextField }) { 14 | const form = useFormContext(); 15 | return ( 16 | ( 20 | 21 | {f.label} 22 | 23 | 24 | 25 | {f.description} 26 | 27 | 28 | )} 29 | /> 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/component-variants/text/textarea.tsx: -------------------------------------------------------------------------------- 1 | import type { TextField, FormFramework } from "formbuilder-core"; 2 | import { 3 | FormControl, 4 | FormDescription, 5 | FormField, 6 | FormItem, 7 | FormLabel, 8 | FormMessage, 9 | } from "@/components/ui/form"; 10 | import { useFormContext } from "react-hook-form"; 11 | import { Textarea as ShadcnTextarea } from "@/components/ui/textarea"; 12 | 13 | export function Textarea({ f }: { f: TextField }) { 14 | const form = useFormContext(); 15 | return ( 16 | ( 20 | 21 | {f.label} 22 | 23 | 28 | 29 | {f.description} 30 | 31 | 32 | )} 33 | /> 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/Preview/index.tsx: -------------------------------------------------------------------------------- 1 | import { Preview } from "./Preview"; 2 | 3 | export { Preview }; 4 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/SortableItem/AddNewFieldArrows.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { useAppState } from "@/state/state"; 3 | import type { Kind } from "formbuilder-core"; 4 | import { 5 | ArrowBigUpDash, 6 | ArrowBigDownDash, 7 | ArrowBigLeftDash, 8 | ArrowBigRightDash, 9 | } from "lucide-react"; 10 | 11 | export function AddNewFieldArrows({ id, kind }: { id: string; kind: Kind }) { 12 | const { addItem, chosenField } = useAppState(); 13 | const hideXArrows = !(kind === "heading" || chosenField?.kind === "heading"); 14 | return ( 15 |
16 | {hideXArrows && ( 17 | 25 | )} 26 |
27 | 35 | 43 |
44 | {hideXArrows && ( 45 | 53 | )} 54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/SortableItem/FormFieldContent.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Badge } from "@/components/ui/badge"; 3 | import { Settings, Trash } from "lucide-react"; 4 | import { cn } from "@/lib/utils"; 5 | import { removeItem, useAppState } from "@/state/state"; 6 | import type { Kind } from "formbuilder-core"; 7 | import { colorMap } from "@/constants"; 8 | 9 | export function FormFieldContent({ 10 | id, 11 | label, 12 | kind, 13 | }: { id: string; label: string; kind: Kind }) { 14 | const state = useAppState(); 15 | return ( 16 |
17 |
18 | 26 | {label} 27 | 28 |
29 | 37 | 50 |
51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/SortableItem/SortableItem.tsx: -------------------------------------------------------------------------------- 1 | import { useSortable } from "@dnd-kit/sortable"; 2 | import { CSS } from "@dnd-kit/utilities"; 3 | import { cva } from "class-variance-authority"; 4 | import { cn } from "@/lib/utils"; 5 | import { useAppState } from "@/state/state"; 6 | import { FormFieldContent } from "./FormFieldContent"; 7 | import { AddNewFieldArrows } from "./AddNewFieldArrows"; 8 | import type { Kind } from "formbuilder-core"; 9 | 10 | interface FormFieldProps { 11 | id: string; 12 | label: string; 13 | isOverlay?: boolean; 14 | kind: Kind; 15 | } 16 | 17 | export function SortableItem({ id, label, isOverlay, kind }: FormFieldProps) { 18 | const { 19 | setNodeRef, 20 | attributes, 21 | listeners, 22 | transform, 23 | transition, 24 | isDragging, 25 | } = useSortable({ 26 | id: id, 27 | animateLayoutChanges: () => false, 28 | }); 29 | 30 | const style = { 31 | transition, 32 | transform: CSS.Translate.toString(transform), 33 | }; 34 | 35 | const state = useAppState(); 36 | const variants = cva("h-[53px] h-fit min-w-[380px] rounded-lg border-2", { 37 | variants: { 38 | drop: { default: "" }, 39 | dragging: { 40 | over: "opacity-30 ring-2", 41 | overlay: "ring-2 ring-primary", 42 | }, 43 | }, 44 | }); 45 | 46 | return ( 47 | <> 48 | {state.renderContent ? ( 49 |
61 | 62 |
63 | ) : ( 64 |
65 | 66 |
67 | )} 68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/_components/SortableItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { SortableItem } from "./SortableItem"; 2 | 3 | export { SortableItem }; 4 | -------------------------------------------------------------------------------- /apps/web/src/app/builder/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { BuilderPage } from "./BuilderPage"; 3 | 4 | export const metadata: Metadata = { 5 | title: "Builder", 6 | description: 7 | "Create beautiful, type-safe forms with our drag-and-drop form builder for Next.js, Vue, and Svelte", 8 | openGraph: { 9 | title: "Form Builder | Create Custom Forms", 10 | description: 11 | "Design and build custom forms with our intuitive drag-and-drop form builder. Export to your favorite framework.", 12 | type: "website", 13 | images: [ 14 | { 15 | url: "/images/builder.png", 16 | width: 1200, 17 | height: 630, 18 | alt: "FormBuilder Interface", 19 | }, 20 | ], 21 | }, 22 | twitter: { 23 | card: "summary_large_image", 24 | title: "Form Builder | Create Custom Forms", 25 | description: 26 | "Design and build custom forms with our intuitive drag-and-drop interface", 27 | images: ["/images/builder.png"], 28 | }, 29 | }; 30 | 31 | export default function Page() { 32 | return ; 33 | } 34 | -------------------------------------------------------------------------------- /apps/web/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css"; 2 | import type { Metadata, Viewport } from "next"; 3 | import { siteConfig } from "@/config/site"; 4 | import { fontSans } from "@/lib/fonts"; 5 | import { cn } from "@/lib/utils"; 6 | import { Toaster } from "@/components/shared/Toaster"; 7 | import { SiteHeader } from "@/components/site-header"; 8 | import { TailwindIndicator } from "@/components/tailwind-indicator"; 9 | import { ThemeProvider } from "@/components/theme-provider"; 10 | 11 | export const viewport: Viewport = { 12 | themeColor: [ 13 | { media: "(prefers-color-scheme: light)", color: "white" }, 14 | { media: "(prefers-color-scheme: dark)", color: "black" }, 15 | ], 16 | }; 17 | 18 | export const metadata: Metadata = { 19 | openGraph: { images: "/images/index.png" }, 20 | 21 | title: { 22 | default: siteConfig.name, 23 | template: `%s - ${siteConfig.name}`, 24 | }, 25 | description: siteConfig.description, 26 | icons: { 27 | icon: "/logo.svg", 28 | shortcut: "/logo.svg", 29 | apple: "/apple-touch-icon.png", 30 | }, 31 | }; 32 | 33 | interface RootLayoutProps { 34 | children: React.ReactNode; 35 | } 36 | 37 | export default function RootLayout({ children }: RootLayoutProps) { 38 | return ( 39 | <> 40 | 41 | 42 | 30 |
31 | {{#each fields}} 32 | {{{lookupComponent this}}} 33 | {{/each}} 34 | Submit 35 |
36 | `; 37 | -------------------------------------------------------------------------------- /packages/core/src/codegen/templates/svelte/number/dual-slider.ts: -------------------------------------------------------------------------------- 1 | // TODO: dual slider with form 2 | export const dualSlider = ` 3 | ( 7 | 8 | {{label}} 9 | 10 | value} 13 | value={field.value || [0, 100]} 14 | onValueChange={(e: any) => field.onChange(e)} 15 | min={f.validation?.min} 16 | max={f.validation?.max} 17 | step={f.validation?.step} 18 | /> 19 | 20 | {{description}} 21 | 22 | 23 | )} 24 | /> 25 | `; 26 | -------------------------------------------------------------------------------- /packages/core/src/codegen/templates/svelte/number/index.ts: -------------------------------------------------------------------------------- 1 | import { dualSlider } from "./dual-slider"; 2 | import { number } from "./number"; 3 | import { phoneNumber } from "./phone-number"; 4 | import { slider } from "./slider"; 5 | 6 | export const numberTemplates = { 7 | dualSlider, 8 | number, 9 | phoneNumber, 10 | slider, 11 | } as const; 12 | -------------------------------------------------------------------------------- /packages/core/src/codegen/templates/svelte/number/number.ts: -------------------------------------------------------------------------------- 1 | // TODO: simple or custom number input? 2 | export const number = ` 3 | 4 | 5 | {#snippet children({ props })} 6 | {{label}} 7 | 8 | {/snippet} 9 | 10 | {{description}} 11 | 12 | 13 | `; 14 | -------------------------------------------------------------------------------- /packages/core/src/codegen/templates/svelte/number/phone-number.ts: -------------------------------------------------------------------------------- 1 | // TODO: phone number input 2 | // https://www.shadcn-svelte-extras.com/components/phone-input 3 | export const phoneNumber = ` 4 | ( 8 | 9 | {{label}} 10 | 11 | 17 | 18 | {{description}} 19 | 20 | 21 | )} 22 | /> 23 | `; 24 | -------------------------------------------------------------------------------- /packages/core/src/codegen/templates/svelte/number/slider.ts: -------------------------------------------------------------------------------- 1 | // TODO: slider with form 2 | export const slider = ` 3 | 4 | 5 | {#snippet children({ props })} 6 | {{label}} 7 | 8 | {/snippet} 9 | 10 | {{description}} 11 | 12 | 13 | 14 | `; 15 | -------------------------------------------------------------------------------- /packages/core/src/codegen/templates/svelte/text/index.ts: -------------------------------------------------------------------------------- 1 | import { textarea } from "./textarea"; 2 | import { inputOTP } from "./input-otp"; 3 | import { input } from "./input"; 4 | import { password } from "./password"; 5 | 6 | export const textTemplates = { 7 | textarea, 8 | inputOTP, 9 | input, 10 | password, 11 | } as const; 12 | -------------------------------------------------------------------------------- /packages/core/src/codegen/templates/svelte/text/input-otp.ts: -------------------------------------------------------------------------------- 1 | export const inputOTP = ` 2 | 3 | {#snippet children({ props })} 4 | {{label}} 5 | 6 | {#snippet children({ cells })} 7 | 8 | {#each cells as cell} 9 | 10 | {/each} 11 | 12 | {/snippet} 13 | 14 | {/snippet} 15 | 16 | {{description}} 17 | 18 | 19 | `; 20 | -------------------------------------------------------------------------------- /packages/core/src/codegen/templates/svelte/text/input.ts: -------------------------------------------------------------------------------- 1 | export const input = ` 2 | 3 | 4 | {#snippet children({ props })} 5 | {{label}} 6 | 7 | {/snippet} 8 | 9 | {{description}} 10 | 11 | 12 | `; 13 | -------------------------------------------------------------------------------- /packages/core/src/codegen/templates/svelte/text/password.ts: -------------------------------------------------------------------------------- 1 | // TODO: svelte custom password strength indicator 2 | export const password = ` 3 | 4 | 5 | {#snippet children({ props })} 6 | {{label}} 7 | 8 | {/snippet} 9 | 10 | {{description}} 11 | 12 | 13 | `; 14 | -------------------------------------------------------------------------------- /packages/core/src/codegen/templates/svelte/text/textarea.ts: -------------------------------------------------------------------------------- 1 | export const textarea = ` 2 | 3 | 4 | {#snippet children({ props })} 5 | {{label}} 6 |