Compare commits

...

5 Commits

Author SHA1 Message Date
Dhruwang
bb8c35440d removed an used file 2025-11-25 16:28:23 +05:30
Dhruwang
57a5b40717 clean ups 2025-11-25 16:25:48 +05:30
Dhruwang
ef601a5437 survey-core -> ui 2025-11-25 16:00:02 +05:30
Dhruwang
c65ee80066 surveys-embed -> surveys package 2025-11-25 15:17:03 +05:30
Dhruwang
ed70c5fb73 introduced survey-embed and survey-core packages 2025-11-25 13:01:10 +05:30
42 changed files with 572 additions and 39 deletions

View File

@@ -13,7 +13,11 @@ function getAbsolutePath(value: string): any {
}
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../../web/modules/ui/**/stories.@(js|jsx|mjs|ts|tsx)"],
stories: [
"../src/**/*.mdx",
"../../web/modules/ui/**/stories.@(js|jsx|mjs|ts|tsx)",
"../../../packages/ui/src/**/stories.@(js|jsx|mjs|ts|tsx)",
],
addons: [
getAbsolutePath("@storybook/addon-onboarding"),
getAbsolutePath("@storybook/addon-links"),

View File

@@ -1,12 +1,12 @@
import type { Preview } from "@storybook/react-vite";
import React from "react";
import { I18nProvider } from "../../web/lingodotdev/client";
import { I18nProvider as WebI18nProvider } from "../../web/lingodotdev/client";
import "../../web/modules/ui/globals.css";
// Create a Storybook-specific Lingodot Dev decorator
// Create a Storybook-specific Lingodot Dev decorator for web components
const withLingodotDev = (Story: any) => {
return React.createElement(
I18nProvider,
WebI18nProvider,
{
language: "en-US",
defaultLanguage: "en-US",
@@ -14,7 +14,6 @@ const withLingodotDev = (Story: any) => {
React.createElement(Story)
);
};
const preview: Preview = {
parameters: {
controls: {

View File

@@ -11,6 +11,7 @@ export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "../web"),
"@ui": path.resolve(__dirname, "../../packages/ui/src"),
},
},
});

View File

@@ -98,13 +98,17 @@ The `packages/` directory contains shared libraries and utilities:
* Migration management
### packages/ui/
* React-authored survey UI components
* Survey rendering logic and UI components
* Used in web app, Storybook, and compiled to Preact for embeds
### packages/surveys/
* Survey-specific functionality
* Survey rendering logic and UI components
* Survey state management
* Preact-compiled survey embed bundle
* Lightweight widget for embedding surveys into customer websites
* Compiled from ui package using preact/compat
## Module Organization

View File

@@ -29,7 +29,7 @@ This document shows how you can use Formbricks to manage survey definitions and
## Core components
1. **Formbricks Backend:** Use the Formbricks app or Management API to create surveys (questions, flows, locales, validations).
2. **Your UI Survey Package:** Renders your custom UI, collects the data and sends to Formbricks backend using Formbricks API. For inspiration, you can start looking [here](https://github.com/formbricks/formbricks/tree/main/packages/surveys). With an active Enterprise license you can even fork our surveys package, make changes and keep them private to your organization (freed from AGPL obligation to also release your changes under AGPL)
2. **Your UI Survey Package:** Renders your custom UI, collects the data and sends to Formbricks backend using Formbricks API. For inspiration, you can start looking [here](https://github.com/formbricks/formbricks/tree/main/packages/ui). With an active Enterprise license you can even fork our survey packages, make changes and keep them private to your organization (freed from AGPL obligation to also release your changes under AGPL)
3. **Webhook Integration:** Using in-built Webhook integration forward the data to your Analysis tool or Data warehouse.
4. **Your Analysis Tool / Data Warehouse:** Receive all the data from Formbricks integration and process it for analysis.
@@ -136,7 +136,7 @@ Body:
Your frontend receives the survey JSON and renders it using your own UI components.
For inspiration, you can start looking [here](https://github.com/formbricks/formbricks/tree/main/packages/surveys). With an active Enterprise license you can even fork our surveys package, make changes and keep them private to your organization (freed from AGPL obligation to also release your changes under AGPL)
For inspiration, you can start looking [here](https://github.com/formbricks/formbricks/tree/main/packages/ui). With an active Enterprise license you can even fork our survey packages, make changes and keep them private to your organization (freed from AGPL obligation to also release your changes under AGPL)
* Question rendering based on type (openText, multipleChoiceSingle, rating, etc.)
* Skip logic and conditional branching

View File

@@ -1,4 +1,4 @@
module.exports = {
extends: ["@formbricks/eslint-config/legacy-react.js"],
parser: "@typescript-eslint/parser",
};
extends: ["@formbricks/eslint-config/legacy-react.js"],
parser: "@typescript-eslint/parser",
};

View File

@@ -21,4 +21,4 @@ dist-ssr
*.ntvs*
*.njsproj
*.sln
*.sw?
*.sw?

View File

@@ -1,6 +1,6 @@
## Overview
The `@formbricks/surveys` package provides a complete survey rendering system built with Preact/React. It features automated translation management through Lingo.dev.
The `@formbricks/surveys` package provides a complete survey rendering system built with Preact. It features automated translation management through Lingo.dev and is compiled from React components in `@formbricks/ui`.
## Features

View File

@@ -39,6 +39,7 @@
},
"dependencies": {
"@calcom/embed-snippet": "1.3.3",
"@formbricks/ui": "workspace:*",
"@formkit/auto-animate": "0.8.2",
"i18next": "25.5.2",
"i18next-icu": "2.4.0",

View File

@@ -2,7 +2,7 @@ import { useMemo, useRef, useState } from "preact/hooks";
import { useCallback } from "react";
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyAddressQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { BackButton } from "@/components/buttons/back-button";
import { BackButton } from "@formbricks/ui";
import { SubmitButton } from "@/components/buttons/submit-button";
import { Headline } from "@/components/general/headline";
import { Input } from "@/components/general/input";

View File

@@ -2,7 +2,7 @@ import { useCallback, useRef, useState } from "preact/hooks";
import { useTranslation } from "react-i18next";
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
import { type TSurveyCalQuestion, type TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { BackButton } from "@/components/buttons/back-button";
import { BackButton } from "@formbricks/ui";
import { SubmitButton } from "@/components/buttons/submit-button";
import { CalEmbed } from "@/components/general/cal-embed";
import { Headline } from "@/components/general/headline";

View File

@@ -1,7 +1,7 @@
import { useCallback, useState } from "preact/hooks";
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyConsentQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { BackButton } from "@/components/buttons/back-button";
import { BackButton } from "@formbricks/ui";
import { SubmitButton } from "@/components/buttons/submit-button";
import { Headline } from "@/components/general/headline";
import { QuestionMedia } from "@/components/general/question-media";

View File

@@ -1,7 +1,7 @@
import { useCallback, useMemo, useRef, useState } from "preact/hooks";
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyContactInfoQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { BackButton } from "@/components/buttons/back-button";
import { BackButton } from "@formbricks/ui";
import { SubmitButton } from "@/components/buttons/submit-button";
import { Headline } from "@/components/general/headline";
import { Input } from "@/components/general/input";

View File

@@ -1,7 +1,7 @@
import { useState } from "preact/hooks";
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyCTAQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { BackButton } from "@/components/buttons/back-button";
import { BackButton } from "@formbricks/ui";
import { SubmitButton } from "@/components/buttons/submit-button";
import { Headline } from "@/components/general/headline";
import { QuestionMedia } from "@/components/general/question-media";

View File

@@ -3,7 +3,7 @@ import DatePicker, { DatePickerProps } from "react-date-picker";
import { useTranslation } from "react-i18next";
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyDateQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { BackButton } from "@/components/buttons/back-button";
import { BackButton } from "@formbricks/ui";
import { SubmitButton } from "@/components/buttons/submit-button";
import { Headline } from "@/components/general/headline";
import { QuestionMedia } from "@/components/general/question-media";

View File

@@ -4,13 +4,13 @@ import { type TJsFileUploadParams } from "@formbricks/types/js";
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
import { type TUploadFileConfig } from "@formbricks/types/storage";
import type { TSurveyFileUploadQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { BackButton } from "@formbricks/ui";
import { SubmitButton } from "@/components/buttons/submit-button";
import { Headline } from "@/components/general/headline";
import { QuestionMedia } from "@/components/general/question-media";
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
import { getLocalizedValue } from "@/lib/i18n";
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
import { BackButton } from "../buttons/back-button";
import { FileInput } from "../general/file-input";
import { Subheader } from "../general/subheader";

View File

@@ -6,7 +6,7 @@ import type {
TSurveyMatrixQuestionChoice,
TSurveyQuestionId,
} from "@formbricks/types/surveys/types";
import { BackButton } from "@/components/buttons/back-button";
import { BackButton } from "@formbricks/ui";
import { SubmitButton } from "@/components/buttons/submit-button";
import { Headline } from "@/components/general/headline";
import { QuestionMedia } from "@/components/general/question-media";

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyMultipleChoiceQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { BackButton } from "@/components/buttons/back-button";
import { BackButton } from "@formbricks/ui";
import { SubmitButton } from "@/components/buttons/submit-button";
import { Headline } from "@/components/general/headline";
import { QuestionMedia } from "@/components/general/question-media";

View File

@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyMultipleChoiceQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { BackButton } from "@/components/buttons/back-button";
import { BackButton } from "@formbricks/ui";
import { SubmitButton } from "@/components/buttons/submit-button";
import { Headline } from "@/components/general/headline";
import { QuestionMedia } from "@/components/general/question-media";

View File

@@ -1,7 +1,7 @@
import { useState } from "preact/hooks";
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyNPSQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { BackButton } from "@/components/buttons/back-button";
import { BackButton } from "@formbricks/ui";
import { SubmitButton } from "@/components/buttons/submit-button";
import { Headline } from "@/components/general/headline";
import { QuestionMedia } from "@/components/general/question-media";

View File

@@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
import { ZEmail, ZUrl } from "@formbricks/types/common";
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyOpenTextQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { BackButton } from "@/components/buttons/back-button";
import { BackButton } from "@formbricks/ui";
import { SubmitButton } from "@/components/buttons/submit-button";
import { Headline } from "@/components/general/headline";
import { QuestionMedia } from "@/components/general/question-media";

View File

@@ -2,7 +2,7 @@ import { useEffect, useState } from "preact/hooks";
import { useTranslation } from "react-i18next";
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyPictureSelectionQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { BackButton } from "@/components/buttons/back-button";
import { BackButton } from "@formbricks/ui";
import { SubmitButton } from "@/components/buttons/submit-button";
import { Headline } from "@/components/general/headline";
import { QuestionMedia } from "@/components/general/question-media";

View File

@@ -7,7 +7,7 @@ import type {
TSurveyQuestionId,
TSurveyRankingQuestion,
} from "@formbricks/types/surveys/types";
import { BackButton } from "@/components/buttons/back-button";
import { BackButton } from "@formbricks/ui";
import { SubmitButton } from "@/components/buttons/submit-button";
import { Headline } from "@/components/general/headline";
import { QuestionMedia } from "@/components/general/question-media";

View File

@@ -2,7 +2,7 @@ import { useEffect, useState } from "preact/hooks";
import type { JSX } from "react";
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyQuestionId, TSurveyRatingQuestion } from "@formbricks/types/surveys/types";
import { BackButton } from "@/components/buttons/back-button";
import { BackButton } from "@formbricks/ui";
import { SubmitButton } from "@/components/buttons/submit-button";
import { Headline } from "@/components/general/headline";
import { QuestionMedia } from "@/components/general/question-media";

View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -7,10 +7,12 @@
"jsx": "react-jsx",
"jsxImportSource": "preact",
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"@formbricks/ui": ["../ui/src/index.ts"],
"@ui/*": ["../ui/src/*"]
},
"resolveJsonModule": true
},
"extends": "@formbricks/config-typescript/js-library.json",
"include": ["src", "../types/surveys.d.ts"]
"include": ["src", "../types/surveys.d.ts", "vite-env.d.ts"]
}

6
packages/surveys/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module "*.css?inline" {
const content: string;
export default content;
}

View File

@@ -1,6 +1,7 @@
import preact from "@preact/preset-vite";
import { dirname, resolve } from "path";
import { fileURLToPath } from "url";
import type { Plugin } from "vite";
import { loadEnv } from "vite";
import dts from "vite-plugin-dts";
import tsconfigPaths from "vite-tsconfig-paths";
@@ -10,6 +11,40 @@ import { copyCompiledAssetsPlugin } from "../vite-plugins/copy-compiled-assets";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* Vite plugin that intercepts ui package's cn utility import
* and replaces it with our prefixed version
*/
function addFbPrefixPlugin(): Plugin {
const uiUtilsPath = resolve(__dirname, "../ui/src/lib/utils.ts");
const uiSrcPath = resolve(__dirname, "../ui/src");
return {
name: "add-fb-prefix-to-ui",
enforce: "pre",
resolveId(id, importer) {
if (!importer) return null;
// Normalize paths for comparison
const normalizedImporter = importer.replace(/\\/g, "/");
const normalizedUiSrc = uiSrcPath.replace(/\\/g, "/");
// Check if the importer is from ui package
const isFromUi =
normalizedImporter.includes("ui/src") ||
normalizedImporter.includes("ui\\src") ||
normalizedImporter.startsWith(normalizedUiSrc) ||
normalizedImporter.includes("/ui/") ||
normalizedImporter.includes("\\ui\\");
// Plugin functionality removed - cn-with-prefix.ts file was deleted
// If prefix functionality is needed, it should be reimplemented
return null;
},
};
}
const config = ({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
@@ -54,10 +89,21 @@ const config = ({ mode }) => {
},
plugins: [
preact(),
addFbPrefixPlugin(),
dts({ rollupTypes: true }),
tsconfigPaths(),
copyCompiledAssetsPlugin({ filename: "surveys", distDir: resolve(__dirname, "dist") }),
],
resolve: {
alias: {
// Alias React to Preact for ui package components
react: "preact/compat",
"react-dom": "preact/compat",
"react/jsx-runtime": "preact/jsx-runtime",
// Allow importing from ui package source files
"@formbricks/ui/src": resolve(__dirname, "../../ui/src"),
},
},
});
};

11
packages/types/.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
node_modules
dist
.turbo
coverage
*.log
.DS_Store
# TypeScript declaration files (generated build artifacts)
**/*.d.ts
**/*.d.ts.map

11
packages/ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
node_modules
dist
.turbo
coverage
*.log
.DS_Store
# TypeScript declaration files (generated build artifacts)
**/*.d.ts
**/*.d.ts.map

83
packages/ui/README.md Normal file
View File

@@ -0,0 +1,83 @@
## Overview
The `@formbricks/ui` package provides React-authored survey UI components. These components are written using standard React APIs (hooks, JSX, etc.) for maximum familiarity and ecosystem compatibility.
## Purpose
This package serves as the source of truth for survey UI components that are used across:
- **Storybook** (React) - Component documentation and visual testing
- **Next.js web app** (React) - Main application UI
- **Embed bundle** (Preact) - Compiled via `@formbricks/surveys` using `preact/compat`
## Architecture
### React-First Development
All components are authored using standard React patterns:
- React hooks (`useState`, `useEffect`, etc.)
- JSX syntax
- React Context API
- Standard React component patterns
### Build Strategy
- **ui**: React components, treated as a normal React library
- **surveys**: Build step aliases `react``preact/compat`, producing a small Preact-powered widget
- **Web app + Storybook**: Continue using real React with no changes
## Features
- **React Developer Experience**: Familiar React patterns for all contributors
- **Type Safety**: Full TypeScript support
- **Testing**: Comprehensive test coverage with Vitest
- **Single Component Codebase**: Same UI code works everywhere
## File Structure
```text
packages/ui/
├── src/
│ ├── components/ # React survey components
│ │ ├── buttons/ # Survey navigation buttons
│ │ ├── general/ # Core survey components
│ │ ├── i18n/ # i18n provider component
│ │ ├── icons/ # Icon components
│ │ ├── questions/ # Question type components
│ │ └── wrappers/ # Layout wrappers
│ ├── lib/ # Utilities and helpers
│ ├── styles/ # CSS styles
│ └── types/ # TypeScript types
└── package.json
```
## Development
### Scripts
- `pnpm dev` - Start development build with watch mode
- `pnpm build` - Build for production
- `pnpm test` - Run tests
- `pnpm test:coverage` - Run tests with coverage
- `pnpm lint` - Lint and fix code
## Usage
### In React Applications
```tsx
import { SurveyComponent } from "@formbricks/ui";
function App() {
return <SurveyComponent {...props} />;
}
```
### In Storybook
Components from this package are automatically available in Storybook for visual testing and documentation.
### In Embed Bundle
The `@formbricks/surveys` package imports from this package and compiles it to Preact for lightweight embeds.

63
packages/ui/package.json Normal file
View File

@@ -0,0 +1,63 @@
{
"name": "@formbricks/ui",
"license": "MIT",
"version": "1.0.0",
"private": true,
"description": "Formbricks UI components - React-authored survey UI components for use in web app, Storybook, and embed bundle.",
"homepage": "https://formbricks.com",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/formbricks/formbricks"
},
"sideEffects": false,
"source": "src/index.ts",
"main": "dist/index.umd.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.umd.cjs"
}
},
"scripts": {
"dev": "vite build --watch --mode dev",
"build": "tsc && vite build",
"build:dev": "tsc && vite build --mode dev",
"go": "vite build --watch --mode dev",
"lint": "eslint src --fix --ext .ts,.js,.tsx,.jsx",
"preview": "vite preview",
"clean": "rimraf .turbo node_modules dist",
"test": "vitest run",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"react": "19.1.0"
},
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*",
"@formbricks/i18n-utils": "workspace:*",
"@formbricks/types": "workspace:*",
"@testing-library/react": "16.3.0",
"@types/react": "19.1.4",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "10.4.21",
"postcss": "8.5.3",
"tailwindcss": "3.4.17",
"terser": "5.39.1",
"vite": "6.4.1",
"vite-plugin-dts": "4.5.3",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.1.3"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}

