This commit is contained in:
Juergen Kunz
2025-06-27 20:50:32 +00:00
parent 216cb0288d
commit 03f215e0f1
2 changed files with 340 additions and 116 deletions

View File

@ -65,26 +65,42 @@ Implemented an advanced editor for complex properties (Arrays and Objects) that
### Features ### Features
1. **Dynamic Layout**: Frame shrinks by 300px from bottom when editor opens 1. **Dynamic Layout**: Frame shrinks by 300px from bottom when editor opens
2. **JSON Editor**: 2. **Multiple Editors**: Can edit multiple properties simultaneously side by side
3. **JSON Editor**:
- Monospace font for code editing - Monospace font for code editing
- Tab key support for indentation - Tab key support for indentation
- Syntax validation with error messages - Syntax validation with error messages
- Live preview of changes - Live preview of changes
3. **Smooth Transitions**: Animated opening/closing with 0.3s ease 4. **Smooth Transitions**: Animated opening/closing with 0.3s ease
4. **Error Handling**: Invalid JSON shows clear error messages that disappear on typing 5. **Error Handling**: Invalid JSON shows clear error messages that disappear on typing
6. **Close All Button**: Single button to close all open editors at once
### Technical Implementation ### Technical Implementation (Updated)
- **State Management**: Added showAdvancedEditor, editingProperty, editorValue, editorError states - **State Management**: Changed from single editor to array of editors with unique IDs
- **Editor Structure**: Each editor instance contains:
- `id`: Unique identifier (`propertyName-timestamp`)
- `name`: Property name
- `value`: Original value
- `element`: Reference to the element
- `editorValue`: Current JSON string
- `editorError`: Validation error message
- **Event System**: Uses custom 'editorStateChanged' event to communicate with parent dashboard - **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 - **Dynamic Styling**: wcc-frame's bottom position changes from 100px to 400px when any editor is open
- **Property Types**: Object and Array properties show "Edit Object/Array" button instead of inline controls - **Property Types**: Object and Array properties show "Edit Object/Array" button instead of inline controls
### User Flow ### User Flow
1. Click "Edit Object/Array" button on complex property 1. Click "Edit Object/Array" button on complex property
2. Editor slides up between properties panel and frame 2. Editor slides up between properties panel and frame
3. Edit JSON with live validation 3. Click additional "Edit" buttons to open more properties side by side
4. Save applies changes and refreshes properties, Cancel discards changes 4. Each editor can be saved/cancelled independently
5. Frame automatically resizes back when editor closes 5. "Close All" button dismisses all editors at once
6. Frame automatically resizes back when all editors are closed
### Layout Details
- **Container**: Flexbox with horizontal scrolling when multiple editors overflow
- **Editor Width**: Min 300px, max 500px, flexible between
- **Scrollbar**: Custom styled thin scrollbar for horizontal overflow
- **Header Bar**: Fixed top bar with "Property Editors" title and "Close All" button
## Properties Panel Element Detection Issue (Fixed) ## Properties Panel Element Detection Issue (Fixed)

View File

