Generated by Spark: Import / export packages as zips, various images and other asserts can go in same zip

This commit is contained in:
2025-12-23 22:43:58 +00:00
parent 60023b7a2c
commit d1b659ea65
7 changed files with 1331 additions and 9 deletions

264
PACKAGE_IMPORT_EXPORT.md Normal file
View File

@@ -0,0 +1,264 @@
# Package Import/Export Guide
## Overview
The MetaBuilder Package Import/Export system allows you to package and share complete applications, features, or database snapshots as ZIP files. This enables modular development and easy distribution of pre-built functionality.
## Features
### Export Capabilities
1. **Custom Package Export**
- Create reusable packages with selected components
- Include or exclude specific data types
- Add metadata (name, version, author, description, tags)
- Automatic README generation
2. **Database Snapshot Export**
- Complete backup of entire database
- One-click export with timestamp
- Includes all schemas, pages, workflows, scripts, and configurations
3. **Selective Export Options**
- ✅ Data schemas
- ✅ Page configurations
- ✅ Workflows
- ✅ Lua scripts
- ✅ Component hierarchies
- ✅ Component configurations
- ✅ CSS classes
- ✅ Dropdown configurations
- ✅ Seed data
- ✅ Assets (images, videos, audio, documents)
### Import Capabilities
1. **Package Installation**
- Import packages from ZIP files
- Automatic validation of package structure
- Merge with existing data
- Asset restoration
2. **Safety Features**
- Package validation before import
- Non-destructive merging (adds to existing data)
- Import warnings and confirmations
## ZIP Package Structure
```
package-name-1.0.0.zip
├── manifest.json # Package metadata
├── content.json # Database content
├── README.md # Auto-generated documentation
└── assets/ # Asset files
├── asset-manifest.json
├── images/
│ └── *.png, *.jpg, *.svg
├── videos/
│ └── *.mp4, *.webm
├── audios/
│ └── *.mp3, *.wav
└── documents/
└── *.pdf, *.txt
```
### manifest.json
```json
{
"id": "pkg_1234567890",
"name": "My Package",
"version": "1.0.0",
"description": "Package description",
"author": "Your Name",
"category": "social",
"icon": "📦",
"screenshots": [],
"tags": ["tag1", "tag2"],
"dependencies": [],
"createdAt": 1234567890,
"updatedAt": 1234567890,
"downloadCount": 0,
"rating": 0,
"installed": false
}
```
### content.json
```json
{
"schemas": [...],
"pages": [...],
"workflows": [...],
"luaScripts": [...],
"componentHierarchy": {...},
"componentConfigs": {...},
"cssClasses": [...],
"dropdownConfigs": [...],
"seedData": {...}
}
```
## Usage
### Exporting a Package
1. Navigate to **Level 4 (God Panel)** or **Level 5 (Super God Panel)**
2. Open **Package Manager**
3. Click **Export** button
4. Choose export type:
- **Custom Package**: Configure metadata and select what to include
- **Full Snapshot**: Export everything instantly
5. For custom packages:
- Fill in package name (required)
- Add version, author, description
- Add tags for searchability
- Select export options (checkboxes)
6. Click **Export Package**
7. ZIP file will download automatically
### Importing a Package
1. Navigate to **Level 4 (God Panel)** or **Level 5 (Super God Panel)**
2. Open **Package Manager**
3. Click **Import** button
4. Click the upload area or drag a ZIP file
5. Package will be validated and imported
6. Success message shows what was imported
7. Refresh the page if needed to see new content
## Pre-Built Packages
The system comes with several pre-built packages in the Package Catalog:
### 1. **Classic Forum** 💬
- Discussion threads and categories
- User profiles and moderation
- Schema: ForumCategory, ForumThread, ForumPost
### 2. **Retro Guestbook** 📖
- 90s-style visitor messages
- Custom backgrounds and GIFs
- Schema: GuestbookEntry
### 3. **Video Platform** 🎥
- Video upload and streaming
- Comments, likes, subscriptions
- Schema: Video, VideoComment, Subscription, Playlist
### 4. **Music Streaming Platform** 🎵
- Artists, albums, tracks
- Playlists and playback
- Schema: Artist, Album, Track, MusicPlaylist
### 5. **Retro Games Arcade** 🕹️
- Game collection with high scores
- Leaderboards and achievements
- Schema: Game, HighScore, Achievement, UserAchievement
### 6. **E-Commerce Store** 🛒
- Product catalog and inventory
- Shopping cart and orders
- Schema: Product, Cart, Order
## Best Practices
### For Package Authors
1. **Descriptive Naming**: Use clear, descriptive package names
2. **Versioning**: Follow semantic versioning (major.minor.patch)
3. **Documentation**: Add comprehensive descriptions and tags
4. **Dependencies**: List any required packages
5. **Testing**: Test your package before distribution
6. **Assets**: Include all necessary assets in the package
### For Package Users
1. **Backup First**: Export a database snapshot before importing new packages
2. **Review Contents**: Check package contents in Package Manager before installing
3. **Test in Development**: Test new packages in a development environment first
4. **Check Conflicts**: Be aware of potential schema or page ID conflicts
5. **Documentation**: Read the package README for setup instructions
## API Reference
### Export Functions
```typescript
import { exportPackageAsZip, downloadZip } from '@/lib/package-export'
// Export a custom package
const blob = await exportPackageAsZip(manifest, content, assets, options)
downloadZip(blob, 'package-name.zip')
// Export database snapshot
const blob = await exportDatabaseSnapshot(
schemas,
pages,
workflows,
luaScripts,
componentHierarchy,
componentConfigs,
cssClasses,
dropdownConfigs,
assets
)
```
### Import Functions
```typescript
import { importPackageFromZip } from '@/lib/package-export'
// Import from ZIP file
const { manifest, content, assets } = await importPackageFromZip(zipFile)
```
## Troubleshooting
### Import Fails
- **Invalid ZIP**: Ensure the ZIP file has the correct structure
- **Missing manifest.json**: Package must include a manifest file
- **Missing content.json**: Package must include content data
- **Corrupted File**: Try re-downloading or re-exporting the package
### Export Fails
- **No Package Name**: Package name is required for custom exports
- **No Data**: Ensure your database has data to export
- **Browser Memory**: Large exports may fail on low-memory devices
### Assets Not Working
- **Path Issues**: Asset paths are preserved from the original location
- **Missing Files**: Ensure all assets were included during export
- **Format Support**: Only specific formats are supported (see structure above)
## Future Enhancements
Planned features for future versions:
- 🔄 Package versioning and updates
- 🔍 Package marketplace/registry
- 🔐 Package signing and verification
- 📦 Dependency resolution
- 🎨 Custom package icons
- 📸 Package screenshots
- 💬 Package reviews and ratings
- 🔗 Remote package installation via URL
- 📊 Package analytics
## Support
For issues or questions:
- Check the console for error messages
- Verify ZIP file structure
- Ensure you have the latest version of MetaBuilder
- Review this documentation for proper usage
---
**Note**: The import/export system is designed to be non-destructive. Imported data is merged with existing data rather than replacing it. Always backup your database before major imports.

