Add badge price tag controls: position, size, ribbon & digit colors
Adds four new controls to the digital-badge price tag so users can fine -tune the look: a vertical position slider (raises/lowers the band within the circle), a size slider (60-160% of the default band scale), and two color rows for the ribbon and the digits — each with six preset swatches plus a native color picker for custom colors. Per-item state (priceY, priceSize, priceBg, priceFg) is preserved across selections and renders consistently in the 240x240 export. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4214,6 +4214,44 @@ select {
|
|||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-color-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-color-swatch {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0,0,0,0.1) inset;
|
||||||
|
transition: transform 0.12s ease, border-color 0.12s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-color-swatch:hover {
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-color-swatch.active {
|
||||||
|
border-color: #007AFF;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-color-row input[type="color"] {
|
||||||
|
width: 36px;
|
||||||
|
height: 30px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.badge-actions {
|
.badge-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -638,6 +638,36 @@ https://live.staticflickr.com/65535/12345678901_abcdef1234_b.jpg"></textarea>
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Положение по вертикали: <span id="badge-price-y-value">0</span></label>
|
||||||
|
<input type="range" id="badge-price-y" min="-50" max="20" step="1" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Размер плашки: <span id="badge-price-size-value">100%</span></label>
|
||||||
|
<input type="range" id="badge-price-size" min="60" max="160" step="5" value="100">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Цвет ленты:</label>
|
||||||
|
<div class="badge-color-row" data-target="bg">
|
||||||
|
<button type="button" class="badge-color-swatch" data-color="#FFCC00" style="background:#FFCC00" title="Жёлтый"></button>
|
||||||
|
<button type="button" class="badge-color-swatch" data-color="#FF3B30" style="background:#FF3B30" title="Красный"></button>
|
||||||
|
<button type="button" class="badge-color-swatch" data-color="#34C759" style="background:#34C759" title="Зелёный"></button>
|
||||||
|
<button type="button" class="badge-color-swatch" data-color="#007AFF" style="background:#007AFF" title="Синий"></button>
|
||||||
|
<button type="button" class="badge-color-swatch" data-color="#000000" style="background:#000000" title="Чёрный"></button>
|
||||||
|
<button type="button" class="badge-color-swatch" data-color="#FFFFFF" style="background:#FFFFFF;border-color:#ccc" title="Белый"></button>
|
||||||
|
<input type="color" id="badge-price-bg" value="#FFCC00" title="Свой цвет ленты">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Цвет цифр:</label>
|
||||||
|
<div class="badge-color-row" data-target="fg">
|
||||||
|
<button type="button" class="badge-color-swatch" data-color="#000000" style="background:#000000" title="Чёрный"></button>
|
||||||
|
<button type="button" class="badge-color-swatch" data-color="#FFFFFF" style="background:#FFFFFF;border-color:#ccc" title="Белый"></button>
|
||||||
|
<button type="button" class="badge-color-swatch" data-color="#FFCC00" style="background:#FFCC00" title="Жёлтый"></button>
|
||||||
|
<button type="button" class="badge-color-swatch" data-color="#FF3B30" style="background:#FF3B30" title="Красный"></button>
|
||||||
|
<input type="color" id="badge-price-fg" value="#000000" title="Свой цвет цифр">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr style="border:none; border-top:1px solid var(--border-color, #ddd); margin:12px 0;">
|
<hr style="border:none; border-top:1px solid var(--border-color, #ddd); margin:12px 0;">
|
||||||
|
|||||||
@@ -4251,6 +4251,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
priceEnabled: document.getElementById('badge-price-enabled'),
|
priceEnabled: document.getElementById('badge-price-enabled'),
|
||||||
priceOptions: document.getElementById('badge-price-options'),
|
priceOptions: document.getElementById('badge-price-options'),
|
||||||
priceValue: document.getElementById('badge-price-value'),
|
priceValue: document.getElementById('badge-price-value'),
|
||||||
|
priceY: document.getElementById('badge-price-y'),
|
||||||
|
priceYValue: document.getElementById('badge-price-y-value'),
|
||||||
|
priceSize: document.getElementById('badge-price-size'),
|
||||||
|
priceSizeValue: document.getElementById('badge-price-size-value'),
|
||||||
|
priceBg: document.getElementById('badge-price-bg'),
|
||||||
|
priceFg: document.getElementById('badge-price-fg'),
|
||||||
nickEnabled: document.getElementById('badge-nickname-enabled'),
|
nickEnabled: document.getElementById('badge-nickname-enabled'),
|
||||||
nickOptions: document.getElementById('badge-nickname-options'),
|
nickOptions: document.getElementById('badge-nickname-options'),
|
||||||
nickValue: document.getElementById('badge-nickname-value'),
|
nickValue: document.getElementById('badge-nickname-value'),
|
||||||
@@ -4308,19 +4314,29 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
c.closePath();
|
c.closePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawPriceArc(c, size, text) {
|
function drawPriceArc(c, size, item, text) {
|
||||||
const yTop = size * 0.74;
|
const scale = (item.priceSize || 100) / 100;
|
||||||
const yBottom = size * 0.965;
|
const offsetY = ((item.priceY || 0) / 100) * size;
|
||||||
|
const baseTopFrac = 0.74;
|
||||||
|
const baseBottomFrac = 0.965;
|
||||||
|
const baseHeight = (baseBottomFrac - baseTopFrac) * size;
|
||||||
|
const height = baseHeight * scale;
|
||||||
|
const centerY = ((baseTopFrac + baseBottomFrac) / 2) * size + offsetY;
|
||||||
|
const yTop = centerY - height / 2;
|
||||||
|
const yBottom = centerY + height / 2;
|
||||||
|
const bg = item.priceBg || '#FFCC00';
|
||||||
|
const fg = item.priceFg || '#000000';
|
||||||
c.save();
|
c.save();
|
||||||
c.fillStyle = '#FFCC00';
|
c.fillStyle = bg;
|
||||||
c.fillRect(0, yTop, size, yBottom - yTop);
|
c.fillRect(0, yTop, size, yBottom - yTop);
|
||||||
const grad = c.createLinearGradient(0, yTop, 0, yTop + size * 0.012);
|
// Subtle inner highlight at top edge of the band.
|
||||||
grad.addColorStop(0, 'rgba(255,255,255,0.35)');
|
const hi = c.createLinearGradient(0, yTop, 0, yTop + size * 0.012);
|
||||||
grad.addColorStop(1, 'rgba(255,255,255,0)');
|
hi.addColorStop(0, 'rgba(255,255,255,0.3)');
|
||||||
c.fillStyle = grad;
|
hi.addColorStop(1, 'rgba(255,255,255,0)');
|
||||||
|
c.fillStyle = hi;
|
||||||
c.fillRect(0, yTop, size, size * 0.012);
|
c.fillRect(0, yTop, size, size * 0.012);
|
||||||
const fontSize = (yBottom - yTop) * 0.55;
|
const fontSize = (yBottom - yTop) * 0.55;
|
||||||
c.fillStyle = '#000';
|
c.fillStyle = fg;
|
||||||
c.textAlign = 'center';
|
c.textAlign = 'center';
|
||||||
c.textBaseline = 'middle';
|
c.textBaseline = 'middle';
|
||||||
c.font = `700 ${fontSize}px ${FONT_STACK}`;
|
c.font = `700 ${fontSize}px ${FONT_STACK}`;
|
||||||
@@ -4328,20 +4344,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
c.restore();
|
c.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawPriceRect(c, size, text) {
|
function drawPriceRect(c, size, item, text) {
|
||||||
const w = size * 0.78;
|
const scale = (item.priceSize || 100) / 100;
|
||||||
const h = size * 0.13;
|
const offsetY = ((item.priceY || 0) / 100) * size;
|
||||||
|
const baseW = size * 0.78;
|
||||||
|
const baseH = size * 0.13;
|
||||||
|
const w = baseW * scale;
|
||||||
|
const h = baseH * scale;
|
||||||
const x = (size - w) / 2;
|
const x = (size - w) / 2;
|
||||||
const y = size * 0.755;
|
const baseTop = size * 0.755;
|
||||||
|
const y = baseTop + offsetY + (baseH - h) / 2;
|
||||||
|
const bg = item.priceBg || '#FFCC00';
|
||||||
|
const fg = item.priceFg || '#000000';
|
||||||
c.save();
|
c.save();
|
||||||
c.fillStyle = 'rgba(0,0,0,0.22)';
|
c.fillStyle = 'rgba(0,0,0,0.22)';
|
||||||
roundRect(c, x, y + size * 0.008, w, h, h * 0.28);
|
roundRect(c, x, y + size * 0.008, w, h, h * 0.28);
|
||||||
c.fill();
|
c.fill();
|
||||||
c.fillStyle = '#FFCC00';
|
c.fillStyle = bg;
|
||||||
roundRect(c, x, y, w, h, h * 0.28);
|
roundRect(c, x, y, w, h, h * 0.28);
|
||||||
c.fill();
|
c.fill();
|
||||||
const fontSize = h * 0.62;
|
const fontSize = h * 0.62;
|
||||||
c.fillStyle = '#000';
|
c.fillStyle = fg;
|
||||||
c.textAlign = 'center';
|
c.textAlign = 'center';
|
||||||
c.textBaseline = 'middle';
|
c.textBaseline = 'middle';
|
||||||
c.font = `700 ${fontSize}px ${FONT_STACK}`;
|
c.font = `700 ${fontSize}px ${FONT_STACK}`;
|
||||||
@@ -4440,9 +4463,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
if (hasPrice) {
|
if (hasPrice) {
|
||||||
if (item.priceStyle === 'rect') {
|
if (item.priceStyle === 'rect') {
|
||||||
drawPriceRect(c, size, priceText);
|
drawPriceRect(c, size, item, priceText);
|
||||||
} else {
|
} else {
|
||||||
drawPriceArc(c, size, priceText);
|
drawPriceArc(c, size, item, priceText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4507,6 +4530,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
showPrice: false,
|
showPrice: false,
|
||||||
priceValue: '',
|
priceValue: '',
|
||||||
priceStyle: 'arc',
|
priceStyle: 'arc',
|
||||||
|
priceY: 0, // -50..+20: vertical offset from default position
|
||||||
|
priceSize: 100, // 60..160 (% scale of band height)
|
||||||
|
priceBg: '#FFCC00', // ribbon color
|
||||||
|
priceFg: '#000000', // digits color
|
||||||
showNickname: false,
|
showNickname: false,
|
||||||
nickname: '',
|
nickname: '',
|
||||||
nickColor: 'white',
|
nickColor: 'white',
|
||||||
@@ -4555,6 +4582,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
document.querySelectorAll('input[name="badge-price-style"]').forEach(r => {
|
document.querySelectorAll('input[name="badge-price-style"]').forEach(r => {
|
||||||
r.checked = r.value === item.priceStyle;
|
r.checked = r.value === item.priceStyle;
|
||||||
});
|
});
|
||||||
|
el.priceY.value = item.priceY;
|
||||||
|
el.priceYValue.textContent = item.priceY;
|
||||||
|
el.priceSize.value = item.priceSize;
|
||||||
|
el.priceSizeValue.textContent = item.priceSize + '%';
|
||||||
|
el.priceBg.value = item.priceBg;
|
||||||
|
el.priceFg.value = item.priceFg;
|
||||||
|
syncSwatches('bg', item.priceBg);
|
||||||
|
syncSwatches('fg', item.priceFg);
|
||||||
el.nickEnabled.checked = item.showNickname;
|
el.nickEnabled.checked = item.showNickname;
|
||||||
el.nickOptions.classList.toggle('hidden', !item.showNickname);
|
el.nickOptions.classList.toggle('hidden', !item.showNickname);
|
||||||
el.nickValue.value = item.nickname || badgeState.defaultNickname || '';
|
el.nickValue.value = item.nickname || badgeState.defaultNickname || '';
|
||||||
@@ -4778,6 +4813,72 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Price vertical position slider
|
||||||
|
el.priceY.addEventListener('input', () => {
|
||||||
|
const item = currentItem();
|
||||||
|
if (!item) return;
|
||||||
|
const v = parseInt(el.priceY.value, 10) || 0;
|
||||||
|
item.priceY = v;
|
||||||
|
el.priceYValue.textContent = v;
|
||||||
|
renderPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Price size slider
|
||||||
|
el.priceSize.addEventListener('input', () => {
|
||||||
|
const item = currentItem();
|
||||||
|
if (!item) return;
|
||||||
|
const v = parseInt(el.priceSize.value, 10) || 100;
|
||||||
|
item.priceSize = v;
|
||||||
|
el.priceSizeValue.textContent = v + '%';
|
||||||
|
renderPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Color: ribbon background
|
||||||
|
function normalizeHex(v) {
|
||||||
|
return (v || '').toString().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncSwatches(target, color) {
|
||||||
|
const norm = normalizeHex(color);
|
||||||
|
document.querySelectorAll(`.badge-color-row[data-target="${target}"] .badge-color-swatch`).forEach(sw => {
|
||||||
|
sw.classList.toggle('active', normalizeHex(sw.dataset.color) === norm);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
el.priceBg.addEventListener('input', () => {
|
||||||
|
const item = currentItem();
|
||||||
|
if (!item) return;
|
||||||
|
item.priceBg = el.priceBg.value;
|
||||||
|
syncSwatches('bg', item.priceBg);
|
||||||
|
renderPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
el.priceFg.addEventListener('input', () => {
|
||||||
|
const item = currentItem();
|
||||||
|
if (!item) return;
|
||||||
|
item.priceFg = el.priceFg.value;
|
||||||
|
syncSwatches('fg', item.priceFg);
|
||||||
|
renderPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.badge-color-row .badge-color-swatch').forEach(sw => {
|
||||||
|
sw.addEventListener('click', () => {
|
||||||
|
const item = currentItem();
|
||||||
|
if (!item) return;
|
||||||
|
const target = sw.parentElement.dataset.target;
|
||||||
|
const color = sw.dataset.color;
|
||||||
|
if (target === 'bg') {
|
||||||
|
item.priceBg = color;
|
||||||
|
el.priceBg.value = color;
|
||||||
|
} else if (target === 'fg') {
|
||||||
|
item.priceFg = color;
|
||||||
|
el.priceFg.value = color;
|
||||||
|
}
|
||||||
|
syncSwatches(target, color);
|
||||||
|
renderPreview();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
el.nickEnabled.addEventListener('change', () => {
|
el.nickEnabled.addEventListener('change', () => {
|
||||||
const item = currentItem();
|
const item = currentItem();
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user