property-panel.js
js
/**
 * 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;
72f0edf

Add rich text formatting and block styling to editor

Connor McCutcheon
@connor
0 stars

Pitch decks to help promote Skyscape Apps

Sign in to comment Sign In