| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
var _typoKeys = { |
| 5 |
family:'font-family', weight:'font-weight', style:'font-style', |
| 6 |
decoration:'text-decoration', transform:'text-transform', |
| 7 |
sizeDesktop:'font-size-d', sizeTablet:'font-size-t', sizeMobile:'font-size-m', |
| 8 |
lineHeightDesktop:'line-height-d', lineHeightTablet:'line-height-t', lineHeightMobile:'line-height-m', |
| 9 |
letterSpacingDesktop:'letter-spacing-d', letterSpacingTablet:'letter-spacing-t', letterSpacingMobile:'letter-spacing-m', |
| 10 |
wordSpacingDesktop:'word-spacing-d', wordSpacingTablet:'word-spacing-t', wordSpacingMobile:'word-spacing-m' |
| 11 |
}; |
| 12 |
function typoCssVarsForEl(el, obj, prefix) { |
| 13 |
if (!obj || typeof obj !== 'object') return; |
| 14 |
Object.keys(_typoKeys).forEach(function(k) { |
| 15 |
var v = obj[k]; |
| 16 |
if (v === undefined || v === '' || v === null) return; |
| 17 |
if (k === 'sizeDesktop' || k === 'sizeTablet' || k === 'sizeMobile') v = v + (obj.sizeUnit || 'px'); |
| 18 |
else if (k === 'lineHeightDesktop' || k === 'lineHeightTablet' || k === 'lineHeightMobile') v = v + (obj.lineHeightUnit || ''); |
| 19 |
else if (k === 'letterSpacingDesktop' || k === 'letterSpacingTablet' || k === 'letterSpacingMobile') v = v + (obj.letterSpacingUnit || 'px'); |
| 20 |
else if (k === 'wordSpacingDesktop' || k === 'wordSpacingTablet' || k === 'wordSpacingMobile') v = v + (obj.wordSpacingUnit || 'px'); |
| 21 |
el.style.setProperty(prefix + _typoKeys[k], String(v)); |
| 22 |
}); |
| 23 |
} |
| 24 |
|
| 25 |
var SAMPLE_INGREDIENTS = [ |
| 26 |
{ qty: 2, unit: 'cups', name: 'All-purpose flour' }, |
| 27 |
{ qty: 2, unit: 'tsp', name: 'Baking powder' }, |
| 28 |
{ qty: 0.5, unit: 'tsp', name: 'Salt' }, |
| 29 |
{ qty: 2, unit: 'tbsp', name: 'Sugar' }, |
| 30 |
{ qty: 2, unit: '', name: 'Large eggs' }, |
| 31 |
{ qty: 1.5, unit: 'cups', name: 'Milk' }, |
| 32 |
{ qty: 3, unit: 'tbsp', name: 'Butter, melted' } |
| 33 |
]; |
| 34 |
|
| 35 |
var QUICK_BTNS = [1, 2, 4, 6, 8, 12]; |
| 36 |
|
| 37 |
function fmtQty(n) { |
| 38 |
if (n <= 0) return '0'; |
| 39 |
var fracs = [[0.125,'� |
| 40 |
�'],[0.25,'¼'],[0.33,'� |
| 41 |
�'],[0.5,'½'],[0.67,'� |
| 42 |
�'],[0.75,'¾']]; |
| 43 |
var whole = Math.floor(n); |
| 44 |
var rem = Math.round((n - whole)*1000)/1000; |
| 45 |
for (var i=0; i<fracs.length; i++) { |
| 46 |
if (Math.abs(rem - fracs[i][0]) < 0.02) { |
| 47 |
return (whole > 0 ? whole + ' ' : '') + fracs[i][1]; |
| 48 |
} |
| 49 |
} |
| 50 |
if (rem === 0) return String(whole); |
| 51 |
var d = Math.round(n * 100) / 100; |
| 52 |
return d.toFixed(2).replace(/\.?0+$/, ''); |
| 53 |
} |
| 54 |
|
| 55 |
function escHtml(s) { |
| 56 |
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); |
| 57 |
} |
| 58 |
|
| 59 |
document.querySelectorAll('.bkbg-rsc-app').forEach(function(root) { |
| 60 |
var opts; |
| 61 |
try { opts = JSON.parse(root.getAttribute('data-opts') || '{}'); } |
| 62 |
catch(e) { return; } |
| 63 |
|
| 64 |
var o = { |
| 65 |
title: opts.title || 'Recipe Scaler', |
| 66 |
subtitle: opts.subtitle || '', |
| 67 |
recipeName: opts.recipeName || 'Recipe', |
| 68 |
defaultServings: parseInt(opts.defaultServings) || 4, |
| 69 |
maxServings: parseInt(opts.maxServings) || 24, |
| 70 |
showQuickBtns: opts.showQuickBtns !== false, |
| 71 |
showNotes: opts.showNotes !== false, |
| 72 |
recipeNotes: opts.recipeNotes || '', |
| 73 |
accentColor: opts.accentColor || '#f97316', |
| 74 |
sectionBg: opts.sectionBg || '#fff7ed', |
| 75 |
cardBg: opts.cardBg || '#ffffff', |
| 76 |
inputBg: opts.inputBg || '#fafafa', |
| 77 |
ingredientBg: opts.ingredientBg || '#fef3c7', |
| 78 |
titleColor: opts.titleColor || '#111827', |
| 79 |
subtitleColor: opts.subtitleColor || '#6b7280', |
| 80 |
labelColor: opts.labelColor || '#374151', |
| 81 |
titleTypo: opts.titleTypo || {}, |
| 82 |
contentMaxWidth: parseInt(opts.contentMaxWidth) || 660 |
| 83 |
}; |
| 84 |
|
| 85 |
var baseServings = o.defaultServings; |
| 86 |
var curServings = o.defaultServings; |
| 87 |
var ingredients = SAMPLE_INGREDIENTS.map(function(i){ return { qty:i.qty, unit:i.unit, name:i.name }; }); |
| 88 |
|
| 89 |
// ── Build shell ──────────────────────────────────────────────── |
| 90 |
root.innerHTML = |
| 91 |
'<div class="bkbg-rsc-wrap" style="background:' + o.sectionBg + ';max-width:' + o.contentMaxWidth + 'px;--rsc-accent:' + o.accentColor + '">' + |
| 92 |
(o.title ? '<h3 class="bkbg-rsc-title" style="color:' + o.titleColor + '">' + o.title + '</h3>' : '') + |
| 93 |
(o.subtitle ? '<p class="bkbg-rsc-subtitle" style="color:' + o.subtitleColor + '">' + o.subtitle + '</p>' : '') + |
| 94 |
|
| 95 |
// Recipe name |
| 96 |
'<div class="bkbg-rsc-name-card" style="background:' + o.cardBg + '">' + |
| 97 |
'<span class="bkbg-rsc-name-icon">🍽️</span>' + |
| 98 |
'<span class="bkbg-rsc-name-text" style="color:' + o.titleColor + '">' + escHtml(o.recipeName) + '</span>' + |
| 99 |
'</div>' + |
| 100 |
|
| 101 |
// Servings control |
| 102 |
'<div class="bkbg-rsc-servings-card" style="background:' + o.cardBg + '">' + |
| 103 |
'<div class="bkbg-rsc-servings-label" style="color:' + o.labelColor + '">Servings</div>' + |
| 104 |
'<div class="bkbg-rsc-servings-row">' + |
| 105 |
'<button class="bkbg-rsc-sv-btn" id="rsc-minus">−</button>' + |
| 106 |
'<input class="bkbg-rsc-sv-input" id="rsc-sv-in" type="number" min="1" max="' + o.maxServings + '" value="' + curServings + '" style="background:' + o.inputBg + ';border-color:#e5e7eb">' + |
| 107 |
'<button class="bkbg-rsc-sv-btn" id="rsc-plus">+</button>' + |
| 108 |
'<span class="bkbg-rsc-sv-unit" style="color:' + o.labelColor + '">servings</span>' + |
| 109 |
'</div>' + |
| 110 |
(o.showQuickBtns ? |
| 111 |
'<div class="bkbg-rsc-quick-row" id="rsc-quick">' + |
| 112 |
QUICK_BTNS.map(function(n) { |
| 113 |
return '<button class="bkbg-rsc-quick-btn' + (n===curServings?' active':'') + '" data-sv="' + n + '" style="color:' + (n===curServings?'#fff':o.labelColor) + '">' + n + 'x</button>'; |
| 114 |
}).join('') + |
| 115 |
'</div>' |
| 116 |
: '') + |
| 117 |
'</div>' + |
| 118 |
|
| 119 |
// Ingredients section |
| 120 |
'<div class="bkbg-rsc-ing-section">' + |
| 121 |
'<div class="bkbg-rsc-ing-heading" style="color:' + o.labelColor + '">Ingredients' + |
| 122 |
'<span class="bkbg-rsc-scale-badge" id="rsc-scale-badge">×' + (curServings/baseServings).toFixed(2).replace(/\.?0+$/,'') + '</span>' + |
| 123 |
'</div>' + |
| 124 |
'<div class="bkbg-rsc-ing-list" id="rsc-ing-list"></div>' + |
| 125 |
'</div>' + |
| 126 |
|
| 127 |
// Add ingredient form |
| 128 |
'<div class="bkbg-rsc-add-form">' + |
| 129 |
'<input class="bkbg-rsc-add-input bkbg-rsc-add-qty" id="rsc-new-qty" type="number" placeholder="Qty" min="0" step="0.25" style="background:' + o.inputBg + ';color:' + o.labelColor + '">' + |
| 130 |
'<input class="bkbg-rsc-add-input bkbg-rsc-add-unit" id="rsc-new-unit" type="text" placeholder="Unit (e.g. cups)" style="background:' + o.inputBg + ';color:' + o.labelColor + '">' + |
| 131 |
'<input class="bkbg-rsc-add-input bkbg-rsc-add-name" id="rsc-new-name" type="text" placeholder="Ingredient name" style="background:' + o.inputBg + ';color:' + o.labelColor + '">' + |
| 132 |
'<button id="rsc-add-btn" class="bkbg-rsc-add-btn">+ Add</button>' + |
| 133 |
'</div>' + |
| 134 |
|
| 135 |
// Notes |
| 136 |
(o.showNotes && o.recipeNotes ? |
| 137 |
'<div class="bkbg-rsc-notes">' + |
| 138 |
'<div class="bkbg-rsc-notes-title">📝 Recipe Notes</div>' + |
| 139 |
'<div class="bkbg-rsc-notes-text">' + escHtml(o.recipeNotes) + '</div>' + |
| 140 |
'</div>' |
| 141 |
: '') + |
| 142 |
'</div>'; |
| 143 |
|
| 144 |
var wrap = root.querySelector('.bkbg-rsc-wrap'); |
| 145 |
if (wrap) typoCssVarsForEl(wrap, o.titleTypo || {}, '--bkrsc-tt-'); |
| 146 |
|
| 147 |
var svInput = root.querySelector('#rsc-sv-in'); |
| 148 |
var minusBtn = root.querySelector('#rsc-minus'); |
| 149 |
var plusBtn = root.querySelector('#rsc-plus'); |
| 150 |
var ingList = root.querySelector('#rsc-ing-list'); |
| 151 |
var badge = root.querySelector('#rsc-scale-badge'); |
| 152 |
var quickRow = root.querySelector('#rsc-quick'); |
| 153 |
var newQty = root.querySelector('#rsc-new-qty'); |
| 154 |
var newUnit = root.querySelector('#rsc-new-unit'); |
| 155 |
var newName = root.querySelector('#rsc-new-name'); |
| 156 |
var addBtn = root.querySelector('#rsc-add-btn'); |
| 157 |
|
| 158 |
// ── Re-render ingredients ────────────────────────────────────── |
| 159 |
function renderIngredients() { |
| 160 |
var factor = curServings / baseServings; |
| 161 |
ingList.innerHTML = ingredients.map(function(ing, idx) { |
| 162 |
var scaled = fmtQty(ing.qty * factor); |
| 163 |
return '<div class="bkbg-rsc-ing-row" style="background:' + o.ingredientBg + '">' + |
| 164 |
'<span class="bkbg-rsc-ing-qty">' + scaled + '</span>' + |
| 165 |
'<span class="bkbg-rsc-ing-unit">' + escHtml(ing.unit) + '</span>' + |
| 166 |
'<span class="bkbg-rsc-ing-name" style="color:' + o.labelColor + '">' + escHtml(ing.name) + '</span>' + |
| 167 |
'<button class="bkbg-rsc-ing-del" data-idx="' + idx + '" title="Remove">✕</button>' + |
| 168 |
'</div>'; |
| 169 |
}).join(''); |
| 170 |
|
| 171 |
ingList.querySelectorAll('.bkbg-rsc-ing-del').forEach(function(btn) { |
| 172 |
btn.addEventListener('click', function() { |
| 173 |
ingredients.splice(parseInt(btn.getAttribute('data-idx')), 1); |
| 174 |
renderIngredients(); |
| 175 |
}); |
| 176 |
}); |
| 177 |
|
| 178 |
if (badge) { |
| 179 |
var f = curServings / baseServings; |
| 180 |
badge.textContent = '×' + (f === 1 ? '1' : f.toFixed(2).replace(/\.?0+$/, '')); |
| 181 |
} |
| 182 |
updateQuickBtns(); |
| 183 |
} |
| 184 |
|
| 185 |
// ── Update servings ──────────────────────────────────────────── |
| 186 |
function setServings(n) { |
| 187 |
n = Math.max(1, Math.min(o.maxServings, Math.round(n))); |
| 188 |
curServings = n; |
| 189 |
if (svInput) svInput.value = n; |
| 190 |
renderIngredients(); |
| 191 |
} |
| 192 |
|
| 193 |
function updateQuickBtns() { |
| 194 |
if (!quickRow) return; |
| 195 |
quickRow.querySelectorAll('.bkbg-rsc-quick-btn').forEach(function(b) { |
| 196 |
var act = parseInt(b.getAttribute('data-sv')) === curServings; |
| 197 |
b.classList.toggle('active', act); |
| 198 |
b.style.color = act ? '#fff' : o.labelColor; |
| 199 |
}); |
| 200 |
} |
| 201 |
|
| 202 |
// ── Bindings ─────────────────────────────────────────────────── |
| 203 |
if (minusBtn) minusBtn.addEventListener('click', function() { setServings(curServings - 1); }); |
| 204 |
if (plusBtn) plusBtn.addEventListener('click', function() { setServings(curServings + 1); }); |
| 205 |
if (svInput) svInput.addEventListener('input', function() { |
| 206 |
var v = parseInt(svInput.value); |
| 207 |
if (!isNaN(v) && v > 0) { curServings = Math.min(o.maxServings, v); renderIngredients(); } |
| 208 |
}); |
| 209 |
|
| 210 |
if (quickRow) { |
| 211 |
quickRow.querySelectorAll('.bkbg-rsc-quick-btn').forEach(function(b) { |
| 212 |
b.addEventListener('click', function() { setServings(parseInt(b.getAttribute('data-sv'))); }); |
| 213 |
}); |
| 214 |
} |
| 215 |
|
| 216 |
if (addBtn) { |
| 217 |
addBtn.addEventListener('click', function() { |
| 218 |
var qty = parseFloat(newQty ? newQty.value : 0) || 0; |
| 219 |
var unit = newUnit ? newUnit.value.trim() : ''; |
| 220 |
var name = newName ? newName.value.trim() : ''; |
| 221 |
if (!name) { if (newName) newName.focus(); return; } |
| 222 |
// Store qty relative to base servings |
| 223 |
ingredients.push({ qty: qty * (baseServings / curServings), unit: unit, name: name }); |
| 224 |
if (newQty) newQty.value = ''; |
| 225 |
if (newUnit) newUnit.value = ''; |
| 226 |
if (newName) newName.value = ''; |
| 227 |
renderIngredients(); |
| 228 |
}); |
| 229 |
} |
| 230 |
|
| 231 |
// ── Initial render ───────────────────────────────────────────── |
| 232 |
renderIngredients(); |
| 233 |
}); |
| 234 |
})(); |
| 235 |
|