update
This commit is contained in:
		| @@ -58,6 +58,34 @@ Updated the WCC Dashboard UI components (properties and sidebar) to use shadcn-l | ||||
| - Grid layout with 1px gaps creating subtle dividers | ||||
| - Warning display with backdrop blur and rounded corners | ||||
|  | ||||
| ## Advanced Complex Properties Editor (2025-06-27) | ||||
|  | ||||
| ### Overview | ||||
| Implemented an advanced editor for complex properties (Arrays and Objects) that appears between the wcc-properties panel and frame when activated. | ||||
|  | ||||
| ### Features | ||||
| 1. **Dynamic Layout**: Frame shrinks by 300px from bottom when editor opens | ||||
| 2. **JSON Editor**:  | ||||
|    - Monospace font for code editing | ||||
|    - Tab key support for indentation | ||||
|    - Syntax validation with error messages | ||||
|    - Live preview of changes | ||||
| 3. **Smooth Transitions**: Animated opening/closing with 0.3s ease | ||||
| 4. **Error Handling**: Invalid JSON shows clear error messages that disappear on typing | ||||
|  | ||||
| ### Technical Implementation | ||||
| - **State Management**: Added showAdvancedEditor, editingProperty, editorValue, editorError states | ||||
| - **Event System**: Uses custom 'editorStateChanged' event to communicate with parent dashboard | ||||
| - **Dynamic Styling**: wcc-frame's bottom position changes from 100px to 400px when editor is open | ||||
| - **Property Types**: Object and Array properties show "Edit Object/Array" button instead of inline controls | ||||
|  | ||||
| ### User Flow | ||||
| 1. Click "Edit Object/Array" button on complex property | ||||
| 2. Editor slides up between properties panel and frame | ||||
| 3. Edit JSON with live validation | ||||
| 4. Save applies changes and refreshes properties, Cancel discards changes | ||||
| 5. Frame automatically resizes back when editor closes | ||||
|  | ||||
| ## Properties Panel Element Detection Issue (Fixed) | ||||
|  | ||||
| ### Problem | ||||
|   | ||||
| @@ -99,6 +99,13 @@ export class WccDashboard extends DeesElement { | ||||
|         @selectedTheme=${(eventArg) => { | ||||
|           this.selectedTheme = eventArg.detail; | ||||
|         }} | ||||
|         @editorStateChanged=${async (eventArg) => { | ||||
|           const frame = await this.wccFrame; | ||||
|           if (frame) { | ||||
|             frame.advancedEditorOpen = eventArg.detail.isOpen; | ||||
|             frame.requestUpdate(); | ||||
|           } | ||||
|         }} | ||||
|       ></wcc-properties> | ||||
|       <wcc-frame id="wccFrame" viewport=${this.selectedViewport}> | ||||
|       </wcc-frame> | ||||
|   | ||||
| @@ -13,6 +13,9 @@ export class WccFrame extends DeesElement { | ||||
|   @property() | ||||
|   public viewport: string; | ||||
|  | ||||
|   @property({ type: Boolean }) | ||||
|   public advancedEditorOpen: boolean = false; | ||||
|  | ||||
|   public static styles = [ | ||||
|     css` | ||||
|       :host { | ||||
| @@ -22,7 +25,6 @@ export class WccFrame extends DeesElement { | ||||
|         left: 200px; | ||||
|         right: 0px; | ||||
|         top: 0px; | ||||
|         bottom: 100px; | ||||
|         overflow-y: auto; | ||||
|         overflow-x: auto; | ||||
|         overscroll-behavior: contain; | ||||
| @@ -41,6 +43,8 @@ export class WccFrame extends DeesElement { | ||||
|     return html` | ||||
|       <style> | ||||
|         :host { | ||||
|           bottom: ${this.advancedEditorOpen ? '400px' : '100px'}; | ||||
|           transition: bottom 0.3s ease; | ||||
|           ${(() => { | ||||
|           switch (this.viewport) { | ||||
|             case 'desktop': | ||||
|   | ||||
| @@ -34,6 +34,20 @@ export class WccProperties extends DeesElement { | ||||
|   @state() | ||||
|   propertyContent: TemplateResult[] = []; | ||||
|  | ||||
|   @state() | ||||
|   showAdvancedEditor: boolean = false; | ||||
|  | ||||
|   @state() | ||||
|   editingProperty: { name: string; value: any; element: HTMLElement } = null; | ||||
|  | ||||
|   @state() | ||||
|   editorValue: string = ''; | ||||
|  | ||||
|   @state() | ||||
|   editorError: string = ''; | ||||
|  | ||||
|   public editorHeight: number = 300; | ||||
|  | ||||
|   public render(): TemplateResult { | ||||
|     return html` | ||||
|       <style> | ||||
| @@ -62,7 +76,7 @@ export class WccProperties extends DeesElement { | ||||
|           box-sizing: border-box; | ||||
|           position: absolute; | ||||
|           left: 200px; | ||||
|           height: 100px; | ||||
|           height: ${this.showAdvancedEditor ? 100 + this.editorHeight : 100}px; | ||||
|           bottom: 0px; | ||||
|           right: 0px; | ||||
|           overflow: hidden; | ||||
| @@ -74,6 +88,10 @@ export class WccProperties extends DeesElement { | ||||
|           grid-template-columns: 1fr 150px 300px 70px; | ||||
|           height: 100%; | ||||
|         } | ||||
|          | ||||
|         .main-content .grid { | ||||
|           height: 100px; | ||||
|         } | ||||
|         .properties { | ||||
|           background: transparent; | ||||
|           overflow-y: auto; | ||||
| @@ -149,6 +167,23 @@ export class WccProperties extends DeesElement { | ||||
|           cursor: pointer; | ||||
|           accent-color: var(--primary); | ||||
|         } | ||||
|          | ||||
|         .properties .editor-button { | ||||
|           padding: 0.25rem 0.5rem; | ||||
|           background: var(--input); | ||||
|           border: 1px solid transparent; | ||||
|           border-radius: var(--radius-sm); | ||||
|           color: var(--foreground); | ||||
|           font-size: 0.7rem; | ||||
|           cursor: pointer; | ||||
|           transition: all 0.15s ease; | ||||
|           text-align: center; | ||||
|         } | ||||
|          | ||||
|         .properties .editor-button:hover { | ||||
|           border-color: var(--primary); | ||||
|           background: rgba(59, 130, 246, 0.1); | ||||
|         } | ||||
|  | ||||
|         .viewportSelector, | ||||
|         .themeSelector { | ||||
| @@ -254,75 +289,217 @@ export class WccProperties extends DeesElement { | ||||
|           border: 1px solid var(--border); | ||||
|           backdrop-filter: blur(8px); | ||||
|         } | ||||
|  | ||||
|         .advanced-editor { | ||||
|           position: absolute; | ||||
|           left: 0; | ||||
|           right: 0; | ||||
|           top: 0; | ||||
|           height: ${this.editorHeight}px; | ||||
|           background: var(--background); | ||||
|           border-bottom: 1px solid rgba(255, 255, 255, 0.08); | ||||
|           display: flex; | ||||
|           flex-direction: column; | ||||
|           transition: all 0.3s ease; | ||||
|         } | ||||
|  | ||||
|         .editor-header { | ||||
|           padding: 0.75rem 1rem; | ||||
|           background: rgba(59, 130, 246, 0.03); | ||||
|           border-bottom: 1px solid var(--border); | ||||
|           display: flex; | ||||
|           justify-content: space-between; | ||||
|           align-items: center; | ||||
|         } | ||||
|  | ||||
|         .editor-title { | ||||
|           font-size: 0.875rem; | ||||
|           font-weight: 500; | ||||
|           color: var(--foreground); | ||||
|         } | ||||
|  | ||||
|         .editor-actions { | ||||
|           display: flex; | ||||
|           gap: 0.5rem; | ||||
|         } | ||||
|  | ||||
|         .editor-button { | ||||
|           padding: 0.375rem 0.75rem; | ||||
|           background: transparent; | ||||
|           border: 1px solid var(--border); | ||||
|           border-radius: var(--radius); | ||||
|           color: #999; | ||||
|           font-size: 0.75rem; | ||||
|           cursor: pointer; | ||||
|           transition: all 0.15s ease; | ||||
|         } | ||||
|  | ||||
|         .editor-button:hover { | ||||
|           background: rgba(255, 255, 255, 0.05); | ||||
|           color: var(--foreground); | ||||
|         } | ||||
|  | ||||
|         .editor-button.primary { | ||||
|           background: var(--primary); | ||||
|           color: var(--primary-foreground); | ||||
|           border-color: var(--primary); | ||||
|         } | ||||
|  | ||||
|         .editor-button.primary:hover { | ||||
|           background: rgba(59, 130, 246, 0.8); | ||||
|         } | ||||
|  | ||||
|         .editor-content { | ||||
|           flex: 1; | ||||
|           padding: 1rem; | ||||
|           overflow: auto; | ||||
|         } | ||||
|  | ||||
|         .editor-textarea { | ||||
|           width: 100%; | ||||
|           height: 100%; | ||||
|           background: var(--input); | ||||
|           border: 1px solid var(--border); | ||||
|           border-radius: var(--radius); | ||||
|           color: var(--foreground); | ||||
|           font-family: 'Consolas', 'Monaco', 'Courier New', monospace; | ||||
|           font-size: 0.875rem; | ||||
|           padding: 0.75rem; | ||||
|           resize: none; | ||||
|           outline: none; | ||||
|           transition: all 0.15s ease; | ||||
|         } | ||||
|  | ||||
|         .editor-textarea:focus { | ||||
|           border-color: var(--primary); | ||||
|           background: rgba(59, 130, 246, 0.05); | ||||
|         } | ||||
|  | ||||
|         .editor-error { | ||||
|           margin-top: 0.5rem; | ||||
|           padding: 0.5rem; | ||||
|           background: rgba(239, 68, 68, 0.1); | ||||
|           border: 1px solid rgba(239, 68, 68, 0.3); | ||||
|           border-radius: var(--radius-sm); | ||||
|           color: #f87171; | ||||
|           font-size: 0.75rem; | ||||
|         } | ||||
|  | ||||
|         .main-content { | ||||
|           position: absolute; | ||||
|           left: 0; | ||||
|           right: 0; | ||||
|           bottom: 0; | ||||
|           height: 100px; | ||||
|           display: grid; | ||||
|           grid-template-columns: 1fr 150px 300px 70px; | ||||
|         } | ||||
|       </style> | ||||
|       <div class="grid"> | ||||
|         <div class="properties"> | ||||
|           ${this.propertyContent} | ||||
|         </div> | ||||
|         <div class="themeSelector"> | ||||
|           <div class="panelheading">Theme</div> | ||||
|           <div class="selectorButtons2"> | ||||
|             <div | ||||
|               class="button ${this.selectedTheme === 'dark' ? 'selected' : null}" | ||||
|               @click=${() => { | ||||
|                 this.selectTheme('dark'); | ||||
|               }} | ||||
|             > | ||||
|               Dark<i class="material-symbols-outlined">brightness_3</i> | ||||
|             </div> | ||||
|             <div | ||||
|               class="button ${this.selectedTheme === 'bright' ? 'selected' : null}" | ||||
|               @click=${() => { | ||||
|                 this.selectTheme('bright'); | ||||
|               }} | ||||
|             > | ||||
|               Bright<i class="material-symbols-outlined">flare</i> | ||||
|       ${this.showAdvancedEditor ? html` | ||||
|         <div class="advanced-editor"> | ||||
|           <div class="editor-header"> | ||||
|             <div class="editor-title">Editing: ${this.editingProperty?.name}</div> | ||||
|             <div class="editor-actions"> | ||||
|               <button class="editor-button" @click=${this.handleEditorCancel}>Cancel</button> | ||||
|               <button class="editor-button primary" @click=${this.handleEditorSave}>Save</button> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="viewportSelector"> | ||||
|           <div class="panelheading">Viewport</div> | ||||
|           <div class="selectorButtons4"> | ||||
|             <div | ||||
|               class="button ${this.selectedViewport === 'phone' ? 'selected' : null}" | ||||
|               @click=${() => { | ||||
|                 this.selectViewport('phone'); | ||||
|           <div class="editor-content"> | ||||
|             <textarea  | ||||
|               class="editor-textarea" | ||||
|               .value=${this.editorValue} | ||||
|               @input=${(e: InputEvent) => { | ||||
|                 this.editorValue = (e.target as HTMLTextAreaElement).value; | ||||
|                 this.editorError = ''; | ||||
|               }} | ||||
|             > | ||||
|               Phone<i class="material-symbols-outlined">smartphone</i> | ||||
|             </div> | ||||
|             <div | ||||
|               class="button ${this.selectedViewport === 'phablet' ? 'selected' : null}" | ||||
|               @click=${() => { | ||||
|                 this.selectViewport('phablet'); | ||||
|               @keydown=${(e: KeyboardEvent) => { | ||||
|                 if (e.key === 'Tab') { | ||||
|                   e.preventDefault(); | ||||
|                   const target = e.target as HTMLTextAreaElement; | ||||
|                   const start = target.selectionStart; | ||||
|                   const end = target.selectionEnd; | ||||
|                   const value = target.value; | ||||
|                   target.value = value.substring(0, start) + '  ' + value.substring(end); | ||||
|                   target.selectionStart = target.selectionEnd = start + 2; | ||||
|                 } | ||||
|               }} | ||||
|             > | ||||
|               Phablet<i class="material-symbols-outlined">smartphone</i> | ||||
|             </div> | ||||
|             <div | ||||
|               class="button ${this.selectedViewport === 'tablet' ? 'selected' : null}" | ||||
|               @click=${() => { | ||||
|                 this.selectViewport('tablet'); | ||||
|               }} | ||||
|             > | ||||
|               Tablet<i class="material-symbols-outlined">tablet</i> | ||||
|             </div> | ||||
|             <div | ||||
|               class="button ${this.selectedViewport === 'desktop' || | ||||
|               this.selectedViewport === 'native' | ||||
|                 ? 'selected' | ||||
|                 : null}" | ||||
|               @click=${() => { | ||||
|                 this.selectViewport('native'); | ||||
|               }} | ||||
|             > | ||||
|               Desktop<i class="material-symbols-outlined">desktop_windows</i> | ||||
|             </div> | ||||
|             ></textarea> | ||||
|             ${this.editorError ? html` | ||||
|               <div class="editor-error">${this.editorError}</div> | ||||
|             ` : null} | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="docs">Docs</div> | ||||
|       ` : null} | ||||
|       <div class="main-content"> | ||||
|         <div class="grid"> | ||||
|           <div class="properties"> | ||||
|             ${this.propertyContent} | ||||
|           </div> | ||||
|           <div class="themeSelector"> | ||||
|             <div class="panelheading">Theme</div> | ||||
|             <div class="selectorButtons2"> | ||||
|               <div | ||||
|                 class="button ${this.selectedTheme === 'dark' ? 'selected' : null}" | ||||
|                 @click=${() => { | ||||
|                   this.selectTheme('dark'); | ||||
|                 }} | ||||
|               > | ||||
|                 Dark<i class="material-symbols-outlined">brightness_3</i> | ||||
|               </div> | ||||
|               <div | ||||
|                 class="button ${this.selectedTheme === 'bright' ? 'selected' : null}" | ||||
|                 @click=${() => { | ||||
|                   this.selectTheme('bright'); | ||||
|                 }} | ||||
|               > | ||||
|                 Bright<i class="material-symbols-outlined">flare</i> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="viewportSelector"> | ||||
|             <div class="panelheading">Viewport</div> | ||||
|             <div class="selectorButtons4"> | ||||
|               <div | ||||
|                 class="button ${this.selectedViewport === 'phone' ? 'selected' : null}" | ||||
|                 @click=${() => { | ||||
|                   this.selectViewport('phone'); | ||||
|                 }} | ||||
|               > | ||||
|                 Phone<i class="material-symbols-outlined">smartphone</i> | ||||
|               </div> | ||||
|               <div | ||||
|                 class="button ${this.selectedViewport === 'phablet' ? 'selected' : null}" | ||||
|                 @click=${() => { | ||||
|                   this.selectViewport('phablet'); | ||||
|                 }} | ||||
|               > | ||||
|                 Phablet<i class="material-symbols-outlined">smartphone</i> | ||||
|               </div> | ||||
|               <div | ||||
|                 class="button ${this.selectedViewport === 'tablet' ? 'selected' : null}" | ||||
|                 @click=${() => { | ||||
|                   this.selectViewport('tablet'); | ||||
|                 }} | ||||
|               > | ||||
|                 Tablet<i class="material-symbols-outlined">tablet</i> | ||||
|               </div> | ||||
|               <div | ||||
|                 class="button ${this.selectedViewport === 'desktop' || | ||||
|                 this.selectedViewport === 'native' | ||||
|                   ? 'selected' | ||||
|                   : null}" | ||||
|                 @click=${() => { | ||||
|                   this.selectViewport('native'); | ||||
|                 }} | ||||
|               > | ||||
|                 Desktop<i class="material-symbols-outlined">desktop_windows</i> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="docs">Docs</div> | ||||
|         </div> | ||||
|         ${this.warning ? html`<div class="warning">${this.warning}</div>` : null} | ||||
|       </div> | ||||
|       ${this.warning ? html`<div class="warning">${this.warning}</div>` : null} | ||||
|     `; | ||||
|   } | ||||
|  | ||||
| @@ -501,6 +678,17 @@ export class WccProperties extends DeesElement { | ||||
|                         `; | ||||
|                       })} | ||||
|                     </select>`; | ||||
|                   case 'Object': | ||||
|                   case 'Array': | ||||
|                     return html`<button | ||||
|                       class="editor-button" | ||||
|                       style="width: 100%; margin-top: 0.25rem;" | ||||
|                       @click="${() => this.openAdvancedEditor(key, firstFoundInstantiatedElement[key], firstFoundInstantiatedElement)}" | ||||
|                     > | ||||
|                       Edit ${propertyTypeString} | ||||
|                     </button>`; | ||||
|                   default: | ||||
|                     return html`<div style="color: #666; font-size: 0.7rem;">Unsupported type</div>`; | ||||
|                 } | ||||
|               })()} | ||||
|             </div> | ||||
| @@ -552,4 +740,62 @@ export class WccProperties extends DeesElement { | ||||
|     ); | ||||
|     this.dashboardRef.buildUrl(); | ||||
|   } | ||||
|  | ||||
|   private openAdvancedEditor(propertyName: string, value: any, element: HTMLElement) { | ||||
|     this.editingProperty = { | ||||
|       name: propertyName, | ||||
|       value: value, | ||||
|       element: element | ||||
|     }; | ||||
|     this.editorValue = JSON.stringify(value, null, 2); | ||||
|     this.editorError = ''; | ||||
|     this.showAdvancedEditor = true; | ||||
|      | ||||
|     // Notify parent to resize frame | ||||
|     this.dispatchEvent( | ||||
|       new CustomEvent('editorStateChanged', { | ||||
|         detail: { isOpen: true }, | ||||
|         bubbles: true | ||||
|       }) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   private handleEditorSave() { | ||||
|     try { | ||||
|       const parsedValue = JSON.parse(this.editorValue); | ||||
|       if (this.editingProperty) { | ||||
|         this.editingProperty.element[this.editingProperty.name] = parsedValue; | ||||
|         this.showAdvancedEditor = false; | ||||
|         this.editorError = ''; | ||||
|          | ||||
|         // Notify parent to resize frame back | ||||
|         this.dispatchEvent( | ||||
|           new CustomEvent('editorStateChanged', { | ||||
|             detail: { isOpen: false }, | ||||
|             bubbles: true | ||||
|           }) | ||||
|         ); | ||||
|          | ||||
|         // Refresh properties display | ||||
|         this.createProperties(); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       this.editorError = `Invalid JSON: ${error.message}`; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private handleEditorCancel() { | ||||
|     this.showAdvancedEditor = false; | ||||
|     this.editingProperty = null; | ||||
|     this.editorValue = ''; | ||||
|     this.editorError = ''; | ||||
|      | ||||
|     // Notify parent to resize frame back | ||||
|     this.dispatchEvent( | ||||
|       new CustomEvent('editorStateChanged', { | ||||
|         detail: { isOpen: false }, | ||||
|         bubbles: true | ||||
|       }) | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user