View File

@@ -0,0 +1,23 @@
import { cn } from "../../lib/utils";
interface BackButtonProps {
onClick: () => void;
backButtonLabel?: string;
tabIndex?: number;
className?: string;
}
export function BackButton({ onClick, backButtonLabel, tabIndex = 2 }: BackButtonProps) {
return (
<button
dir="auto"
tabIndex={tabIndex}
type="button"
className={cn(
"hover:bg-input-bg text-heading focus:ring-focus rounded-custom mb-1 flex items-center px-3 py-3 text-base font-medium leading-4 focus:outline-none focus:ring-2 focus:ring-offset-2"
)}
onClick={onClick}>
{backButtonLabel}
</button>
);
}

View File

@@ -0,0 +1,100 @@
import { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { BackButton } from "./back-button";
const meta: Meta<typeof BackButton> = {
title: "Survey Core/Common/BackButton",
component: BackButton,
tags: ["autodocs"],
parameters: {
layout: "centered",
controls: { sort: "alpha" },
docs: {
description: {
component:
"The **BackButton** component is used in surveys to allow users to navigate to the previous question. It supports internationalization and custom labels.",
},
},
},
argTypes: {
onClick: {
action: "clicked",
description: "Click handler function",
table: {
category: "Behavior",
type: { summary: "() => void" },
},
},
backButtonLabel: {
control: "text",
description:
"Custom label for the back button. If not provided, uses the translated 'common.back' key.",
table: {
category: "Content",
type: { summary: "string" },
},
},
tabIndex: {
control: "number",
description: "Tab index for keyboard navigation",
table: {
category: "Accessibility",
type: { summary: "number" },
defaultValue: { summary: "2" },
},
},
className: {
control: "text",
description: "Additional CSS classes",
table: {
category: "Appearance",
type: { summary: "string" },
},
},
},
args: { onClick: fn() },
};
export default meta;
type Story = StoryObj<typeof BackButton>;
export const Default: Story = {
args: {
onClick: fn(),
},
parameters: {
docs: {
description: {
story: "Default back button using the translated 'common.back' text.",
},
},
},
};
export const CustomLabel: Story = {
args: {
onClick: fn(),
backButtonLabel: "Go Back",
},
parameters: {
docs: {
description: {
story: "Back button with a custom label instead of the translated text.",
},
},
},
};
export const CustomStyling: Story = {
args: {
onClick: fn(),
className: "bg-blue-500 hover:bg-blue-600 text-white",
},
parameters: {
docs: {
description: {
story: "Back button with custom styling applied via className prop.",
},
},
},
};

13
packages/ui/src/index.ts Normal file
View File

@@ -0,0 +1,13 @@
// @formbricks/ui
// React-authored survey UI components
//
// This package exports React components that can be used in:
// - Storybook (React)
// - Next.js web app (React)
// - Embed bundle (compiled to Preact via @formbricks/surveys)
// Common components
export { BackButton } from "./components/common/back-button";
// Utilities
export { cn } from "./lib/utils";

View File

@@ -0,0 +1,7 @@
/**
* Utility function to combine class names
* Filters out falsy values and joins them with spaces
*/
export const cn = (...classes: (string | undefined | null | false)[]): string => {
return classes.filter(Boolean).join(" ");
};

15
packages/ui/tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"allowImportingTsExtensions": true,
"baseUrl": ".",
"isolatedModules": true,
"jsx": "react-jsx",
"noEmit": true,
"paths": {
"@/*": ["./src/*"]
},
"resolveJsonModule": true
},
"extends": "@formbricks/config-typescript/react-library.json",
"include": ["src", "../types/surveys.d.ts"]
}