26
PRD.md
View File

@@ -104,7 +104,25 @@ Elevate MetaBuilder to support multi-tenant architecture with a Super God level
- Search and filter by category, rating, downloads
- Seed data automatically loaded with packages
### 2. CSS Class Builder
### 8. Package Import/Export System
**Functionality:** Export database configurations and packages as ZIP files, import packages from ZIP files, including support for assets (images, videos, audio, documents)
**Purpose:** Enable sharing of complete application packages, backing up database configurations, and distributing reusable modules across MetaBuilder instances
**Trigger:** User clicks Import/Export buttons in Package Manager
**Progression:**
- **Export**: Click Export → Choose Custom Package or Full Snapshot → Enter metadata (name, version, author, description, tags) → Select export options → Click Export Package → ZIP downloads
- **Import**: Click Import → Select/drag ZIP file → Package validated → Data merged with existing database → Assets restored → Success notification
**Success Criteria:**
- Export packages as ZIP files with manifest.json, content.json, README.md, and assets folder
- Import packages from ZIP files with validation
- Selective export options (schemas, pages, workflows, Lua scripts, components, CSS, dropdowns, seed data, assets)
- Full database snapshot export for backup
- Non-destructive import (merges with existing data)
- Asset support for images, videos, audio, and documents
- Auto-generated README in packages
- Import/Export accessible from Package Manager
- Visual feedback during import/export operations
### 9. CSS Class Builder
**Functionality:** Visual selector for Tailwind CSS classes organized into logical categories
**Purpose:** Eliminate the need to memorize or type CSS class names, reducing errors and speeding up styling
**Trigger:** User clicks palette icon next to any className field in PropertyInspector
@@ -115,7 +133,7 @@ Elevate MetaBuilder to support multi-tenant architecture with a Super God level
- 200+ predefined classes organized into 10 categories
- Custom class input available for edge cases
### 3. Dynamic Dropdown Configuration
### 10. Dynamic Dropdown Configuration
**Functionality:** Centralized management of dropdown option sets usable across multiple components
**Purpose:** Prevent duplication and ensure consistency when the same options appear in multiple places
**Trigger:** User navigates to "Dropdowns" tab in god-tier panel or components reference dropdown by name
@@ -126,7 +144,7 @@ Elevate MetaBuilder to support multi-tenant architecture with a Super God level
- Visual GUI for managing options (no JSON required)
- Pre-loaded with common examples (status, priority, category)
### 4. CSS Class Library Manager
### 11. CSS Class Library Manager
**Functionality:** Manage the catalog of CSS classes available in the builder
**Purpose:** Allow customization of available classes and organization for project-specific needs
**Trigger:** User navigates to "CSS Classes" tab in god-tier panel
@@ -137,7 +155,7 @@ Elevate MetaBuilder to support multi-tenant architecture with a Super God level
- Changes immediately reflected in CSS Class Builder
- System initializes with comprehensive Tailwind utilities
### 5. Monaco Code Editor Integration
### 12. Monaco Code Editor Integration
**Functionality:** Professional-grade code editor for JSON and Lua with syntax highlighting and validation
**Purpose:** When code editing is necessary, provide best-in-class tooling comparable to VS Code
**Trigger:** User opens SchemaEditor, LuaEditor, or JsonEditor components