@ -35,16 +35,14 @@ export class WccProperties extends DeesElement {
propertyContent: TemplateResult[] = []; propertyContent: TemplateResult[] = [];
@state() @state()
showAdvancedEditor: boolean = false; editingProperties: Array<{
id: string;
@state() name: string;
editingProperty: { name: string; value: any; element: HTMLElement } = null; value: any;
element: HTMLElement;
@state() editorValue: string;
editorValue: string = ''; editorError: string;
}> = [];
@state()
editorError: string = '';
public editorHeight: number = 300; public editorHeight: number = 300;
@ -76,7 +74,7 @@ export class WccProperties extends DeesElement {
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
left: 200px; left: 200px;
height: ${this.showAdvancedEditor ? 100 + this.editorHeight : 100}px; height: ${this.editingProperties.length > 0 ? 100 + this.editorHeight : 100}px;
bottom: 0px; bottom: 0px;
right: 0px; right: 0px;
overflow: hidden; overflow: hidden;
@ -286,99 +284,244 @@ export class WccProperties extends DeesElement {
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
} }
.advanced-editor { .advanced-editor-container {
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
top: 0; top: 0;
height: ${this.editorHeight}px; height: ${this.editorHeight}px;
background: var(--background); background: #050505;
border-bottom: 1px solid rgba(255, 255, 255, 0.08); border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
transition: all 0.3s ease; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
} }
.editor-header { .editor-header-bar {
padding: 0.75rem 1rem; padding: 0.75rem 1.25rem;
background: rgba(59, 130, 246, 0.03); background: linear-gradient(to bottom, rgba(59, 130, 246, 0.05), transparent);
border-bottom: 1px solid var(--border); border-bottom: 1px solid rgba(59, 130, 246, 0.1);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
height: 48px;
backdrop-filter: blur(8px);
}
.editor-header-title {
font-size: 0.875rem;
font-weight: 600;
color: var(--foreground);
text-transform: uppercase;
letter-spacing: 0.1em;
display: flex;
align-items: center;
gap: 0.5rem;
}
.editor-header-title::before {
content: '';
display: inline-block;
width: 3px;
height: 16px;
background: var(--primary);
border-radius: 1px;
}
.editor-close-all {
padding: 0.375rem 0.75rem;
background: rgba(239, 68, 68, 0.05);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: var(--radius);
color: #f87171;
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
gap: 0.375rem;
}
.editor-close-all:hover {
background: rgba(239, 68, 68, 0.15);
border-color: rgba(239, 68, 68, 0.4);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(239, 68, 68, 0.2);
}
.editor-close-all::before {
content: '×';
font-size: 1.125rem;
line-height: 1;
}
.editors-container {
flex: 1;
display: flex;
overflow-x: auto;
overflow-y: hidden;
gap: 0;
background: rgba(255, 255, 255, 0.02);
padding: 0.75rem;
}
.editors-container::-webkit-scrollbar {
height: 8px;
}
.editors-container::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.02);
border-radius: 4px;
}
.editors-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.08);
border-radius: 4px;
}
.editors-container::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.12);
}
.editor-instance {
min-width: 320px;
flex: 1;
max-width: 480px;
background: rgba(10, 10, 10, 0.6);
display: flex;
flex-direction: column;
border-radius: var(--radius);
overflow: hidden;
margin-right: 0.75rem;
border: 1px solid rgba(255, 255, 255, 0.06);
transition: all 0.2s ease;
}
.editor-instance:hover {
border-color: rgba(255, 255, 255, 0.1);
}
.editor-instance:last-child {
margin-right: 0;
}
.editor-header {
padding: 0.5rem 0.75rem;
background: rgba(255, 255, 255, 0.02);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
justify-content: space-between;
align-items: center;
height: 36px;
} }
.editor-title { .editor-title {
font-size: 0.875rem; font-size: 0.75rem;
font-weight: 500; font-weight: 500;
color: var(--foreground); color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: 'Consolas', 'Monaco', monospace;
} }
.editor-actions { .editor-actions {
display: flex; display: flex;
gap: 0.5rem; gap: 0.25rem;
} }
.editor-button { .editor-button {
padding: 0.375rem 0.75rem; width: 24px;
height: 24px;
padding: 0;
background: transparent; background: transparent;
border: 1px solid var(--border); border: none;
border-radius: var(--radius); color: #666;
color: #999; font-size: 1rem;
font-size: 0.75rem;
cursor: pointer; cursor: pointer;
transition: all 0.15s ease; transition: all 0.15s ease;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
} }
.editor-button:hover { .editor-button:hover {
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
color: var(--foreground); color: #999;
} }
.editor-button.primary { .editor-button.primary {
background: var(--primary); color: #4ade80;
color: var(--primary-foreground);
border-color: var(--primary);
} }
.editor-button.primary:hover { .editor-button.primary:hover {
background: rgba(59, 130, 246, 0.8); background: rgba(74, 222, 128, 0.1);
} }
.editor-content { .editor-content {
flex: 1; flex: 1;
padding: 1rem; display: flex;
overflow: auto; flex-direction: column;
overflow: hidden;
min-height: 0;
position: relative;
} }
.editor-textarea { .editor-textarea {
width: 100%; width: 100%;
height: 100%; height: 100%;
background: var(--input); background: transparent;
border: 1px solid var(--border); border: none;
border-radius: var(--radius); color: #d0d0d0;
color: var(--foreground);
font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.875rem; font-size: 0.8125rem;
line-height: 1.6;
padding: 0.75rem; padding: 0.75rem;
resize: none; resize: none;
outline: none; outline: none;
transition: all 0.15s ease; transition: all 0.15s ease;
overflow: auto;
} }
.editor-textarea:focus { .editor-textarea:focus {
border-color: var(--primary); background: rgba(255, 255, 255, 0.01);
background: rgba(59, 130, 246, 0.05); }
.editor-textarea::selection {
background: rgba(59, 130, 246, 0.3);
} }
.editor-error { .editor-error {
margin-top: 0.5rem; position: absolute;
padding: 0.5rem; bottom: 0;
background: rgba(239, 68, 68, 0.1); left: 0;
border: 1px solid rgba(239, 68, 68, 0.3); right: 0;
border-radius: var(--radius-sm); padding: 0.5rem 0.75rem;
color: #f87171; background: rgba(239, 68, 68, 0.9);
font-size: 0.75rem; backdrop-filter: blur(4px);
color: #fff;
font-size: 0.7rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.375rem;
border-top: 1px solid rgba(239, 68, 68, 0.5);
}
.editor-error::before {
content: '!';
display: inline-flex;
width: 16px;
height: 16px;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
font-size: 0.65rem;
font-weight: bold;
} }
.main-content { .main-content {
@ -389,38 +532,72 @@ export class WccProperties extends DeesElement {
height: 100px; height: 100px;
} }
</style> </style>
${this.showAdvancedEditor ? html` ${this.editingProperties.length > 0 ? html`
<div class="advanced-editor"> <div class="advanced-editor-container">
<div class="editor-header"> <div class="editor-header-bar">
<div class="editor-title">Editing: ${this.editingProperty?.name}</div> <div class="editor-header-title">Property Editors</div>
<div class="editor-actions"> <button class="editor-close-all" @click=${this.closeAllEditors}>
<button class="editor-button" @click=${this.handleEditorCancel}>Cancel</button> Close All
<button class="editor-button primary" @click=${this.handleEditorSave}>Save</button> </button>
</div>
</div> </div>
<div class="editor-content"> <div class="editors-container">
<textarea ${this.editingProperties.length === 0 ? html`
class="editor-textarea" <div style="
.value=${this.editorValue} flex: 1;
@input=${(e: InputEvent) => { display: flex;
this.editorValue = (e.target as HTMLTextAreaElement).value; align-items: center;
this.editorError = ''; justify-content: center;
}} color: #666;
@keydown=${(e: KeyboardEvent) => { font-size: 0.875rem;
if (e.key === 'Tab') { text-align: center;
e.preventDefault(); padding: 2rem;
const target = e.target as HTMLTextAreaElement; ">
const start = target.selectionStart; <div>
const end = target.selectionEnd; <div style="margin-bottom: 0.5rem; font-size: 1.5rem; opacity: 0.5;">{ }</div>
const value = target.value; <div>No properties being edited</div>
target.value = value.substring(0, start) + ' ' + value.substring(end); <div style="font-size: 0.75rem; margin-top: 0.25rem; opacity: 0.7;">Click "Edit Object/Array" buttons to start editing</div>
target.selectionStart = target.selectionEnd = start + 2; </div>
} </div>
}}
></textarea>
${this.editorError ? html`
<div class="editor-error">${this.editorError}</div>
` : null} ` : null}
${this.editingProperties.map(prop => html`
<div class="editor-instance">
<div class="editor-header">
<div class="editor-title">${prop.name}</div>
<div class="editor-actions">
<button class="editor-button" @click=${() => this.handleEditorCancel(prop.id)}>✕</button>
<button class="editor-button primary" @click=${() => this.handleEditorSave(prop.id)}>✓</button>
</div>
</div>
<div class="editor-content">
<textarea
class="editor-textarea"
.value=${prop.editorValue}
@input=${(e: InputEvent) => {
const editor = this.editingProperties.find(p => p.id === prop.id);
if (editor) {
editor.editorValue = (e.target as HTMLTextAreaElement).value;
editor.editorError = '';
this.requestUpdate();
}
}}
@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;
}
}}
></textarea>
${prop.editorError ? html`
<div class="editor-error">${prop.editorError}</div>
` : null}
</div>
</div>
`)}
</div> </div>
</div> </div>
` : null} ` : null}
@ -736,53 +913,84 @@ export class WccProperties extends DeesElement {
} }
private openAdvancedEditor(propertyName: string, value: any, element: HTMLElement) { private openAdvancedEditor(propertyName: string, value: any, element: HTMLElement) {
this.editingProperty = { // Check if this property is already being edited
const existingEditor = this.editingProperties.find(p => p.name === propertyName && p.element === element);
if (existingEditor) {
return; // Property is already open for editing
}
const newEditor = {
id: `${propertyName}-${Date.now()}`,
name: propertyName, name: propertyName,
value: value, value: value,
element: element element: element,
editorValue: JSON.stringify(value, null, 2),
editorError: ''
}; };
this.editorValue = JSON.stringify(value, null, 2);
this.editorError = '';
this.showAdvancedEditor = true;
// Notify parent to resize frame this.editingProperties = [...this.editingProperties, newEditor];
this.dispatchEvent(
new CustomEvent('editorStateChanged', { // Notify parent to resize frame if this is the first editor
detail: { isOpen: true }, if (this.editingProperties.length === 1) {
bubbles: true this.dispatchEvent(
}) new CustomEvent('editorStateChanged', {
); detail: { isOpen: true },
bubbles: true
})
);
}
} }
private handleEditorSave() { private handleEditorSave(editorId: string) {
try { const editor = this.editingProperties.find(p => p.id === editorId);
const parsedValue = JSON.parse(this.editorValue); if (!editor) return;
if (this.editingProperty) {
this.editingProperty.element[this.editingProperty.name] = parsedValue;
this.showAdvancedEditor = false;
this.editorError = '';
// Notify parent to resize frame back try {
const parsedValue = JSON.parse(editor.editorValue);
editor.element[editor.name] = parsedValue;
// Remove this editor from the list
this.editingProperties = this.editingProperties.filter(p => p.id !== editorId);
// If no more editors, notify parent to resize frame
if (this.editingProperties.length === 0) {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('editorStateChanged', { new CustomEvent('editorStateChanged', {
detail: { isOpen: false }, detail: { isOpen: false },
bubbles: true bubbles: true
}) })
); );
// Refresh properties display
this.createProperties();
} }
// Refresh properties display
this.createProperties();
} catch (error) { } catch (error) {
this.editorError = `Invalid JSON: ${error.message}`; // Update error for this specific editor
const editorIndex = this.editingProperties.findIndex(p => p.id === editorId);
if (editorIndex !== -1) {
this.editingProperties[editorIndex].editorError = `Invalid JSON: ${error.message}`;
this.requestUpdate();
}
} }
} }
private handleEditorCancel() { private handleEditorCancel(editorId: string) {
this.showAdvancedEditor = false; // Remove this editor from the list
this.editingProperty = null; this.editingProperties = this.editingProperties.filter(p => p.id !== editorId);
this.editorValue = '';
this.editorError = ''; // If no more editors, notify parent to resize frame
if (this.editingProperties.length === 0) {
this.dispatchEvent(
new CustomEvent('editorStateChanged', {
detail: { isOpen: false },
bubbles: true
})
);
}
}
private closeAllEditors() {
this.editingProperties = [];
// Notify parent to resize frame back // Notify parent to resize frame back
this.dispatchEvent( this.dispatchEvent(