@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.
- package/dist/backend/index.cjs +1023 -0
- package/dist/backend/index.d.cts +10 -0
- package/dist/backend/index.d.ts +10 -0
- package/dist/backend/index.js +1 -0
- package/dist/chunk-WY4YMBWZ.js +1044 -0
- package/dist/frontend/bundle.iife.js +2163 -0
- package/dist/frontend/bundle.iife.js.map +1 -0
- package/dist/index.cjs +1135 -0
- package/dist/index.d.cts +36 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +76 -0
- package/package.json +81 -0
- package/src/frontend/index.ts +110 -0
- package/src/frontend/pages/Playground/Editor/AddFieldDialog.tsx +187 -0
- package/src/frontend/pages/Playground/Editor/CodePreview.tsx +59 -0
- package/src/frontend/pages/Playground/Editor/FieldCard.tsx +161 -0
- package/src/frontend/pages/Playground/Editor/FieldList.tsx +121 -0
- package/src/frontend/pages/Playground/Editor/FieldSettingsPanel.tsx +652 -0
- package/src/frontend/pages/Playground/Editor/RelationConfigModal.tsx +292 -0
- package/src/frontend/pages/Playground/Editor/SchemaList.tsx +76 -0
- package/src/frontend/pages/Playground/Editor/SchemaOptionsDialog.tsx +109 -0
- package/src/frontend/pages/Playground/Editor/index.tsx +322 -0
- package/src/frontend/pages/Playground/constants/field-types.ts +384 -0
- package/src/frontend/pages/Playground/hooks/useSchemaBuilder.ts +280 -0
- package/src/frontend/pages/Playground/index.tsx +19 -0
- package/src/frontend/pages/Playground/types/builder.types.ts +191 -0
- package/src/frontend/pages/Playground/utils/code-generator.ts +319 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Badge, Button, SortableItemHandle } from '@magnet-cms/ui/components'
|
|
2
|
+
import { cn } from '@magnet-cms/ui/lib'
|
|
3
|
+
import { GripVertical, Pencil, Trash2 } from 'lucide-react'
|
|
4
|
+
import {
|
|
5
|
+
FIELD_TYPE_COLORS,
|
|
6
|
+
getFieldTypeDefinition,
|
|
7
|
+
} from '../constants/field-types'
|
|
8
|
+
import type { SchemaField } from '../types/builder.types'
|
|
9
|
+
|
|
10
|
+
interface FieldCardProps {
|
|
11
|
+
field: SchemaField
|
|
12
|
+
isSelected?: boolean
|
|
13
|
+
isDragging?: boolean
|
|
14
|
+
onClick?: () => void
|
|
15
|
+
onEdit?: () => void
|
|
16
|
+
onDelete?: () => void
|
|
17
|
+
showActions?: boolean
|
|
18
|
+
showDragHandle?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function FieldCard({
|
|
22
|
+
field,
|
|
23
|
+
isSelected,
|
|
24
|
+
isDragging,
|
|
25
|
+
onClick,
|
|
26
|
+
onEdit,
|
|
27
|
+
onDelete,
|
|
28
|
+
showActions = true,
|
|
29
|
+
showDragHandle = true,
|
|
30
|
+
}: FieldCardProps) {
|
|
31
|
+
const definition = getFieldTypeDefinition(field.type)
|
|
32
|
+
const Icon = definition?.icon
|
|
33
|
+
|
|
34
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
35
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
36
|
+
e.preventDefault()
|
|
37
|
+
onClick?.()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
// biome-ignore lint/a11y/useSemanticElements: Using div for drag-and-drop compatibility
|
|
43
|
+
<div
|
|
44
|
+
role="button"
|
|
45
|
+
tabIndex={0}
|
|
46
|
+
className={cn(
|
|
47
|
+
'group p-4 flex items-center justify-between transition-colors cursor-pointer border-l-2',
|
|
48
|
+
isSelected
|
|
49
|
+
? 'bg-muted/80 border-l-foreground'
|
|
50
|
+
: 'hover:bg-muted/50 border-l-transparent hover:border-l-muted-foreground/50',
|
|
51
|
+
isDragging && 'opacity-50',
|
|
52
|
+
)}
|
|
53
|
+
onClick={onClick}
|
|
54
|
+
onKeyDown={handleKeyDown}
|
|
55
|
+
>
|
|
56
|
+
{/* Left side: Icon + Field Info */}
|
|
57
|
+
<div className="flex items-center gap-4">
|
|
58
|
+
{/* Drag Handle */}
|
|
59
|
+
{showDragHandle ? (
|
|
60
|
+
<SortableItemHandle className="text-muted-foreground/50 hover:text-muted-foreground touch-none">
|
|
61
|
+
<GripVertical className="h-4 w-4" />
|
|
62
|
+
</SortableItemHandle>
|
|
63
|
+
) : (
|
|
64
|
+
<div className="text-muted-foreground/50">
|
|
65
|
+
<GripVertical className="h-4 w-4" />
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
{/* Field Type Icon */}
|
|
70
|
+
<div
|
|
71
|
+
className={cn(
|
|
72
|
+
'w-8 h-8 rounded-lg flex items-center justify-center border',
|
|
73
|
+
FIELD_TYPE_COLORS[field.type] ||
|
|
74
|
+
'bg-muted text-muted-foreground border-muted',
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
{Icon && <Icon className="h-4 w-4" />}
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Field Name & Type */}
|
|
81
|
+
<div className="space-y-1">
|
|
82
|
+
<div className="flex items-center gap-3">
|
|
83
|
+
<span className="text-sm font-semibold">{field.displayName}</span>
|
|
84
|
+
<span className="text-[10px] font-mono text-muted-foreground bg-muted px-1.5 py-0.5 rounded border">
|
|
85
|
+
{field.name}
|
|
86
|
+
</span>
|
|
87
|
+
</div>
|
|
88
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
89
|
+
<span className="text-xs text-muted-foreground capitalize">
|
|
90
|
+
{definition?.label || field.type}
|
|
91
|
+
</span>
|
|
92
|
+
{field.prop.required && (
|
|
93
|
+
<>
|
|
94
|
+
<span className="w-0.5 h-0.5 rounded-full bg-muted-foreground/50" />
|
|
95
|
+
<Badge
|
|
96
|
+
variant="outline"
|
|
97
|
+
className="text-[10px] px-1.5 py-0 h-4 bg-emerald-50 text-emerald-700 border-emerald-200"
|
|
98
|
+
>
|
|
99
|
+
Required
|
|
100
|
+
</Badge>
|
|
101
|
+
</>
|
|
102
|
+
)}
|
|
103
|
+
{field.prop.unique && (
|
|
104
|
+
<>
|
|
105
|
+
<span className="w-0.5 h-0.5 rounded-full bg-muted-foreground/50" />
|
|
106
|
+
<Badge
|
|
107
|
+
variant="outline"
|
|
108
|
+
className="text-[10px] px-1.5 py-0 h-4 bg-amber-50 text-amber-700 border-amber-200"
|
|
109
|
+
>
|
|
110
|
+
Unique
|
|
111
|
+
</Badge>
|
|
112
|
+
</>
|
|
113
|
+
)}
|
|
114
|
+
{field.relationConfig && (
|
|
115
|
+
<>
|
|
116
|
+
<span className="w-0.5 h-0.5 rounded-full bg-muted-foreground/50" />
|
|
117
|
+
<span className="text-xs text-muted-foreground">
|
|
118
|
+
{field.relationConfig.relationType} (
|
|
119
|
+
{field.relationConfig.targetSchema || '...'})
|
|
120
|
+
</span>
|
|
121
|
+
</>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{/* Right side: Actions */}
|
|
128
|
+
{showActions && (
|
|
129
|
+
<div
|
|
130
|
+
className={cn(
|
|
131
|
+
'flex items-center gap-1 transition-opacity',
|
|
132
|
+
isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100',
|
|
133
|
+
)}
|
|
134
|
+
>
|
|
135
|
+
<Button
|
|
136
|
+
variant="ghost"
|
|
137
|
+
size="icon"
|
|
138
|
+
className="h-8 w-8"
|
|
139
|
+
onClick={(e) => {
|
|
140
|
+
e.stopPropagation()
|
|
141
|
+
onEdit?.()
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
145
|
+
</Button>
|
|
146
|
+
<Button
|
|
147
|
+
variant="ghost"
|
|
148
|
+
size="icon"
|
|
149
|
+
className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
|
150
|
+
onClick={(e) => {
|
|
151
|
+
e.stopPropagation()
|
|
152
|
+
onDelete?.()
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
156
|
+
</Button>
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { UniqueIdentifier } from '@dnd-kit/core'
|
|
2
|
+
import {
|
|
3
|
+
Sortable,
|
|
4
|
+
SortableContent,
|
|
5
|
+
SortableItem,
|
|
6
|
+
SortableOverlay,
|
|
7
|
+
} from '@magnet-cms/ui/components'
|
|
8
|
+
import { AlertCircle, Plus } from 'lucide-react'
|
|
9
|
+
import { useSchemaBuilder } from '../hooks/useSchemaBuilder'
|
|
10
|
+
import type { SchemaField } from '../types/builder.types'
|
|
11
|
+
import { FieldCard } from './FieldCard'
|
|
12
|
+
|
|
13
|
+
interface FieldListProps {
|
|
14
|
+
onAddField?: () => void
|
|
15
|
+
readOnly?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function FieldList({ onAddField, readOnly = false }: FieldListProps) {
|
|
19
|
+
const { state, selectField, deleteField, reorderFields } = useSchemaBuilder()
|
|
20
|
+
|
|
21
|
+
const handleReorder = (reorderedFields: SchemaField[]) => {
|
|
22
|
+
reorderFields(reorderedFields)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="border rounded-xl overflow-hidden bg-background shadow-sm">
|
|
27
|
+
{/* Header */}
|
|
28
|
+
<div className="bg-muted/50 border-b px-4 py-2 flex items-center justify-between">
|
|
29
|
+
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
30
|
+
Fields ({state.fields.length})
|
|
31
|
+
</h3>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{/* Field List */}
|
|
35
|
+
{state.fields.length === 0 ? (
|
|
36
|
+
<div className="p-8 text-center text-muted-foreground">
|
|
37
|
+
<AlertCircle className="mx-auto h-10 w-10 mb-3 opacity-50" />
|
|
38
|
+
<p className="text-sm font-medium mb-1">No fields yet</p>
|
|
39
|
+
<p className="text-xs">
|
|
40
|
+
{readOnly
|
|
41
|
+
? 'This schema has no fields defined'
|
|
42
|
+
: 'Add your first field to start building your schema'}
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
) : readOnly ? (
|
|
46
|
+
// Read-only mode: simple list without drag/drop
|
|
47
|
+
<div className="divide-y">
|
|
48
|
+
{state.fields.map((field) => (
|
|
49
|
+
<FieldCard
|
|
50
|
+
key={field.id}
|
|
51
|
+
field={field}
|
|
52
|
+
isSelected={false}
|
|
53
|
+
showActions={false}
|
|
54
|
+
showDragHandle={false}
|
|
55
|
+
/>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
) : (
|
|
59
|
+
<Sortable
|
|
60
|
+
value={state.fields}
|
|
61
|
+
onValueChange={handleReorder}
|
|
62
|
+
getItemValue={(field) => field.id}
|
|
63
|
+
>
|
|
64
|
+
<SortableContent className="divide-y">
|
|
65
|
+
{state.fields.map((field) => (
|
|
66
|
+
<SortableItem key={field.id} value={field.id}>
|
|
67
|
+
<FieldCard
|
|
68
|
+
field={field}
|
|
69
|
+
isSelected={field.id === state.selectedFieldId}
|
|
70
|
+
onClick={() => selectField(field.id)}
|
|
71
|
+
onEdit={() => selectField(field.id)}
|
|
72
|
+
onDelete={() => {
|
|
73
|
+
if (
|
|
74
|
+
window.confirm(
|
|
75
|
+
`Are you sure you want to delete "${field.displayName}"?`,
|
|
76
|
+
)
|
|
77
|
+
) {
|
|
78
|
+
deleteField(field.id)
|
|
79
|
+
}
|
|
80
|
+
}}
|
|
81
|
+
/>
|
|
82
|
+
</SortableItem>
|
|
83
|
+
))}
|
|
84
|
+
</SortableContent>
|
|
85
|
+
|
|
86
|
+
<SortableOverlay>
|
|
87
|
+
{({ value }: { value: UniqueIdentifier }) => {
|
|
88
|
+
const field = state.fields.find((f) => f.id === value)
|
|
89
|
+
return field ? (
|
|
90
|
+
<div className="bg-background border rounded-lg shadow-lg">
|
|
91
|
+
<FieldCard
|
|
92
|
+
field={field}
|
|
93
|
+
isSelected
|
|
94
|
+
showActions={false}
|
|
95
|
+
showDragHandle={false}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
) : null
|
|
99
|
+
}}
|
|
100
|
+
</SortableOverlay>
|
|
101
|
+
</Sortable>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
{/* Add Field Button - only show in edit mode */}
|
|
105
|
+
{!readOnly && onAddField && (
|
|
106
|
+
<div className="p-2 bg-muted/30 border-t">
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
onClick={onAddField}
|
|
110
|
+
className="w-full py-2.5 rounded-lg border-2 border-dashed border-muted-foreground/25 hover:border-muted-foreground/50 hover:bg-muted/50 transition-all text-xs font-semibold text-muted-foreground hover:text-foreground flex items-center justify-center gap-2 group"
|
|
111
|
+
>
|
|
112
|
+
<div className="bg-muted rounded p-0.5 group-hover:bg-muted-foreground/20 transition-colors">
|
|
113
|
+
<Plus className="h-3 w-3" />
|
|
114
|
+
</div>
|
|
115
|
+
Add new field
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|