Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 69 additions & 18 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,26 @@
}
.ing-remove:hover{color:var(--bad);background:rgba(232,80,63,.12);}
.ing-controls{display:flex;align-items:center;gap:14px;}
.ing-controls input[type="range"]{flex:1;accent-color:var(--accent);height:6px;}
.ing-controls input[type="range"]{
-webkit-appearance:none;appearance:none;
flex:1;accent-color:var(--accent);height:28px;cursor:pointer;
background:transparent;
}
.ing-controls input[type="range"]::-webkit-slider-runnable-track{
height:8px;border-radius:999px;background:rgba(255,255,255,.12);
}
.ing-controls input[type="range"]::-webkit-slider-thumb{
-webkit-appearance:none;appearance:none;width:20px;height:20px;
margin-top:-6px;border-radius:50%;background:var(--accent);
border:2px solid #23140c;box-shadow:0 0 0 2px rgba(240,90,40,.22);
}
.ing-controls input[type="range"]::-moz-range-track{
height:8px;border-radius:999px;background:rgba(255,255,255,.12);
}
.ing-controls input[type="range"]::-moz-range-thumb{
width:18px;height:18px;border-radius:50%;background:var(--accent);
border:2px solid #23140c;box-shadow:0 0 0 2px rgba(240,90,40,.22);
}
.ing-num{
width:78px;background:var(--card);border:1px solid var(--border);color:var(--text);
padding:7px 8px;border-radius:var(--radius-sm);font-size:.9rem;text-align:right;font-weight:600;
Expand Down Expand Up @@ -1138,6 +1157,10 @@ <h2 id="communityHeading">Community Recipes</h2>
========================================================= */
function clamp(v,min,max){ return Math.max(min, Math.min(max, v)); }
function round1(v){ return Math.round(v*10)/10; }
function roundWhole(v){ return Math.round(Number(v) || 0); }
function clampWholePct(value){
return clamp(roundWhole(value), 0, 100);
}
function roundToQuarter(v){ return Math.round(v*4)/4; }
function formatQuarterValue(v){ return Number.isInteger(v) ? String(v) : String(v).replace(/0$/,""); }
function el(tag, cls, html){ const e=document.createElement(tag); if(cls) e.className=cls; if(html!==undefined) e.innerHTML=html; return e; }
Expand Down Expand Up @@ -1179,6 +1202,24 @@ <h2 id="communityHeading">Community Recipes</h2>

function activeKeys(){ return Object.keys(blend).filter(k => blend[k] !== undefined); }

function normalizeBlendPercentages(source, balanceTo100){
const normalized = {};
Object.keys(source || {}).forEach(key=>{
if(!INGREDIENTS[key]) return;
normalized[key] = clampWholePct(source[key]);
});
const keys = Object.keys(normalized);
if(balanceTo100 && keys.length){
const total = keys.reduce((sum,key)=> sum + normalized[key], 0);
const drift = 100 - total;
if(drift !== 0){
const adjustKey = keys.reduce((best,key)=> normalized[key] > normalized[best] ? key : best, keys[0]);
normalized[adjustKey] = clamp(normalized[adjustKey] + drift, 0, 100);
}
}
return normalized;
}

function totalPercent(){
return activeKeys().reduce((sum,k)=> sum + (Number(blend[k])||0), 0);
}
Expand Down Expand Up @@ -1516,8 +1557,8 @@ <h2 id="communityHeading">Community Recipes</h2>
'</div>'+
'<div class="ing-controls">'+
'<label class="skip-link" for="range_'+key+'">'+ing.name+' percentage</label>'+
'<input type="range" id="range_'+key+'" min="0" max="100" step="0.5" value="'+pct+'" data-slider="'+key+'">'+
'<input type="number" class="ing-num" id="num_'+key+'" min="0" max="100" step="0.5" value="'+pct+'" data-numinput="'+key+'" aria-label="'+ing.name+' percentage value">'+
'<input type="range" id="range_'+key+'" min="0" max="100" step="1" value="'+pct+'" data-slider="'+key+'">'+
'<input type="number" class="ing-num" id="num_'+key+'" min="0" max="100" step="1" value="'+pct+'" data-numinput="'+key+'" aria-label="'+ing.name+' percentage value">'+
'<span class="ing-pct-sign">%</span>'+
'</div>';
container.appendChild(row);
Expand Down Expand Up @@ -1774,10 +1815,8 @@ <h2 id="communityHeading">Community Recipes</h2>
renderNutritionFacts();
}

