/**
* Property Panel Component
* Right sidebar panel for block box model controls
*/
class PropertyPanel {
constructor(options = {}) {
this.element = null;
this.currentBlock = null;
this.onUpdate = options.onUpdate || null;
this.init();
}
init() {
this.element = document.getElementById('property-panel-content');
if (!this.element) {
console.warn('Property panel container not found');
return;
}
}
show(block) {
if (!this.element) return;
this.currentBlock = block;
this.render();
}
hide() {
if (!this.element) return;
this.currentBlock = null;
this.element.innerHTML = `
<div class="text-center text-base-content/50 py-8">
<p class="text-sm">Select a block to edit its properties</p>
</div>
`;
}
render() {
if (!this.currentBlock || !this.element) return;
const block = this.currentBlock;
const boxStyle = block.boxStyle || {};
this.element.innerHTML = `
<!-- Block Type Badge -->
<div class="flex items-center gap-2 mb-4 pb-4 border-b border-white/10">
<span class="badge badge-sm badge-primary">${block.type}</span>
<span class="text-xs text-base-content/50 truncate">${block.id.slice(0, 8)}...</span>
</div>
<!-- Dimensions -->
<div class="collapse collapse-arrow bg-base-300/50 mb-2">
<input type="checkbox" checked />
<div class="collapse-title text-sm font-medium py-2 min-h-0">
Dimensions
</div>
<div class="collapse-content">
<div class="grid grid-cols-2 gap-2">
<div class="form-control">
<label class="label py-1">
<span class="label-text text-xs">Width</span>
</label>
<select class="select select-bordered select-xs w-full"
data-property="boxStyle.width"
onchange="propertyPanel.updateProperty('boxStyle.width', this.value)">
<option value="" ${!boxStyle.width ? 'selected' : ''}>Auto</option>
<option value="25%" ${boxStyle.width === '25%' ? 'selected' : ''}>25%</option>
<option value="50%" ${boxStyle.width === '50%' ? 'selected' : ''}>50%</option>
<option value="75%" ${boxStyle.width === '75%' ? 'selected' : ''}>75%</option>
<option value="100%" ${boxStyle.width === '100%' ? 'selected' : ''}>100%</option>
</select>
</div>
<div class="form-control">
<label class="label py-1">
<span class="label-text text-xs">Height</span>
</label>
<input type="text" class="input input-bordered input-xs w-full"
placeholder="auto"
value="${boxStyle.height || ''}"
data-property="boxStyle.height"
onchange="propertyPanel.updateProperty('boxStyle.height', this.value)" />
</div>
</div>
</div>
</div>
<!-- Spacing -->
<div class="collapse collapse-arrow bg-base-300/50 mb-2">
<input type="checkbox" checked />
<div class="collapse-title text-sm font-medium py-2 min-h-0">
Spacing
</div>
<div class="collapse-content">
<div class="form-control mb-2">
<label class="label py-1">
<span class="label-text text-xs">Padding</span>
</label>
<div class="grid grid-cols-4 gap-1">
${this.renderSpacingInputs('padding', boxStyle.padding)}
</div>
</div>
<div class="form-control">
<label class="label py-1">
<span class="label-text text-xs">Margin</span>
</label>
<div class="grid grid-cols-4 gap-1">
${this.renderSpacingInputs('margin', boxStyle.margin)}
</div>
</div>
</div>
</div>
<!-- Background -->
<div class="collapse collapse-arrow bg-base-300/50 mb-2">
<input type="checkbox" />
<div class="collapse-title text-sm font-medium py-2 min-h-0">
Background
</div>
<div class="collapse-content">
<div class="form-control mb-2">
<label class="label py-1">
<span class="label-text text-xs">Color</span>
</label>
<div class="flex gap-2">
<input type="color" class="w-8 h-8 cursor-pointer rounded"
value="${boxStyle.backgroundColor || '#000000'}"
onchange="propertyPanel.updateProperty('boxStyle.backgroundColor', this.value)" />
<input type="text" class="input input-bordered input-xs flex-1"
placeholder="#000000"
value="${boxStyle.backgroundColor || ''}"
onchange="propertyPanel.updateProperty('boxStyle.backgroundColor', this.value)" />
</div>
</div>
<div class="form-control">
<label class="label py-1">
<span class="label-text text-xs">Opacity</span>
</label>
<input type="range" class="range range-xs range-primary"
min="0" max="1" step="0.1"
value="${boxStyle.backgroundOpacity || 1}"
onchange="propertyPanel.updateProperty('boxStyle.backgroundOpacity', this.value)" />
<div class="text-xs text-center text-base-content/50 mt-1">
${Math.round((boxStyle.backgroundOpacity || 1) * 100)}%
</div>
</div>
</div>
</div>
<!-- Border -->
<div class="collapse collapse-arrow bg-base-300/50 mb-2">
<input type="checkbox" />
<div class="collapse-title text-sm font-medium py-2 min-h-0">
Border
</div>
<div class="collapse-content">
<div class="grid grid-cols-2 gap-2 mb-2">
<div class="form-control">
<label class="label py-1">
<span class="label-text text-xs">Width</span>
</label>
<select class="select select-bordered select-xs w-full"
onchange="propertyPanel.updateProperty('boxStyle.borderWidth', this.value)">
<option value="" ${!boxStyle.borderWidth ? 'selected' : ''}>None</option>
<option value="1px" ${boxStyle.borderWidth === '1px' ? 'selected' : ''}>1px</option>
<option value="2px" ${boxStyle.borderWidth === '2px' ? 'selected' : ''}>2px</option>
<option value="4px" ${boxStyle.borderWidth === '4px' ? 'selected' : ''}>4px</option>
</select>
</div>
<div class="form-control">
<label class="label py-1">
<span class="label-text text-xs">Style</span>
</label>
<select class="select select-bordered select-xs w-full"
onchange="propertyPanel.updateProperty('boxStyle.borderStyle', this.value)">
<option value="solid" ${boxStyle.borderStyle === 'solid' || !boxStyle.borderStyle ? 'selected' : ''}>Solid</option>
<option value="dashed" ${boxStyle.borderStyle === 'dashed' ? 'selected' : ''}>Dashed</option>
<option value="dotted" ${boxStyle.borderStyle === 'dotted' ? 'selected' : ''}>Dotted</option>
</select>
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="form-control">
<label class="label py-1">
<span class="label-text text-xs">Color</span>
</label>
<div class="flex gap-1">
<input type="color" class="w-6 h-6 cursor-pointer rounded"
value="${boxStyle.borderColor || '#ffffff'}"
onchange="propertyPanel.updateProperty('boxStyle.borderColor', this.value)" />
<input type="text" class="input input-bordered input-xs flex-1"
placeholder="#fff"
value="${boxStyle.borderColor || ''}"
onchange="propertyPanel.updateProperty('boxStyle.borderColor', this.value)" />
</div>
</div>
<div class="form-control">
<label class="label py-1">
<span class="label-text text-xs">Radius</span>
</label>
<select class="select select-bordered select-xs w-full"
onchange="propertyPanel.updateProperty('boxStyle.borderRadius', this.value)">
<option value="" ${!boxStyle.borderRadius ? 'selected' : ''}>None</option>
<option value="0.25rem" ${boxStyle.borderRadius === '0.25rem' ? 'selected' : ''}>Small</option>
<option value="0.5rem" ${boxStyle.borderRadius === '0.5rem' ? 'selected' : ''}>Medium</option>
<option value="1rem" ${boxStyle.borderRadius === '1rem' ? 'selected' : ''}>Large</option>
<option value="9999px" ${boxStyle.borderRadius === '9999px' ? 'selected' : ''}>Full</option>
</select>
</div>
</div>
</div>
</div>
<!-- Text Styling (for text blocks) -->
${['heading', 'text', 'quote'].includes(block.type) ? `
<div class="collapse collapse-arrow bg-base-300/50 mb-2">
<input type="checkbox" checked />
<div class="collapse-title text-sm font-medium py-2 min-h-0">
Text
</div>
<div class="collapse-content">
<div class="form-control mb-2">
<label class="label py-1">
<span class="label-text text-xs">Color</span>
</label>
<div class="flex gap-2">
<input type="color" class="w-8 h-8 cursor-pointer rounded"
value="${block.textColor || '#ffffff'}"
onchange="propertyPanel.updateProperty('textColor', this.value)" />
<input type="text" class="input input-bordered input-xs flex-1"
placeholder="#ffffff"
value="${block.textColor || ''}"
onchange="propertyPanel.updateProperty('textColor', this.value)" />
</div>
</div>
<div class="form-control mb-2">
<label class="label py-1">
<span class="label-text text-xs">Font Size</span>
</label>
<input type="text" class="input input-bordered input-xs w-full"
placeholder="inherit"
value="${block.fontSize || ''}"
onchange="propertyPanel.updateProperty('fontSize', this.value)" />
</div>
<div class="form-control">
<label class="label py-1">
<span class="label-text text-xs">Alignment</span>
</label>
<div class="flex gap-1">
${['left', 'center', 'right', 'justify'].map(align => `
<button type="button"
class="btn btn-xs ${block.textAlign === align ? 'btn-primary' : 'btn-ghost'}"
onclick="propertyPanel.updateProperty('textAlign', '${align}')">
${this.getAlignIcon(align)}
</button>
`).join('')}
</div>
</div>
</div>
</div>
` : ''}
`;
}
renderSpacingInputs(property, value) {
// Parse CSS shorthand value (e.g., "10px 20px 10px 20px" or "10px")
const values = this.parseSpacing(value);
const labels = ['T', 'R', 'B', 'L'];
return labels.map((label, i) => `
<input type="text" class="input input-bordered input-xs text-center"
placeholder="${label}"
value="${values[i] || ''}"
title="${['Top', 'Right', 'Bottom', 'Left'][i]}"
data-spacing="${property}"
data-index="${i}"
onchange="propertyPanel.updateSpacing('${property}', ${i}, this.value)" />
`).join('');
}
parseSpacing(value) {
if (!value) return ['', '', '', ''];
const parts = value.split(/\s+/);
switch (parts.length) {
case 1: return [parts[0], parts[0], parts[0], parts[0]];
case 2: return [parts[0], parts[1], parts[0], parts[1]];
case 3: return [parts[0], parts[1], parts[2], parts[1]];
case 4: return parts;
default: return ['', '', '', ''];
}
}
getAlignIcon(align) {
const icons = {
left: '<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h10M4 18h14"/></svg>',
center: '<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M7 12h10M5 18h14"/></svg>',
right: '<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M10 12h10M6 18h14"/></svg>',
justify: '<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>'
};
return icons[align] || '';
}
updateProperty(path, value) {
if (!this.currentBlock) return;
// Handle nested properties like "boxStyle.width"
const parts = path.split('.');
if (parts.length === 2 && parts[0] === 'boxStyle') {
if (!this.currentBlock.boxStyle) {
this.currentBlock.boxStyle = {};
}
this.currentBlock.boxStyle[parts[1]] = value || undefined;
// Clean up empty values
if (!value) delete this.currentBlock.boxStyle[parts[1]];
} else {
this.currentBlock[path] = value || undefined;
if (!value) delete this.currentBlock[path];
}
if (this.onUpdate) {
this.onUpdate(this.currentBlock.id, this.currentBlock);
}
}
updateSpacing(property, index, value) {
if (!this.currentBlock) return;
if (!this.currentBlock.boxStyle) {
this.currentBlock.boxStyle = {};
}
const current = this.parseSpacing(this.currentBlock.boxStyle[property]);
current[index] = value;
// Build CSS shorthand
const allSame = current.every(v => v === current[0]);
const tbSame = current[0] === current[2];
const lrSame = current[1] === current[3];
let newValue;
if (allSame) {
newValue = current[0] || '';
} else if (tbSame && lrSame) {
newValue = `${current[0] || '0'} ${current[1] || '0'}`;
} else {
newValue = current.map(v => v || '0').join(' ');
}
this.currentBlock.boxStyle[property] = newValue || undefined;
if (!newValue) delete this.currentBlock.boxStyle[property];
if (this.onUpdate) {
this.onUpdate(this.currentBlock.id, this.currentBlock);
}
}
}
// Export
window.PropertyPanel = PropertyPanel;
Add rich text formatting and block styling to editor
Pitch decks to help promote Skyscape Apps