feat: enhance card customization options and UI layout in the editor

This commit is contained in:
Ross
2026-01-04 21:50:37 +00:00
parent cda15991e3
commit de9ed94dbf
3 changed files with 543 additions and 150 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ A standalone web application for creating and customizing Yoto card covers. This
- **Multiple Templates**: Classic, Frozen, Minimal, and Vintage cover styles
- **Title Styles**: Classic, Large, Small, Folded, and Condensed title effects
- **Color Customization**: Customize accent colors and title colors
- **Full Bleed Option**: Choose whether images cover the full card background
- **Full Cover Image (Full Bleed) Option**: Choose whether images cover the full card background
- **Title Shadows**: Add depth with optional title shadows
### Card Variations
+301 -41
View File
@@ -36,6 +36,15 @@ class YotoCoversApp {
fontFamily: 'Comic Sans MS',
fontWeight: 'bold',
footerColor: '#ff6b6b'
},
polaroid: {
backgroundStyle: 'polaroid',
fontFamily: 'Arial',
fontWeight: 'normal',
footerColor: '#666666',
fullBleed: false,
showFooter: false,
imagePosition: 'center'
}
};
@@ -58,7 +67,6 @@ class YotoCoversApp {
this.cardVariations = [];
this.activeCardIndex = -1; // Currently selected card for editing
this.globalTitleFontSize = 20; // Global title font size in pixels
this.init();
}
@@ -208,9 +216,26 @@ class YotoCoversApp {
this.updateActiveCard();
});
document.getElementById('card-full-bleed').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-show-accent').addEventListener('change', (e) => {
const accentGroup = document.getElementById('accent-color-group');
accentGroup.style.display = e.target.checked ? 'block' : 'none';
this.updateActiveCard();
});
document.getElementById('card-title-shadow').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-show-title').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-show-footer').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-show-title').addEventListener('change', () => {
this.updateActiveCard();
this.updateSectionVisibility();
});
document.getElementById('card-show-footer').addEventListener('change', () => {
this.updateActiveCard();
this.updateSectionVisibility();
});
document.getElementById('card-title-font-size').addEventListener('input', () => this.updateActiveCard());
document.getElementById('card-title-x').addEventListener('input', () => this.updateActiveCard());
document.getElementById('card-title-y').addEventListener('input', () => this.updateActiveCard());
document.getElementById('card-title-draggable').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-image-fit').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-image-position').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-image-select').addEventListener('change', () => this.updateActiveCard());
// Title style selection auto-update
@@ -223,10 +248,10 @@ class YotoCoversApp {
});
// Global title font size
document.getElementById('global-title-font-size').addEventListener('input', (e) => {
this.globalTitleFontSize = parseInt(e.target.value);
this.generatePreview();
});
// document.getElementById('global-title-font-size').addEventListener('input', (e) => {
// this.globalTitleFontSize = parseInt(e.target.value);
// this.generatePreview();
// });
// Mobile toggles
document.getElementById('toggle-settings').addEventListener('click', () => {
@@ -238,6 +263,14 @@ class YotoCoversApp {
const panel = document.querySelector('.edit-panel');
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
});
// Section header click handlers for collapsing/expanding
document.querySelectorAll('.config-section h4').forEach(header => {
header.addEventListener('click', () => {
const section = header.parentElement;
section.classList.toggle('collapsed');
});
});
}
async handleImageUpload(files) {
@@ -329,6 +362,17 @@ class YotoCoversApp {
document.getElementById('card-footer-color').value = preset.footerColor;
document.getElementById('card-footer-color-text').value = preset.footerColor;
// Set additional properties if defined in preset
if (preset.fullBleed !== undefined) {
document.getElementById('card-full-bleed').checked = preset.fullBleed;
}
if (preset.showFooter !== undefined) {
document.getElementById('card-show-footer').checked = preset.showFooter;
}
if (preset.imagePosition !== undefined) {
document.getElementById('card-image-position').value = preset.imagePosition;
}
this.updateActiveCard();
}
@@ -341,13 +385,16 @@ class YotoCoversApp {
fontFamily: 'Arial',
fontWeight: 'normal',
footerColor: '#111111',
titleStyle: 'folded',
titleStyle: 'classic',
accentColor: '#f1c40f',
showAccent: false,
titleColor: '#111111',
fullBleed: true,
titleShadow: false,
showTitle: true,
showFooter: true
showFooter: true,
imageFit: 'cover',
imagePosition: 'center'
};
this.cardVariations.push(variation);
this.updateCardList();
@@ -451,10 +498,17 @@ class YotoCoversApp {
// Set template preset (detect which preset matches current features)
let matchingPreset = '';
for (const [presetName, preset] of Object.entries(this.templates)) {
if (variation.backgroundStyle === preset.backgroundStyle &&
let matches = variation.backgroundStyle === preset.backgroundStyle &&
variation.fontFamily === preset.fontFamily &&
variation.fontWeight === preset.fontWeight &&
variation.footerColor === preset.footerColor) {
variation.footerColor === preset.footerColor;
// Check additional properties if defined in preset
if (preset.fullBleed !== undefined && variation.fullBleed !== preset.fullBleed) matches = false;
if (preset.showFooter !== undefined && variation.showFooter !== preset.showFooter) matches = false;
if (preset.imagePosition !== undefined && variation.imagePosition !== preset.imagePosition) matches = false;
if (matches) {
matchingPreset = presetName;
break;
}
@@ -476,12 +530,15 @@ class YotoCoversApp {
// Set title style
document.querySelectorAll('.title-style-option').forEach(option => {
option.classList.toggle('selected', option.dataset.style === (variation.titleStyle || 'folded'));
option.classList.toggle('selected', option.dataset.style === (variation.titleStyle || 'classic'));
});
// Set colors
document.getElementById('card-accent-color').value = variation.accentColor || '#f1c40f';
document.getElementById('card-accent-color-text').value = variation.accentColor || '#f1c40f';
document.getElementById('card-show-accent').checked = variation.showAccent === true;
const accentGroup = document.getElementById('accent-color-group');
accentGroup.style.display = variation.showAccent ? 'block' : 'none';
document.getElementById('card-title-color').value = variation.titleColor || '#111111';
document.getElementById('card-title-color-text').value = variation.titleColor || '#111111';
@@ -491,6 +548,16 @@ class YotoCoversApp {
document.getElementById('card-show-title').checked = variation.showTitle !== false;
document.getElementById('card-show-footer').checked = variation.showFooter !== false;
// Set title options
document.getElementById('card-title-font-size').value = variation.titleFontSize || 20;
document.getElementById('card-title-x').value = variation.titleX !== undefined ? variation.titleX : 50;
document.getElementById('card-title-y').value = variation.titleY !== undefined ? variation.titleY : 20;
document.getElementById('card-title-draggable').checked = variation.titleDraggable === true;
// Set image options
document.getElementById('card-image-fit').value = variation.imageFit || 'cover';
document.getElementById('card-image-position').value = variation.imagePosition || 'center';
// Populate image options
const imageSelect = document.getElementById('card-image-select');
imageSelect.innerHTML = '<option value="-1">Use default image</option>';
@@ -503,6 +570,158 @@ class YotoCoversApp {
});
editor.style.display = 'block';
// Update section visibility based on show/hide toggles
this.updateSectionVisibility();
}
updateSectionVisibility() {
const showTitle = document.getElementById('card-show-title').checked;
const showFooter = document.getElementById('card-show-footer').checked;
const titleSection = document.getElementById('title-section');
const footerSection = document.getElementById('footer-section');
if (showTitle) {
titleSection.classList.remove('collapsed');
} else {
titleSection.classList.add('collapsed');
}
if (showFooter) {
footerSection.classList.remove('collapsed');
} else {
footerSection.classList.add('collapsed');
}
}
initDragFunctionality() {
console.log('initDragFunctionality called');
// This will be called when the preview iframe loads
const iframe = document.getElementById('preview-iframe');
if (!iframe) {
console.log('No iframe found');
return;
}
const initializeDrag = () => {
console.log('initializeDrag called');
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
if (!iframeDoc) {
console.log('No iframe document');
return;
}
const titles = iframeDoc.querySelectorAll('.title');
console.log('Found titles:', titles.length);
titles.forEach((title, index) => {
console.log(`Title ${index} cursor:`, title.style.cursor);
// Only make draggable if cursor is set to 'move'
if (title.style.cursor !== 'move') {
console.log(`Title ${index} not draggable, skipping`);
return;
}
let isDragging = false;
let startX, startY, initialLeft, initialTop;
title.addEventListener('mousedown', (e) => {
console.log('Title mousedown fired');
if (title.style.cursor !== 'move') return;
isDragging = true;
// Get iframe position and convert mouse coordinates to iframe-relative
const iframeRect = iframe.getBoundingClientRect();
startX = e.clientX - iframeRect.left;
startY = e.clientY - iframeRect.top;
// Store initial position
initialLeft = parseFloat(title.style.left) || 50;
initialTop = parseFloat(title.style.top) || 20;
title.style.zIndex = '1000';
title.style.userSelect = 'none';
e.preventDefault();
});
const handleMouseMove = (e) => {
if (!isDragging) return;
// Get iframe position in main document
const iframeRect = iframe.getBoundingClientRect();
// Convert mouse coordinates to be relative to iframe viewport
const mouseX = e.clientX - iframeRect.left;
const mouseY = e.clientY - iframeRect.top;
const card = title.closest('.card');
const cardRect = card.getBoundingClientRect();
// Convert card position to be relative to iframe
const cardLeftRelative = cardRect.left - iframeRect.left;
const cardTopRelative = cardRect.top - iframeRect.top;
// Calculate mouse position relative to the card (within iframe)
const relativeMouseX = mouseX - cardLeftRelative;
const relativeMouseY = mouseY - cardTopRelative;
// Convert to percentage
const newLeft = Math.max(0, Math.min(100, (relativeMouseX / cardRect.width) * 100));
const newTop = Math.max(0, Math.min(100, (relativeMouseY / cardRect.height) * 100));
console.log('Drag debug:', { mouseX, mouseY, iframeRect, cardRect, cardLeftRelative, cardTopRelative, relativeMouseX, relativeMouseY, newLeft, newTop });
title.style.left = `${newLeft}%`;
title.style.top = `${newTop}%`;
console.log('Title style set to:', title.style.left, title.style.top);
// Update the form values if this card is currently selected
const cardIndex = parseInt(card.getAttribute('data-card-index'));
if (cardIndex === this.activeCardIndex && this.cardVariations[cardIndex]) {
document.getElementById('card-title-x').value = Math.round(newLeft);
document.getElementById('card-title-y').value = Math.round(newTop);
// Update the variation data
this.cardVariations[cardIndex].titleX = Math.round(newLeft);
this.cardVariations[cardIndex].titleY = Math.round(newTop);
}
};
const handleMouseUp = () => {
if (isDragging) {
isDragging = false;
title.style.zIndex = '';
title.style.userSelect = '';
}
};
// Add event listeners to both iframe document and main document
// to handle mouse events that might occur outside the iframe
iframeDoc.addEventListener('mousemove', handleMouseMove);
iframeDoc.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
});
} catch (e) {
// Ignore cross-origin errors
console.log('Drag functionality initialization failed:', e);
}
};
// Try to initialize immediately if the iframe is already loaded
if (iframe.contentDocument && (iframe.contentDocument.readyState === 'complete' || iframe.contentDocument.readyState === 'interactive')) {
console.log('Iframe already loaded, initializing immediately');
initializeDrag();
} else {
console.log('Setting iframe onload');
iframe.onload = () => {
console.log('iframe onload fired');
initializeDrag();
};
}
}
updateActiveCard() {
@@ -515,13 +734,20 @@ class YotoCoversApp {
variation.footerColor = document.getElementById('card-footer-color').value;
variation.title = document.getElementById('card-title-input').value;
variation.footer = document.getElementById('card-footer-input').value;
variation.titleStyle = document.querySelector('.title-style-option.selected')?.dataset.style || 'folded';
variation.titleStyle = document.querySelector('.title-style-option.selected')?.dataset.style || 'classic';
variation.accentColor = document.getElementById('card-accent-color').value;
variation.showAccent = document.getElementById('card-show-accent').checked;
variation.titleColor = document.getElementById('card-title-color').value;
variation.fullBleed = document.getElementById('card-full-bleed').checked;
variation.titleShadow = document.getElementById('card-title-shadow').checked;
variation.showTitle = document.getElementById('card-show-title').checked;
variation.showFooter = document.getElementById('card-show-footer').checked;
variation.titleFontSize = parseInt(document.getElementById('card-title-font-size').value);
variation.titleX = parseInt(document.getElementById('card-title-x').value);
variation.titleY = parseInt(document.getElementById('card-title-y').value);
variation.titleDraggable = document.getElementById('card-title-draggable').checked;
variation.imageFit = document.getElementById('card-image-fit').value;
variation.imagePosition = document.getElementById('card-image-position').value;
variation.imageIndex = parseInt(document.getElementById('card-image-select').value);
this.updateCardList();
@@ -566,7 +792,7 @@ class YotoCoversApp {
// Apply zoom after iframe loads
iframe.onload = () => {
this.applyZoomToPreview();
this.initDragFunctionality();
};
// Clean up the previous blob URL
@@ -604,6 +830,8 @@ class YotoCoversApp {
if (i < totalCards) {
let imageIndex, cardTitle, cardFooter, cardTitleStyle, cardAccentColor, cardTitleColor, cardFullBleed, cardTitleShadow;
let cardBackgroundStyle, cardFontFamily, cardFontWeight, cardFooterColor, cardShowTitle, cardShowFooter;
let cardImageFit, cardImagePosition, cardTitleFontSize, cardTitleX, cardTitleY, cardTitleDraggable;
let cardShowAccent;
if (this.cardVariations.length > 0) {
// Use variation settings
@@ -611,8 +839,9 @@ class YotoCoversApp {
imageIndex = variation && variation.imageIndex >= 0 ? variation.imageIndex : 0;
cardTitle = (variation && variation.title) || `Card ${i + 1}`;
cardFooter = (variation && variation.footer) || '';
cardTitleStyle = (variation && variation.titleStyle) || 'folded';
cardTitleStyle = (variation && variation.titleStyle) || 'classic';
cardAccentColor = (variation && variation.accentColor) || '#f1c40f';
cardShowAccent = variation && variation.showAccent !== undefined ? variation.showAccent : false;
cardTitleColor = (variation && variation.titleColor) || '#111111';
cardFullBleed = variation && variation.fullBleed !== undefined ? variation.fullBleed : true;
cardTitleShadow = variation && variation.titleShadow !== undefined ? variation.titleShadow : false;
@@ -623,13 +852,20 @@ class YotoCoversApp {
cardFooterColor = (variation && variation.footerColor) || '#111111';
cardShowTitle = variation && variation.showTitle !== undefined ? variation.showTitle : true;
cardShowFooter = variation && variation.showFooter !== undefined ? variation.showFooter : true;
cardImageFit = (variation && variation.imageFit) || 'cover';
cardImagePosition = (variation && variation.imagePosition) || 'center';
cardTitleFontSize = (variation && variation.titleFontSize) || 20;
cardTitleX = variation && variation.titleX !== undefined ? variation.titleX : 50;
cardTitleY = variation && variation.titleY !== undefined ? variation.titleY : 20;
cardTitleDraggable = variation && variation.titleDraggable !== undefined ? variation.titleDraggable : false;
} else {
// No variations - auto-assign images
imageIndex = i < this.images.length ? i : 0;
cardTitle = `Card ${i + 1}`;
cardFooter = '';
cardTitleStyle = 'folded';
cardTitleStyle = 'classic';
cardAccentColor = '#f1c40f';
cardShowAccent = false;
cardTitleColor = '#111111';
cardFullBleed = true;
cardTitleShadow = false;
@@ -640,6 +876,12 @@ class YotoCoversApp {
cardFooterColor = '#111111';
cardShowTitle = true;
cardShowFooter = true;
cardImageFit = 'cover';
cardImagePosition = 'center';
cardTitleFontSize = 20;
cardTitleX = 50;
cardTitleY = 20;
cardTitleDraggable = false;
}
const imageUrl = this.images.length > 0 ? this.images[imageIndex].dataUrl : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
@@ -654,11 +896,18 @@ class YotoCoversApp {
footerColor: cardFooterColor,
titleStyle: cardTitleStyle,
accentColor: cardAccentColor,
showAccent: cardShowAccent,
titleColor: cardTitleColor,
fullBleed: cardFullBleed,
titleShadow: cardTitleShadow,
showTitle: cardShowTitle,
showFooter: cardShowFooter,
imageFit: cardImageFit,
imagePosition: cardImagePosition,
titleFontSize: cardTitleFontSize,
titleX: cardTitleX,
titleY: cardTitleY,
titleDraggable: cardTitleDraggable,
isVisible: true
});
} else {
@@ -673,11 +922,18 @@ class YotoCoversApp {
footerColor: '#111111',
titleStyle: 'folded',
accentColor: '#f1c40f',
showAccent: false,
titleColor: '#111111',
fullBleed: true,
titleShadow: false,
showTitle: false,
showFooter: false,
imageFit: 'cover',
imagePosition: 'center',
titleFontSize: 20,
titleX: 50,
titleY: 20,
titleDraggable: false,
isVisible: false
});
}
@@ -719,6 +975,7 @@ class YotoCoversApp {
${pageCards.map((card, index) => `
<div class="card card-${startIndex + index} ${card.isVisible ? '' : 'empty-card'}"
data-card-index="${startIndex + index}"
data-background-style="${card.backgroundStyle}"
onclick="if (window.parent && window.parent.selectCardFromPreview) { window.parent.selectCardFromPreview(${startIndex + index}); }"
style="cursor: pointer;
width: ${cardWidthMm}mm;
@@ -727,9 +984,11 @@ class YotoCoversApp {
font-family: 'DejaVuSans', serif;
${card.isVisible && card.backgroundStyle === 'gradient' ? 'background: linear-gradient(135deg, #e8f4fd 0%, #d1e8f0 100%);' : ''}
${card.isVisible && card.backgroundStyle === 'pattern' ? 'background: linear-gradient(45deg, #f5f5dc 0%, #deb887 100%);' : ''}
${card.isVisible && card.backgroundStyle === 'comic' ? 'background: radial-gradient(circle at 30% 40%, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4, #ffeaa7);' : ''}
${card.isVisible && card.backgroundStyle === 'comic' ? 'background: repeating-linear-gradient(45deg, #fff 0px, #fff 2px, #000 2px, #000 4px), radial-gradient(circle at 20% 30%, #ff4444, #ff8844, #ffff44, #44ff44, #4444ff, #8844ff); box-shadow: inset 0 0 20px rgba(0,0,0,0.3); border: 3px solid #000;' : ''}
${card.isVisible && card.backgroundStyle === 'polaroid' ? 'border: 8px solid #fff; border-bottom: 24px solid #fff; box-shadow: 0 4px 12px rgba(0,0,0,0.3); position: relative;' : ''}
${card.isVisible && card.backgroundStyle === 'polaroid' && !card.fullBleed ? 'background: #f8f9fa;' : ''}
${card.isVisible && card.backgroundStyle === 'solid' ? 'background: #f8f9fa;' : ''}
${card.isVisible && card.fullBleed ? `background: url('${card.imageUrl}') no-repeat center/cover;` : ''}
${card.isVisible && card.fullBleed ? `background: url('${card.imageUrl}') no-repeat ${card.imagePosition}/${card.imageFit};` : ''}
position: relative;
border: ${card.isVisible ? 'none' : '1px dashed #ccc'};
overflow: hidden;
@@ -739,11 +998,16 @@ class YotoCoversApp {
color: ${card.titleColor};
font-weight: ${card.fontWeight === 'bold' ? '700' : '400'};
font-family: '${card.fontFamily}', serif;
font-size: calc(${this.globalTitleFontSize}px + 0.5rem);
font-size: ${card.titleFontSize}px;
${card.titleShadow ? 'text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);' : ''}
position: absolute;
left: ${card.titleX}%;
top: ${card.titleY}%;
transform: translate(-50%, -50%);
${card.titleDraggable ? 'cursor: move; user-select: none;' : ''}
">${this.escapeHtml(card.title)}</div>` : ''}
${!card.fullBleed ? `<div class="hero"><img src="${card.imageUrl}" alt="cover"/></div>` : ''}
<div class="overlay" style="background: ${card.accentColor}40;"></div>
${!card.fullBleed ? `<div class="hero"><img src="${card.imageUrl}" alt="cover" style="object-fit: ${card.imageFit}; object-position: ${card.imagePosition};"/></div>` : ''}
${card.showAccent ? `<div class="overlay" style="background: ${card.accentColor}40;"></div>` : `<div class="overlay" style="background: rgba(0,0,0,0);"></div>`}
${card.showFooter ? `<div class="footer" style="
color: ${card.footerColor};
font-family: '${card.fontFamily}', serif;
@@ -810,34 +1074,17 @@ class YotoCoversApp {
.title.classic {
/* Default classic style - already defined above */
}
.title.large {
font-size: calc(120px + 0.5rem);
font-weight: 900;
}
.title.small {
font-size: calc(80px + 0.5rem);
font-weight: 700;
}
.title.condensed {
font-size: calc(96px + 0.5rem);
font-weight: 900;
letter-spacing: -2px;
font-stretch: condensed;
}
.title.italic {
font-style: italic;
}
.title.bold {
font-weight: 900;
}
.title.outlined {
-webkit-text-stroke: 2px white;
text-stroke: 2px white;
color: transparent;
}
.title.shadow {
text-shadow: 2px 2px 4px rgba(0,0,0,0.7);
}
.title.folded {
position: absolute;
left: 6%;
@@ -885,6 +1132,14 @@ class YotoCoversApp {
align-items: center;
justify-content: center;
}
${cards.some(card => card.backgroundStyle === 'polaroid') ? `
.card[data-background-style="polaroid"] .hero {
top: 12%;
left: 8%;
right: 8%;
bottom: 25%;
}
` : ''}
.hero img {
width: 100%;
height: 100%;
@@ -901,6 +1156,14 @@ class YotoCoversApp {
pointer-events: none;
background: rgba(0,0,0,0);
}
${cards.some(card => card.backgroundStyle === 'polaroid') ? `
.card[data-background-style="polaroid"] .overlay {
top: 12%;
left: 8%;
right: 8%;
bottom: 25%;
}
` : ''}
.footer {
position: absolute;
bottom: 4%;
@@ -988,9 +1251,6 @@ class YotoCoversApp {
document.getElementById('cards-per-column').value = this.printLayout.cardsPerColumn;
document.getElementById('page-margin').value = this.printLayout.pageMargin;
document.getElementById('card-spacing').value = this.printLayout.cardSpacing;
// Initialize global title font size
document.getElementById('global-title-font-size').value = this.globalTitleFontSize;
}
}
+241 -108
View File
@@ -445,10 +445,69 @@
border: 1px solid #f5c6cb;
}
.status-info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
.config-section {
margin-bottom: 20px;
border: 1px solid #e0e0e0;
border-radius: 6px;
overflow: hidden;
}
.config-section h4 {
margin: 0;
padding: 12px 15px;
background: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
font-size: 14px;
font-weight: 600;
color: #2c3e50;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.config-section h4:hover {
background: #e9ecef;
}
.config-section h4::after {
content: "▼";
font-size: 12px;
transition: transform 0.3s;
}
.config-section.collapsed h4::after {
transform: rotate(-90deg);
}
.section-controls {
padding: 10px 15px;
background: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
}
.section-content {
padding: 15px;
}
.position-controls {
display: flex;
gap: 15px;
align-items: end;
}
.form-group.inline {
flex: 1;
margin-bottom: 0;
}
.form-group.inline label {
font-size: 12px;
margin-bottom: 2px;
}
.form-group.inline input {
width: 100%;
}
.loading {
@@ -607,127 +666,201 @@
<div class="section">
<h3 id="card-editor-title">Edit Card</h3>
<div class="form-group">
<label for="card-template-preset">Template Preset</label>
<select id="card-template-preset">
<option value="">Custom</option>
<option value="classic">Classic</option>
<option value="frozen">Frozen</option>
<option value="minimal">Minimal</option>
<option value="vintage">Vintage</option>
<option value="comic">Comic</option>
</select>
</div>
<!-- Background/Style Section -->
<div class="config-section">
<h4>Style</h4>
<div class="section-content">
<div class="form-group">
<label for="card-template-preset">Template Preset</label>
<select id="card-template-preset">
<option value="">Custom</option>
<option value="classic">Classic</option>
<option value="frozen">Frozen</option>
<option value="minimal">Minimal</option>
<option value="vintage">Vintage</option>
<option value="comic">Comic</option>
<option value="polaroid">Polaroid</option>
</select>
</div>
<div class="form-group">
<label for="card-background-style">Background Style</label>
<select id="card-background-style">
<option value="solid">Solid</option>
<option value="gradient">Gradient</option>
<option value="pattern">Pattern</option>
<option value="comic">Comic</option>
</select>
</div>
<div class="form-group">
<label for="card-background-style">Background Style</label>
<select id="card-background-style">
<option value="solid">Solid</option>
<option value="gradient">Gradient</option>
<option value="pattern">Pattern</option>
<option value="comic">Comic</option>
<option value="polaroid">Polaroid</option>
</select>
</div>
<div class="form-group">
<label for="card-font-family">Font Family</label>
<select id="card-font-family">
<option value="Arial">Arial</option>
<option value="Georgia">Georgia</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Comic Sans MS">Comic Sans MS</option>
<option value="Impact">Impact</option>
<option value="Courier New">Courier New</option>
</select>
</div>
<div class="form-group">
<label for="card-font-family">Font Family</label>
<select id="card-font-family">
<option value="Arial">Arial</option>
<option value="Georgia">Georgia</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Comic Sans MS">Comic Sans MS</option>
<option value="Impact">Impact</option>
<option value="Courier New">Courier New</option>
</select>
</div>
<div class="form-group">
<label for="card-font-weight">Font Weight</label>
<select id="card-font-weight">
<option value="normal">Normal</option>
<option value="bold">Bold</option>
</select>
</div>
<div class="form-group">
<label for="card-footer-color">Footer Color</label>
<div class="color-picker">
<input type="color" id="card-footer-color" value="#111111" class="color-input">
<input type="text" id="card-footer-color-text" value="#111111" class="color-value" readonly>
<div class="form-group">
<label for="card-font-weight">Font Weight</label>
<select id="card-font-weight">
<option value="normal">Normal</option>
<option value="bold">Bold</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<label for="card-title-input">Title</label>
<input type="text" id="card-title-input" placeholder="Enter card title">
<div class="checkbox-group inline">
<input type="checkbox" id="card-show-title" checked>
<label for="card-show-title">Show title</label>
<!-- Title Section -->
<div class="config-section" id="title-section">
<h4>Title</h4>
<div class="section-controls">
<div class="checkbox-group">
<input type="checkbox" id="card-show-title" checked>
<label for="card-show-title">Show title</label>
</div>
</div>
<div class="section-content">
<div class="form-group">
<label for="card-title-input">Title Text</label>
<input type="text" id="card-title-input" placeholder="Enter card title">
</div>
<div class="form-group">
<label>Title Style</label>
<div class="title-style-grid">
<div class="title-style-option" data-style="folded">Folded</div>
<div class="title-style-option" data-style="condensed">Condensed</div>
<div class="title-style-option" data-style="outlined">Outlined</div>
<div class="title-style-option selected" data-style="classic">Classic</div>
</div>
</div>
<div class="form-group">
<label for="card-title-color">Title Color</label>
<div class="color-picker">
<input type="color" id="card-title-color" value="#111111" class="color-input">
<input type="text" id="card-title-color-text" value="#111111" class="color-value" readonly>
</div>
</div>
<div class="checkbox-group">
<input type="checkbox" id="card-title-shadow">
<label for="card-title-shadow">Title shadow</label>
</div>
<div class="form-group">
<label for="card-title-font-size">Title Font Size (px)</label>
<input type="number" id="card-title-font-size" value="20" min="10" max="200" step="5">
</div>
<div class="form-group">
<label>Title Position</label>
<div class="position-controls">
<div class="form-group inline">
<label for="card-title-x">X (%)</label>
<input type="number" id="card-title-x" value="50" min="0" max="100" step="1">
</div>
<div class="form-group inline">
<label for="card-title-y">Y (%)</label>
<input type="number" id="card-title-y" value="20" min="0" max="100" step="1">
</div>
</div>
<div class="checkbox-group">
<input type="checkbox" id="card-title-draggable">
<label for="card-title-draggable">Enable drag to reposition</label>
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="card-footer-input">Footer</label>
<input type="text" id="card-footer-input" placeholder="Enter footer text">
<div class="checkbox-group inline">
<input type="checkbox" id="card-show-footer" checked>
<label for="card-show-footer">Show footer</label>
<!-- Image Section -->
<div class="config-section" id="image-section">
<h4>Image</h4>
<div class="section-content">
<div class="form-group">
<label>Image</label>
<select id="card-image-select">
<option value="-1">Use default image</option>
</select>
</div>
<div class="form-group">
<label for="card-image-fit">Image Fit</label>
<select id="card-image-fit">
<option value="cover">Cover (fill area)</option>
<option value="contain">Contain (show all)</option>
<option value="fill">Fill (stretch)</option>
<option value="none">None (original size)</option>
<option value="scale-down">Scale Down</option>
</select>
</div>
<div class="form-group">
<label for="card-image-position">Image Position</label>
<select id="card-image-position">
<option value="center">Center</option>
<option value="top">Top</option>
<option value="bottom">Bottom</option>
<option value="left">Left</option>
<option value="right">Right</option>
<option value="top left">Top Left</option>
<option value="top right">Top Right</option>
<option value="bottom left">Bottom Left</option>
<option value="bottom right">Bottom Right</option>
</select>
</div>
<div class="checkbox-group">
<input type="checkbox" id="card-full-bleed" checked>
<label for="card-full-bleed">Full cover image (full bleed)</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="card-show-accent">
<label for="card-show-accent">Show accent overlay</label>
</div>
<div class="form-group" id="accent-color-group" style="display: none;">
<label for="card-accent-color">Accent Color</label>
<div class="color-picker">
<input type="color" id="card-accent-color" value="#f1c40f" class="color-input">
<input type="text" id="card-accent-color-text" value="#f1c40f" class="color-value" readonly>
</div>
</div>
</div>
</div>
<div class="form-group">
<label>Title Style</label>
<div class="title-style-grid">
<div class="title-style-option" data-style="classic">Classic</div>
<div class="title-style-option" data-style="large">Large</div>
<div class="title-style-option" data-style="small">Small</div>
<div class="title-style-option selected" data-style="folded">Folded</div>
<div class="title-style-option" data-style="condensed">Condensed</div>
<div class="title-style-option" data-style="italic">Italic</div>
<div class="title-style-option" data-style="bold">Bold</div>
<div class="title-style-option" data-style="outlined">Outlined</div>
<div class="title-style-option" data-style="shadow">Shadow</div>
<!-- Footer Section -->
<div class="config-section" id="footer-section">
<h4>Footer</h4>
<div class="section-controls">
<div class="checkbox-group">
<input type="checkbox" id="card-show-footer" checked>
<label for="card-show-footer">Show footer</label>
</div>
</div>
</div>
<div class="section-content">
<div class="form-group">
<label for="card-footer-input">Footer Text</label>
<input type="text" id="card-footer-input" placeholder="Enter footer text">
</div>
<div class="form-group">
<label for="global-title-font-size">Title Font Size (px)</label>
<input type="number" id="global-title-font-size" value="20" min="10" max="200" step="5">
</div>
<div class="form-group">
<label for="card-accent-color">Accent Color</label>
<div class="color-picker">
<input type="color" id="card-accent-color" value="#f1c40f" class="color-input">
<input type="text" id="card-accent-color-text" value="#f1c40f" class="color-value" readonly>
<div class="form-group">
<label for="card-footer-color">Footer Color</label>
<div class="color-picker">
<input type="color" id="card-footer-color" value="#111111" class="color-input">
<input type="text" id="card-footer-color-text" value="#111111" class="color-value" readonly>
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="card-title-color">Title Color</label>
<div class="color-picker">
<input type="color" id="card-title-color" value="#111111" class="color-input">
<input type="text" id="card-title-color-text" value="#111111" class="color-value" readonly>
</div>
</div>
<div class="checkbox-group">
<input type="checkbox" id="card-full-bleed" checked>
<label for="card-full-bleed">Full bleed background</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="card-title-shadow">
<label for="card-title-shadow">Title shadow</label>
</div>
<div class="form-group">
<label>Image</label>
<select id="card-image-select">
<option value="-1">Use default image</option>
</select>
</div>
<div class="btn-group" style="margin-top: 15px;">
<button class="btn btn-primary" id="update-card">Update Card</button>
<button class="btn btn-danger" id="delete-card">Delete Card</button>