View File

@@ -0,0 +1,63 @@
import react from "@vitejs/plugin-react";
import { dirname, resolve } from "path";
import { fileURLToPath } from "url";
import { loadEnv } from "vite";
import dts from "vite-plugin-dts";
import tsconfigPaths from "vite-tsconfig-paths";
import { defineConfig } from "vitest/config";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const config = ({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
return defineConfig({
test: {
environment: "jsdom",
environmentMatchGlobs: [
["**/*.test.tsx", "jsdom"],
["**/lib/**/*.test.ts", "jsdom"],
],
setupFiles: ["./vitestSetup.ts"],
exclude: ["dist/**", "node_modules/**"],
env: env,
coverage: {
provider: "v8",
reporter: ["text", "html", "lcov"],
reportsDirectory: "./coverage",
include: ["src/lib/**/*.ts"],
exclude: ["**/*.tsx"],
},
},
define: {
"process.env.NODE_ENV": JSON.stringify(mode),
},
build: {
emptyOutDir: false,
minify: "terser",
rollupOptions: {
// Externalize node-html-parser to keep bundle size small (~53KB)
// It's pulled in via @formbricks/types but not used in browser runtime
external: ["node-html-parser"],
output: {
inlineDynamicImports: true,
},
},
lib: {
entry: resolve(__dirname, "src/index.ts"),
name: "formbricksSurveyCore",
formats: ["es", "umd"],
fileName: "index",
},
},
plugins: [
react(),
dts({ rollupTypes: true }),
tsconfigPaths(),
],
});
};
export default config;

View File

@@ -0,0 +1,11 @@
import * as matchers from "@testing-library/jest-dom/matchers";
import { cleanup } from "@testing-library/react";
import { afterEach, expect } from "vitest";
// Extend Vitest's expect with jest-dom matchers
expect.extend(matchers);
// Cleanup after each test
afterEach(() => {
cleanup();
});

58
pnpm-lock.yaml generated
View File

@@ -803,6 +803,9 @@ importers:
'@calcom/embed-snippet':
specifier: 1.3.3
version: 1.3.3
'@formbricks/ui':
specifier: workspace:*
version: link:../ui
'@formkit/auto-animate':
specifier: 0.8.2
version: 0.8.2
@@ -893,6 +896,61 @@ importers:
specifier: workspace:*
version: link:../database
packages/ui:
dependencies:
react:
specifier: 19.1.0
version: 19.1.0
react-dom:
specifier: ^19.0.0
version: 19.1.0(react@19.1.0)
devDependencies:
'@formbricks/config-typescript':
specifier: workspace:*
version: link:../config-typescript
'@formbricks/eslint-config':
specifier: workspace:*
version: link:../config-eslint
'@formbricks/i18n-utils':
specifier: workspace:*
version: link:../i18n-utils
'@formbricks/types':
specifier: workspace:*
version: link:../types
'@testing-library/react':
specifier: 16.3.0
version: 16.3.0(@testing-library/dom@8.20.1)(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@types/react':
specifier: 19.1.4
version: 19.1.4
'@vitejs/plugin-react':
specifier: ^4.3.4
version: 4.4.1(vite@6.4.1(@types/node@22.15.18)(jiti@2.4.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.1))
autoprefixer:
specifier: 10.4.21
version: 10.4.21(postcss@8.5.3)
postcss:
specifier: 8.5.3
version: 8.5.3
tailwindcss:
specifier: 3.4.17
version: 3.4.17(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.8.3))
terser:
specifier: 5.39.1
version: 5.39.1
vite:
specifier: 6.4.1
version: 6.4.1(@types/node@22.15.18)(jiti@2.4.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.1)
vite-plugin-dts:
specifier: 4.5.3
version: 4.5.3(@types/node@22.15.18)(rollup@4.52.5)(typescript@5.8.3)(vite@6.4.1(@types/node@22.15.18)(jiti@2.4.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.1))
vite-tsconfig-paths:
specifier: 5.1.4
version: 5.1.4(typescript@5.8.3)(vite@6.4.1(@types/node@22.15.18)(jiti@2.4.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.1))
vitest:
specifier: 3.1.3
version: 3.1.3(@types/node@22.15.18)(jiti@2.4.2)(jsdom@26.1.0)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.1)
packages/vite-plugins:
devDependencies:
'@formbricks/config-typescript':