110
package-lock.json generated
View File

@@ -44,6 +44,7 @@
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.83.1",
"@types/jszip": "^3.4.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -54,6 +55,7 @@
"fengari-web": "^0.1.4",
"framer-motion": "^12.6.2",
"input-otp": "^1.4.2",
"jszip": "^3.10.1",
"lucide-react": "^0.484.0",
"marked": "^15.0.7",
"next-themes": "^0.4.6",
@@ -4358,6 +4360,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/jszip": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@types/jszip/-/jszip-3.4.0.tgz",
"integrity": "sha512-GFHqtQQP3R4NNuvZH3hNCYD0NbyBZ42bkN7kO3NDrU/SnvIZWMS8Bp38XCsRKBT5BXvgm0y1zqpZWp/ZkRzBzg==",
"license": "MIT",
"dependencies": {
"jszip": "*"
}
},
"node_modules/@types/node": {
"version": "22.19.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
@@ -5382,6 +5393,12 @@
"node": ">=6.6.0"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -7270,6 +7287,12 @@
"node": ">= 4"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -7446,6 +7469,12 @@
"@types/estree": "*"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -7543,6 +7572,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -7567,6 +7608,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lightningcss": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
@@ -8259,6 +8309,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -8468,6 +8524,12 @@
"license": "MIT",
"peer": true
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -8774,6 +8836,27 @@
"react-dom": ">=16.6.0"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readable-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/readline-sync": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz",
@@ -9139,6 +9222,12 @@
"node": ">= 18"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -9342,6 +9431,21 @@
"dev": true,
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string_decoder/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
@@ -9863,6 +9967,12 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",

View File

@@ -48,6 +48,7 @@
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.83.1",
"@types/jszip": "^3.4.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -58,6 +59,7 @@
"fengari-web": "^0.1.4",
"framer-motion": "^12.6.2",
"input-otp": "^1.4.2",
"jszip": "^3.10.1",
"lucide-react": "^0.484.0",
"marked": "^15.0.7",
"next-themes": "^0.4.6",

View File

@@ -0,0 +1,638 @@
import { useState, useRef } from 'react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Checkbox } from '@/components/ui/checkbox'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import { toast } from 'sonner'
import { Database } from '@/lib/database'
import { exportPackageAsZip, importPackageFromZip, downloadZip, exportDatabaseSnapshot } from '@/lib/package-export'
import type { PackageManifest, PackageContent } from '@/lib/package-types'
import type { ExportPackageOptions } from '@/lib/package-export'
import {
Export,
ArrowSquareIn,
FileArchive,
FileArrowDown,
FileArrowUp,
Package,
CloudArrowDown,
Database as DatabaseIcon,
CheckCircle,
Warning,
Image as ImageIcon,
FilmStrip,
MusicNote,
FileText
} from '@phosphor-icons/react'
interface PackageImportExportProps {
open: boolean
onOpenChange: (open: boolean) => void
mode: 'export' | 'import'
}
export function PackageImportExport({ open, onOpenChange, mode }: PackageImportExportProps) {
const [exporting, setExporting] = useState(false)
const [importing, setImporting] = useState(false)
const [exportOptions, setExportOptions] = useState<ExportPackageOptions>({
includeAssets: true,
includeSchemas: true,
includePages: true,
includeWorkflows: true,
includeLuaScripts: true,
includeComponentHierarchy: true,
includeComponentConfigs: true,
includeCssClasses: true,
includeDropdownConfigs: true,
includeSeedData: true,
})
const [manifest, setManifest] = useState<Partial<PackageManifest>>({
name: '',
version: '1.0.0',
description: '',
author: '',
category: 'other',
tags: [],
})
const [tagInput, setTagInput] = useState('')
const fileInputRef = useRef<HTMLInputElement>(null)
const handleExport = async () => {
if (!manifest.name) {
toast.error('Please provide a package name')
return
}
setExporting(true)
try {
const schemas = await Database.getSchemas()
const pages = await Database.getPages()
const workflows = await Database.getWorkflows()
const luaScripts = await Database.getLuaScripts()
const componentHierarchy = await Database.getComponentHierarchy()
const componentConfigs = await Database.getComponentConfigs()
const cssClasses = await Database.getCssClasses()
const dropdownConfigs = await Database.getDropdownConfigs()
const fullManifest: PackageManifest = {
id: `pkg_${Date.now()}`,
name: manifest.name!,
version: manifest.version || '1.0.0',
description: manifest.description || '',
author: manifest.author || 'Anonymous',
category: manifest.category as any || 'other',
icon: '📦',
screenshots: [],
tags: manifest.tags || [],
dependencies: [],
createdAt: Date.now(),
updatedAt: Date.now(),
downloadCount: 0,
rating: 0,
installed: false,
}
const content: PackageContent = {
schemas: exportOptions.includeSchemas ? schemas : [],
pages: exportOptions.includePages ? pages : [],
workflows: exportOptions.includeWorkflows ? workflows : [],
luaScripts: exportOptions.includeLuaScripts ? luaScripts : [],
componentHierarchy: exportOptions.includeComponentHierarchy ? componentHierarchy : {},
componentConfigs: exportOptions.includeComponentConfigs ? componentConfigs : {},
cssClasses: exportOptions.includeCssClasses ? cssClasses : undefined,
dropdownConfigs: exportOptions.includeDropdownConfigs ? dropdownConfigs : undefined,
}
const blob = await exportPackageAsZip(fullManifest, content, [], exportOptions)
const fileName = `${manifest.name.toLowerCase().replace(/\s+/g, '-')}-${manifest.version}.zip`
downloadZip(blob, fileName)
toast.success('Package exported successfully!')
onOpenChange(false)
} catch (error) {
console.error('Export error:', error)
toast.error('Failed to export package')
} finally {
setExporting(false)
}
}
const handleExportSnapshot = async () => {
setExporting(true)
try {
const schemas = await Database.getSchemas()
const pages = await Database.getPages()
const workflows = await Database.getWorkflows()
const luaScripts = await Database.getLuaScripts()
const componentHierarchy = await Database.getComponentHierarchy()
const componentConfigs = await Database.getComponentConfigs()
const cssClasses = await Database.getCssClasses()
const dropdownConfigs = await Database.getDropdownConfigs()
const blob = await exportDatabaseSnapshot(
schemas,
pages,
workflows,
luaScripts,
componentHierarchy,
componentConfigs,
cssClasses,
dropdownConfigs
)
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)
downloadZip(blob, `database-snapshot-${timestamp}.zip`)
toast.success('Database snapshot exported successfully!')
onOpenChange(false)
} catch (error) {
console.error('Snapshot export error:', error)
toast.error('Failed to export database snapshot')
} finally {
setExporting(false)
}
}
const handleImport = async (file: File) => {
setImporting(true)
try {
const { manifest: importedManifest, content, assets } = await importPackageFromZip(file)
const currentSchemas = await Database.getSchemas()
const currentPages = await Database.getPages()
const currentWorkflows = await Database.getWorkflows()
const currentLuaScripts = await Database.getLuaScripts()
const currentHierarchy = await Database.getComponentHierarchy()
const currentConfigs = await Database.getComponentConfigs()
const newSchemas = [...currentSchemas, ...content.schemas]
const newPages = [...currentPages, ...content.pages]
const newWorkflows = [...currentWorkflows, ...content.workflows]
const newLuaScripts = [...currentLuaScripts, ...content.luaScripts]
const newHierarchy = { ...currentHierarchy, ...content.componentHierarchy }
const newConfigs = { ...currentConfigs, ...content.componentConfigs }
await Database.setSchemas(newSchemas)
await Database.setPages(newPages)
await Database.setWorkflows(newWorkflows)
await Database.setLuaScripts(newLuaScripts)
await Database.setComponentHierarchy(newHierarchy)
await Database.setComponentConfigs(newConfigs)
if (content.cssClasses) {
const currentCssClasses = await Database.getCssClasses()
await Database.setCssClasses([...currentCssClasses, ...content.cssClasses])
}
if (content.dropdownConfigs) {
const currentDropdowns = await Database.getDropdownConfigs()
await Database.setDropdownConfigs([...currentDropdowns, ...content.dropdownConfigs])
}
if (content.seedData) {
await Database.setPackageData(importedManifest.id, content.seedData)
}
const installedPackage = {
packageId: importedManifest.id,
installedAt: Date.now(),
version: importedManifest.version,
enabled: true,
}
await Database.installPackage(installedPackage)
toast.success(`Package "${importedManifest.name}" imported successfully!`)
toast.info(`Imported: ${content.schemas.length} schemas, ${content.pages.length} pages, ${content.workflows.length} workflows, ${assets.length} assets`)
onOpenChange(false)
} catch (error) {
console.error('Import error:', error)
toast.error(`Failed to import package: ${error instanceof Error ? error.message : 'Unknown error'}`)
} finally {
setImporting(false)
}
}
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) {
if (!file.name.endsWith('.zip')) {
toast.error('Please select a .zip file')
return
}
handleImport(file)
}
}
const handleAddTag = () => {
if (tagInput.trim() && !manifest.tags?.includes(tagInput.trim())) {
setManifest(prev => ({
...prev,
tags: [...(prev.tags || []), tagInput.trim()]
}))
setTagInput('')
}
}
const handleRemoveTag = (tag: string) => {
setManifest(prev => ({
...prev,
tags: (prev.tags || []).filter(t => t !== tag)
}))
}
if (mode === 'import') {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center">
<ArrowSquareIn size={24} weight="duotone" className="text-white" />
</div>
<div>
<DialogTitle>Import Package</DialogTitle>
<DialogDescription>Import a package from a ZIP file</DialogDescription>
</div>
</div>
</DialogHeader>
<div className="space-y-4 py-4">
<Card>
<CardHeader>
<CardTitle className="text-lg">Select Package File</CardTitle>
<CardDescription>Choose a .zip file containing a MetaBuilder package</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div
className="border-2 border-dashed rounded-lg p-8 text-center hover:border-primary hover:bg-accent/50 transition-colors cursor-pointer"
onClick={() => fileInputRef.current?.click()}
>
<FileArrowUp size={48} className="mx-auto mb-4 text-muted-foreground" />
<p className="font-medium mb-1">Click to select a package file</p>
<p className="text-sm text-muted-foreground">Supports .zip files only</p>
<input
ref={fileInputRef}
type="file"
accept=".zip"
onChange={handleFileSelect}
className="hidden"
/>
</div>
{importing && (
<div className="flex items-center justify-center gap-2 text-sm text-muted-foreground">
<div className="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
<span>Importing package...</span>
</div>
)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">What's Included in Packages?</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span>Data schemas</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span>Page configurations</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span>Workflows</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span>Lua scripts</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span>Component hierarchies</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span>CSS configurations</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span>Assets (images, etc.)</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span>Seed data</span>
</div>
</div>
</CardContent>
</Card>
<div className="p-4 rounded-lg bg-yellow-500/10 border border-yellow-500/20 flex items-start gap-3">
<Warning size={20} className="text-yellow-600 flex-shrink-0 mt-0.5" />
<div className="text-sm">
<p className="font-medium text-yellow-900 dark:text-yellow-100 mb-1">Import Warning</p>
<p className="text-yellow-800 dark:text-yellow-200">Imported packages will be merged with existing data. Make sure to back up your database before importing.</p>
</div>
</div>
</div>
</DialogContent>
</Dialog>
)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-green-500 to-green-700 flex items-center justify-center">
<Export size={24} weight="duotone" className="text-white" />
</div>
<div>
<DialogTitle>Export Package</DialogTitle>
<DialogDescription>Create a shareable package or database snapshot</DialogDescription>
</div>
</div>
</DialogHeader>
<ScrollArea className="flex-1 -mx-6 px-6">
<div className="space-y-6 py-4">
<div className="grid grid-cols-2 gap-3">
<Card className="cursor-pointer hover:border-primary transition-colors">
<CardHeader>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-green-500 to-green-700 flex items-center justify-center">
<Package size={20} className="text-white" />
</div>
<CardTitle className="text-base">Custom Package</CardTitle>
</div>
<CardDescription>Export selected data as a reusable package</CardDescription>
</CardHeader>
</Card>
<Card
className="cursor-pointer hover:border-primary transition-colors"
onClick={handleExportSnapshot}
>
<CardHeader>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center">
<DatabaseIcon size={20} className="text-white" />
</div>
<CardTitle className="text-base">Full Snapshot</CardTitle>
</div>
<CardDescription>Export entire database as backup</CardDescription>
</CardHeader>
</Card>
</div>
<Separator />
<div className="space-y-4">
<div>
<Label htmlFor="package-name">Package Name *</Label>
<Input
id="package-name"
placeholder="My Awesome Package"
value={manifest.name}
onChange={e => setManifest(prev => ({ ...prev, name: e.target.value }))}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="package-version">Version</Label>
<Input
id="package-version"
placeholder="1.0.0"
value={manifest.version}
onChange={e => setManifest(prev => ({ ...prev, version: e.target.value }))}
/>
</div>
<div>
<Label htmlFor="package-author">Author</Label>
<Input
id="package-author"
placeholder="Your Name"
value={manifest.author}
onChange={e => setManifest(prev => ({ ...prev, author: e.target.value }))}
/>
</div>
</div>
<div>
<Label htmlFor="package-description">Description</Label>
<Textarea
id="package-description"
placeholder="Describe what this package does..."
value={manifest.description}
onChange={e => setManifest(prev => ({ ...prev, description: e.target.value }))}
rows={3}
/>
</div>
<div>
<Label htmlFor="package-tags">Tags</Label>
<div className="flex gap-2 mb-2">
<Input
id="package-tags"
placeholder="Add a tag..."
value={tagInput}
onChange={e => setTagInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && (e.preventDefault(), handleAddTag())}
/>
<Button type="button" onClick={handleAddTag}>Add</Button>
</div>
{manifest.tags && manifest.tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{manifest.tags.map(tag => (
<div key={tag} className="px-2 py-1 bg-secondary rounded-md text-sm flex items-center gap-2">
<span>{tag}</span>
<button
type="button"
onClick={() => handleRemoveTag(tag)}
className="text-muted-foreground hover:text-foreground"
>
×
</button>
</div>
))}
</div>
)}
</div>
</div>
<Separator />
<div>
<Label className="mb-3 block">Export Options</Label>
<div className="space-y-3">
<div className="flex items-center gap-2">
<Checkbox
id="export-schemas"
checked={exportOptions.includeSchemas}
onCheckedChange={checked =>
setExportOptions(prev => ({ ...prev, includeSchemas: checked as boolean }))
}
/>
<Label htmlFor="export-schemas" className="font-normal cursor-pointer">
Include data schemas
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="export-pages"
checked={exportOptions.includePages}
onCheckedChange={checked =>
setExportOptions(prev => ({ ...prev, includePages: checked as boolean }))
}
/>
<Label htmlFor="export-pages" className="font-normal cursor-pointer">
Include page configurations
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="export-workflows"
checked={exportOptions.includeWorkflows}
onCheckedChange={checked =>
setExportOptions(prev => ({ ...prev, includeWorkflows: checked as boolean }))
}
/>
<Label htmlFor="export-workflows" className="font-normal cursor-pointer">
Include workflows
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="export-lua"
checked={exportOptions.includeLuaScripts}
onCheckedChange={checked =>
setExportOptions(prev => ({ ...prev, includeLuaScripts: checked as boolean }))
}
/>
<Label htmlFor="export-lua" className="font-normal cursor-pointer">
Include Lua scripts
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="export-components"
checked={exportOptions.includeComponentHierarchy}
onCheckedChange={checked =>
setExportOptions(prev => ({ ...prev, includeComponentHierarchy: checked as boolean }))
}
/>
<Label htmlFor="export-components" className="font-normal cursor-pointer">
Include component hierarchies
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="export-configs"
checked={exportOptions.includeComponentConfigs}
onCheckedChange={checked =>
setExportOptions(prev => ({ ...prev, includeComponentConfigs: checked as boolean }))
}
/>
<Label htmlFor="export-configs" className="font-normal cursor-pointer">
Include component configurations
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="export-css"
checked={exportOptions.includeCssClasses}
onCheckedChange={checked =>
setExportOptions(prev => ({ ...prev, includeCssClasses: checked as boolean }))
}
/>
<Label htmlFor="export-css" className="font-normal cursor-pointer">
Include CSS classes
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="export-dropdowns"
checked={exportOptions.includeDropdownConfigs}
onCheckedChange={checked =>
setExportOptions(prev => ({ ...prev, includeDropdownConfigs: checked as boolean }))
}
/>
<Label htmlFor="export-dropdowns" className="font-normal cursor-pointer">
Include dropdown configurations
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="export-seed"
checked={exportOptions.includeSeedData}
onCheckedChange={checked =>
setExportOptions(prev => ({ ...prev, includeSeedData: checked as boolean }))
}
/>
<Label htmlFor="export-seed" className="font-normal cursor-pointer">
Include seed data
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="export-assets"
checked={exportOptions.includeAssets}
onCheckedChange={checked =>
setExportOptions(prev => ({ ...prev, includeAssets: checked as boolean }))
}
/>
<Label htmlFor="export-assets" className="font-normal cursor-pointer">
Include assets (images, videos, audio, documents)
</Label>
</div>
</div>
</div>
</div>
</ScrollArea>
<DialogFooter className="gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleExport} disabled={exporting || !manifest.name}>
{exporting ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2" />
Exporting...
</>
) : (
<>
<FileArrowDown size={16} className="mr-2" />
Export Package
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -12,7 +12,8 @@ import { toast } from 'sonner'
import { Database } from '@/lib/database'
import { PACKAGE_CATALOG } from '@/lib/package-catalog'
import type { PackageManifest, PackageContent, InstalledPackage } from '@/lib/package-types'
import { Package, Download, Trash, Power, MagnifyingGlass, Star, Tag, User, TrendUp, Funnel } from '@phosphor-icons/react'
import { Package, Download, Trash, Power, MagnifyingGlass, Star, Tag, User, TrendUp, Funnel, Export, ArrowSquareIn } from '@phosphor-icons/react'
import { PackageImportExport } from './PackageImportExport'
interface PackageManagerProps {
onClose?: () => void
@@ -27,6 +28,8 @@ export function PackageManager({ onClose }: PackageManagerProps) {
const [sortBy, setSortBy] = useState<'name' | 'downloads' | 'rating'>('downloads')
const [showDetails, setShowDetails] = useState(false)
const [installing, setInstalling] = useState(false)
const [showImportExport, setShowImportExport] = useState(false)
const [importExportMode, setImportExportMode] = useState<'import' | 'export'>('export')
useEffect(() => {
loadPackages()
@@ -198,11 +201,33 @@ export function PackageManager({ onClose }: PackageManagerProps) {
<p className="text-sm text-muted-foreground">Install pre-built applications and features</p>
</div>
</div>
{onClose && (
<Button variant="ghost" onClick={onClose}>
Close
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={() => {
setImportExportMode('import')
setShowImportExport(true)
}}
>
<ArrowSquareIn size={16} className="mr-2" />
Import
</Button>
)}
<Button
variant="outline"
onClick={() => {
setImportExportMode('export')
setShowImportExport(true)
}}
>
<Export size={16} className="mr-2" />
Export
</Button>
{onClose && (
<Button variant="ghost" onClick={onClose}>
Close
</Button>
)}
</div>
</div>
<div className="flex-1 overflow-hidden">
@@ -453,6 +478,17 @@ export function PackageManager({ onClose }: PackageManagerProps) {
)}
</DialogContent>
</Dialog>
<PackageImportExport
open={showImportExport}
onOpenChange={(open) => {
setShowImportExport(open)
if (!open) {
loadPackages()
}
}}
mode={importExportMode}
/>
</div>
)
}

254
src/lib/package-export.ts Normal file
View File

@@ -0,0 +1,254 @@
import JSZip from 'jszip'
import type { PackageManifest, PackageContent } from './package-types'
import type { DatabaseSchema } from './database'
export interface ExportPackageOptions {
includeAssets?: boolean
includeSchemas?: boolean
includePages?: boolean
includeWorkflows?: boolean
includeLuaScripts?: boolean
includeComponentHierarchy?: boolean
includeComponentConfigs?: boolean
includeCssClasses?: boolean
includeDropdownConfigs?: boolean
includeSeedData?: boolean
}
export interface AssetFile {
path: string
blob: Blob
type: 'image' | 'video' | 'audio' | 'document'
}
export async function exportPackageAsZip(
manifest: PackageManifest,
content: PackageContent,
assets: AssetFile[] = [],
options: ExportPackageOptions = {}
): Promise<Blob> {
const zip = new JSZip()
const opts = {
includeAssets: true,
includeSchemas: true,
includePages: true,
includeWorkflows: true,
includeLuaScripts: true,
includeComponentHierarchy: true,
includeComponentConfigs: true,
includeCssClasses: true,
includeDropdownConfigs: true,
includeSeedData: true,
...options,
}
zip.file('manifest.json', JSON.stringify(manifest, null, 2))
const packageContent: Partial<PackageContent> = {}
if (opts.includeSchemas && content.schemas) {
packageContent.schemas = content.schemas
}
if (opts.includePages && content.pages) {
packageContent.pages = content.pages
}
if (opts.includeWorkflows && content.workflows) {
packageContent.workflows = content.workflows
}
if (opts.includeLuaScripts && content.luaScripts) {
packageContent.luaScripts = content.luaScripts
}
if (opts.includeComponentHierarchy && content.componentHierarchy) {
packageContent.componentHierarchy = content.componentHierarchy
}
if (opts.includeComponentConfigs && content.componentConfigs) {
packageContent.componentConfigs = content.componentConfigs
}
if (opts.includeCssClasses && content.cssClasses) {
packageContent.cssClasses = content.cssClasses
}
if (opts.includeDropdownConfigs && content.dropdownConfigs) {
packageContent.dropdownConfigs = content.dropdownConfigs
}
if (opts.includeSeedData && content.seedData) {
packageContent.seedData = content.seedData
}
zip.file('content.json', JSON.stringify(packageContent, null, 2))
if (opts.includeAssets && assets.length > 0) {
const assetsFolder = zip.folder('assets')
if (assetsFolder) {
for (const asset of assets) {
const typeFolder = assetsFolder.folder(asset.type + 's')
if (typeFolder) {
const fileName = asset.path.split('/').pop() || 'unnamed'
typeFolder.file(fileName, asset.blob)
}
}
const assetManifest = assets.map(asset => ({
originalPath: asset.path,
type: asset.type,
fileName: asset.path.split('/').pop(),
}))
assetsFolder.file('asset-manifest.json', JSON.stringify(assetManifest, null, 2))
}
}
zip.file('README.md', generateReadme(manifest, content))
const blob = await zip.generateAsync({ type: 'blob' })
return blob
}
export async function importPackageFromZip(zipFile: File): Promise<{
manifest: PackageManifest
content: PackageContent
assets: Array<{ path: string; blob: Blob; type: 'image' | 'video' | 'audio' | 'document' }>
}> {
const zip = await JSZip.loadAsync(zipFile)
const manifestFile = zip.file('manifest.json')
if (!manifestFile) {
throw new Error('Invalid package: manifest.json not found')
}
const manifestText = await manifestFile.async('text')
const manifest: PackageManifest = JSON.parse(manifestText)
const contentFile = zip.file('content.json')
if (!contentFile) {
throw new Error('Invalid package: content.json not found')
}
const contentText = await contentFile.async('text')
const content: PackageContent = JSON.parse(contentText)
const assets: Array<{ path: string; blob: Blob; type: 'image' | 'video' | 'audio' | 'document' }> = []
const assetManifestFile = zip.file('assets/asset-manifest.json')
if (assetManifestFile) {
const assetManifestText = await assetManifestFile.async('text')
const assetManifest: Array<{ originalPath: string; type: string; fileName: string }> = JSON.parse(assetManifestText)
for (const assetInfo of assetManifest) {
const assetPath = `assets/${assetInfo.type}s/${assetInfo.fileName}`
const assetFile = zip.file(assetPath)
if (assetFile) {
const blob = await assetFile.async('blob')
assets.push({
path: assetInfo.originalPath,
blob,
type: assetInfo.type as 'image' | 'video' | 'audio' | 'document',
})
}
}
}
return { manifest, content, assets }
}
export function downloadZip(blob: Blob, fileName: string) {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = fileName
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
function generateReadme(manifest: PackageManifest, content: PackageContent): string {
return `# ${manifest.name}
Version: ${manifest.version}
Author: ${manifest.author}
Category: ${manifest.category}
## Description
${manifest.description}
## Contents
- **Schemas**: ${content.schemas?.length || 0} data models
- **Pages**: ${content.pages?.length || 0} page configurations
- **Workflows**: ${content.workflows?.length || 0} workflows
- **Lua Scripts**: ${content.luaScripts?.length || 0} scripts
- **Components**: ${Object.keys(content.componentHierarchy || {}).length} component hierarchies
- **CSS Classes**: ${content.cssClasses?.length || 0} CSS categories
- **Dropdown Configs**: ${content.dropdownConfigs?.length || 0} dropdown configurations
## Tags
${manifest.tags.join(', ')}
## Installation
Import this package through the MetaBuilder Package Manager.
## Dependencies
${manifest.dependencies.length > 0 ? manifest.dependencies.join(', ') : 'None'}
---
Generated by MetaBuilder Package Exporter
`
}
export async function exportDatabaseSnapshot(
schemas: any[],
pages: any[],
workflows: any[],
luaScripts: any[],
componentHierarchy: Record<string, any>,
componentConfigs: Record<string, any>,
cssClasses: any[],
dropdownConfigs: any[],
assets: AssetFile[] = []
): Promise<Blob> {
const manifest: PackageManifest = {
id: `snapshot_${Date.now()}`,
name: 'Database Snapshot',
version: '1.0.0',
description: 'Complete database snapshot export',
author: 'User Export',
category: 'other',
icon: '💾',
screenshots: [],
tags: ['snapshot', 'backup', 'export'],
dependencies: [],
createdAt: Date.now(),
updatedAt: Date.now(),
downloadCount: 0,
rating: 0,
installed: false,
}
const content: PackageContent = {
schemas,
pages,
workflows,
luaScripts,
componentHierarchy,
componentConfigs,
cssClasses,
dropdownConfigs,
}
return exportPackageAsZip(manifest, content, assets)
}