@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,36 @@
1
+ import { PluginMagnetProvider } from '@magnet-cms/common';
2
+ export { PlaygroundModule } from './backend/index.cjs';
3
+
4
+ /**
5
+ * Playground plugin (`@magnet-cms/plugin-playground`)
6
+ *
7
+ * Provides the Playground UI for creating and managing content schemas without
8
+ * writing code: visual editor, TypeScript generation, and NestJS module output.
9
+ *
10
+ * Features:
11
+ * - Visual schema editor with drag-and-drop field management
12
+ * - Real-time TypeScript code generation
13
+ * - Automatic module, controller, service, and DTO generation
14
+ * - Schema versioning and i18n support configuration
15
+ *
16
+ * The admin UI loads Playground from `dist/frontend/bundle.iife.js`. Build this
17
+ * package (`bun run build` here or via the monorepo) before expecting Playground
18
+ * in the sidebar.
19
+ */
20
+ declare class PlaygroundPlugin {
21
+ /**
22
+ * Create a configured plugin provider for MagnetModule.forRoot().
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * MagnetModule.forRoot([
27
+ * PlaygroundPlugin.forRoot(),
28
+ * ])
29
+ * ```
30
+ */
31
+ static forRoot(config?: {
32
+ modulesPath?: string;
33
+ }): PluginMagnetProvider;
34
+ }
35
+
36
+ export { PlaygroundPlugin };
@@ -0,0 +1,36 @@
1
+ import { PluginMagnetProvider } from '@magnet-cms/common';
2
+ export { PlaygroundModule } from './backend/index.js';
3
+
4
+ /**
5
+ * Playground plugin (`@magnet-cms/plugin-playground`)
6
+ *
7
+ * Provides the Playground UI for creating and managing content schemas without
8
+ * writing code: visual editor, TypeScript generation, and NestJS module output.
9
+ *
10
+ * Features:
11
+ * - Visual schema editor with drag-and-drop field management
12
+ * - Real-time TypeScript code generation
13
+ * - Automatic module, controller, service, and DTO generation
14
+ * - Schema versioning and i18n support configuration
15
+ *
16
+ * The admin UI loads Playground from `dist/frontend/bundle.iife.js`. Build this
17
+ * package (`bun run build` here or via the monorepo) before expecting Playground
18
+ * in the sidebar.
19
+ */
20
+ declare class PlaygroundPlugin {
21
+ /**
22
+ * Create a configured plugin provider for MagnetModule.forRoot().
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * MagnetModule.forRoot([
27
+ * PlaygroundPlugin.forRoot(),
28
+ * ])
29
+ * ```
30
+ */
31
+ static forRoot(config?: {
32
+ modulesPath?: string;
33
+ }): PluginMagnetProvider;
34
+ }
35
+
36
+ export { PlaygroundPlugin };
package/dist/index.js ADDED
@@ -0,0 +1,76 @@
1
+ import { __name, __toCommonJS, init_playground_module, playground_module_exports } from './chunk-WY4YMBWZ.js';
2
+ export { PlaygroundModule } from './chunk-WY4YMBWZ.js';
3
+ import { Plugin } from '@magnet-cms/core';
4
+
5
+ function _ts_decorate(decorators, target, key, desc) {
6
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
7
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
8
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
9
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
10
+ }
11
+ __name(_ts_decorate, "_ts_decorate");
12
+ var PlaygroundPlugin = class _PlaygroundPlugin {
13
+ static {
14
+ __name(this, "PlaygroundPlugin");
15
+ }
16
+ /**
17
+ * Create a configured plugin provider for MagnetModule.forRoot().
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * MagnetModule.forRoot([
22
+ * PlaygroundPlugin.forRoot(),
23
+ * ])
24
+ * ```
25
+ */
26
+ static forRoot(config) {
27
+ return {
28
+ type: "plugin",
29
+ plugin: _PlaygroundPlugin,
30
+ options: config,
31
+ envVars: []
32
+ };
33
+ }
34
+ };
35
+ PlaygroundPlugin = _ts_decorate([
36
+ Plugin({
37
+ name: "playground",
38
+ description: "Playground: visual schema builder and code generator for Magnet CMS",
39
+ version: "1.0.0",
40
+ module: /* @__PURE__ */ __name(() => (init_playground_module(), __toCommonJS(playground_module_exports)).PlaygroundModule, "module"),
41
+ frontend: {
42
+ routes: [
43
+ {
44
+ path: "playground",
45
+ componentId: "PlaygroundIndex",
46
+ requiresAuth: true,
47
+ children: [
48
+ {
49
+ path: "",
50
+ componentId: "PlaygroundIndex"
51
+ },
52
+ {
53
+ path: "new",
54
+ componentId: "PlaygroundEditor"
55
+ },
56
+ {
57
+ path: ":schemaName",
58
+ componentId: "PlaygroundEditor"
59
+ }
60
+ ]
61
+ }
62
+ ],
63
+ sidebar: [
64
+ {
65
+ id: "playground",
66
+ title: "Playground",
67
+ url: "/playground",
68
+ icon: "Boxes",
69
+ order: 20
70
+ }
71
+ ]
72
+ }
73
+ })
74
+ ], PlaygroundPlugin);
75
+
76
+ export { PlaygroundPlugin };
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@magnet-cms/plugin-playground",
3
+ "version": "2.0.0",
4
+ "description": "Playground: visual schema builder and code generator for Magnet CMS",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "require": {
15
+ "types": "./dist/index.d.cts",
16
+ "default": "./dist/index.cjs"
17
+ }
18
+ },
19
+ "./backend": {
20
+ "import": {
21
+ "types": "./dist/backend/index.d.ts",
22
+ "default": "./dist/backend/index.js"
23
+ },
24
+ "require": {
25
+ "types": "./dist/backend/index.d.cts",
26
+ "default": "./dist/backend/index.cjs"
27
+ }
28
+ },
29
+ "./frontend": {
30
+ "import": "./src/frontend/index.ts",
31
+ "types": "./src/frontend/index.ts"
32
+ }
33
+ },
34
+ "types": "./dist/index.d.ts",
35
+ "files": [
36
+ "dist",
37
+ "src/frontend"
38
+ ],
39
+ "scripts": {
40
+ "build:dev": "tsup --watch",
41
+ "build": "tsup && bun run build:frontend",
42
+ "build:frontend": "vite build",
43
+ "build:frontend:watch": "vite build --watch"
44
+ },
45
+ "devDependencies": {
46
+ "@dnd-kit/core": "^6.3.1",
47
+ "@magnet-cms/admin": "workspace:*",
48
+ "@magnet-cms/common": "workspace:*",
49
+ "@magnet-cms/core": "workspace:*",
50
+ "@magnet-cms/ui": "workspace:*",
51
+ "@magnet-cms/utils": "workspace:*",
52
+ "@nestjs/common": "^11.1.12",
53
+ "@repo/biome": "workspace:*",
54
+ "@repo/tsup": "workspace:*",
55
+ "@repo/typescript-config": "workspace:*",
56
+ "@types/react": "^19.0.1",
57
+ "@vitejs/plugin-react": "^4.3.4",
58
+ "lucide-react": "^0.468.0",
59
+ "react": "^19.0.0",
60
+ "react-router-dom": "^7.1.1",
61
+ "reflect-metadata": "0.2.2",
62
+ "vite": "^6.0.7"
63
+ },
64
+ "peerDependencies": {
65
+ "@magnet-cms/admin": "^0.2.0",
66
+ "@magnet-cms/common": "^0.2.0",
67
+ "@magnet-cms/core": "^2.0.0",
68
+ "@magnet-cms/ui": "^0.1.3",
69
+ "@magnet-cms/utils": "^0.1.1",
70
+ "@nestjs/common": "^11.1.12",
71
+ "lucide-react": ">=0.400.0",
72
+ "react": ">=18.0.0",
73
+ "react-router-dom": ">=6.0.0",
74
+ "reflect-metadata": "0.2.2"
75
+ },
76
+ "magnet": {
77
+ "type": "plugin",
78
+ "backend": true,
79
+ "frontend": true
80
+ }
81
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Playground plugin — frontend entry
3
+ *
4
+ * This file automatically registers the plugin when loaded via script injection.
5
+ * The admin app loads plugin bundles at runtime and plugins self-register
6
+ * on window.__MAGNET_PLUGINS__.
7
+ */
8
+
9
+ import type { ComponentType } from 'react'
10
+
11
+ /**
12
+ * Plugin manifest type (inline to avoid import issues in UMD bundle)
13
+ */
14
+ interface FrontendPluginManifest {
15
+ pluginName: string
16
+ routes?: {
17
+ path: string
18
+ componentId: string
19
+ children?: { path: string; componentId: string }[]
20
+ }[]
21
+ sidebar?: {
22
+ id: string
23
+ title: string
24
+ url: string
25
+ icon: string
26
+ order?: number
27
+ }[]
28
+ }
29
+
30
+ /**
31
+ * Plugin registration type
32
+ */
33
+ interface PluginRegistration {
34
+ manifest: FrontendPluginManifest
35
+ components: Record<string, () => Promise<{ default: ComponentType<unknown> }>>
36
+ }
37
+
38
+ // Extend window for plugin registry
39
+ declare global {
40
+ interface Window {
41
+ __MAGNET_PLUGINS__?: PluginRegistration[]
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Plugin manifest defining routes and sidebar items
47
+ */
48
+ const manifest: FrontendPluginManifest = {
49
+ pluginName: 'playground',
50
+ routes: [
51
+ {
52
+ path: 'playground',
53
+ componentId: 'PlaygroundIndex',
54
+ children: [
55
+ { path: '', componentId: 'PlaygroundIndex' },
56
+ { path: 'new', componentId: 'PlaygroundEditor' },
57
+ { path: ':schemaName', componentId: 'PlaygroundEditor' },
58
+ ],
59
+ },
60
+ ],
61
+ sidebar: [
62
+ {
63
+ id: 'playground',
64
+ title: 'Playground',
65
+ url: '/playground',
66
+ icon: 'Boxes',
67
+ order: 20,
68
+ },
69
+ ],
70
+ }
71
+
72
+ /**
73
+ * Component loaders for lazy loading
74
+ */
75
+ const components: Record<
76
+ string,
77
+ () => Promise<{ default: ComponentType<unknown> }>
78
+ > = {
79
+ PlaygroundIndex: () => import('./pages/Playground'),
80
+ PlaygroundEditor: () => import('./pages/Playground/Editor'),
81
+ }
82
+
83
+ /**
84
+ * Self-register the plugin when the script is loaded
85
+ */
86
+ function registerPlugin() {
87
+ // Initialize registry if needed
88
+ if (!window.__MAGNET_PLUGINS__) {
89
+ window.__MAGNET_PLUGINS__ = []
90
+ }
91
+
92
+ // Check if already registered
93
+ const alreadyRegistered = window.__MAGNET_PLUGINS__.some(
94
+ (p) => p.manifest.pluginName === manifest.pluginName,
95
+ )
96
+
97
+ if (!alreadyRegistered) {
98
+ window.__MAGNET_PLUGINS__.push({ manifest, components })
99
+ console.log(`[Magnet] Plugin registered: ${manifest.pluginName}`)
100
+ }
101
+ }
102
+
103
+ // Auto-register on load
104
+ registerPlugin()
105
+
106
+ /**
107
+ * For `createMagnetAdmin({ plugins: [playgroundPlugin] })` (optional; admin usually loads the IIFE at runtime).
108
+ */
109
+ export const playgroundPlugin = () => ({ manifest, components })
110
+ export default playgroundPlugin
@@ -0,0 +1,187 @@
1
+ import {
2
+ Button,
3
+ Dialog,
4
+ DialogContent,
5
+ DialogDescription,
6
+ DialogFooter,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ Input,
10
+ Label,
11
+ } from '@magnet-cms/ui/components'
12
+ import { cn } from '@magnet-cms/ui/lib'
13
+ import { useState } from 'react'
14
+ import {
15
+ FIELD_TYPES,
16
+ FIELD_TYPE_COLORS,
17
+ type FieldTypeDefinition,
18
+ } from '../constants/field-types'
19
+ import {
20
+ createFieldFromType,
21
+ useSchemaBuilder,
22
+ } from '../hooks/useSchemaBuilder'
23
+ import type { FieldType } from '../types/builder.types'
24
+
25
+ interface AddFieldDialogProps {
26
+ open: boolean
27
+ onOpenChange: (open: boolean) => void
28
+ }
29
+
30
+ export function AddFieldDialog({ open, onOpenChange }: AddFieldDialogProps) {
31
+ const { addField } = useSchemaBuilder()
32
+ const [selectedType, setSelectedType] = useState<FieldType>('text')
33
+ const [displayName, setDisplayName] = useState('')
34
+ const [error, setError] = useState('')
35
+
36
+ const handleAdd = () => {
37
+ if (!displayName.trim()) {
38
+ setError('Display name is required')
39
+ return
40
+ }
41
+
42
+ const fieldData = createFieldFromType(selectedType, displayName.trim())
43
+ addField(fieldData)
44
+ handleClose()
45
+ }
46
+
47
+ const handleClose = () => {
48
+ setSelectedType('text')
49
+ setDisplayName('')
50
+ setError('')
51
+ onOpenChange(false)
52
+ }
53
+
54
+ const handleQuickAdd = (type: FieldTypeDefinition) => {
55
+ const fieldData = createFieldFromType(type.id, type.label)
56
+ addField(fieldData)
57
+ handleClose()
58
+ }
59
+
60
+ return (
61
+ <Dialog open={open} onOpenChange={onOpenChange}>
62
+ <DialogContent className="sm:max-w-lg">
63
+ <DialogHeader>
64
+ <DialogTitle>Add New Field</DialogTitle>
65
+ <DialogDescription>
66
+ Choose a field type and enter a display name for your new field.
67
+ </DialogDescription>
68
+ </DialogHeader>
69
+
70
+ <div className="space-y-6 py-4">
71
+ {/* Quick Add Section */}
72
+ <div>
73
+ <Label className="text-xs text-muted-foreground uppercase tracking-wider mb-3 block">
74
+ Quick Add
75
+ </Label>
76
+ <div className="grid grid-cols-3 gap-2">
77
+ {FIELD_TYPES.map((type) => {
78
+ const Icon = type.icon
79
+ return (
80
+ <button
81
+ key={type.id}
82
+ type="button"
83
+ onClick={() => handleQuickAdd(type)}
84
+ className={cn(
85
+ 'p-3 rounded-lg border text-left hover:shadow-sm transition-all',
86
+ 'hover:border-muted-foreground/50',
87
+ )}
88
+ >
89
+ <div
90
+ className={cn(
91
+ 'w-8 h-8 rounded-lg flex items-center justify-center mb-2',
92
+ FIELD_TYPE_COLORS[type.id],
93
+ )}
94
+ >
95
+ <Icon className="h-4 w-4" />
96
+ </div>
97
+ <div className="text-sm font-medium">{type.label}</div>
98
+ <div className="text-[10px] text-muted-foreground line-clamp-1">
99
+ {type.description}
100
+ </div>
101
+ </button>
102
+ )
103
+ })}
104
+ </div>
105
+ </div>
106
+
107
+ <div className="relative">
108
+ <div className="absolute inset-0 flex items-center">
109
+ <span className="w-full border-t" />
110
+ </div>
111
+ <div className="relative flex justify-center text-xs uppercase">
112
+ <span className="bg-background px-2 text-muted-foreground">
113
+ Or customize
114
+ </span>
115
+ </div>
116
+ </div>
117
+
118
+ {/* Custom Field Section */}
119
+ <div className="space-y-4">
120
+ <div className="space-y-2">
121
+ <Label htmlFor="fieldType">Field Type</Label>
122
+ <div className="grid grid-cols-3 gap-2">
123
+ {FIELD_TYPES.map((type) => {
124
+ const Icon = type.icon
125
+ return (
126
+ <button
127
+ key={type.id}
128
+ type="button"
129
+ onClick={() => setSelectedType(type.id)}
130
+ className={cn(
131
+ 'p-2 rounded-lg border flex items-center gap-2 transition-all',
132
+ selectedType === type.id
133
+ ? 'border-foreground bg-muted'
134
+ : 'hover:border-muted-foreground/50',
135
+ )}
136
+ >
137
+ <div
138
+ className={cn(
139
+ 'w-6 h-6 rounded flex items-center justify-center',
140
+ FIELD_TYPE_COLORS[type.id],
141
+ )}
142
+ >
143
+ <Icon className="h-3 w-3" />
144
+ </div>
145
+ <span className="text-sm">{type.label}</span>
146
+ </button>
147
+ )
148
+ })}
149
+ </div>
150
+ </div>
151
+
152
+ <div className="space-y-2">
153
+ <Label htmlFor="displayName">Display Name</Label>
154
+ <Input
155
+ id="displayName"
156
+ placeholder="e.g., First Name, Email Address, Birth Date..."
157
+ value={displayName}
158
+ onChange={(e) => {
159
+ setDisplayName(e.target.value)
160
+ setError('')
161
+ }}
162
+ onKeyDown={(e) => {
163
+ if (e.key === 'Enter' && displayName.trim()) {
164
+ handleAdd()
165
+ }
166
+ }}
167
+ />
168
+ {error && <p className="text-xs text-destructive">{error}</p>}
169
+ <p className="text-xs text-muted-foreground">
170
+ The API ID will be automatically generated from the display name
171
+ </p>
172
+ </div>
173
+ </div>
174
+ </div>
175
+
176
+ <DialogFooter>
177
+ <Button variant="outline" onClick={handleClose}>
178
+ Cancel
179
+ </Button>
180
+ <Button onClick={handleAdd} disabled={!displayName.trim()}>
181
+ Add Field
182
+ </Button>
183
+ </DialogFooter>
184
+ </DialogContent>
185
+ </Dialog>
186
+ )
187
+ }
@@ -0,0 +1,59 @@
1
+ import { Button } from '@magnet-cms/ui/components'
2
+ import { Check, Copy } from 'lucide-react'
3
+ import { useState } from 'react'
4
+ import { useSchemaBuilder } from '../hooks/useSchemaBuilder'
5
+ import type { ViewMode } from '../types/builder.types'
6
+
7
+ interface CodePreviewProps {
8
+ mode: ViewMode
9
+ }
10
+
11
+ export function CodePreview({ mode }: CodePreviewProps) {
12
+ const { generatedCode, generatedJSON } = useSchemaBuilder()
13
+ const [copied, setCopied] = useState(false)
14
+
15
+ const content =
16
+ mode === 'code' ? generatedCode : JSON.stringify(generatedJSON, null, 2)
17
+
18
+ const handleCopy = async () => {
19
+ await navigator.clipboard.writeText(content)
20
+ setCopied(true)
21
+ setTimeout(() => setCopied(false), 2000)
22
+ }
23
+
24
+ return (
25
+ <div className="h-full flex flex-col border rounded-xl overflow-hidden bg-muted/30">
26
+ {/* Header */}
27
+ <div className="bg-muted/50 border-b px-4 py-2 flex items-center justify-between">
28
+ <h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
29
+ {mode === 'code' ? 'Generated TypeScript' : 'JSON Schema'}
30
+ </h3>
31
+ <Button
32
+ variant="ghost"
33
+ size="sm"
34
+ className="h-7 text-xs"
35
+ onClick={handleCopy}
36
+ >
37
+ {copied ? (
38
+ <>
39
+ <Check className="h-3 w-3 mr-1.5 text-green-500" />
40
+ Copied
41
+ </>
42
+ ) : (
43
+ <>
44
+ <Copy className="h-3 w-3 mr-1.5" />
45
+ Copy
46
+ </>
47
+ )}
48
+ </Button>
49
+ </div>
50
+
51
+ {/* Code Content */}
52
+ <div className="flex-1 overflow-auto p-4">
53
+ <pre className="text-xs font-mono text-muted-foreground whitespace-pre-wrap">
54
+ <code>{content}</code>
55
+ </pre>
56
+ </div>
57
+ </div>
58
+ )
59
+ }