@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,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
+ }