@magnet-cms/plugin-playground 2.0.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.
@@ -0,0 +1,322 @@
1
+ import { PageContent, PageHeader, useSchema } from '@magnet-cms/admin'
2
+ import { Button, Spinner } from '@magnet-cms/ui/components'
3
+ import {
4
+ AlertCircle,
5
+ Boxes,
6
+ Code2,
7
+ FileJson,
8
+ Info,
9
+ Lock,
10
+ PencilRuler,
11
+ Rocket,
12
+ Settings2,
13
+ } from 'lucide-react'
14
+ import { createContext, useContext, useState } from 'react'
15
+ import { useParams } from 'react-router-dom'
16
+
17
+ // Context to track if schema is read-only (existing code-defined schema)
18
+ const ReadOnlyContext = createContext(false)
19
+ import {
20
+ SchemaBuilderContext,
21
+ useSchemaBuilder,
22
+ useSchemaBuilderState,
23
+ } from '../hooks/useSchemaBuilder'
24
+ import type { SchemaBuilderState, SchemaField } from '../types/builder.types'
25
+ import { DEFAULT_BUILDER_STATE } from '../types/builder.types'
26
+ import { AddFieldDialog } from './AddFieldDialog'
27
+ import { CodePreview } from './CodePreview'
28
+ import { FieldList } from './FieldList'
29
+ import { FieldSettingsPanel } from './FieldSettingsPanel'
30
+ import { SchemaList } from './SchemaList'
31
+ import { SchemaOptionsDialog } from './SchemaOptionsDialog'
32
+
33
+ /**
34
+ * Transform SchemaMetadata from discovery API to builder state
35
+ */
36
+ function schemaMetadataToBuilderState(
37
+ name: string,
38
+ metadata: any,
39
+ ): SchemaBuilderState {
40
+ return {
41
+ schema: {
42
+ name: name.charAt(0).toUpperCase() + name.slice(1),
43
+ apiId: name,
44
+ versioning: metadata.options?.versioning ?? true,
45
+ i18n: metadata.options?.i18n ?? true,
46
+ },
47
+ fields:
48
+ metadata.properties?.map((prop: any, index: number) => ({
49
+ id: `field_${index}_${prop.name}`,
50
+ name: prop.name,
51
+ displayName: prop.ui?.label || prop.name,
52
+ type: inferFieldType(prop),
53
+ tsType: prop.type || 'string',
54
+ prop: {
55
+ required: prop.required || false,
56
+ unique: prop.unique || false,
57
+ intl: prop.intl || false,
58
+ hidden: prop.hidden || false,
59
+ readonly: prop.readonly || false,
60
+ },
61
+ ui: prop.ui || {},
62
+ validations: (prop.validations || []).map((v: any) => ({
63
+ type: v.name || v.type,
64
+ constraints: v.constraints,
65
+ })),
66
+ })) || [],
67
+ selectedFieldId: null,
68
+ viewMode: 'builder',
69
+ isDirty: false,
70
+ lastSaved: null,
71
+ isNew: false,
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Infer field type from property metadata
77
+ */
78
+ function inferFieldType(prop: any): SchemaField['type'] {
79
+ const uiType = prop.ui?.type
80
+ if (uiType === 'switch' || uiType === 'checkbox') return 'boolean'
81
+ if (uiType === 'date') return 'date'
82
+ if (uiType === 'number') return 'number'
83
+ if (uiType === 'select' || uiType === 'radio') return 'select'
84
+ if (uiType === 'relationship') return 'relation'
85
+
86
+ // prop.type can be a string, object, or array - only process if string
87
+ const tsType = typeof prop.type === 'string' ? prop.type.toLowerCase() : null
88
+ if (tsType === 'number') return 'number'
89
+ if (tsType === 'boolean') return 'boolean'
90
+ if (tsType === 'date') return 'date'
91
+
92
+ return 'text'
93
+ }
94
+
95
+ export function SchemaPlaygroundEditor() {
96
+ const { schemaName } = useParams<{ schemaName: string }>()
97
+ const isNewSchema = !schemaName || schemaName === 'new'
98
+
99
+ // For existing schemas, fetch metadata
100
+ const {
101
+ data: schemaMetadata,
102
+ isLoading,
103
+ error,
104
+ } = useSchema(schemaName && schemaName !== 'new' ? schemaName : undefined)
105
+
106
+ if (!isNewSchema && isLoading) {
107
+ return (
108
+ <div className="flex justify-center items-center h-64">
109
+ <Spinner />
110
+ </div>
111
+ )
112
+ }
113
+
114
+ if (!isNewSchema && error) {
115
+ return (
116
+ <div className="p-4 border-l-4 border-red-500 bg-red-50 text-red-700 rounded m-8">
117
+ <h3 className="font-medium flex items-center gap-2">
118
+ <AlertCircle className="h-4 w-4" />
119
+ Error loading schema
120
+ </h3>
121
+ <p className="text-sm mt-1">{error.message}</p>
122
+ </div>
123
+ )
124
+ }
125
+
126
+ // If editing existing schema, transform metadata to builder state
127
+ const initialState =
128
+ !isNewSchema && schemaMetadata && schemaName && schemaName !== 'new'
129
+ ? schemaMetadataToBuilderState(schemaName, schemaMetadata)
130
+ : { ...DEFAULT_BUILDER_STATE, isNew: true }
131
+
132
+ // TODO: In the future, we need to track schema source (plugin/core vs user-defined)
133
+ // to determine if a schema should be read-only. For now, all schemas are editable.
134
+ // Plugin and core schemas should be read-only once we have a way to identify them.
135
+ const isReadOnly = false
136
+
137
+ return (
138
+ <SchemaEditorContentWithState
139
+ key={schemaName || 'new'}
140
+ initialState={initialState}
141
+ isReadOnly={isReadOnly}
142
+ />
143
+ )
144
+ }
145
+
146
+ function SchemaEditorContentWithState({
147
+ initialState,
148
+ isReadOnly,
149
+ }: {
150
+ initialState: SchemaBuilderState
151
+ isReadOnly: boolean
152
+ }) {
153
+ const context = useSchemaBuilderState(initialState)
154
+
155
+ return (
156
+ <ReadOnlyContext.Provider value={isReadOnly}>
157
+ <SchemaBuilderContext.Provider value={context}>
158
+ <SchemaEditorInner />
159
+ </SchemaBuilderContext.Provider>
160
+ </ReadOnlyContext.Provider>
161
+ )
162
+ }
163
+
164
+ function SchemaEditorInner() {
165
+ const { state, setViewMode, updateSchema, generatedCode } = useSchemaBuilder()
166
+ const isReadOnly = useContext(ReadOnlyContext)
167
+ const [addFieldOpen, setAddFieldOpen] = useState(false)
168
+ const [optionsOpen, setOptionsOpen] = useState(false)
169
+
170
+ const handleDeploy = async () => {
171
+ // TODO: Implement actual deployment
172
+ alert(
173
+ `Deploy functionality will be implemented in the backend phase.\n\nGenerated code:\n\n${generatedCode}`,
174
+ )
175
+ }
176
+
177
+ const statusBadges: {
178
+ type: 'warning' | 'success' | 'error' | 'info'
179
+ label: string
180
+ dot?: boolean
181
+ }[] = []
182
+ if (isReadOnly) {
183
+ statusBadges.push({
184
+ type: 'info',
185
+ label: 'Read-only',
186
+ })
187
+ } else if (state.isDirty) {
188
+ statusBadges.push({
189
+ type: 'warning',
190
+ label: 'Unsaved changes',
191
+ dot: true,
192
+ })
193
+ }
194
+ if (!isReadOnly && state.lastSaved) {
195
+ statusBadges.push({ type: 'success', label: 'Saved', dot: true })
196
+ }
197
+
198
+ return (
199
+ <div className="flex flex-col h-full">
200
+ <PageHeader
201
+ status={statusBadges.length > 0 ? statusBadges : undefined}
202
+ icon={isReadOnly ? Lock : Boxes}
203
+ title={
204
+ isReadOnly
205
+ ? state.schema.name || 'Schema'
206
+ : {
207
+ value: state.schema.name,
208
+ onChange: (name) => updateSchema({ name }),
209
+ placeholder: 'SchemaName',
210
+ editable: true as const,
211
+ }
212
+ }
213
+ description={`api::${state.schema.name?.toLowerCase() || 'schema'}.${state.schema.name?.toLowerCase() || 'schema'}`}
214
+ tabs={{
215
+ items: [
216
+ { id: 'builder', label: 'Builder', icon: PencilRuler },
217
+ { id: 'json', label: 'JSON', icon: FileJson },
218
+ { id: 'code', label: 'Code', icon: Code2 },
219
+ ],
220
+ value: state.viewMode,
221
+ onChange: setViewMode,
222
+ }}
223
+ actions={
224
+ isReadOnly ? undefined : (
225
+ <>
226
+ <Button
227
+ variant="outline"
228
+ size="icon"
229
+ className="h-8 w-8"
230
+ onClick={() => setOptionsOpen(true)}
231
+ title="Schema Options"
232
+ >
233
+ <Settings2 className="h-4 w-4" />
234
+ </Button>
235
+ <Button variant="outline" size="sm" disabled>
236
+ Preview API
237
+ </Button>
238
+ <Button
239
+ size="sm"
240
+ onClick={handleDeploy}
241
+ disabled={!state.schema.name}
242
+ >
243
+ <Rocket className="h-4 w-4 mr-2" />
244
+ Deploy Changes
245
+ </Button>
246
+ </>
247
+ )
248
+ }
249
+ />
250
+
251
+ {/* Read-only banner for existing schemas */}
252
+ {isReadOnly && (
253
+ <div className="px-6 py-2 border-b border-border bg-amber-50 dark:bg-amber-950/30 flex items-center gap-2">
254
+ <Lock className="h-3.5 w-3.5 text-amber-600 dark:text-amber-400" />
255
+ <span className="text-xs text-amber-700 dark:text-amber-400">
256
+ This schema is defined in code and cannot be modified here. View the
257
+ generated code in the Code tab to copy and customize.
258
+ </span>
259
+ </div>
260
+ )}
261
+
262
+ {/* Main Content - 2x7x3 grid layout */}
263
+ <div className="flex-1 overflow-hidden grid grid-cols-12 h-full">
264
+ {/* Schema List - 2 columns */}
265
+ <div className="col-span-2 border-r bg-muted/30 overflow-y-auto">
266
+ <SchemaList />
267
+ </div>
268
+
269
+ {/* Editor Content - 7 columns */}
270
+ <div className="col-span-7 overflow-hidden flex flex-col">
271
+ {state.viewMode === 'builder' ? (
272
+ <PageContent className="p-6 overflow-y-auto flex-1">
273
+ <div className="space-y-4">
274
+ {/* Info Alert - only show for new schemas */}
275
+ {!isReadOnly && (
276
+ <div className="p-3 bg-blue-50 border border-blue-100 rounded-lg flex items-start gap-3">
277
+ <Info className="h-4 w-4 text-blue-500 mt-0.5 shrink-0" />
278
+ <div className="text-xs text-blue-900">
279
+ <span className="font-medium">Schema Guidelines:</span>{' '}
280
+ Keep field names in camelCase for better compatibility.
281
+ </div>
282
+ </div>
283
+ )}
284
+
285
+ {/* Field List */}
286
+ <FieldList
287
+ onAddField={
288
+ isReadOnly ? undefined : () => setAddFieldOpen(true)
289
+ }
290
+ readOnly={isReadOnly}
291
+ />
292
+ </div>
293
+ </PageContent>
294
+ ) : (
295
+ /* Code/JSON View */
296
+ <PageContent className="p-6 overflow-y-auto flex-1">
297
+ <CodePreview mode={state.viewMode} />
298
+ </PageContent>
299
+ )}
300
+ </div>
301
+
302
+ {/* Settings Panel - 3 columns */}
303
+ <div className="col-span-3 border-l bg-background overflow-y-auto">
304
+ <FieldSettingsPanel />
305
+ </div>
306
+ </div>
307
+
308
+ {/* Dialogs - only for editable schemas */}
309
+ {!isReadOnly && (
310
+ <>
311
+ <AddFieldDialog open={addFieldOpen} onOpenChange={setAddFieldOpen} />
312
+ <SchemaOptionsDialog
313
+ open={optionsOpen}
314
+ onOpenChange={setOptionsOpen}
315
+ />
316
+ </>
317
+ )}
318
+ </div>
319
+ )
320
+ }
321
+
322
+ export default SchemaPlaygroundEditor
@@ -0,0 +1,384 @@
1
+ import {
2
+ Calendar,
3
+ Hash,
4
+ Link2,
5
+ List,
6
+ type LucideIcon,
7
+ ToggleLeft,
8
+ Type,
9
+ } from 'lucide-react'
10
+ import type {
11
+ FieldType,
12
+ FieldUIConfig,
13
+ ValidationRule,
14
+ } from '../types/builder.types'
15
+
16
+ /**
17
+ * Field type definition for the builder UI
18
+ */
19
+ export interface FieldTypeDefinition {
20
+ id: FieldType
21
+ label: string
22
+ description: string
23
+ icon: LucideIcon
24
+ tsType: string
25
+ defaultValidations: ValidationRule[]
26
+ defaultUI: FieldUIConfig
27
+ hasOptions?: boolean // For select fields
28
+ hasRelationConfig?: boolean // For relation fields
29
+ requiresTransformer?: boolean // For Date fields
30
+ }
31
+
32
+ /**
33
+ * All available field types in the schema builder
34
+ */
35
+ export const FIELD_TYPES: FieldTypeDefinition[] = [
36
+ {
37
+ id: 'text',
38
+ label: 'Text',
39
+ description: 'Short or long text content',
40
+ icon: Type,
41
+ tsType: 'string',
42
+ defaultValidations: [{ type: 'IsString' }, { type: 'IsNotEmpty' }],
43
+ defaultUI: { type: 'text' },
44
+ },
45
+ {
46
+ id: 'number',
47
+ label: 'Number',
48
+ description: 'Integer or decimal numbers',
49
+ icon: Hash,
50
+ tsType: 'number',
51
+ defaultValidations: [{ type: 'IsNumber' }],
52
+ defaultUI: { type: 'number' },
53
+ },
54
+ {
55
+ id: 'date',
56
+ label: 'Date',
57
+ description: 'Date and time values',
58
+ icon: Calendar,
59
+ tsType: 'Date',
60
+ defaultValidations: [{ type: 'IsDate' }],
61
+ defaultUI: { type: 'date' },
62
+ requiresTransformer: true,
63
+ },
64
+ {
65
+ id: 'boolean',
66
+ label: 'Boolean',
67
+ description: 'True or false toggle',
68
+ icon: ToggleLeft,
69
+ tsType: 'boolean',
70
+ defaultValidations: [{ type: 'IsBoolean' }],
71
+ defaultUI: { type: 'switch' },
72
+ },
73
+ {
74
+ id: 'select',
75
+ label: 'Select',
76
+ description: 'Dropdown with predefined options',
77
+ icon: List,
78
+ tsType: 'string',
79
+ defaultValidations: [{ type: 'IsString' }],
80
+ defaultUI: { type: 'select', options: [] },
81
+ hasOptions: true,
82
+ },
83
+ {
84
+ id: 'relation',
85
+ label: 'Relation',
86
+ description: 'Reference to another schema',
87
+ icon: Link2,
88
+ tsType: 'string',
89
+ defaultValidations: [{ type: 'IsString' }],
90
+ defaultUI: { type: 'relationship' },
91
+ hasRelationConfig: true,
92
+ },
93
+ ]
94
+
95
+ /**
96
+ * Get field type definition by ID
97
+ */
98
+ export function getFieldTypeDefinition(
99
+ type: FieldType,
100
+ ): FieldTypeDefinition | undefined {
101
+ return FIELD_TYPES.find((ft) => ft.id === type)
102
+ }
103
+
104
+ /**
105
+ * UI subtype definition for field type variations
106
+ */
107
+ export interface UISubtypeDefinition {
108
+ id: string
109
+ label: string
110
+ description: string
111
+ }
112
+
113
+ /**
114
+ * Available UI subtypes for each base field type.
115
+ * These map to the UITypes in @magnet-cms/common and are rendered by FormBuilder.
116
+ */
117
+ export const UI_SUBTYPES: Record<FieldType, UISubtypeDefinition[]> = {
118
+ text: [
119
+ { id: 'text', label: 'Text Input', description: 'Single line text' },
120
+ { id: 'textarea', label: 'Text Area', description: 'Multi-line text' },
121
+ { id: 'email', label: 'Email', description: 'Email with validation' },
122
+ { id: 'phone', label: 'Phone', description: 'Phone number input' },
123
+ { id: 'richText', label: 'Rich Text', description: 'Rich text editor' },
124
+ ],
125
+ number: [
126
+ {
127
+ id: 'number',
128
+ label: 'Number Input',
129
+ description: 'Standard number field',
130
+ },
131
+ {
132
+ id: 'quantity',
133
+ label: 'Quantity',
134
+ description: 'Number with +/- buttons',
135
+ },
136
+ ],
137
+ date: [
138
+ {
139
+ id: 'date',
140
+ label: 'Date Picker',
141
+ description: 'Calendar date selection',
142
+ },
143
+ ],
144
+ boolean: [
145
+ { id: 'switch', label: 'Switch', description: 'Toggle switch' },
146
+ { id: 'checkbox', label: 'Checkbox', description: 'Checkbox input' },
147
+ ],
148
+ select: [
149
+ { id: 'select', label: 'Dropdown', description: 'Single select dropdown' },
150
+ {
151
+ id: 'multiSelect',
152
+ label: 'Multi Select',
153
+ description: 'Select multiple options',
154
+ },
155
+ { id: 'combobox', label: 'Combobox', description: 'Searchable dropdown' },
156
+ { id: 'radio', label: 'Radio Group', description: 'Radio button options' },
157
+ ],
158
+ relation: [
159
+ {
160
+ id: 'relationship',
161
+ label: 'Relationship',
162
+ description: 'Link to another schema',
163
+ },
164
+ ],
165
+ }
166
+
167
+ /**
168
+ * Get UI subtypes for a field type
169
+ */
170
+ export function getUISubtypes(type: FieldType): UISubtypeDefinition[] {
171
+ return UI_SUBTYPES[type] || []
172
+ }
173
+
174
+ /**
175
+ * Get the icon component for a field type
176
+ */
177
+ export function getFieldTypeIcon(type: FieldType): LucideIcon {
178
+ const definition = getFieldTypeDefinition(type)
179
+ return definition?.icon ?? Type
180
+ }
181
+
182
+ /**
183
+ * Available validation rules for each field type
184
+ */
185
+ export const VALIDATION_RULES_BY_TYPE: Record<FieldType, string[]> = {
186
+ text: [
187
+ 'IsString',
188
+ 'IsNotEmpty',
189
+ 'Length',
190
+ 'MinLength',
191
+ 'MaxLength',
192
+ 'IsEmail',
193
+ 'IsUrl',
194
+ 'IsUUID',
195
+ 'Matches',
196
+ ],
197
+ number: ['IsNumber', 'IsInt', 'IsPositive', 'IsNegative', 'Min', 'Max'],
198
+ date: ['IsDate', 'IsNotEmpty', 'MinDate', 'MaxDate'],
199
+ boolean: ['IsBoolean'],
200
+ select: ['IsString', 'IsNotEmpty', 'IsIn'],
201
+ relation: ['IsString', 'IsNotEmpty', 'IsMongoId'],
202
+ }
203
+
204
+ /**
205
+ * Validation rule definitions with constraint info
206
+ */
207
+ export interface ValidationRuleDefinition {
208
+ type: string
209
+ label: string
210
+ description: string
211
+ constraintCount: number // 0 = no args, 1 = single arg, 2 = two args (min, max)
212
+ constraintLabels?: string[]
213
+ }
214
+
215
+ export const VALIDATION_RULE_DEFINITIONS: ValidationRuleDefinition[] = [
216
+ {
217
+ type: 'IsString',
218
+ label: 'Is String',
219
+ description: 'Must be a string',
220
+ constraintCount: 0,
221
+ },
222
+ {
223
+ type: 'IsNumber',
224
+ label: 'Is Number',
225
+ description: 'Must be a number',
226
+ constraintCount: 0,
227
+ },
228
+ {
229
+ type: 'IsInt',
230
+ label: 'Is Integer',
231
+ description: 'Must be an integer',
232
+ constraintCount: 0,
233
+ },
234
+ {
235
+ type: 'IsBoolean',
236
+ label: 'Is Boolean',
237
+ description: 'Must be true or false',
238
+ constraintCount: 0,
239
+ },
240
+ {
241
+ type: 'IsDate',
242
+ label: 'Is Date',
243
+ description: 'Must be a valid date',
244
+ constraintCount: 0,
245
+ },
246
+ {
247
+ type: 'IsNotEmpty',
248
+ label: 'Not Empty',
249
+ description: 'Cannot be empty',
250
+ constraintCount: 0,
251
+ },
252
+ {
253
+ type: 'IsEmail',
254
+ label: 'Is Email',
255
+ description: 'Must be a valid email',
256
+ constraintCount: 0,
257
+ },
258
+ {
259
+ type: 'IsUrl',
260
+ label: 'Is URL',
261
+ description: 'Must be a valid URL',
262
+ constraintCount: 0,
263
+ },
264
+ {
265
+ type: 'IsUUID',
266
+ label: 'Is UUID',
267
+ description: 'Must be a valid UUID',
268
+ constraintCount: 0,
269
+ },
270
+ {
271
+ type: 'IsMongoId',
272
+ label: 'Is MongoDB ID',
273
+ description: 'Must be a valid MongoDB ObjectId',
274
+ constraintCount: 0,
275
+ },
276
+ {
277
+ type: 'IsPositive',
278
+ label: 'Is Positive',
279
+ description: 'Must be a positive number',
280
+ constraintCount: 0,
281
+ },
282
+ {
283
+ type: 'IsNegative',
284
+ label: 'Is Negative',
285
+ description: 'Must be a negative number',
286
+ constraintCount: 0,
287
+ },
288
+ {
289
+ type: 'Length',
290
+ label: 'Length',
291
+ description: 'String length between min and max',
292
+ constraintCount: 2,
293
+ constraintLabels: ['Min', 'Max'],
294
+ },
295
+ {
296
+ type: 'MinLength',
297
+ label: 'Min Length',
298
+ description: 'Minimum string length',
299
+ constraintCount: 1,
300
+ constraintLabels: ['Min'],
301
+ },
302
+ {
303
+ type: 'MaxLength',
304
+ label: 'Max Length',
305
+ description: 'Maximum string length',
306
+ constraintCount: 1,
307
+ constraintLabels: ['Max'],
308
+ },
309
+ {
310
+ type: 'Min',
311
+ label: 'Minimum',
312
+ description: 'Minimum number value',
313
+ constraintCount: 1,
314
+ constraintLabels: ['Min'],
315
+ },
316
+ {
317
+ type: 'Max',
318
+ label: 'Maximum',
319
+ description: 'Maximum number value',
320
+ constraintCount: 1,
321
+ constraintLabels: ['Max'],
322
+ },
323
+ {
324
+ type: 'Matches',
325
+ label: 'Matches Pattern',
326
+ description: 'Must match regex pattern',
327
+ constraintCount: 1,
328
+ constraintLabels: ['Pattern'],
329
+ },
330
+ {
331
+ type: 'IsIn',
332
+ label: 'Is In List',
333
+ description: 'Must be one of the allowed values',
334
+ constraintCount: 1,
335
+ constraintLabels: ['Values (comma-separated)'],
336
+ },
337
+ ]
338
+
339
+ /**
340
+ * Get validation rule definition by type
341
+ */
342
+ export function getValidationRuleDefinition(
343
+ type: string,
344
+ ): ValidationRuleDefinition | undefined {
345
+ return VALIDATION_RULE_DEFINITIONS.find((r) => r.type === type)
346
+ }
347
+
348
+ /**
349
+ * Relation type options
350
+ */
351
+ export const RELATION_TYPES = [
352
+ {
353
+ value: 'oneToOne',
354
+ label: 'One to One',
355
+ description: 'Each record relates to exactly one other',
356
+ },
357
+ {
358
+ value: 'oneToMany',
359
+ label: 'One to Many',
360
+ description: 'One record relates to many others',
361
+ },
362
+ {
363
+ value: 'manyToOne',
364
+ label: 'Many to One',
365
+ description: 'Many records relate to one',
366
+ },
367
+ {
368
+ value: 'manyToMany',
369
+ label: 'Many to Many',
370
+ description: 'Many records relate to many others',
371
+ },
372
+ ] as const
373
+
374
+ /**
375
+ * Color mapping for field type badges
376
+ */
377
+ export const FIELD_TYPE_COLORS: Record<FieldType, string> = {
378
+ text: 'bg-blue-50 text-blue-700 border-blue-200',
379
+ number: 'bg-amber-50 text-amber-700 border-amber-200',
380
+ date: 'bg-green-50 text-green-700 border-green-200',
381
+ boolean: 'bg-purple-50 text-purple-700 border-purple-200',
382
+ select: 'bg-cyan-50 text-cyan-700 border-cyan-200',
383
+ relation: 'bg-indigo-50 text-indigo-700 border-indigo-200',
384
+ }