function renderAll(){
function renderBlendOutputs(){
renderTotal();
renderActiveList();
renderPicker();
const profile = computeFlavorProfile();
renderRadar(profile);
renderBars(profile);
Expand All @@ -1789,6 +1828,12 @@ <h2 id="communityHeading">Community Recipes</h2>
persistCurrent();
}

function renderAll(){
renderActiveList();
renderPicker();
renderBlendOutputs();
}

/* =========================================================
PERSISTENCE
========================================================= */
Expand All @@ -1809,7 +1854,7 @@ <h2 id="communityHeading">Community Recipes</h2>
if(!raw) return false;
const data = JSON.parse(raw);
if(data && data.blend && Object.keys(data.blend).length){
blend = data.blend;
blend = normalizeBlendPercentages(data.blend, false);
currentBatchIndex = data.batchIndex || 0;
customGrams = data.customGrams || 200;
recipeCardUnit = data.recipeCardUnit === "volume" ? "volume" : "grams";
Expand Down Expand Up @@ -1840,26 +1885,32 @@ <h2 id="communityHeading">Community Recipes</h2>
let v = Number(value);
if(isNaN(v) || v < 0) v = 0;
if(v > 100) v = 100;
blend[key] = Math.round(v*10)/10;
renderAll();
blend[key] = clampWholePct(v);
const slider = document.getElementById("range_"+key);
const num = document.getElementById("num_"+key);
if(slider) slider.value = blend[key];
if(num) num.value = blend[key];
renderBlendOutputs();
}

function autoBalance(){
const keys = activeKeys();
if(keys.length===0){ showToast("Add ingredients before auto-balancing.","error"); return; }
const total = totalPercent();
if(total <= 0){
const even = round1(100/keys.length);
keys.forEach((k,i)=> blend[k] = i===keys.length-1 ? round1(100 - even*(keys.length-1)) : even);
const base = Math.floor(100/keys.length);
const remainder = 100 - base*keys.length;
keys.forEach((k,i)=> blend[k] = base + (i < remainder ? 1 : 0));
} else {
keys.forEach(k=>{
blend[k] = round1((Number(blend[k])||0) / total * 100);
blend[k] = clampWholePct((Number(blend[k])||0) / total * 100);
});
// correct rounding drift on the last item
const newTotal = keys.reduce((s,k)=> s+blend[k],0);
const drift = round1(100-newTotal);
if(Math.abs(drift) >= 0.05){
blend[keys[keys.length-1]] = round1(blend[keys[keys.length-1]] + drift);
const drift = 100-newTotal;
if(drift !== 0){
const adjustKey = keys.reduce((best,key)=> blend[key] > blend[best] ? key : best, keys[0]);
blend[adjustKey] = clamp(blend[adjustKey] + drift, 0, 100);
}
}
renderAll();
Expand All @@ -1877,7 +1928,7 @@ <h2 id="communityHeading">Community Recipes</h2>
function loadRecipeById(id){
const recipe = allRecipes.find(r => r.id === id);
if(!recipe) return;
blend = JSON.parse(JSON.stringify(recipe.items));
blend = normalizeBlendPercentages(recipe.items, true);
document.getElementById("blendNameInput").value = recipe.name;
document.getElementById("rcNotes").value = recipe.notes || "";
renderAll();
Expand Down Expand Up @@ -1988,11 +2039,11 @@ <h2 id="communityHeading">Community Recipes</h2>
const cleaned = {};
Object.keys(data.items).forEach(k=>{
if(INGREDIENTS[k] !== undefined){
cleaned[k] = Number(data.items[k]) || 0;
cleaned[k] = data.items[k];
}
});
if(Object.keys(cleaned).length===0) throw new Error("no valid ingredients");
blend = cleaned;
blend = normalizeBlendPercentages(cleaned, true);
recipeCardUnit = data.recipeCardUnit === "volume" ? "volume" : "grams";
document.getElementById("blendNameInput").value = data.name || "Imported Blend";
document.getElementById("rcNotes").value = data.notes || "";
Expand Down
6 changes: 3 additions & 3 deletions ingredients.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@
"color": 2
}
},
"blackPeppercorns": {
"blackPeppercorns": {
"name": "Whole Black Peppercorns",
"category": "Pepper / Whole Spice"
"desc": "Whole peppercorns for grinding fresh; sharp, woody bite with mild background heat."
"category": "Pepper / Whole Spice",
"desc": "Whole peppercorns for grinding fresh; sharp, woody bite with mild background heat.",
"density": 6,
"affiliateLink": "https://amzn.to/3ThzhDl",
"affiliateAddToCartLink": null,
Expand Down
2 changes: 1 addition & 1 deletion ingredients/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ <h1>Seasoning Ingredient Flavor Profiles</h1>
<a class="ingredient-link" href="/ingredients/thyme/"><strong>Thyme</strong><span>Herb - Earthy, minty-woody herb note.</span></a>
<a class="ingredient-link" href="/ingredients/turmeric/"><strong>Turmeric</strong><span>Color / Earthy - Earthy bitterness and a bold golden color.</span></a>
<a class="ingredient-link" href="/ingredients/white-sugar/"><strong>White Sugar</strong><span>Sweetener - Clean, direct sweetness with fast caramelization.</span></a>
<a class="ingredient-link" href="/ingredients/whole-black-peppercorns/"><strong>Whole Black Peppercorns</strong><span>Pepper - Sharp, woody bite with mild background heat.</span></a></section>
<a class="ingredient-link" href="/ingredients/whole-black-peppercorns/"><strong>Whole Black Peppercorns</strong><span>Pepper / Whole Spice - Whole peppercorns for grinding fresh; sharp, woody bite with mild background heat.</span></a></section>
</main>
</body>
</html>
8 changes: 4 additions & 4 deletions ingredients/whole-black-peppercorns/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Whole Black Peppercorns Flavor Profile for Seasoning Blends | PitBlend</title>
<meta name="description" content="Whole Black Peppercorns flavor profile for seasoning blends. Sharp, woody bite with mild background heat. Learn its category, flavor impact, and recipes that use it.">
<meta name="description" content="Whole Black Peppercorns flavor profile for seasoning blends. Whole peppercorns for grinding fresh; sharp, woody bite with mild background heat. Learn its category, flavor impact, and recipes that use it.">
<link rel="canonical" href="https://pitblend.com/ingredients/whole-black-peppercorns/">
<link rel="icon" type="image/png" sizes="512x512" href="/assets/pitblend-icon.png">
<link rel="apple-touch-icon" href="/assets/apple-touch-icon.png">
Expand Down Expand Up @@ -54,9 +54,9 @@
</div>
</header>
<main class="wrap">
<p class="eyebrow">Pepper</p>
<p class="eyebrow">Pepper / Whole Spice</p>
<h1>Whole Black Peppercorns</h1>
<p class="lead">Sharp, woody bite with mild background heat. It mainly contributes pepper, heat and bark.</p>
<p class="lead">Whole peppercorns for grinding fresh; sharp, woody bite with mild background heat. It mainly contributes pepper, heat and bark.</p>
<div class="grid">
<section class="card">
<h2>Flavor Profile</h2>
Expand All @@ -75,7 +75,7 @@ <h2>Flavor Profile</h2>
<aside class="card">
<h2>Ingredient Details</h2>
<div class="meta">
<div><span class="label">Category</span>Pepper</div>
<div><span class="label">Category</span>Pepper / Whole Spice</div>
<div><span class="label">Density</span>6 g/tbsp</div>
</div>
<a class="btn" href="https://amzn.to/3ThzhDl" target="_blank" rel="noopener sponsored nofollow">View Product</a><p class="disclosure">Product links may be affiliate links.</p>
Expand Down
Loading