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:
2025-12-23 22:14:19 +00:00
parent 8981e08d58
commit 3b7a325651
11 changed files with 1546 additions and 212 deletions

143
IMPROVEMENTS.md Normal file
View 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
View File

@@ -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 templatesPreview 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

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View File

@@ -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>

View File

@@ -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>
)
}

View 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>
)
}

View File

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

View File

@@ -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)
}
}