aii-form-renderer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,514 @@
1
+ # form-preview
2
+
3
+ React 表单渲染器库 - 专注于在业务中直接使用配置好的表单
4
+
5
+ ## 🎯 项目定位
6
+
7
+ 本库已精简为**纯业务表单渲染器**,移除了所有编辑器/设计器相关组件,专注于:
8
+ - ✅ 在业务系统中渲染已配置好的表单
9
+ - ✅ 提供稳定、高性能的表单展示和数据收集
10
+ - ✅ 支持复杂的表单逻辑和验证
11
+ - ❌ 不再提供可视化表单设计/编辑功能
12
+
13
+ ## ✨ 核心特性
14
+
15
+ - 🖥️ **表单渲染器** - 高性能的表单运行时渲染引擎
16
+ - 📱 **响应式设计** - 支持桌面和移动端展示模式
17
+ - 🎨 **主题定制** - 灵活的样式和主题配置
18
+ - 🌐 **国际化** - 内置中英文支持
19
+ - 🔄 **条件逻辑** - 支持字段间的动态显示/隐藏逻辑
20
+ - ✅ **表单验证** - 完整的表单验证支持
21
+ - 🧩 **30+ 组件类型** - 丰富的表单元素支持
22
+
23
+ ## 📦 安装
24
+
25
+ ```bash
26
+ npm install form-preview
27
+ # 或
28
+ pnpm add form-preview
29
+ # 或
30
+ yarn add form-preview
31
+ ```
32
+
33
+ ### 依赖要求
34
+
35
+ 确保项目中已安装以下依赖:
36
+
37
+ ```bash
38
+ npm install react react-dom antd dayjs
39
+ ```
40
+
41
+ ### 字体资源
42
+
43
+ 在 HTML 中添加 Material Symbols 字体:
44
+
45
+ ```html
46
+ <link
47
+ href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght@300&display=swap"
48
+ rel="stylesheet"
49
+ />
50
+ ```
51
+
52
+ ## 🚀 快速开始
53
+
54
+ ### 基础使用
55
+
56
+ ```tsx
57
+ import { FormRenderer } from 'form-preview';
58
+ import type { FormSchema } from 'form-preview';
59
+ import 'form-preview/styles.css';
60
+
61
+ function MyForm() {
62
+ // 从配置文件或 API 加载表单 Schema
63
+ const formSchema: FormSchema = {
64
+ elements: {
65
+ 'name': {
66
+ id: 'name',
67
+ type: 'TEXT_INPUT',
68
+ label: '姓名',
69
+ fieldName: 'name',
70
+ placeholder: '请输入姓名',
71
+ props: { required: true },
72
+ },
73
+ 'email': {
74
+ id: 'email',
75
+ type: 'EMAIL',
76
+ label: '邮箱',
77
+ fieldName: 'email',
78
+ placeholder: '请输入邮箱',
79
+ props: { required: true },
80
+ },
81
+ },
82
+ rootIds: ['name', 'email'],
83
+ conditions: [],
84
+ formSettings: {
85
+ density: 'normal',
86
+ maxWidth: '800px',
87
+ labelPlacement: 'top',
88
+ primaryColor: '#1890ff',
89
+ backgroundColor: '#ffffff',
90
+ backgroundOpacity: 100,
91
+ fontFamily: 'Inter',
92
+ borderRadius: 'md',
93
+ },
94
+ };
95
+
96
+ const handleSubmit = (values: Record<string, unknown>) => {
97
+ console.log('表单提交:', values);
98
+ // 处理表单提交逻辑
99
+ };
100
+
101
+ return (
102
+ <FormRenderer
103
+ schema={formSchema}
104
+ locale="zh"
105
+ onSubmit={handleSubmit}
106
+ />
107
+ );
108
+ }
109
+ ```
110
+
111
+ ### FormRenderer Props
112
+
113
+ | 属性 | 类型 | 默认值 | 说明 |
114
+ |------|------|--------|------|
115
+ | schema | FormSchema | - | 必需,表单配置 |
116
+ | locale | 'en' \| 'zh' | 'en' | 语言设置 |
117
+ | initialValues | Record<string, unknown> | {} | 表单初始值 |
118
+ | onChange | (values) => void | - | 表单值变化回调 |
119
+ | onSubmit | (values) => void | - | 表单提交回调 |
120
+ | readOnly | boolean | false | 只读模式 |
121
+ | mode | 'desktop' \| 'mobile' | 'desktop' | 显示模式 |
122
+ | className | string | - | 自定义样式类名 |
123
+
124
+ ## 📖 使用指南
125
+
126
+ ### 从 JSON 文件加载表单
127
+
128
+ ```tsx
129
+ import formSchema from './schemas/contact-form.json';
130
+
131
+ <FormRenderer schema={formSchema} locale="zh" />
132
+ ```
133
+
134
+ ### 从 API 动态加载
135
+
136
+ ```tsx
137
+ const [schema, setSchema] = useState<FormSchema | null>(null);
138
+
139
+ useEffect(() => {
140
+ fetch('/api/forms/contact-form')
141
+ .then(res => res.json())
142
+ .then(setSchema);
143
+ }, []);
144
+
145
+ if (!schema) return <div>加载中...</div>;
146
+
147
+ return <FormRenderer schema={schema} locale="zh" />;
148
+ ```
149
+
150
+ ### 带预览的模态框
151
+
152
+ ```tsx
153
+ import { PreviewModal } from 'form-preview';
154
+
155
+ function App() {
156
+ const [visible, setVisible] = useState(false);
157
+
158
+ return (
159
+ <>
160
+ <button onClick={() => setVisible(true)}>
161
+ 预览表单
162
+ </button>
163
+
164
+ <PreviewModal
165
+ visible={visible}
166
+ onClose={() => setVisible(false)}
167
+ schema={formSchema}
168
+ locale="zh"
169
+ />
170
+ </>
171
+ );
172
+ }
173
+ ```
174
+
175
+ ## 📚 文档
176
+
177
+ - [完整使用指南](./USAGE.md) - 详细的 API 文档和使用示例
178
+ - [示例代码](./example/) - 完整的示例项目
179
+
180
+ ## 🎨 支持的表单元素
181
+
182
+ ### 布局元素
183
+ - Section Header - 章节标题
184
+ - Grid (2/3/自定义列) - 网格布局
185
+ - Tabs - 标签页
186
+ - Accordion - 折叠面板
187
+ - Wizard - 多步骤向导
188
+ - Divider - 分隔线
189
+ - Rich Text - HTML 内容
190
+
191
+ ### 输入元素
192
+ - Text Input - 文本输入
193
+ - Number Input - 数字输入
194
+ - Email - 邮箱输入
195
+ - Password - 密码输入
196
+ - Textarea - 多行文本
197
+ - Select - 下拉选择
198
+ - Multi-Select - 多选下拉
199
+ - Radio Group - 单选组
200
+ - Checkbox Group - 多选组
201
+ - Switch - 开关
202
+ - Slider - 滑块
203
+
204
+ ### 高级元素
205
+ - Date Picker - 日期选择
206
+ - Time Picker - 时间选择
207
+ - DateTime Picker - 日期时间选择
208
+ - File Upload - 文件上传
209
+ - Rating - 星级评分
210
+ - Signature - 签名板
211
+ - Cascader - 级联选择
212
+
213
+ ## 🔧 FormSchema 结构
214
+
215
+ ```typescript
216
+ interface FormSchema {
217
+ elements: Record<string, FormElement>; // 表单元素定义
218
+ rootIds: string[]; // 根级元素 ID
219
+ conditions: ConditionRule[]; // 条件规则
220
+ formSettings: FormSettings; // 全局设置
221
+ }
222
+ ```
223
+
224
+ 详细的类型定义请参考 [USAGE.md](./USAGE.md)
225
+
226
+ ## 💡 示例
227
+
228
+ 查看 [example](./example/) 目录获取完整的使用示例:
229
+ - 联系表单示例
230
+ - 动态数据加载
231
+ - 表单验证
232
+ - 条件逻辑
233
+
234
+ ## 🛠️ 技术栈
235
+
236
+ - React 18+
237
+ - TypeScript
238
+ - Ant Design 5.x
239
+ - Tailwind CSS
240
+ - Day.js
241
+
242
+ ## 📄 License
243
+
244
+ MIT
245
+
246
+ ## 🙋 常见问题
247
+
248
+ ### 1. 如何获取表单的 Schema 配置?
249
+
250
+ 表单 Schema 通常由表单设计器生成,或者手动编写 JSON 配置文件。本库专注于渲染已配置好的表单。
251
+
252
+ ### 2. 是否支持自定义表单元素?
253
+
254
+ 当前版本主要支持内置的 30+ 种元素类型。后续版本可能会支持自定义元素扩展。
255
+
256
+ ### 3. 如何处理文件上传?
257
+
258
+ 文件上传元素需要配置 uploadUrl 和相关参数。具体请参考 [USAGE.md](./USAGE.md)。
259
+
260
+ ### 4. 是否支持移动端?
261
+
262
+ 是的,通过设置 `mode="mobile"` 可以启用移动端优化的显示模式。
263
+
264
+ ### 5. 条件逻辑如何工作?
265
+
266
+ 通过配置 `conditions` 数组,可以实现基于字段值的动态显示/隐藏/启用/禁用逻辑。详见文档。
267
+
268
+ function App() {
269
+ const handleChange = (schema: FormSchema) => {
270
+ console.log('Schema updated:', schema);
271
+ };
272
+
273
+ const handleSave = (schema: FormSchema) => {
274
+ // Save to backend
275
+ saveFormSchema(schema);
276
+ };
277
+
278
+ return (
279
+ <FormDesigner
280
+ locale="en" // or "zh"
281
+ onChange={handleChange}
282
+ onSave={handleSave}
283
+ />
284
+ );
285
+ }
286
+ ```
287
+
288
+ ### Form Renderer
289
+
290
+ ```tsx
291
+ import { FormRenderer, FormSchema } from 'form-preview';
292
+ import 'form-preview/style.css';
293
+
294
+ function FormPage({ schema }: { schema: FormSchema }) {
295
+ const handleSubmit = (data: Record<string, unknown>) => {
296
+ console.log('Form submitted:', data);
297
+ };
298
+
299
+ return (
300
+ <FormRenderer
301
+ schema={schema}
302
+ locale="en"
303
+ onSubmit={handleSubmit}
304
+ />
305
+ );
306
+ }
307
+ ```
308
+
309
+ ### Preview Modal
310
+
311
+ ```tsx
312
+ import { useState } from 'react';
313
+ import { PreviewModal, FormSchema } from 'form-preview';
314
+ import 'form-preview/style.css';
315
+
316
+ function FormWithPreview({ schema }: { schema: FormSchema }) {
317
+ const [showPreview, setShowPreview] = useState(false);
318
+
319
+ return (
320
+ <>
321
+ <button onClick={() => setShowPreview(true)}>Preview</button>
322
+ <PreviewModal
323
+ open={showPreview}
324
+ onClose={() => setShowPreview(false)}
325
+ schema={schema}
326
+ locale="en"
327
+ />
328
+ </>
329
+ );
330
+ }
331
+ ```
332
+
333
+ ## API Reference
334
+
335
+ ### FormDesigner
336
+
337
+ | Prop | Type | Default | Description |
338
+ |------|------|---------|-------------|
339
+ | `locale` | `'en' \| 'zh'` | `'en'` | UI language |
340
+ | `initialSchema` | `FormSchema` | - | Initial form schema |
341
+ | `onChange` | `(schema: FormSchema) => void` | - | Called when schema changes |
342
+ | `onSave` | `(schema: FormSchema) => void` | - | Called when save button clicked |
343
+ | `onPreview` | `() => void` | - | Called when preview button clicked |
344
+ | `className` | `string` | - | Additional CSS classes |
345
+
346
+ ### FormRenderer
347
+
348
+ | Prop | Type | Default | Description |
349
+ |------|------|---------|-------------|
350
+ | `schema` | `FormSchema` | **required** | Form schema to render |
351
+ | `locale` | `'en' \| 'zh'` | `'en'` | UI language |
352
+ | `initialValues` | `Record<string, unknown>` | `{}` | Initial form values |
353
+ | `onChange` | `(values) => void` | - | Called when any value changes |
354
+ | `onSubmit` | `(values) => void` | - | Called on form submit |
355
+ | `readOnly` | `boolean` | `false` | Disable all inputs |
356
+ | `mode` | `'desktop' \| 'mobile'` | `'desktop'` | Responsive mode |
357
+ | `className` | `string` | - | Additional CSS classes |
358
+
359
+ ### PreviewModal
360
+
361
+ | Prop | Type | Default | Description |
362
+ |------|------|---------|-------------|
363
+ | `open` | `boolean` | **required** | Modal visibility |
364
+ | `onClose` | `() => void` | **required** | Close handler |
365
+ | `schema` | `FormSchema` | **required** | Form schema to preview |
366
+ | `locale` | `'en' \| 'zh'` | `'en'` | UI language |
367
+
368
+ ## Supported Element Types
369
+
370
+ ### Layout Elements (9)
371
+ - Section Header
372
+ - 2-Column Grid
373
+ - 3-Column Grid
374
+ - Custom Grid
375
+ - Tabs
376
+ - Accordion
377
+ - Wizard (Multi-step)
378
+ - Divider
379
+ - Rich Text
380
+
381
+ ### Input Elements (10)
382
+ - Text Input
383
+ - Number Input
384
+ - Password
385
+ - Email
386
+ - Textarea
387
+ - Select (Dropdown)
388
+ - Multi-Select
389
+ - Radio Group
390
+ - Checkbox Group
391
+ - Switch
392
+
393
+ ### Advanced Elements (8)
394
+ - Slider
395
+ - Date Picker
396
+ - Time Picker
397
+ - DateTime Picker
398
+ - File Upload
399
+ - Rating
400
+ - Signature
401
+ - Cascader
402
+
403
+ ## FormSchema Structure
404
+
405
+ ```typescript
406
+ interface FormSchema {
407
+ rootIds: string[];
408
+ elements: Record<string, FormElement>;
409
+ conditions: ConditionRule[];
410
+ formSettings: FormSettings;
411
+ }
412
+
413
+ interface FormElement {
414
+ id: string;
415
+ type: ElementType;
416
+ label: string;
417
+ placeholder?: string;
418
+ fieldName?: string;
419
+ children?: string[];
420
+ props: FormElementProps;
421
+ }
422
+
423
+ interface FormSettings {
424
+ density: 'compact' | 'normal' | 'comfortable';
425
+ maxWidth: string;
426
+ borderRadius: 'none' | 'small' | 'medium' | 'large' | 'full';
427
+ primaryColor: string;
428
+ backgroundColor: string;
429
+ fontFamily: string;
430
+ }
431
+ ```
432
+
433
+ ## Conditional Logic
434
+
435
+ Define conditions to control element visibility and state:
436
+
437
+ ```typescript
438
+ const schema: FormSchema = {
439
+ // ...elements
440
+ conditions: [
441
+ {
442
+ id: 'cond1',
443
+ sourceId: 'country-select',
444
+ operator: 'equals',
445
+ value: 'US',
446
+ action: 'show',
447
+ targetId: 'state-select'
448
+ }
449
+ ]
450
+ };
451
+ ```
452
+
453
+ ### Operators
454
+ - `equals`: Equals value
455
+ - `not_equals`: Not equals value
456
+ - `contains`: Contains substring
457
+ - `greater_than`: Greater than number
458
+ - `less_than`: Less than number
459
+
460
+ ### Actions
461
+ - `show`: Show target element
462
+ - `hide`: Hide target element
463
+ - `enable`: Enable target element
464
+ - `disable`: Disable target element
465
+
466
+ ## Customization
467
+
468
+ ### Custom Theme Colors
469
+
470
+ ```tsx
471
+ <FormDesigner
472
+ initialSchema={{
473
+ formSettings: {
474
+ primaryColor: '#6366F1',
475
+ backgroundColor: '#F8FAFC',
476
+ // ...
477
+ },
478
+ // ...
479
+ }}
480
+ />
481
+ ```
482
+
483
+ ### Using Sub-Components
484
+
485
+ For custom layouts, import individual components:
486
+
487
+ ```tsx
488
+ import { Sidebar, Canvas, Inspector, EditorState } from 'form-preview';
489
+
490
+ function CustomDesigner() {
491
+ const [state, setState] = useState<EditorState>({...});
492
+
493
+ return (
494
+ <div className="flex">
495
+ <Sidebar locale="en" onDragStart={...} />
496
+ <Canvas
497
+ rootIds={state.rootIds}
498
+ elements={state.elements}
499
+ selectedId={state.selectedId}
500
+ // ...
501
+ />
502
+ <Inspector
503
+ element={state.elements[state.selectedId]}
504
+ onChange={...}
505
+ // ...
506
+ />
507
+ </div>
508
+ );
509
+ }
510
+ ```
511
+
512
+ ## License
513
+
514
+ MIT © Your Name
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media(min-width:640px){.container{max-width:640px}}@media(min-width:768px){.container{max-width:768px}}@media(min-width:1024px){.container{max-width:1024px}}@media(min-width:1280px){.container{max-width:1280px}}@media(min-width:1536px){.container{max-width:1536px}}.visible{visibility:visible}.static{position:static}.absolute{position:absolute}.relative{position:relative}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.break-all{word-break:break-all}.border{border-width:1px}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.form-preview-root{--form-primary-color: #3b82f6;--form-primary-hover: #2563eb;--form-border-radius: .375rem;font-family:Inter,system-ui,sans-serif}.form-theme-scope{--theme-primary: var(--form-primary-color)}.form-theme-scope .text-primary{color:var(--theme-primary)!important}.form-theme-scope .bg-primary{background-color:var(--theme-primary)!important}.form-theme-scope .border-primary{border-color:var(--theme-primary)!important}.form-theme-scope .ring-primary{--tw-ring-color: var(--theme-primary) !important}.form-theme-scope .accent-primary{accent-color:var(--theme-primary)!important}.material-symbols-outlined{font-family:Material Symbols Outlined;font-weight:400;font-style:normal;font-size:24px;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-feature-settings:"liga";-webkit-font-smoothing:antialiased}.canvas-bg{background-color:#f8fafc;background-image:radial-gradient(circle,#e2e8f0 1px,transparent 1px);background-size:20px 20px}.form-preview-root ::-webkit-scrollbar{width:6px;height:6px}.form-preview-root ::-webkit-scrollbar-track{background:transparent}.form-preview-root ::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:3px}.form-preview-root ::-webkit-scrollbar-thumb:hover{background:#94a3b8}.element-selected{outline:2px solid var(--theme-primary, #3b82f6);outline-offset:2px}.drop-indicator{height:3px;background:var(--theme-primary, #3b82f6);border-radius:2px;animation:pulse 1s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.dragging{opacity:.5}.form-field:focus-within{border-color:var(--theme-primary, #3b82f6);box-shadow:0 0 0 3px #3b82f61a}.btn{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;border-radius:.5rem;padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem;font-weight:500;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.btn:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-offset-width: 2px}.btn-primary{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.btn-primary:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.btn-primary:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.btn-secondary{border-width:1px;--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity, 1))}.btn-secondary:hover{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity, 1))}.btn-secondary:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(100 116 139 / var(--tw-ring-opacity, 1))}.btn-ghost{--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1))}.btn-ghost:hover{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.btn-ghost:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(100 116 139 / var(--tw-ring-opacity, 1))}.input{width:100%;border-radius:.5rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));padding:.5rem .75rem;font-size:.875rem;line-height:1.25rem}.input::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(148 163 184 / var(--tw-placeholder-opacity, 1))}.input::placeholder{--tw-placeholder-opacity: 1;color:rgb(148 163 184 / var(--tw-placeholder-opacity, 1))}.input:focus{border-color:transparent;outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.input-error{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.input-error:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity, 1))}.signature-canvas{touch-action:none}.rating-star{cursor:pointer;transition:transform .1s ease}.rating-star:hover{transform:scale(1.1)}.mobile-frame{width:375px;border-radius:40px;border:8px solid #1f2937;position:relative;overflow:hidden}.mobile-frame:before{content:"";position:absolute;top:0;left:50%;transform:translate(-50%);width:120px;height:24px;background:#1f2937;border-radius:0 0 16px 16px;z-index:10}