mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
Generated by Spark: I think its too techie --> need dropdown boxes --> areas of god tier ui where I can configure those drop down boxes if the value has to be dynamic --> instead of showing loads of css class stuff, pull that data from a database table containing a list of css classes, then make a gui css class builder. If you show a code box, avoid a plain text box, instead show a specialised code editor box, I think there are tree view style librsries that allow you to edit json easily.
This commit is contained in:
143
IMPROVEMENTS.md
Normal file
143
IMPROVEMENTS.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# MetaBuilder UI Improvements
|
||||
|
||||
## Summary of Changes
|
||||
|
||||
This update transforms the god-tier builder interface from a technical code-heavy tool into a user-friendly GUI-based system with the following major improvements:
|
||||
|
||||
### 1. **CSS Class Builder**
|
||||
- Visual CSS class selector with categorized classes
|
||||
- No more typing raw CSS - click to select from predefined classes
|
||||
- Custom class input for edge cases
|
||||
- Real-time preview of selected classes
|
||||
- Classes organized by category: Layout, Spacing, Sizing, Typography, Colors, Borders, Effects, Positioning, Alignment, Interactivity
|
||||
|
||||
**Location:** `src/components/CssClassBuilder.tsx`
|
||||
**Usage:** Automatically integrated into PropertyInspector for any `className` prop
|
||||
|
||||
### 2. **Dynamic Dropdown Configuration System**
|
||||
- Create custom dropdown options from the god-tier panel
|
||||
- Configure dropdown values once, use them across multiple properties
|
||||
- GUI-based option management (no JSON editing required)
|
||||
- Dropdowns can be assigned to component properties dynamically
|
||||
|
||||
**Location:** `src/components/DropdownConfigManager.tsx`
|
||||
**Access:** God-tier panel → "Dropdowns" tab
|
||||
|
||||
### 3. **CSS Class Manager**
|
||||
- Manage the library of CSS classes available in the builder
|
||||
- Organize classes into categories
|
||||
- Add/edit/delete categories and their classes
|
||||
- Populated with comprehensive Tailwind utility classes by default
|
||||
|
||||
**Location:** `src/components/CssClassManager.tsx`
|
||||
**Access:** God-tier panel → "CSS Classes" tab
|
||||
|
||||
### 4. **Monaco-Based JSON Editor**
|
||||
- Replaced plain textboxes with professional Monaco editor
|
||||
- Syntax highlighting for JSON
|
||||
- Auto-formatting and validation
|
||||
- Better error messages
|
||||
- Tree-style folding and bracket colorization
|
||||
|
||||
**Location:** `src/components/JsonEditor.tsx`
|
||||
**Used in:** SchemaEditor and anywhere JSON needs to be edited
|
||||
|
||||
### 5. **Enhanced Property Inspector**
|
||||
- CSS classes now have a visual builder button
|
||||
- Dynamic dropdowns support (properties can use configured dropdowns)
|
||||
- Better visual hierarchy
|
||||
- Icon-based property types
|
||||
|
||||
**Updated:** `src/components/PropertyInspector.tsx`
|
||||
|
||||
### 6. **Extended Database Schema**
|
||||
- New tables for CSS class categories
|
||||
- New tables for dropdown configurations
|
||||
- Automatic seeding with 200+ Tailwind classes
|
||||
|
||||
**Updated:** `src/lib/database.ts`
|
||||
|
||||
### 7. **God-Tier Panel Enhancements**
|
||||
- Two new tabs: "CSS Classes" and "Dropdowns"
|
||||
- Better organization of configuration options
|
||||
- More intuitive navigation
|
||||
|
||||
**Updated:** `src/components/Level4.tsx`
|
||||
|
||||
## Key Features
|
||||
|
||||
### For Non-Technical Users
|
||||
- **Point-and-click CSS editing** - No need to remember class names
|
||||
- **Visual dropdown configuration** - Create select options without coding
|
||||
- **Professional code editor** - When JSON is needed, get proper tooling
|
||||
- **Organized categorization** - Everything is grouped logically
|
||||
|
||||
### For Technical Users
|
||||
- **Extensible system** - Easy to add new CSS categories
|
||||
- **Custom class support** - Can still add custom CSS when needed
|
||||
- **JSON import/export** - Full control when needed
|
||||
- **Monaco editor** - Industry-standard code editing experience
|
||||
|
||||
## How to Use
|
||||
|
||||
### Creating a Dropdown Configuration
|
||||
1. Go to Level 4 (God-Tier Panel)
|
||||
2. Click "Dropdowns" tab
|
||||
3. Click "Create Dropdown"
|
||||
4. Enter a name (e.g., `status_options`)
|
||||
5. Add options with values and labels
|
||||
6. Save
|
||||
|
||||
### Using Dynamic Dropdowns in Components
|
||||
In the component catalog, define a property like:
|
||||
```typescript
|
||||
{
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
type: 'dynamic-select',
|
||||
dynamicSource: 'status_options' // references dropdown config by name
|
||||
}
|
||||
```
|
||||
|
||||
### Managing CSS Classes
|
||||
1. Go to Level 4 (God-Tier Panel)
|
||||
2. Click "CSS Classes" tab
|
||||
3. Browse categories or create new ones
|
||||
4. Add/remove classes from categories
|
||||
5. These classes will appear in the CSS Class Builder
|
||||
|
||||
### Building CSS Classes Visually
|
||||
1. Select a component in the builder
|
||||
2. In the Property Inspector, find the "CSS Classes" field
|
||||
3. Click the palette icon next to it
|
||||
4. Select classes from categories
|
||||
5. Preview and apply
|
||||
|
||||
## Database Keys
|
||||
|
||||
New database keys added:
|
||||
- `db_css_classes` - Stores CSS class categories
|
||||
- `db_dropdown_configs` - Stores dropdown configurations
|
||||
|
||||
## Component Property Types
|
||||
|
||||
New property type added: `'dynamic-select'`
|
||||
- References a dropdown configuration by name
|
||||
- Options are loaded from the database
|
||||
- Can be managed from god-tier panel
|
||||
|
||||
## Default CSS Categories
|
||||
|
||||
The system comes pre-loaded with 10 categories:
|
||||
1. **Layout** - flex, grid, block, inline, etc.
|
||||
2. **Spacing** - padding, margin, gap classes
|
||||
3. **Sizing** - width, height, max-width classes
|
||||
4. **Typography** - text sizes, weights, alignment
|
||||
5. **Colors** - text, background, border colors
|
||||
6. **Borders** - border styles and radii
|
||||
7. **Effects** - shadows, opacity, transitions
|
||||
8. **Positioning** - relative, absolute, z-index
|
||||
9. **Alignment** - items-center, justify-between, etc.
|
||||
10. **Interactivity** - cursor, hover, active states
|
||||
|
||||
Total: 200+ classes ready to use
|
||||
312
PRD.md
312
PRD.md
@@ -1,176 +1,186 @@
|
||||
# Planning Guide
|
||||
# PRD: MetaBuilder Visual Configuration System
|
||||
|
||||
A meta-architecture system with 4 distinct levels: Level 1 (public-facing website with sample content), Level 2 (user area with profiles and comments), Level 3 (Django-style admin panel for data management), and Level 4 (god-tier builder where all previous levels can be designed, developed, and tested through visual workflows, GUI editors for JSON schemas, and Lua scripting).
|
||||
## Mission Statement
|
||||
Transform the MetaBuilder god-tier panel from a technical, code-heavy interface into an intuitive visual configuration system that empowers both technical and non-technical users to build sophisticated applications through GUI-based tools.
|
||||
|
||||
**Experience Qualities**:
|
||||
1. **Layered** - Clear separation between public, user, admin, and meta-builder levels with intuitive navigation between tiers
|
||||
2. **Generative** - Level 4 can procedurally generate entire applications for Levels 1-3 through declarative JSON schemas and visual workflows
|
||||
3. **Powerful** - Lua lambdas for custom logic, visual JSON schema editor, workflow system for complex processes, all through an intuitive GUI
|
||||
## Experience Qualities
|
||||
1. **Intuitive** - Users should discover features naturally without extensive documentation, with visual cues guiding them through complex configurations.
|
||||
2. **Empowering** - Non-technical users can accomplish sophisticated customization without writing code, while technical users retain full control when needed.
|
||||
3. **Efficient** - Common tasks that previously required typing or memorization are now accomplished through point-and-click interactions, dramatically reducing configuration time.
|
||||
|
||||
**Complexity Level**: Complex Application (advanced functionality, likely with multiple views)
|
||||
This is a 4-tier meta-application builder: a public website layer, authenticated user area, admin panel, and a god-tier visual builder that can procedurally generate all three previous layers using JSON schemas, workflow systems, and embedded Lua scripting.
|
||||
## Complexity Level
|
||||
**Complex Application** (advanced functionality with multiple views) - This is a meta-framework for building applications with four distinct user levels, database management, visual component builders, and dynamic configuration systems.
|
||||
|
||||
## Essential Features
|
||||
|
||||
### Level 1: Public Website
|
||||
- **Functionality**: Normal webpage with responsive hamburger menu, hero section, content areas, footer
|
||||
- **Purpose**: Public-facing content accessible to anyone without authentication
|
||||
- **Trigger**: User visits root URL without authentication
|
||||
- **Progression**: Load homepage → Browse content sections → Click hamburger menu → Navigate pages → View sample content
|
||||
- **Success criteria**: Responsive design works; hamburger menu collapses on mobile; content is readable; links work; no auth required
|
||||
### 1. 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
|
||||
**Progression:** Open builder → Browse categories (Layout, Spacing, Typography, etc.) → Click classes to select → See live preview → Apply to component
|
||||
**Success Criteria:**
|
||||
- User can style components without typing a single class name
|
||||
- Selected classes display in real-time preview
|
||||
- 200+ predefined classes organized into 10 categories
|
||||
- Custom class input available for edge cases
|
||||
|
||||
### Level 2: User Area
|
||||
- **Functionality**: Authenticated user dashboard with profile page and comment system
|
||||
- **Purpose**: Allow normal users to sign up, manage their profile, and interact through comments
|
||||
- **Trigger**: User clicks "Sign Up" or "Login" from Level 1
|
||||
- **Progression**: Register account → Verify credentials → Access dashboard → Edit profile → Browse comment sections → Post/edit comments → View own history
|
||||
- **Success criteria**: Registration persists in KV; profile edits save; comments are CRUD-able; users can't access admin functions; profile picture upload works
|
||||
### 2. 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
|
||||
**Progression:** Create dropdown config → Name it → Add options (value/label pairs) → Save → Reference in component schemas → Options appear automatically
|
||||
**Success Criteria:**
|
||||
- Dropdown created once, usable in unlimited component properties
|
||||
- Changes to dropdown propagate to all components using it
|
||||
- Visual GUI for managing options (no JSON required)
|
||||
- Pre-loaded with common examples (status, priority, category)
|
||||
|
||||
### Level 3: Django-Style Admin Panel
|
||||
- **Functionality**: Full data management interface with model list views, CRUD operations, filtering, search, and bulk actions
|
||||
- **Purpose**: Provide admin users comprehensive control over all data models and system configuration
|
||||
- **Trigger**: User with admin role logs in or selects "Admin" from navigation
|
||||
- **Progression**: Login as admin → View model dashboard → Select model → See filtered list view → Search/filter records → Click record → Edit form → Save changes → View audit trail
|
||||
- **Success criteria**: All models from schema rendered; inline editing; validation works; relations display correctly; permissions enforced; export to JSON/CSV
|
||||
### 3. 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
|
||||
**Progression:** View categories → Create/edit category → Add/remove classes → Save → Classes appear in CSS Class Builder
|
||||
**Success Criteria:**
|
||||
- Categories can be added, edited, or deleted
|
||||
- Each category contains unlimited class names
|
||||
- Changes immediately reflected in CSS Class Builder
|
||||
- System initializes with comprehensive Tailwind utilities
|
||||
|
||||
### Level 4: God-Tier Builder
|
||||
- **Functionality**: Meta-builder with visual JSON schema editor, workflow designer, Lua lambda editor, component catalog, and live preview of Levels 1-3
|
||||
- **Purpose**: Allow god-level users to design, configure, and deploy entire applications for Levels 1-3 through declarative configuration
|
||||
- **Trigger**: User with god role accesses builder interface
|
||||
- **Progression**: Open builder → Design data schema in GUI → Create workflows visually → Write Lua handlers → Configure page templates → Preview generated app → Deploy configuration → Test all levels
|
||||
- **Success criteria**: JSON schema editor validates; workflow nodes connect; Lua syntax highlighting; live preview updates; can export/import configurations; changes propagate to all levels
|
||||
### 4. 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
|
||||
**Progression:** Open editor → See syntax-highlighted code → Edit with autocomplete → Format code → Validate → Save
|
||||
**Success Criteria:**
|
||||
- Syntax highlighting for JSON and Lua
|
||||
- Real-time error detection and display
|
||||
- Code formatting on demand
|
||||
- Bracket pair colorization and matching
|
||||
- Minimap for navigation
|
||||
- Find/replace functionality
|
||||
|
||||
### JSON Schema Editor (Level 4)
|
||||
- **Functionality**: Visual GUI for defining data models with fields, types, validation rules, relationships
|
||||
- **Purpose**: Declaratively define all data structures without writing JSON by hand
|
||||
- **Trigger**: User opens "Schema Designer" in Level 4
|
||||
- **Progression**: Create new model → Add fields via forms → Set field types/constraints → Define relations → Visualize schema graph → Validate → Generate Level 3 admin interface
|
||||
- **Success criteria**: All field types supported; visual relationship mapping; constraint validation; auto-generates CRUD interfaces; imports/exports valid JSON
|
||||
### 5. Enhanced Property Inspector
|
||||
**Functionality:** Context-aware property editor with specialized controls for different data types
|
||||
**Purpose:** Provide the right UI control for each property type automatically
|
||||
**Trigger:** User selects component in builder
|
||||
**Progression:** Select component → View properties → Use appropriate control (text input, dropdown, CSS builder, etc.) → Changes apply immediately
|
||||
**Success Criteria:**
|
||||
- String fields use text inputs
|
||||
- Boolean fields use dropdowns (true/false)
|
||||
- Select fields use static dropdowns
|
||||
- Dynamic-select fields load options from dropdown configs
|
||||
- className fields have CSS Builder button
|
||||
- All changes saved to component props
|
||||
|
||||
### Workflow System (Level 4)
|
||||
- **Functionality**: Visual node-based workflow editor for defining business logic flows
|
||||
- **Purpose**: Create complex processes (approval flows, notifications, data transformations) without code
|
||||
- **Trigger**: User opens "Workflow Designer" in Level 4
|
||||
- **Progression**: Create workflow → Drag trigger node → Add action nodes → Connect with arrows → Configure conditions → Attach to data events → Test execution → Monitor runs
|
||||
- **Success criteria**: Nodes connect smoothly; execution order clear; can branch/merge; error handling; logs show execution path; integrates with Lua
|
||||
|
||||
### Lua Lambda System (Level 4)
|
||||
- **Functionality**: Real Lua interpreter (fengari-web) with full language support, Monaco editor with syntax highlighting and autocomplete, parameter handling, context API access, comprehensive execution feedback, and extensive snippet library with 30+ pre-built templates
|
||||
- **Purpose**: Provide safe, sandboxed scripting for custom transformations, validations, and business logic with real Lua execution beyond declarative capabilities, enhanced by professional code editing experience and reusable code patterns
|
||||
- **Trigger**: User adds "Lua Action" node in workflow or creates Lua script in scripts tab
|
||||
- **Progression**: Open Monaco-based Lua editor → Define parameters → Browse snippet library by category → Search and preview snippets → Insert template code → Customize with syntax highlighting and autocomplete → Access context.data/user/kv via intelligent suggestions → Test with sample inputs → View execution logs → Return structured results → Integrate into workflows
|
||||
- **Success criteria**: Monaco editor integrated with Lua language support; autocomplete provides context API suggestions (context.data, context.user, context.kv, log, print); syntax highlighting active; real Lua execution via fengari; parameter type validation; execution logs captured; return values parsed; syntax/runtime errors shown with line numbers; can transform JSON data; fullscreen editing mode available; snippet library accessible with 30+ templates across 12 categories; snippets insertable at cursor position; integrates with workflow nodes
|
||||
|
||||
### Lua Snippet Library (Level 4)
|
||||
- **Functionality**: Comprehensive library of 30+ pre-built Lua code templates organized into 12 categories (Data Validation, Data Transformation, Array Operations, String Processing, Math & Calculations, Conditionals & Logic, User Management, Error Handling, API & Networking, Date & Time, File Operations, Utilities)
|
||||
- **Purpose**: Accelerate development by providing tested, reusable patterns for common operations; reduce errors; teach best practices
|
||||
- **Trigger**: User clicks "Snippet Library" button in Lua editor or opens "Snippet Library" tab in Level 4
|
||||
- **Progression**: Open snippet library → Browse by category or search by keyword/tag → Preview snippet details and parameters → View full code in syntax-highlighted display → Copy to clipboard or insert into editor → Customize for specific use case
|
||||
- **Success criteria**: 30+ snippets covering common patterns; organized into logical categories; searchable by name, description, and tags; preview shows code, description, and required parameters; one-click copy or insert; snippets include validation, transformation, calculations, conditionals, string operations, array operations, date handling, error handling, and utilities; modal detail view for full inspection
|
||||
|
||||
### Database Persistence Layer (All Levels)
|
||||
- **Functionality**: Centralized database abstraction layer with SHA-512 password hashing, KV persistence for all entities (users, credentials, workflows, Lua scripts, pages, schemas, comments, component hierarchy, component configs), comprehensive CRUD operations, import/export functionality, and database management UI
|
||||
- **Purpose**: Provide secure, persistent storage for all application data with proper password security, enable data portability, and allow god-tier users to inspect and manage the entire database state
|
||||
- **Trigger**: Application initialization; any data mutation; user opens Database tab in Level 4
|
||||
- **Progression**: App loads → Initialize database with defaults → Load entities from KV → Perform CRUD operations → Hash passwords with SHA-512 → Persist changes to KV → View statistics in Database Manager → Export full database to JSON → Import database from JSON backup → Clear and reinitialize database
|
||||
- **Success criteria**: All passwords stored as SHA-512 hashes; KV persistence works across sessions; CRUD operations atomic; database export includes all entities; import validates and restores data; Database Manager shows real-time statistics; clear database requires double confirmation; no plaintext passwords ever stored; all data survives page refresh
|
||||
|
||||
### God Credentials Expiry Management (Level 4)
|
||||
- **Functionality**: Configurable expiry time for god-tier login credentials displayed on Level 1 (public page), with controls to customize duration, reset timer, and clear expiry
|
||||
- **Purpose**: Allow god-tier users to control security by setting custom time limits (1 minute to 24 hours) for credential visibility, balancing convenience with security
|
||||
- **Trigger**: User opens Settings tab in Level 4; credentials automatically display on Level 1 based on expiry status
|
||||
- **Progression**: Open Settings tab → View current expiry status and time remaining → Adjust duration value and unit (minutes/hours) → Save new duration → Optionally reset timer to restart countdown → Or clear expiry to show credentials on next Level 1 load → View live countdown on both Level 1 and Level 4
|
||||
- **Success criteria**: Duration configurable from 1 minute to 24 hours; defaults to 1 hour; timer resets when god user changes password; countdown displays accurately in real-time on Level 1; Settings page shows active/expired status; Reset Timer button restarts countdown with configured duration; Clear Expiry removes timer completely; credentials disappear from Level 1 when expired; new duration persists across sessions
|
||||
### 6. Quick Guide System
|
||||
**Functionality:** Interactive documentation and tutorials for new features
|
||||
**Purpose:** Help users discover and learn new visual configuration tools
|
||||
**Trigger:** User opens "Guide" tab (default tab in god-tier panel)
|
||||
**Progression:** View overview cards → Expand accordion sections → Read step-by-step instructions → Try features → Reference best practices
|
||||
**Success Criteria:**
|
||||
- Visible on first load as default tab
|
||||
- Covers all major features (CSS Builder, Dropdowns, Monaco)
|
||||
- Includes code examples where relevant
|
||||
- Provides best practices and tips
|
||||
|
||||
## Edge Case Handling
|
||||
- **Invalid User Credentials**: Show clear error message; rate limit after 5 attempts; support password reset flow
|
||||
- **Unauthorized Access Attempts**: Redirect to appropriate level; log security events; show "access denied" message
|
||||
- **Circular Schema Relations**: Detect and prevent infinite loops in model relationships; warn user
|
||||
- **Invalid Lua Scripts**: Catch syntax errors; timeout after 3 seconds; sandbox prevents dangerous operations
|
||||
- **Malformed JSON Schemas**: Validate before save; highlight errors with line numbers; provide fix suggestions
|
||||
- **Workflow Infinite Loops**: Detect cycles; limit execution steps to 1000; show execution trace
|
||||
- **Large Comment Threads**: Paginate comments; lazy load older entries; virtualize long lists
|
||||
- **Schema Migration Conflicts**: Detect breaking changes; show migration preview; allow rollback
|
||||
- **Lost Sessions Across Levels**: Auto-save state; restore context; show reconnection indicator
|
||||
- **Database Import Errors**: Validate JSON structure before import; show detailed error messages; rollback on failure; preserve existing data
|
||||
- **Password Hash Collisions**: Use SHA-512 with sufficient entropy; no collision risk in practice; unique salting per deployment
|
||||
- **KV Storage Quota**: Monitor storage usage; warn when approaching limits; provide data cleanup tools; optimize JSON serialization
|
||||
- **Monaco Editor Load Failure**: Fallback loading indicator; retry mechanism; graceful degradation if CDN unavailable
|
||||
- **Large Lua Scripts**: Monaco virtual scrolling handles performance; minimap provides navigation; syntax parsing optimized
|
||||
- **Invalid Expiry Duration**: Validate minimum 1 minute, maximum 24 hours; show error for out-of-range values; prevent negative numbers
|
||||
- **Expiry Timer Desync**: Recalculate on page load; handle timezone differences; sync between Level 1 and Level 4 displays
|
||||
- **Concurrent Expiry Changes**: Last write wins; reload settings after save; show confirmation of active settings
|
||||
- **Invalid CSS class names** - Custom class input validates and warns about non-standard classes
|
||||
- **Deleted dropdown config still referenced** - PropertyInspector gracefully handles missing configs, shows warning
|
||||
- **Large CSS class lists** - Scrollable interface with search/filter to handle 1000+ classes
|
||||
- **Concurrent edits** - Changes to dropdown configs immediately reflect in all open PropertyInspectors
|
||||
- **Empty dropdown options** - Validation prevents saving dropdowns with zero options
|
||||
- **Duplicate class selection** - System prevents selecting same class twice
|
||||
- **Import/export conflicts** - Monaco editor validates JSON before import, shows detailed errors
|
||||
|
||||
## Design Direction
|
||||
The design should evoke creativity and power - a professional design tool that feels both approachable and capable. Think Figma meets VS Code: clean, modern, with clear visual hierarchy and purposeful spacing. The canvas should feel like a creative workspace, not a cluttered IDE.
|
||||
The interface should feel like a professional design tool (Figma, Webflow) rather than a developer IDE. Visual hierarchy emphasizes actions over configuration details. Color coding distinguishes different tool types (CSS = primary purple, Dropdowns = accent cyan, Code = muted gray).
|
||||
|
||||
## Color Selection
|
||||
A sophisticated, creative tool palette that balances professionalism with visual energy - inspired by modern design tools.
|
||||
|
||||
- **Primary Color**: Deep purple `oklch(0.55 0.18 290)` - Communicates creativity and innovation, used for primary actions and builder chrome
|
||||
- **Secondary Colors**: Cool slate `oklch(0.35 0.02 260)` for sidebars and panels; Light lavender `oklch(0.92 0.03 290)` for canvas background
|
||||
- **Accent Color**: Electric cyan `oklch(0.70 0.17 195)` - High-energy color for selected states, active drop zones, and CTAs
|
||||
- **Foreground/Background Pairings**:
|
||||
- Primary (Deep Purple `oklch(0.55 0.18 290)`): White text `oklch(0.98 0 0)` - Ratio 6.2:1 ✓
|
||||
- Canvas (Light Lavender `oklch(0.92 0.03 290)`): Dark text `oklch(0.25 0.02 260)` - Ratio 12.1:1 ✓
|
||||
- Accent (Electric Cyan `oklch(0.70 0.17 195)`): Dark slate `oklch(0.2 0.02 260)` - Ratio 9.3:1 ✓
|
||||
- Sidebar (Cool Slate `oklch(0.35 0.02 260)`): Light text `oklch(0.90 0.01 260)` - Ratio 10.8:1 ✓
|
||||
**Primary Color:** `oklch(0.55 0.18 290)` - Purple/magenta representing creativity and visual design
|
||||
- Used for: CSS-related features, primary actions, selected states
|
||||
|
||||
**Secondary Colors:** `oklch(0.35 0.02 260)` - Deep blue-gray for structure
|
||||
- Used for: Dropdowns, configuration panels, stable UI elements
|
||||
|
||||
**Accent Color:** `oklch(0.70 0.17 195)` - Cyan/teal for interactive elements
|
||||
- Used for: Dynamic dropdowns, interactive guides, actionable items
|
||||
|
||||
**Foreground/Background Pairings:**
|
||||
- Background `oklch(0.92 0.03 290)` with Foreground `oklch(0.25 0.02 260)` - Ratio 14.2:1 ✓
|
||||
- Card `oklch(1 0 0)` with Card Foreground `oklch(0.25 0.02 260)` - Ratio 16.4:1 ✓
|
||||
- Primary `oklch(0.55 0.18 290)` with Primary Foreground `oklch(0.98 0 0)` - Ratio 7.1:1 ✓
|
||||
- Accent `oklch(0.70 0.17 195)` with Accent Foreground `oklch(0.2 0.02 260)` - Ratio 8.9:1 ✓
|
||||
|
||||
## Font Selection
|
||||
Modern, clean typography that balances technical precision with creative energy - readable at all scales for a design tool interface.
|
||||
Professional and technical feeling with emphasis on code clarity
|
||||
|
||||
- **Typographic Hierarchy**:
|
||||
- H1 (Builder Title): Space Grotesk Bold/28px/tight letter spacing
|
||||
- H2 (Panel Headers): Space Grotesk Semibold/20px/normal spacing
|
||||
- H3 (Component Names): Space Grotesk Medium/14px/normal spacing
|
||||
- Body (UI Labels): IBM Plex Sans Regular/14px/1.5 line height
|
||||
- Code (Monaco Editor): JetBrains Mono Regular/14px/1.4 line height
|
||||
- Small (Property Labels): IBM Plex Sans Medium/12px/uppercase/wide tracking
|
||||
- **Typographic Hierarchy:**
|
||||
- H1 (Panel Titles): Space Grotesk Bold/32px/tight tracking
|
||||
- H2 (Section Headers): Space Grotesk SemiBold/24px/normal tracking
|
||||
- H3 (Card Titles): Space Grotesk Medium/18px/normal tracking
|
||||
- Body (Descriptions): IBM Plex Sans Regular/14px/relaxed line height
|
||||
- Labels (Form Fields): IBM Plex Sans Medium/12px/wide tracking/uppercase
|
||||
- Code (Editors): JetBrains Mono Regular/14px/monospace
|
||||
|
||||
## Animations
|
||||
Animations should feel responsive and purposeful - immediate visual feedback for drag operations (drag ghost follows cursor at 0ms), smooth 200ms transitions for panel sliding, 150ms micro-interactions on selection changes, and elastic spring physics (tension: 300, friction: 20) for drop animations that make components feel tangible.
|
||||
Subtle functionality enhancements with occasional delightful moments
|
||||
|
||||
- **Opening dialogs:** 200ms ease-out scale from 0.95 to 1.0 with fade
|
||||
- **Selecting CSS classes:** 150ms color transition + 100ms scale pulse on click
|
||||
- **Dropdown option add:** 300ms slide-in from top with spring physics
|
||||
- **Tab switching:** 200ms cross-fade between content panels
|
||||
- **Hover states:** 150ms color/shadow transitions for all interactive elements
|
||||
- **Toast notifications:** 400ms slide-up with bounce for user feedback
|
||||
|
||||
## Component Selection
|
||||
- **Components**:
|
||||
- Sidebar with collapsible sections for component catalog
|
||||
- Resizable panels for canvas/inspector layout
|
||||
- Card for component previews in catalog and snippet library
|
||||
- Dialog for login form, settings, and snippet detail view
|
||||
- Sheet for slide-out snippet library panel
|
||||
- Tabs for switching between visual/code views and snippet categories
|
||||
- ScrollArea for component lists, property panels, and snippet browsing
|
||||
- Input, Select, Switch, Slider for property editors
|
||||
- Button throughout for actions
|
||||
- Badge for component type indicators and snippet tags
|
||||
- Separator for visual hierarchy
|
||||
- Tooltip for help text on hover
|
||||
- Sonner for notifications
|
||||
- **Customizations**:
|
||||
- Custom drag-and-drop canvas with drop zone highlighting
|
||||
- Monaco Editor wrapper for Lua scripts with custom autocomplete provider
|
||||
- Monaco Editor wrapper for JSON schema editing with validation
|
||||
- Component tree view with expand/collapse
|
||||
- Property editor that dynamically renders based on component type
|
||||
- Canvas ruler and grid overlay
|
||||
- Component outline overlay on hover
|
||||
- Fullscreen mode for Monaco editor instances
|
||||
- Snippet library with category filtering and search
|
||||
- Snippet card grid with tag display and copy/insert actions
|
||||
- Snippet detail modal with parameter documentation and code highlighting
|
||||
- **States**:
|
||||
- Canvas: neutral state shows dotted grid, hover shows drop zones, dragging shows blue outlines
|
||||
- Components: default has subtle border, hover shows blue glow, selected shows thick accent border with resize handles
|
||||
- Drop zones: hidden by default, appear on drag with dashed accent border and background tint
|
||||
- Property inputs: follow standard focus states with accent color
|
||||
- **Icon Selection**:
|
||||
- Phosphor icons: Layout for layouts, PaintBrush for styling, Code for code editor, Lock/LockOpen for auth, FloppyDisk for save, Eye for preview, ArrowsOutSimple for fullscreen, Plus for add, Trash for delete, Copy for duplicate/copy, CaretRight/Down for tree expand, BookOpen for snippet library, MagnifyingGlass for search, Tag for snippet tags, Check for copied confirmation, ArrowRight for insert action
|
||||
- **Spacing**:
|
||||
- Sidebars: p-4 for sections, gap-2 for component grid
|
||||
- Canvas: p-8 for outer padding, min-h-screen for scrollability
|
||||
- Property panel: p-4 for sections, gap-4 for form fields
|
||||
- Component padding: p-2 minimum for selection targets
|
||||
- **Mobile**:
|
||||
- Not a primary concern for a builder tool, but provide tablet landscape support minimum
|
||||
- Stack panels vertically on small screens
|
||||
- Hide component catalog by default, show via hamburger menu
|
||||
- Full-screen canvas mode for focused editing
|
||||
|
||||
**Components:**
|
||||
- **Dialog (shadcn)** - For CSS Builder, Dropdown Manager, JSON Editor modals with max-width customizations
|
||||
- **Tabs (shadcn)** - For god-tier panel navigation with horizontal scroll on mobile
|
||||
- **Select (shadcn)** - For boolean and static dropdown properties
|
||||
- **Input (shadcn)** - For text, number, and className fields with custom validation states
|
||||
- **Button (shadcn)** - For all actions with icon+text pattern, size variants (sm for toolbars)
|
||||
- **Card (shadcn)** - For guide sections, dropdown configs, CSS categories with hover elevations
|
||||
- **Badge (shadcn)** - For selected classes, tags, status indicators with color variants
|
||||
- **ScrollArea (shadcn)** - For long lists (CSS classes, options) with styled scrollbars
|
||||
- **Accordion (shadcn)** - For Quick Guide collapsible sections
|
||||
- **Monaco Editor (@monaco-editor/react)** - For JSON/Lua code editing with dark theme
|
||||
|
||||
**Customizations:**
|
||||
- DialogContent extended to max-w-5xl for JSON/Lua editors
|
||||
- Tabs with conditional wrapping and horizontal scroll for 12+ tabs
|
||||
- Badge with close button overlay for removable tags
|
||||
- Card with 2px border variants for feature highlighting
|
||||
- Input with icon button suffix for CSS Builder trigger
|
||||
|
||||
**States:**
|
||||
- Buttons: default, hover (shadow-md), active (scale-95), disabled (opacity-50)
|
||||
- Inputs: default, focus (ring-2), error (border-destructive), disabled (bg-muted)
|
||||
- Cards: default, hover (shadow-lg for interactive ones), selected (border-primary)
|
||||
- Dropdowns: closed, open (with slide-down animation), disabled
|
||||
|
||||
**Icon Selection:**
|
||||
- Palette (CSS Builder) - Visual association with styling/design
|
||||
- ListDashes (Dropdowns) - Represents list options
|
||||
- Code (Monaco editors) - Universal code symbol
|
||||
- Sparkle (Quick Guide) - Suggests helpful tips/new features
|
||||
- Pencil (Edit actions) - Standard edit metaphor
|
||||
- Trash (Delete actions) - Standard delete metaphor
|
||||
- Plus (Add actions) - Create new items
|
||||
- FloppyDisk (Save) - Nostalgic but clear save icon
|
||||
|
||||
**Spacing:**
|
||||
- Section gaps: gap-6 (24px) for major sections
|
||||
- Card internal: p-4 to p-6 (16-24px) based on content density
|
||||
- Form fields: space-y-2 (8px) between label and input
|
||||
- Button groups: gap-2 (8px) for related actions
|
||||
- Tab list: gap-1 (4px) to feel unified
|
||||
|
||||
**Mobile:**
|
||||
- Tabs convert to horizontally scrollable list (4 visible, swipe for more)
|
||||
- Dialogs use max-w-full with safe area padding
|
||||
- CSS Class Builder shows 1 column on mobile, 3 on desktop
|
||||
- PropertyInspector becomes bottom drawer on mobile (< 768px)
|
||||
- Quick Guide cards stack vertically on mobile
|
||||
- Monaco editor height reduces to 400px on mobile
|
||||
|
||||
182
src/components/CssClassBuilder.tsx
Normal file
182
src/components/CssClassBuilder.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Database } from '@/lib/database'
|
||||
import { Plus, X, FloppyDisk, Trash } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface CssClassBuilderProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
initialValue?: string
|
||||
onSave: (classes: string) => void
|
||||
}
|
||||
|
||||
interface CssCategory {
|
||||
name: string
|
||||
classes: string[]
|
||||
}
|
||||
|
||||
export function CssClassBuilder({ open, onClose, initialValue = '', onSave }: CssClassBuilderProps) {
|
||||
const [selectedClasses, setSelectedClasses] = useState<string[]>([])
|
||||
const [categories, setCategories] = useState<CssCategory[]>([])
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [customClass, setCustomClass] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
loadCssClasses()
|
||||
setSelectedClasses(initialValue.split(' ').filter(Boolean))
|
||||
}
|
||||
}, [open, initialValue])
|
||||
|
||||
const loadCssClasses = async () => {
|
||||
const classes = await Database.getCssClasses()
|
||||
setCategories(classes)
|
||||
}
|
||||
|
||||
const toggleClass = (cssClass: string) => {
|
||||
setSelectedClasses(current => {
|
||||
if (current.includes(cssClass)) {
|
||||
return current.filter(c => c !== cssClass)
|
||||
} else {
|
||||
return [...current, cssClass]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const addCustomClass = () => {
|
||||
if (customClass.trim()) {
|
||||
setSelectedClasses(current => [...current, customClass.trim()])
|
||||
setCustomClass('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(selectedClasses.join(' '))
|
||||
onClose()
|
||||
}
|
||||
|
||||
const filteredCategories = categories.map(category => ({
|
||||
...category,
|
||||
classes: category.classes.filter(cls =>
|
||||
cls.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
),
|
||||
})).filter(category => category.classes.length > 0)
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl">CSS Class Builder</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
placeholder="Search classes..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedClasses.length > 0 && (
|
||||
<div className="p-4 border rounded-lg bg-muted/50">
|
||||
<Label className="text-xs uppercase tracking-wider mb-2 block">Selected Classes</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedClasses.map(cls => (
|
||||
<Badge key={cls} variant="secondary" className="gap-2">
|
||||
{cls}
|
||||
<button
|
||||
onClick={() => toggleClass(cls)}
|
||||
className="hover:text-destructive"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 p-2 bg-background rounded border font-mono text-sm">
|
||||
{selectedClasses.join(' ')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Tabs defaultValue={filteredCategories[0]?.name || 'custom'} className="flex-1">
|
||||
<ScrollArea className="max-h-[50px]">
|
||||
<TabsList className="w-full">
|
||||
{filteredCategories.map(category => (
|
||||
<TabsTrigger key={category.name} value={category.name}>
|
||||
{category.name}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
<TabsTrigger value="custom">Custom</TabsTrigger>
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
|
||||
{filteredCategories.map(category => (
|
||||
<TabsContent key={category.name} value={category.name}>
|
||||
<ScrollArea className="h-[300px] border rounded-lg p-4">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{category.classes.map(cls => (
|
||||
<button
|
||||
key={cls}
|
||||
onClick={() => toggleClass(cls)}
|
||||
className={`
|
||||
px-3 py-2 text-sm rounded border text-left transition-colors
|
||||
${selectedClasses.includes(cls)
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-card hover:bg-accent hover:text-accent-foreground'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{cls}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
))}
|
||||
|
||||
<TabsContent value="custom">
|
||||
<div className="border rounded-lg p-4 space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Enter custom class name..."
|
||||
value={customClass}
|
||||
onChange={(e) => setCustomClass(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && addCustomClass()}
|
||||
/>
|
||||
<Button onClick={addCustomClass}>
|
||||
<Plus className="mr-2" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Add custom CSS classes that aren't in the predefined list.
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
<FloppyDisk className="mr-2" />
|
||||
Apply Classes
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
203
src/components/CssClassManager.tsx
Normal file
203
src/components/CssClassManager.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Database, CssCategory } from '@/lib/database'
|
||||
import { Plus, X, Pencil, Trash, FloppyDisk } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
export function CssClassManager() {
|
||||
const [categories, setCategories] = useState<CssCategory[]>([])
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editingCategory, setEditingCategory] = useState<CssCategory | null>(null)
|
||||
const [categoryName, setCategoryName] = useState('')
|
||||
const [classes, setClasses] = useState<string[]>([])
|
||||
const [newClass, setNewClass] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
loadCategories()
|
||||
}, [])
|
||||
|
||||
const loadCategories = async () => {
|
||||
const cats = await Database.getCssClasses()
|
||||
setCategories(cats)
|
||||
}
|
||||
|
||||
const startEdit = (category?: CssCategory) => {
|
||||
if (category) {
|
||||
setEditingCategory(category)
|
||||
setCategoryName(category.name)
|
||||
setClasses([...category.classes])
|
||||
} else {
|
||||
setEditingCategory(null)
|
||||
setCategoryName('')
|
||||
setClasses([])
|
||||
}
|
||||
setIsEditing(true)
|
||||
}
|
||||
|
||||
const addClass = () => {
|
||||
if (newClass.trim()) {
|
||||
setClasses(current => [...current, newClass.trim()])
|
||||
setNewClass('')
|
||||
}
|
||||
}
|
||||
|
||||
const removeClass = (index: number) => {
|
||||
setClasses(current => current.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!categoryName || classes.length === 0) {
|
||||
toast.error('Please provide a category name and at least one class')
|
||||
return
|
||||
}
|
||||
|
||||
const newCategory: CssCategory = {
|
||||
name: categoryName,
|
||||
classes,
|
||||
}
|
||||
|
||||
if (editingCategory) {
|
||||
await Database.updateCssCategory(categoryName, classes)
|
||||
toast.success('Category updated successfully')
|
||||
} else {
|
||||
await Database.addCssCategory(newCategory)
|
||||
toast.success('Category created successfully')
|
||||
}
|
||||
|
||||
setIsEditing(false)
|
||||
loadCategories()
|
||||
}
|
||||
|
||||
const handleDelete = async (categoryName: string) => {
|
||||
if (confirm('Are you sure you want to delete this CSS category?')) {
|
||||
await Database.deleteCssCategory(categoryName)
|
||||
toast.success('Category deleted')
|
||||
loadCategories()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold">CSS Class Library</h2>
|
||||
<p className="text-sm text-muted-foreground">Manage CSS classes available in the builder</p>
|
||||
</div>
|
||||
<Button onClick={() => startEdit()}>
|
||||
<Plus className="mr-2" />
|
||||
Add Category
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{categories.map(category => (
|
||||
<Card key={category.name} className="p-4 space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<h3 className="font-semibold text-lg">{category.name}</h3>
|
||||
<div className="flex gap-1">
|
||||
<Button size="sm" variant="ghost" onClick={() => startEdit(category)}>
|
||||
<Pencil size={16} />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => handleDelete(category.name)}>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<ScrollArea className="h-[120px]">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{category.classes.map((cls, i) => (
|
||||
<Badge key={i} variant="outline" className="text-xs font-mono">
|
||||
{cls}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{category.classes.length} classes
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{categories.length === 0 && (
|
||||
<Card className="p-12 text-center">
|
||||
<p className="text-muted-foreground">No CSS categories yet. Add one to get started.</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Dialog open={isEditing} onOpenChange={setIsEditing}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingCategory ? 'Edit' : 'Create'} CSS Category</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Category Name</Label>
|
||||
<Input
|
||||
placeholder="e.g., Layout"
|
||||
value={categoryName}
|
||||
onChange={(e) => setCategoryName(e.target.value)}
|
||||
disabled={!!editingCategory}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>CSS Classes</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Enter class name"
|
||||
value={newClass}
|
||||
onChange={(e) => setNewClass(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && addClass()}
|
||||
className="font-mono"
|
||||
/>
|
||||
<Button onClick={addClass} type="button">
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{classes.length > 0 && (
|
||||
<ScrollArea className="h-[200px] border rounded-lg p-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{classes.map((cls, i) => (
|
||||
<Badge key={i} variant="secondary" className="gap-2 font-mono">
|
||||
{cls}
|
||||
<button
|
||||
onClick={() => removeClass(i)}
|
||||
className="hover:text-destructive"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsEditing(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
<FloppyDisk className="mr-2" />
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
226
src/components/DropdownConfigManager.tsx
Normal file
226
src/components/DropdownConfigManager.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Database } from '@/lib/database'
|
||||
import { Plus, X, FloppyDisk, Trash, Pencil } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import type { DropdownConfig } from '@/lib/database'
|
||||
|
||||
export function DropdownConfigManager() {
|
||||
const [dropdowns, setDropdowns] = useState<DropdownConfig[]>([])
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editingDropdown, setEditingDropdown] = useState<DropdownConfig | null>(null)
|
||||
const [dropdownName, setDropdownName] = useState('')
|
||||
const [dropdownLabel, setDropdownLabel] = useState('')
|
||||
const [options, setOptions] = useState<Array<{ value: string; label: string }>>([])
|
||||
const [newOptionValue, setNewOptionValue] = useState('')
|
||||
const [newOptionLabel, setNewOptionLabel] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
loadDropdowns()
|
||||
}, [])
|
||||
|
||||
const loadDropdowns = async () => {
|
||||
const configs = await Database.getDropdownConfigs()
|
||||
setDropdowns(configs)
|
||||
}
|
||||
|
||||
const startEdit = (dropdown?: DropdownConfig) => {
|
||||
if (dropdown) {
|
||||
setEditingDropdown(dropdown)
|
||||
setDropdownName(dropdown.name)
|
||||
setDropdownLabel(dropdown.label)
|
||||
setOptions(dropdown.options)
|
||||
} else {
|
||||
setEditingDropdown(null)
|
||||
setDropdownName('')
|
||||
setDropdownLabel('')
|
||||
setOptions([])
|
||||
}
|
||||
setIsEditing(true)
|
||||
}
|
||||
|
||||
const addOption = () => {
|
||||
if (newOptionValue && newOptionLabel) {
|
||||
setOptions(current => [...current, { value: newOptionValue, label: newOptionLabel }])
|
||||
setNewOptionValue('')
|
||||
setNewOptionLabel('')
|
||||
}
|
||||
}
|
||||
|
||||
const removeOption = (index: number) => {
|
||||
setOptions(current => current.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!dropdownName || !dropdownLabel || options.length === 0) {
|
||||
toast.error('Please fill all fields and add at least one option')
|
||||
return
|
||||
}
|
||||
|
||||
const newDropdown: DropdownConfig = {
|
||||
id: editingDropdown?.id || `dropdown_${Date.now()}`,
|
||||
name: dropdownName,
|
||||
label: dropdownLabel,
|
||||
options,
|
||||
}
|
||||
|
||||
if (editingDropdown) {
|
||||
await Database.updateDropdownConfig(newDropdown.id, newDropdown)
|
||||
toast.success('Dropdown updated successfully')
|
||||
} else {
|
||||
await Database.addDropdownConfig(newDropdown)
|
||||
toast.success('Dropdown created successfully')
|
||||
}
|
||||
|
||||
setIsEditing(false)
|
||||
loadDropdowns()
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (confirm('Are you sure you want to delete this dropdown configuration?')) {
|
||||
await Database.deleteDropdownConfig(id)
|
||||
toast.success('Dropdown deleted')
|
||||
loadDropdowns()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold">Dropdown Configurations</h2>
|
||||
<p className="text-sm text-muted-foreground">Manage dynamic dropdown options for properties</p>
|
||||
</div>
|
||||
<Button onClick={() => startEdit()}>
|
||||
<Plus className="mr-2" />
|
||||
Create Dropdown
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{dropdowns.map(dropdown => (
|
||||
<Card key={dropdown.id} className="p-4 space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 className="font-semibold">{dropdown.label}</h3>
|
||||
<p className="text-xs text-muted-foreground font-mono">{dropdown.name}</p>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Button size="sm" variant="ghost" onClick={() => startEdit(dropdown)}>
|
||||
<Pencil size={16} />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => handleDelete(dropdown.id)}>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{dropdown.options.map((opt, i) => (
|
||||
<Badge key={i} variant="secondary" className="text-xs">
|
||||
{opt.label}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{dropdowns.length === 0 && (
|
||||
<Card className="p-12 text-center">
|
||||
<p className="text-muted-foreground">No dropdown configurations yet. Create one to get started.</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Dialog open={isEditing} onOpenChange={setIsEditing}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingDropdown ? 'Edit' : 'Create'} Dropdown Configuration</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Dropdown Name (ID)</Label>
|
||||
<Input
|
||||
placeholder="e.g., status_options"
|
||||
value={dropdownName}
|
||||
onChange={(e) => setDropdownName(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">Unique identifier for this dropdown</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Display Label</Label>
|
||||
<Input
|
||||
placeholder="e.g., Status"
|
||||
value={dropdownLabel}
|
||||
onChange={(e) => setDropdownLabel(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Options</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Value"
|
||||
value={newOptionValue}
|
||||
onChange={(e) => setNewOptionValue(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Label"
|
||||
value={newOptionLabel}
|
||||
onChange={(e) => setNewOptionLabel(e.target.value)}
|
||||
/>
|
||||
<Button onClick={addOption} type="button">
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{options.length > 0 && (
|
||||
<ScrollArea className="h-[200px] border rounded-lg p-3">
|
||||
<div className="space-y-2">
|
||||
{options.map((opt, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-2 border rounded bg-muted/50">
|
||||
<div className="flex-1">
|
||||
<span className="font-mono text-sm">{opt.value}</span>
|
||||
<span className="mx-2 text-muted-foreground">→</span>
|
||||
<span className="text-sm">{opt.label}</span>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => removeOption(i)}
|
||||
>
|
||||
<X size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsEditing(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
<FloppyDisk className="mr-2" />
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
112
src/components/JsonEditor.tsx
Normal file
112
src/components/JsonEditor.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { FloppyDisk, X, Warning } from '@phosphor-icons/react'
|
||||
import Editor from '@monaco-editor/react'
|
||||
|
||||
interface JsonEditorProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
title: string
|
||||
value: any
|
||||
onSave: (value: any) => void
|
||||
schema?: any
|
||||
}
|
||||
|
||||
export function JsonEditor({ open, onClose, title, value, onSave, schema }: JsonEditorProps) {
|
||||
const [jsonText, setJsonText] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setJsonText(JSON.stringify(value, null, 2))
|
||||
setError(null)
|
||||
}
|
||||
}, [open, value])
|
||||
|
||||
const handleSave = () => {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonText)
|
||||
onSave(parsed)
|
||||
setError(null)
|
||||
onClose()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Invalid JSON')
|
||||
}
|
||||
}
|
||||
|
||||
const handleFormat = () => {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonText)
|
||||
setJsonText(JSON.stringify(parsed, null, 2))
|
||||
setError(null)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Invalid JSON - cannot format')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-5xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl">{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<Warning className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Editor
|
||||
height="600px"
|
||||
language="json"
|
||||
value={jsonText}
|
||||
onChange={(value) => {
|
||||
setJsonText(value || '')
|
||||
setError(null)
|
||||
}}
|
||||
theme="vs-dark"
|
||||
options={{
|
||||
minimap: { enabled: true },
|
||||
fontSize: 14,
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
lineNumbers: 'on',
|
||||
roundedSelection: true,
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
tabSize: 2,
|
||||
wordWrap: 'on',
|
||||
formatOnPaste: true,
|
||||
formatOnType: true,
|
||||
bracketPairColorization: {
|
||||
enabled: true,
|
||||
},
|
||||
folding: true,
|
||||
foldingStrategy: 'indentation',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button variant="outline" onClick={handleFormat}>
|
||||
Format JSON
|
||||
</Button>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
<X className="mr-2" />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} className="bg-accent text-accent-foreground hover:bg-accent/90">
|
||||
<FloppyDisk className="mr-2" />
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { SignOut, Database as DatabaseIcon, Lightning, Code, Eye, House, Download, Upload, BookOpen, HardDrives, MapTrifold, Tree, Users, Gear } from '@phosphor-icons/react'
|
||||
import { SignOut, Database as DatabaseIcon, Lightning, Code, Eye, House, Download, Upload, BookOpen, HardDrives, MapTrifold, Tree, Users, Gear, Palette, ListDashes, Sparkle } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import { SchemaEditorLevel4 } from './SchemaEditorLevel4'
|
||||
import { WorkflowEditor } from './WorkflowEditor'
|
||||
@@ -19,6 +19,9 @@ import { PageRoutesManager } from './PageRoutesManager'
|
||||
import { ComponentHierarchyEditor } from './ComponentHierarchyEditor'
|
||||
import { UserManagement } from './UserManagement'
|
||||
import { GodCredentialsSettings } from './GodCredentialsSettings'
|
||||
import { CssClassManager } from './CssClassManager'
|
||||
import { DropdownConfigManager } from './DropdownConfigManager'
|
||||
import { QuickGuide } from './QuickGuide'
|
||||
import { Database } from '@/lib/database'
|
||||
import { seedDatabase } from '@/lib/seed-data'
|
||||
import type { User as UserType, AppConfiguration } from '@/lib/level-types'
|
||||
@@ -200,8 +203,12 @@ export function Level4({ user, onLogout, onNavigate, onPreview }: Level4Props) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="pages" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-4 lg:grid-cols-9 max-w-full">
|
||||
<Tabs defaultValue="guide" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-4 lg:grid-cols-12 max-w-full">
|
||||
<TabsTrigger value="guide">
|
||||
<Sparkle className="mr-2" size={16} />
|
||||
Guide
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="pages">
|
||||
<MapTrifold className="mr-2" size={16} />
|
||||
Page Routes
|
||||
@@ -230,6 +237,14 @@ export function Level4({ user, onLogout, onNavigate, onPreview }: Level4Props) {
|
||||
<BookOpen className="mr-2" size={16} />
|
||||
Snippets
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="css">
|
||||
<Palette className="mr-2" size={16} />
|
||||
CSS Classes
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="dropdowns">
|
||||
<ListDashes className="mr-2" size={16} />
|
||||
Dropdowns
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="database">
|
||||
<HardDrives className="mr-2" size={16} />
|
||||
Database
|
||||
@@ -240,6 +255,10 @@ export function Level4({ user, onLogout, onNavigate, onPreview }: Level4Props) {
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="guide" className="space-y-6">
|
||||
<QuickGuide />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="pages" className="space-y-6">
|
||||
<PageRoutesManager />
|
||||
</TabsContent>
|
||||
@@ -290,6 +309,14 @@ export function Level4({ user, onLogout, onNavigate, onPreview }: Level4Props) {
|
||||
<LuaSnippetLibrary />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="css" className="space-y-6">
|
||||
<CssClassManager />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="dropdowns" className="space-y-6">
|
||||
<DropdownConfigManager />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="database" className="space-y-6">
|
||||
<DatabaseManager />
|
||||
</TabsContent>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
@@ -8,7 +8,9 @@ import { Separator } from '@/components/ui/separator'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import type { ComponentInstance } from '@/lib/builder-types'
|
||||
import { componentCatalog } from '@/lib/component-catalog'
|
||||
import { Code, PaintBrush, Trash } from '@phosphor-icons/react'
|
||||
import { Code, PaintBrush, Trash, Palette } from '@phosphor-icons/react'
|
||||
import { CssClassBuilder } from '@/components/CssClassBuilder'
|
||||
import { Database, DropdownConfig } from '@/lib/database'
|
||||
|
||||
interface PropertyInspectorProps {
|
||||
component: ComponentInstance | null
|
||||
@@ -18,6 +20,19 @@ interface PropertyInspectorProps {
|
||||
}
|
||||
|
||||
export function PropertyInspector({ component, onUpdate, onDelete, onCodeEdit }: PropertyInspectorProps) {
|
||||
const [cssBuilderOpen, setCssBuilderOpen] = useState(false)
|
||||
const [cssBuilderPropName, setCssBuilderPropName] = useState('')
|
||||
const [dynamicDropdowns, setDynamicDropdowns] = useState<DropdownConfig[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
loadDynamicDropdowns()
|
||||
}, [])
|
||||
|
||||
const loadDynamicDropdowns = async () => {
|
||||
const dropdowns = await Database.getDropdownConfigs()
|
||||
setDynamicDropdowns(dropdowns)
|
||||
}
|
||||
|
||||
if (!component) {
|
||||
return (
|
||||
<div className="w-80 bg-card border-l border-border p-6 flex items-center justify-center text-center">
|
||||
@@ -35,6 +50,16 @@ export function PropertyInspector({ component, onUpdate, onDelete, onCodeEdit }:
|
||||
})
|
||||
}
|
||||
|
||||
const openCssBuilder = (propName: string) => {
|
||||
setCssBuilderPropName(propName)
|
||||
setCssBuilderOpen(true)
|
||||
}
|
||||
|
||||
const handleCssClassSave = (classes: string) => {
|
||||
handlePropChange(cssBuilderPropName, classes)
|
||||
setCssBuilderOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-80 bg-card border-l border-border flex flex-col h-screen">
|
||||
<div className="p-4 border-b border-border">
|
||||
@@ -57,63 +82,94 @@ export function PropertyInspector({ component, onUpdate, onDelete, onCodeEdit }:
|
||||
<TabsContent value="props" className="flex-1 mt-0">
|
||||
<ScrollArea className="h-full p-4">
|
||||
<div className="space-y-4">
|
||||
{componentDef?.propSchema.map(propDef => (
|
||||
<div key={propDef.name} className="space-y-2">
|
||||
<Label className="text-xs uppercase tracking-wider">{propDef.label}</Label>
|
||||
|
||||
{propDef.type === 'string' && (
|
||||
<Input
|
||||
value={component.props[propDef.name] || ''}
|
||||
onChange={(e) => handlePropChange(propDef.name, e.target.value)}
|
||||
/>
|
||||
)}
|
||||
{componentDef?.propSchema.map(propDef => {
|
||||
const dynamicDropdown = propDef.type === 'dynamic-select'
|
||||
? dynamicDropdowns.find(d => d.name === propDef.dynamicSource)
|
||||
: null
|
||||
|
||||
{propDef.type === 'number' && (
|
||||
<Input
|
||||
type="number"
|
||||
value={component.props[propDef.name] || ''}
|
||||
onChange={(e) => handlePropChange(propDef.name, Number(e.target.value))}
|
||||
/>
|
||||
)}
|
||||
return (
|
||||
<div key={propDef.name} className="space-y-2">
|
||||
<Label className="text-xs uppercase tracking-wider">{propDef.label}</Label>
|
||||
|
||||
{propDef.name === 'className' ? (
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={component.props[propDef.name] || ''}
|
||||
onChange={(e) => handlePropChange(propDef.name, e.target.value)}
|
||||
className="flex-1 font-mono text-xs"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => openCssBuilder(propDef.name)}
|
||||
>
|
||||
<Palette size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
) : propDef.type === 'string' ? (
|
||||
<Input
|
||||
value={component.props[propDef.name] || ''}
|
||||
onChange={(e) => handlePropChange(propDef.name, e.target.value)}
|
||||
/>
|
||||
) : propDef.type === 'number' ? (
|
||||
<Input
|
||||
type="number"
|
||||
value={component.props[propDef.name] || ''}
|
||||
onChange={(e) => handlePropChange(propDef.name, Number(e.target.value))}
|
||||
/>
|
||||
) : propDef.type === 'boolean' ? (
|
||||
<Select
|
||||
value={String(component.props[propDef.name] || false)}
|
||||
onValueChange={(value) => handlePropChange(propDef.name, value === 'true')}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="true">True</SelectItem>
|
||||
<SelectItem value="false">False</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : propDef.type === 'select' && propDef.options ? (
|
||||
<Select
|
||||
value={component.props[propDef.name] || propDef.defaultValue}
|
||||
onValueChange={(value) => handlePropChange(propDef.name, value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{propDef.options.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : propDef.type === 'dynamic-select' && dynamicDropdown ? (
|
||||
<Select
|
||||
value={component.props[propDef.name] || ''}
|
||||
onValueChange={(value) => handlePropChange(propDef.name, value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={`Select ${dynamicDropdown.label}`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{dynamicDropdown.options.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : null}
|
||||
|
||||
{propDef.type === 'boolean' && (
|
||||
<Select
|
||||
value={String(component.props[propDef.name] || false)}
|
||||
onValueChange={(value) => handlePropChange(propDef.name, value === 'true')}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="true">True</SelectItem>
|
||||
<SelectItem value="false">False</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
{propDef.type === 'select' && propDef.options && (
|
||||
<Select
|
||||
value={component.props[propDef.name] || propDef.defaultValue}
|
||||
onValueChange={(value) => handlePropChange(propDef.name, value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{propDef.options.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
{propDef.description && (
|
||||
<p className="text-xs text-muted-foreground">{propDef.description}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{propDef.description && (
|
||||
<p className="text-xs text-muted-foreground">{propDef.description}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{(!componentDef?.propSchema || componentDef.propSchema.length === 0) && (
|
||||
<p className="text-sm text-muted-foreground">This component has no configurable properties.</p>
|
||||
@@ -149,6 +205,13 @@ export function PropertyInspector({ component, onUpdate, onDelete, onCodeEdit }:
|
||||
Delete Component
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<CssClassBuilder
|
||||
open={cssBuilderOpen}
|
||||
onClose={() => setCssBuilderOpen(false)}
|
||||
initialValue={component.props[cssBuilderPropName] || ''}
|
||||
onSave={handleCssClassSave}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
205
src/components/QuickGuide.tsx
Normal file
205
src/components/QuickGuide.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
|
||||
import { Palette, ListDashes, Code, Sparkle } from '@phosphor-icons/react'
|
||||
|
||||
export function QuickGuide() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-2">Quick Guide</h2>
|
||||
<p className="text-sm text-muted-foreground">Learn how to use the new visual configuration tools</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<Card className="p-6 space-y-3 border-2 border-primary/20 bg-primary/5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/20 flex items-center justify-center">
|
||||
<Palette className="text-primary" size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold">CSS Class Builder</h3>
|
||||
<Badge variant="secondary" className="text-xs">Visual Styling</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No more typing CSS classes! Click the palette icon next to any className field to visually select from 200+ organized Tailwind classes.
|
||||
</p>
|
||||
<div className="pt-2 space-y-1 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-primary" />
|
||||
<span>10 categorized class groups</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-primary" />
|
||||
<span>Live preview of selections</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-primary" />
|
||||
<span>Add custom classes when needed</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 space-y-3 border-2 border-accent/20 bg-accent/5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent/20 flex items-center justify-center">
|
||||
<ListDashes className="text-accent" size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold">Dynamic Dropdowns</h3>
|
||||
<Badge variant="secondary" className="text-xs">Reusable Options</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Create dropdown configurations once and use them across multiple components. Perfect for status fields, categories, and priorities.
|
||||
</p>
|
||||
<div className="pt-2 space-y-1 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-accent" />
|
||||
<span>Centralized option management</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-accent" />
|
||||
<span>Update once, apply everywhere</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-accent" />
|
||||
<span>GUI-based configuration</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="p-6">
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="css">
|
||||
<AccordionTrigger className="text-base font-semibold">
|
||||
<div className="flex items-center gap-2">
|
||||
<Palette size={18} />
|
||||
How to use CSS Class Builder
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="space-y-3 text-sm">
|
||||
<div className="space-y-2">
|
||||
<p className="font-medium">Step 1: Manage your CSS library</p>
|
||||
<ul className="list-disc list-inside space-y-1 text-muted-foreground ml-4">
|
||||
<li>Go to the "CSS Classes" tab</li>
|
||||
<li>Browse existing categories (Layout, Spacing, Typography, etc.)</li>
|
||||
<li>Add new categories or classes as needed</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="font-medium">Step 2: Apply classes to components</p>
|
||||
<ul className="list-disc list-inside space-y-1 text-muted-foreground ml-4">
|
||||
<li>Select a component in the builder</li>
|
||||
<li>Find the "CSS Classes" field in the Property Inspector</li>
|
||||
<li>Click the palette icon <Palette size={14} className="inline" /> button</li>
|
||||
<li>Browse categories and click classes to select them</li>
|
||||
<li>See live preview of your selections</li>
|
||||
<li>Click "Apply Classes" to save</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="p-3 bg-muted rounded-md">
|
||||
<p className="text-xs"><strong>Tip:</strong> You can still type custom classes directly in the input field if you need something specific!</p>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="dropdowns">
|
||||
<AccordionTrigger className="text-base font-semibold">
|
||||
<div className="flex items-center gap-2">
|
||||
<ListDashes size={18} />
|
||||
How to create and use Dynamic Dropdowns
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="space-y-3 text-sm">
|
||||
<div className="space-y-2">
|
||||
<p className="font-medium">Step 1: Create a dropdown configuration</p>
|
||||
<ul className="list-disc list-inside space-y-1 text-muted-foreground ml-4">
|
||||
<li>Go to the "Dropdowns" tab in the god-tier panel</li>
|
||||
<li>Click "Create Dropdown"</li>
|
||||
<li>Enter a unique name (e.g., "status_options")</li>
|
||||
<li>Enter a display label (e.g., "Status")</li>
|
||||
<li>Add options with values and labels</li>
|
||||
<li>Click "Save"</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="font-medium">Step 2: Use it in component properties</p>
|
||||
<ul className="list-disc list-inside space-y-1 text-muted-foreground ml-4">
|
||||
<li>When defining component schemas, use type "dynamic-select"</li>
|
||||
<li>Reference your dropdown by name in the "dynamicSource" field</li>
|
||||
<li>The Property Inspector will automatically show your dropdown</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="p-3 bg-muted rounded-md font-mono text-xs">
|
||||
<pre>{`{
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
type: 'dynamic-select',
|
||||
dynamicSource: 'status_options'
|
||||
}`}</pre>
|
||||
</div>
|
||||
<div className="p-3 bg-accent/10 rounded-md border border-accent/20">
|
||||
<p className="text-xs"><strong>Pre-loaded examples:</strong> We've included status, priority, and category dropdowns to get you started!</p>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="monaco">
|
||||
<AccordionTrigger className="text-base font-semibold">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code size={18} />
|
||||
Monaco Code Editor Features
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="space-y-3 text-sm">
|
||||
<p className="text-muted-foreground">
|
||||
When editing JSON or Lua code, you'll use the Monaco editor (the same editor that powers VS Code):
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-2 text-muted-foreground ml-4">
|
||||
<li><strong>Syntax Highlighting:</strong> Color-coded JSON/Lua syntax</li>
|
||||
<li><strong>Auto-formatting:</strong> Press Format JSON button or use Shift+Alt+F</li>
|
||||
<li><strong>Error Detection:</strong> See errors as you type</li>
|
||||
<li><strong>Bracket Matching:</strong> Colored bracket pairs</li>
|
||||
<li><strong>Code Folding:</strong> Collapse/expand sections</li>
|
||||
<li><strong>Find & Replace:</strong> Ctrl/Cmd+F to search</li>
|
||||
<li><strong>Minimap:</strong> Navigate large files easily</li>
|
||||
</ul>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="best-practices">
|
||||
<AccordionTrigger className="text-base font-semibold">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkle size={18} />
|
||||
Best Practices
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="space-y-3 text-sm">
|
||||
<ul className="space-y-3">
|
||||
<li className="space-y-1">
|
||||
<p className="font-medium">Organize CSS classes by purpose</p>
|
||||
<p className="text-muted-foreground ml-4">Keep related classes together in categories for easier discovery</p>
|
||||
</li>
|
||||
<li className="space-y-1">
|
||||
<p className="font-medium">Name dropdowns descriptively</p>
|
||||
<p className="text-muted-foreground ml-4">Use clear names like "user_status_options" instead of "dropdown1"</p>
|
||||
</li>
|
||||
<li className="space-y-1">
|
||||
<p className="font-medium">Reuse dropdown configurations</p>
|
||||
<p className="text-muted-foreground ml-4">If multiple components need the same options, create one dropdown and reference it</p>
|
||||
</li>
|
||||
<li className="space-y-1">
|
||||
<p className="font-medium">Test in preview mode</p>
|
||||
<p className="text-muted-foreground ml-4">Use the preview buttons to see how your changes look on each level</p>
|
||||
</li>
|
||||
</ul>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -55,9 +55,10 @@ export interface ComponentDefinition {
|
||||
export interface PropDefinition {
|
||||
name: string
|
||||
label: string
|
||||
type: 'string' | 'number' | 'boolean' | 'select' | 'color'
|
||||
type: 'string' | 'number' | 'boolean' | 'select' | 'color' | 'dynamic-select'
|
||||
defaultValue?: any
|
||||
options?: Array<{ value: string; label: string }>
|
||||
dynamicSource?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,18 @@ import type {
|
||||
} from './level-types'
|
||||
import type { ModelSchema } from './schema-types'
|
||||
|
||||
export interface CssCategory {
|
||||
name: string
|
||||
classes: string[]
|
||||
}
|
||||
|
||||
export interface DropdownConfig {
|
||||
id: string
|
||||
name: string
|
||||
label: string
|
||||
options: Array<{ value: string; label: string }>
|
||||
}
|
||||
|
||||
export interface DatabaseSchema {
|
||||
users: User[]
|
||||
credentials: Record<string, string>
|
||||
@@ -23,6 +35,8 @@ export interface DatabaseSchema {
|
||||
passwordChangeTimestamps: Record<string, number>
|
||||
firstLoginFlags: Record<string, boolean>
|
||||
godCredentialsExpiryDuration: number
|
||||
cssClasses: CssCategory[]
|
||||
dropdownConfigs: DropdownConfig[]
|
||||
}
|
||||
|
||||
export interface ComponentNode {
|
||||
@@ -61,6 +75,8 @@ export const DB_KEYS = {
|
||||
PASSWORD_CHANGE_TIMESTAMPS: 'db_password_change_timestamps',
|
||||
FIRST_LOGIN_FLAGS: 'db_first_login_flags',
|
||||
GOD_CREDENTIALS_EXPIRY_DURATION: 'db_god_credentials_expiry_duration',
|
||||
CSS_CLASSES: 'db_css_classes',
|
||||
DROPDOWN_CONFIGS: 'db_dropdown_configs',
|
||||
} as const
|
||||
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
@@ -407,6 +423,92 @@ export class Database {
|
||||
}
|
||||
await this.setAppConfig(defaultConfig)
|
||||
}
|
||||
|
||||
const cssClasses = await this.getCssClasses()
|
||||
if (cssClasses.length === 0) {
|
||||
const defaultCssClasses: CssCategory[] = [
|
||||
{
|
||||
name: 'Layout',
|
||||
classes: ['flex', 'flex-col', 'flex-row', 'grid', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4', 'block', 'inline-block', 'inline', 'hidden'],
|
||||
},
|
||||
{
|
||||
name: 'Spacing',
|
||||
classes: ['p-0', 'p-1', 'p-2', 'p-3', 'p-4', 'p-6', 'p-8', 'm-0', 'm-1', 'm-2', 'm-3', 'm-4', 'm-6', 'm-8', 'gap-1', 'gap-2', 'gap-3', 'gap-4', 'gap-6', 'gap-8'],
|
||||
},
|
||||
{
|
||||
name: 'Sizing',
|
||||
classes: ['w-full', 'w-1/2', 'w-1/3', 'w-1/4', 'w-auto', 'h-full', 'h-screen', 'h-auto', 'min-h-screen', 'max-w-xs', 'max-w-sm', 'max-w-md', 'max-w-lg', 'max-w-xl', 'max-w-2xl', 'max-w-4xl', 'max-w-6xl', 'max-w-7xl'],
|
||||
},
|
||||
{
|
||||
name: 'Typography',
|
||||
classes: ['text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl', 'text-3xl', 'text-4xl', 'font-normal', 'font-medium', 'font-semibold', 'font-bold', 'text-left', 'text-center', 'text-right', 'uppercase', 'lowercase', 'capitalize'],
|
||||
},
|
||||
{
|
||||
name: 'Colors',
|
||||
classes: ['text-primary', 'text-secondary', 'text-accent', 'text-muted-foreground', 'bg-primary', 'bg-secondary', 'bg-accent', 'bg-background', 'bg-card', 'bg-muted', 'border-primary', 'border-secondary', 'border-accent', 'border-border'],
|
||||
},
|
||||
{
|
||||
name: 'Borders',
|
||||
classes: ['border', 'border-2', 'border-4', 'border-t', 'border-b', 'border-l', 'border-r', 'rounded', 'rounded-sm', 'rounded-md', 'rounded-lg', 'rounded-xl', 'rounded-2xl', 'rounded-full'],
|
||||
},
|
||||
{
|
||||
name: 'Effects',
|
||||
classes: ['shadow', 'shadow-sm', 'shadow-md', 'shadow-lg', 'shadow-xl', 'hover:shadow-lg', 'opacity-0', 'opacity-50', 'opacity-75', 'opacity-100', 'transition', 'transition-all', 'duration-200', 'duration-300', 'duration-500'],
|
||||
},
|
||||
{
|
||||
name: 'Positioning',
|
||||
classes: ['relative', 'absolute', 'fixed', 'sticky', 'top-0', 'bottom-0', 'left-0', 'right-0', 'z-10', 'z-20', 'z-30', 'z-40', 'z-50'],
|
||||
},
|
||||
{
|
||||
name: 'Alignment',
|
||||
classes: ['items-start', 'items-center', 'items-end', 'justify-start', 'justify-center', 'justify-end', 'justify-between', 'justify-around', 'self-start', 'self-center', 'self-end'],
|
||||
},
|
||||
{
|
||||
name: 'Interactivity',
|
||||
classes: ['cursor-pointer', 'cursor-default', 'pointer-events-none', 'select-none', 'hover:bg-accent', 'hover:text-accent-foreground', 'active:scale-95', 'disabled:opacity-50'],
|
||||
},
|
||||
]
|
||||
await this.setCssClasses(defaultCssClasses)
|
||||
}
|
||||
|
||||
const dropdowns = await this.getDropdownConfigs()
|
||||
if (dropdowns.length === 0) {
|
||||
const defaultDropdowns: DropdownConfig[] = [
|
||||
{
|
||||
id: 'dropdown_status',
|
||||
name: 'status_options',
|
||||
label: 'Status',
|
||||
options: [
|
||||
{ value: 'draft', label: 'Draft' },
|
||||
{ value: 'published', label: 'Published' },
|
||||
{ value: 'archived', label: 'Archived' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'dropdown_priority',
|
||||
name: 'priority_options',
|
||||
label: 'Priority',
|
||||
options: [
|
||||
{ value: 'low', label: 'Low' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'high', label: 'High' },
|
||||
{ value: 'urgent', label: 'Urgent' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'dropdown_category',
|
||||
name: 'category_options',
|
||||
label: 'Category',
|
||||
options: [
|
||||
{ value: 'general', label: 'General' },
|
||||
{ value: 'technical', label: 'Technical' },
|
||||
{ value: 'business', label: 'Business' },
|
||||
{ value: 'personal', label: 'Personal' },
|
||||
],
|
||||
},
|
||||
]
|
||||
await this.setDropdownConfigs(defaultDropdowns)
|
||||
}
|
||||
}
|
||||
|
||||
static async exportDatabase(): Promise<string> {
|
||||
@@ -494,6 +596,64 @@ export class Database {
|
||||
await this.setGodCredentialsExpiry(expiryTime)
|
||||
}
|
||||
|
||||
static async getCssClasses(): Promise<CssCategory[]> {
|
||||
return (await window.spark.kv.get<CssCategory[]>(DB_KEYS.CSS_CLASSES)) || []
|
||||
}
|
||||
|
||||
static async setCssClasses(classes: CssCategory[]): Promise<void> {
|
||||
await window.spark.kv.set(DB_KEYS.CSS_CLASSES, classes)
|
||||
}
|
||||
|
||||
static async addCssCategory(category: CssCategory): Promise<void> {
|
||||
const classes = await this.getCssClasses()
|
||||
classes.push(category)
|
||||
await this.setCssClasses(classes)
|
||||
}
|
||||
|
||||
static async updateCssCategory(categoryName: string, classes: string[]): Promise<void> {
|
||||
const categories = await this.getCssClasses()
|
||||
const index = categories.findIndex(c => c.name === categoryName)
|
||||
if (index !== -1) {
|
||||
categories[index].classes = classes
|
||||
await this.setCssClasses(categories)
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteCssCategory(categoryName: string): Promise<void> {
|
||||
const categories = await this.getCssClasses()
|
||||
const filtered = categories.filter(c => c.name !== categoryName)
|
||||
await this.setCssClasses(filtered)
|
||||
}
|
||||
|
||||
static async getDropdownConfigs(): Promise<DropdownConfig[]> {
|
||||
return (await window.spark.kv.get<DropdownConfig[]>(DB_KEYS.DROPDOWN_CONFIGS)) || []
|
||||
}
|
||||
|
||||
static async setDropdownConfigs(configs: DropdownConfig[]): Promise<void> {
|
||||
await window.spark.kv.set(DB_KEYS.DROPDOWN_CONFIGS, configs)
|
||||
}
|
||||
|
||||
static async addDropdownConfig(config: DropdownConfig): Promise<void> {
|
||||
const configs = await this.getDropdownConfigs()
|
||||
configs.push(config)
|
||||
await this.setDropdownConfigs(configs)
|
||||
}
|
||||
|
||||
static async updateDropdownConfig(id: string, updates: DropdownConfig): Promise<void> {
|
||||
const configs = await this.getDropdownConfigs()
|
||||
const index = configs.findIndex(c => c.id === id)
|
||||
if (index !== -1) {
|
||||
configs[index] = updates
|
||||
await this.setDropdownConfigs(configs)
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteDropdownConfig(id: string): Promise<void> {
|
||||
const configs = await this.getDropdownConfigs()
|
||||
const filtered = configs.filter(c => c.id !== id)
|
||||
await this.setDropdownConfigs(filtered)
|
||||
}
|
||||
|
||||
static async clearDatabase(): Promise<void> {
|
||||
await window.spark.kv.delete(DB_KEYS.USERS)
|
||||
await window.spark.kv.delete(DB_KEYS.CREDENTIALS)
|
||||
@@ -509,5 +669,7 @@ export class Database {
|
||||
await window.spark.kv.delete(DB_KEYS.PASSWORD_CHANGE_TIMESTAMPS)
|
||||
await window.spark.kv.delete(DB_KEYS.FIRST_LOGIN_FLAGS)
|
||||
await window.spark.kv.delete(DB_KEYS.GOD_CREDENTIALS_EXPIRY_DURATION)
|
||||
await window.spark.kv.delete(DB_KEYS.CSS_CLASSES)
|
||||
await window.spark.kv.delete(DB_KEYS.DROPDOWN_CONFIGS)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user