View File

@@ -2,16 +2,16 @@ sonar.projectKey=formbricks_formbricks
sonar.organization=formbricks
# Sources
sonar.sources=apps/web,packages/surveys,packages/js-core,packages/cache,packages/storage
sonar.sources=apps/web,packages/surveys,packages/ui,packages/js-core,packages/cache,packages/storage
sonar.exclusions=**/node_modules/**,**/.next/**,**/dist/**,**/build/**,**/*.test.*,**/*.spec.*,**/__mocks__/**
# Tests
sonar.tests=apps/web,packages/surveys,packages/js-core,packages/cache,packages/storage
sonar.tests=apps/web,packages/surveys,packages/ui,packages/js-core,packages/cache,packages/storage
sonar.test.inclusions=**/*.test.ts,**/*.spec.ts
sonar.javascript.lcov.reportPaths=apps/web/coverage/lcov.info,packages/surveys/coverage/lcov.info,packages/js-core/coverage/lcov.info,packages/cache/coverage/lcov.info,packages/storage/coverage/lcov.info
sonar.javascript.lcov.reportPaths=apps/web/coverage/lcov.info,packages/surveys/coverage/lcov.info,packages/ui/coverage/lcov.info,packages/js-core/coverage/lcov.info,packages/cache/coverage/lcov.info,packages/storage/coverage/lcov.info
# TypeScript configuration
sonar.typescript.tsconfigPath=apps/web/tsconfig.json,packages/surveys/tsconfig.json,packages/js-core/tsconfig.json,packages/cache/tsconfig.json,packages/storage/tsconfig.json
sonar.typescript.tsconfigPath=apps/web/tsconfig.json,packages/surveys/tsconfig.json,packages/ui/tsconfig.json,packages/js-core/tsconfig.json,packages/cache/tsconfig.json,packages/storage/tsconfig.json
# SCM
sonar.scm.provider=git

View File

@@ -87,6 +87,19 @@
"dependsOn": ["@formbricks/surveys#build"],
"persistent": true
},
"@formbricks/ui#build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"@formbricks/ui#build:dev": {
"dependsOn": ["^build:dev", "@formbricks/i18n-utils#build"],
"outputs": ["dist/**"]
},
"@formbricks/ui#go": {
"cache": false,
"dependsOn": ["@formbricks/ui#build"],
"persistent": true
},
"@formbricks/web#go": {
"cache": false,
"dependsOn": ["@formbricks/database#db:setup"],