@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
package/dist/index.d.cts
ADDED
|
@@ -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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|