@jjlmoya/utils-cooking 1.37.0 → 1.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/index.ts +4 -0
- package/src/entries.ts +5 -1
- package/src/index.ts +2 -0
- package/src/tests/i18n-titles.test.ts +1 -1
- package/src/tests/leavener-acid-neutralizer.test.ts +42 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/leavener-acid-neutralizer/bibliography.astro +6 -0
- package/src/tool/leavener-acid-neutralizer/bibliography.ts +10 -0
- package/src/tool/leavener-acid-neutralizer/component.astro +335 -0
- package/src/tool/leavener-acid-neutralizer/entry.ts +26 -0
- package/src/tool/leavener-acid-neutralizer/i18n/de.ts +279 -0
- package/src/tool/leavener-acid-neutralizer/i18n/en.ts +279 -0
- package/src/tool/leavener-acid-neutralizer/i18n/es.ts +275 -0
- package/src/tool/leavener-acid-neutralizer/i18n/fr.ts +279 -0
- package/src/tool/leavener-acid-neutralizer/i18n/id.ts +279 -0
- package/src/tool/leavener-acid-neutralizer/i18n/it.ts +275 -0
- package/src/tool/leavener-acid-neutralizer/i18n/ja.ts +279 -0
- package/src/tool/leavener-acid-neutralizer/i18n/ko.ts +279 -0
- package/src/tool/leavener-acid-neutralizer/i18n/nl.ts +279 -0
- package/src/tool/leavener-acid-neutralizer/i18n/pl.ts +279 -0
- package/src/tool/leavener-acid-neutralizer/i18n/pt.ts +279 -0
- package/src/tool/leavener-acid-neutralizer/i18n/ru.ts +279 -0
- package/src/tool/leavener-acid-neutralizer/i18n/sv.ts +279 -0
- package/src/tool/leavener-acid-neutralizer/i18n/tr.ts +279 -0
- package/src/tool/leavener-acid-neutralizer/i18n/zh.ts +279 -0
- package/src/tool/leavener-acid-neutralizer/index.ts +10 -0
- package/src/tool/leavener-acid-neutralizer/leavener-acid-neutralizer.css +424 -0
- package/src/tool/leavener-acid-neutralizer/logic.ts +57 -0
- package/src/tool/leavener-acid-neutralizer/seo.astro +15 -0
- package/src/tool/pectin-jam-setting-calculator/bibliography.astro +6 -0
- package/src/tool/pectin-jam-setting-calculator/bibliography.ts +10 -0
- package/src/tool/pectin-jam-setting-calculator/component.astro +170 -0
- package/src/tool/pectin-jam-setting-calculator/components/CalculatorInputs.astro +44 -0
- package/src/tool/pectin-jam-setting-calculator/components/DropTestVisualizer.astro +40 -0
- package/src/tool/pectin-jam-setting-calculator/components/FruitSelector.astro +38 -0
- package/src/tool/pectin-jam-setting-calculator/components/RecipeResults.astro +72 -0
- package/src/tool/pectin-jam-setting-calculator/entry.ts +26 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/de.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/en.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/es.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/fr.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/id.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/it.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/ja.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/ko.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/nl.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/pl.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/pt.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/ru.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/sv.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/tr.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/zh.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/index.ts +11 -0
- package/src/tool/pectin-jam-setting-calculator/logic.ts +96 -0
- package/src/tool/pectin-jam-setting-calculator/pectin-jam-setting-calculator.css +730 -0
- package/src/tool/pectin-jam-setting-calculator/seo.astro +15 -0
- package/src/tools.ts +4 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface AcidIngredient {
|
|
2
|
+
type: string;
|
|
3
|
+
weight: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface NeutralizerInput {
|
|
7
|
+
acidIngredients: AcidIngredient[];
|
|
8
|
+
flour: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface NeutralizerResult {
|
|
12
|
+
neutralizedBakingSoda: number;
|
|
13
|
+
requiredBakingPowder: number;
|
|
14
|
+
providedBakingPowderEquivalent: number;
|
|
15
|
+
boosterBakingPowder: number;
|
|
16
|
+
bakingSodaTeaspoons: number;
|
|
17
|
+
boosterBakingPowderTeaspoons: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const ACID_RATIOS: Record<string, number> = {
|
|
21
|
+
buttermilk: 1.5 / 120,
|
|
22
|
+
yogurt: 1.5 / 120,
|
|
23
|
+
sour_cream: 1.5 / 120,
|
|
24
|
+
honey: 3 / 340,
|
|
25
|
+
molasses: 3 / 328,
|
|
26
|
+
cocoa: 3 / 80,
|
|
27
|
+
lemon_juice: 1.5 / 15,
|
|
28
|
+
vinegar: 1.5 / 15,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export class LeavenerLogic {
|
|
32
|
+
static calculate(input: NeutralizerInput): NeutralizerResult {
|
|
33
|
+
let neutralizedBakingSoda = 0;
|
|
34
|
+
for (const ingredient of input.acidIngredients) {
|
|
35
|
+
const ratio = ACID_RATIOS[ingredient.type] || 0;
|
|
36
|
+
neutralizedBakingSoda += ingredient.weight * ratio;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
neutralizedBakingSoda = parseFloat(neutralizedBakingSoda.toFixed(2));
|
|
40
|
+
|
|
41
|
+
const requiredBakingPowder = parseFloat((input.flour * 0.04).toFixed(2));
|
|
42
|
+
const providedBakingPowderEquivalent = parseFloat((neutralizedBakingSoda * 4).toFixed(2));
|
|
43
|
+
const boosterBakingPowder = parseFloat(Math.max(0, requiredBakingPowder - providedBakingPowderEquivalent).toFixed(2));
|
|
44
|
+
|
|
45
|
+
const bakingSodaTeaspoons = parseFloat((neutralizedBakingSoda / 6).toFixed(2));
|
|
46
|
+
const boosterBakingPowderTeaspoons = parseFloat((boosterBakingPowder / 4.8).toFixed(2));
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
neutralizedBakingSoda,
|
|
50
|
+
requiredBakingPowder,
|
|
51
|
+
providedBakingPowderEquivalent,
|
|
52
|
+
boosterBakingPowder,
|
|
53
|
+
bakingSodaTeaspoons,
|
|
54
|
+
boosterBakingPowderTeaspoons,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { leavenerAcidNeutralizer } from './entry';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'es' } = Astro.props;
|
|
11
|
+
const content = await leavenerAcidNeutralizer.i18n[locale]?.();
|
|
12
|
+
if (!content) return null;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const bibliography = [
|
|
2
|
+
{
|
|
3
|
+
name: 'Pectin based gels and their advanced application in food: From hydrogel to emulsion gel',
|
|
4
|
+
url: 'https://www.sciencedirect.com/science/article/abs/pii/S0268005X24011159',
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
name: 'Pectin: Technological and Physiological Properties (Springer)',
|
|
8
|
+
url: 'https://link.springer.com/book/10.1007/978-3-030-53421-9',
|
|
9
|
+
},
|
|
10
|
+
];
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
import FruitSelector from './components/FruitSelector.astro';
|
|
3
|
+
import CalculatorInputs from './components/CalculatorInputs.astro';
|
|
4
|
+
import DropTestVisualizer from './components/DropTestVisualizer.astro';
|
|
5
|
+
import RecipeResults from './components/RecipeResults.astro';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
ui: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { ui } = Astro.props;
|
|
12
|
+
|
|
13
|
+
const fruits = [
|
|
14
|
+
{ id: 'apple', name: 'Apple', pectin: 'high', acidity: 'high', color: '#dc2626' },
|
|
15
|
+
{ id: 'quince', name: 'Quince', pectin: 'high', acidity: 'medium', color: '#ea580c' },
|
|
16
|
+
{ id: 'blackberry', name: 'Blackberry', pectin: 'high', acidity: 'low', color: '#7c3aed' },
|
|
17
|
+
{ id: 'cranberry', name: 'Cranberry', pectin: 'high', acidity: 'high', color: '#be123c' },
|
|
18
|
+
{ id: 'gooseberry', name: 'Gooseberry', pectin: 'high', acidity: 'high', color: '#65a30d' },
|
|
19
|
+
{ id: 'plum', name: 'Plum', pectin: 'medium', acidity: 'medium', color: '#7c3aed' },
|
|
20
|
+
{ id: 'apricot', name: 'Apricot', pectin: 'medium', acidity: 'medium', color: '#d97706' },
|
|
21
|
+
{ id: 'blueberry', name: 'Blueberry', pectin: 'medium', acidity: 'low', color: '#4f46e5' },
|
|
22
|
+
{ id: 'raspberry', name: 'Raspberry', pectin: 'medium', acidity: 'high', color: '#e11d48' },
|
|
23
|
+
{ id: 'peach', name: 'Peach', pectin: 'low', acidity: 'medium', color: '#d97706' },
|
|
24
|
+
{ id: 'strawberry', name: 'Strawberry', pectin: 'low', acidity: 'low', color: '#e11d48' },
|
|
25
|
+
{ id: 'pear', name: 'Pear', pectin: 'low', acidity: 'low', color: '#65a30d' },
|
|
26
|
+
{ id: 'fig', name: 'Fig', pectin: 'low', acidity: 'low', color: '#92400e' },
|
|
27
|
+
{ id: 'cherry', name: 'Cherry', pectin: 'low', acidity: 'high', color: '#be123c' },
|
|
28
|
+
{ id: 'grape', name: 'Grape', pectin: 'low', acidity: 'high', color: '#7c3aed' },
|
|
29
|
+
{ id: 'mango', name: 'Mango', pectin: 'low', acidity: 'medium', color: '#d97706' },
|
|
30
|
+
];
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<div class="jam-container">
|
|
34
|
+
<div class="jam-card">
|
|
35
|
+
<div class="jam-glow-1"></div>
|
|
36
|
+
<div class="jam-glow-2"></div>
|
|
37
|
+
<div class="jam-glow-3"></div>
|
|
38
|
+
|
|
39
|
+
<div class="jam-main-grid">
|
|
40
|
+
<FruitSelector ui={ui} fruits={fruits} />
|
|
41
|
+
<CalculatorInputs ui={ui} />
|
|
42
|
+
<DropTestVisualizer ui={ui} />
|
|
43
|
+
<RecipeResults ui={ui} />
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<script is:inline define:vars={{ ui }}>
|
|
49
|
+
const STORAGE_KEY = 'jamcalc_state';
|
|
50
|
+
let aP = 'HM', aSM = 'auto', aF = 'all';
|
|
51
|
+
const $ = function(id){ return document.getElementById(id); };
|
|
52
|
+
const $$ = function(sel){ return document.querySelectorAll(sel); };
|
|
53
|
+
const fs = $('fruit-scroll'), fw = $('fruit-weight'), sw = $('sugar-weight');
|
|
54
|
+
const smr = $('sugar-manual-row'), sah = $('sugar-auto-hint'), sh = $('sugar-hint');
|
|
55
|
+
const jd = $('jam-drop'), jdh = $('jam-drop-highlight'), dst = $('drop-status-text');
|
|
56
|
+
const ds = $('drop-status'), dsi = $('drop-status-icon'), rs = $('recipe-section');
|
|
57
|
+
|
|
58
|
+
const ICONS = {
|
|
59
|
+
perfect: '<svg width="18" height="18" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2.5"/><path d="M7 12l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
60
|
+
warn: '<svg width="18" height="18" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2.5"/><line x1="12" y1="8" x2="12" y2="13" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/><circle cx="12" cy="17" r="1.2" fill="currentColor"/></svg>',
|
|
61
|
+
fail: '<svg width="18" height="18" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2.5"/><line x1="15" y1="9" x2="9" y2="15" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/><line x1="9" y1="9" x2="15" y2="15" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/></svg>'
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function gd(p){ return { id: p.getAttribute('data-fruit'), pectin: p.getAttribute('data-pectin'), acidity: p.getAttribute('data-acidity'), color: p.getAttribute('data-color') }; }
|
|
65
|
+
function rgb(h){ return parseInt(h.slice(1,3),16)+','+parseInt(h.slice(3,5),16)+','+parseInt(h.slice(5,7),16); }
|
|
66
|
+
function lc(h,f){ const c=function(i){ return Math.min(255,parseInt(h.slice(i,i+2),16)+Math.round((255-parseInt(h.slice(i,i+2),16))*f)); }; return '#'+c(1).toString(16).padStart(2,'0')+c(3).toString(16).padStart(2,'0')+c(5).toString(16).padStart(2,'0'); }
|
|
67
|
+
function dc(h,f){ const c=function(i){ return Math.max(0,Math.round(parseInt(h.slice(i,i+2),16)*(1-f))); }; return '#'+c(1).toString(16).padStart(2,'0')+c(3).toString(16).padStart(2,'0')+c(5).toString(16).padStart(2,'0'); }
|
|
68
|
+
|
|
69
|
+
function save(){
|
|
70
|
+
const ae = fs.querySelector('.jam-fruit-pill.active');
|
|
71
|
+
try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ fruit: ae?ae.getAttribute('data-fruit'):'strawberry', weight: fw.value, pectin: aP, sugarMode: aSM, sugarWeight: sw.value, filter: aF })); } catch {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function setTgl(id, v){ $$('#'+id+' .jam-toggle-btn').forEach(function(b){ b.classList.remove('active'); if(b.getAttribute('data-value')===v) b.classList.add('active'); }); }
|
|
75
|
+
function setFl(v){ aF=v; $$('#filter-pills .jam-filter-pill').forEach(function(p){ p.classList.remove('active'); if(p.getAttribute('data-filter')===v) p.classList.add('active'); }); $$('.jam-fruit-pill').forEach(function(p){ p.style.display=(v==='all'||p.getAttribute('data-pectin')===v)?'':'none'; }); }
|
|
76
|
+
|
|
77
|
+
function restoreFruit(s){
|
|
78
|
+
if(!s.fruit) return;
|
|
79
|
+
const t=document.querySelector('.jam-fruit-pill[data-fruit="'+s.fruit+'"]');
|
|
80
|
+
if(!t) return;
|
|
81
|
+
$$('.jam-fruit-pill').forEach(function(p){ p.classList.remove('active'); });
|
|
82
|
+
t.classList.add('active');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function applyState(s){
|
|
86
|
+
if(s.pectin){ setTgl('pectin-toggle',s.pectin); aP=s.pectin; }
|
|
87
|
+
if(s.sugarMode){ setTgl('sugar-mode-toggle',s.sugarMode); aSM=s.sugarMode; }
|
|
88
|
+
if(s.filter) setFl(s.filter);
|
|
89
|
+
if(s.weight) fw.value=s.weight;
|
|
90
|
+
if(s.sugarWeight) sw.value=s.sugarWeight;
|
|
91
|
+
if(s.sugarMode==='manual'){ smr.classList.remove('jam-hidden'); sah.style.display='none'; }
|
|
92
|
+
restoreFruit(s);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function restore(){
|
|
96
|
+
try {
|
|
97
|
+
const raw = localStorage.getItem(STORAGE_KEY); if(!raw) return;
|
|
98
|
+
applyState(JSON.parse(raw));
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function gf(){
|
|
103
|
+
const p = fs.querySelector('.jam-fruit-pill.active');
|
|
104
|
+
return p ? gd(p) : { id:'strawberry',pectin:'low',acidity:'low',color:'#e11d48' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function cp(fwG, p){ if(p==='high') return 0; const r = p==='medium'?0.003:0.006; return parseFloat((fwG*r).toFixed(1)); }
|
|
108
|
+
function cc(fwG, a){ if(a==='high') return 0; const r = a==='medium'?0.003:0.008; return parseFloat((fwG*r).toFixed(1)); }
|
|
109
|
+
function csg(fwG){ if(aSM==='manual') return parseFloat(sw.value)||0; const r=aP==='LM'?0.1:1.0; return parseFloat((fwG*r).toFixed(0)); }
|
|
110
|
+
function cst(sp, hp, pg){
|
|
111
|
+
if(aP==='LM'){ if(hp) return 'perfect'; return pg>0?'slightly-thin':'too-thin'; }
|
|
112
|
+
const t = aSM==='auto'?55:60;
|
|
113
|
+
if(sp>=t && hp) return 'perfect';
|
|
114
|
+
if(sp>=50) return 'slightly-thin';
|
|
115
|
+
return 'too-thin';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function calc(){
|
|
119
|
+
const fr = gf(), fwG = Math.max(10,parseFloat(fw.value)||1000);
|
|
120
|
+
const pg = cp(fwG, fr.pectin), ca = cc(fwG, fr.acidity);
|
|
121
|
+
const sg = csg(fwG), cooked = parseFloat((fwG*0.55).toFixed(0));
|
|
122
|
+
const ty = parseFloat((cooked+sg+pg+ca).toFixed(0));
|
|
123
|
+
const sp = ty>0?parseFloat(((sg/ty)*100).toFixed(1)):0, spF = parseFloat(((sg/fwG)*100).toFixed(0));
|
|
124
|
+
const hp = fr.pectin!=='low'||pg>0, st = cst(sp, hp, pg);
|
|
125
|
+
return { pectinGrams:pg, citricGrams:ca, lemonMl:parseFloat((ca*15).toFixed(0)), sugarGrams:sg, totalYield:ty, sugarPct:sp, sugarPctFruit:spF, status:st, fruitColor:fr.color };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function upd(r){
|
|
129
|
+
$('pectin-val').textContent=r.pectinGrams.toFixed(1); $('citric-val').textContent=r.citricGrams.toFixed(1);
|
|
130
|
+
$('lemon-val').textContent=ui.lemonJuiceNeeded+': '+r.lemonMl+' ml';
|
|
131
|
+
$('sugar-val').textContent=r.sugarGrams; $('yield-val').textContent=r.totalYield;
|
|
132
|
+
$('sugar-pct-val').textContent=r.sugarPct; $('sugar-pct-context').textContent=ui.sugarOfTotal+' | '+r.sugarPctFruit+'% '+ui.sugarOfFruit;
|
|
133
|
+
const bar=$('sugar-bar-fill'); bar.style.width=Math.min(100,Math.max(0,r.sugarPct))+'%';
|
|
134
|
+
bar.style.setProperty('--fruit-color',r.fruitColor);
|
|
135
|
+
bar.className='jam-sugar-bar-fill '+(r.status==='perfect'?'good':'warn');
|
|
136
|
+
sh.textContent = aSM==='auto'?'('+ui.sugarAutoHint+')':'';
|
|
137
|
+
rs.style.setProperty('--fruit-color',r.fruitColor); rs.style.setProperty('--fruit-color-rgb',rgb(r.fruitColor));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function anim(r){
|
|
141
|
+
const s=$('plate-stage'); s.style.setProperty('--jam-fruit-color',r.fruitColor);
|
|
142
|
+
s.style.setProperty('--jam-fruit-color-light',lc(r.fruitColor,0.3)); s.style.setProperty('--jam-fruit-color-dark',dc(r.fruitColor,0.3));
|
|
143
|
+
jd.setAttribute('cy','30'); jd.style.transition='none'; jdh.setAttribute('cy','22'); jdh.style.transition='none';
|
|
144
|
+
requestAnimationFrame(function(){ requestAnimationFrame(function(){ jd.style.transition='cy 0.5s cubic-bezier(0.25,0.46,0.45,0.94)'; jdh.style.transition='cy 0.5s cubic-bezier(0.25,0.46,0.45,0.94)'; jd.setAttribute('cy','110'); jdh.setAttribute('cy','102'); }); });
|
|
145
|
+
setTimeout(function(){
|
|
146
|
+
let cy, txt, cls, ico;
|
|
147
|
+
if(r.status==='perfect'){ cy='118'; txt=ui.dropTestPerfect; cls='perfect'; ico='perfect'; }
|
|
148
|
+
else if(r.status==='too-thin'){ cy='145'; txt=ui.dropTestThin; cls='too-thin'; ico='fail'; }
|
|
149
|
+
else { cy='128'; txt=ui.dropTestThin; cls='slightly-thin'; ico='warn'; }
|
|
150
|
+
jd.style.transition='cy 0.7s cubic-bezier(0.25,0.1,0.25,1)'; jdh.style.transition='cy 0.7s cubic-bezier(0.25,0.1,0.25,1)';
|
|
151
|
+
jd.setAttribute('cy',cy); jdh.setAttribute('cy',(parseFloat(cy)-8).toString());
|
|
152
|
+
dst.textContent=txt; ds.className='jam-drop-status visible '+cls; dsi.innerHTML=ICONS[ico];
|
|
153
|
+
},500);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function ua(){ const r=calc(); upd(r); anim(r); if(aSM==='auto') sw.value=Math.round(r.sugarGrams); save(); }
|
|
157
|
+
|
|
158
|
+
fw.addEventListener('input',ua); sw.addEventListener('input',function(){ if(aSM==='manual') ua(); });
|
|
159
|
+
|
|
160
|
+
function st(id,cb){ $$('#'+id+' .jam-toggle-btn').forEach(function(b){ b.addEventListener('click',function(){ $$('#'+id+' .jam-toggle-btn').forEach(function(x){ x.classList.remove('active'); }); b.classList.add('active'); cb(b.getAttribute('data-value')); }); }); }
|
|
161
|
+
st('pectin-toggle',function(v){ aP=v; ua(); });
|
|
162
|
+
st('sugar-mode-toggle',function(v){ aSM=v; if(v==='manual'){ smr.classList.remove('jam-hidden'); sah.style.display='none'; } else { smr.classList.add('jam-hidden'); sah.style.display=''; } ua(); });
|
|
163
|
+
|
|
164
|
+
$$('#filter-pills .jam-filter-pill').forEach(function(p){ p.addEventListener('click',function(){ $$('#filter-pills .jam-filter-pill').forEach(function(x){ x.classList.remove('active'); }); p.classList.add('active'); setFl(p.getAttribute('data-filter')); }); });
|
|
165
|
+
$$('.jam-fruit-pill').forEach(function(p){ p.addEventListener('click',function(){ $$('.jam-fruit-pill').forEach(function(x){ x.classList.remove('active'); }); p.classList.add('active'); ua(); }); });
|
|
166
|
+
|
|
167
|
+
restore();
|
|
168
|
+
if(!fs.querySelector('.jam-fruit-pill.active')){ const f = fs.querySelector('[data-fruit="strawberry"]'); if(f) f.classList.add('active'); }
|
|
169
|
+
ua();
|
|
170
|
+
</script>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
ui: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { ui } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div class="jam-inputs-section">
|
|
10
|
+
<div class="jam-input-row">
|
|
11
|
+
<label class="jam-input-label" for="fruit-weight">{ui.weightLabel}</label>
|
|
12
|
+
<div class="jam-weight-input-wrap">
|
|
13
|
+
<input type="number" id="fruit-weight" class="jam-weight-input" value="1000" min="10" max="99999"
|
|
14
|
+
placeholder={ui.weightPlaceholder} />
|
|
15
|
+
<span class="jam-weight-unit" id="weight-unit-label">{ui.weightUnitMetric}</span>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="jam-input-row">
|
|
20
|
+
<label class="jam-input-label">{ui.pectinTypeLabel}</label>
|
|
21
|
+
<div class="jam-toggle-wrap" id="pectin-toggle">
|
|
22
|
+
<button class="jam-toggle-btn active" data-value="HM">{ui.pectinHM}</button>
|
|
23
|
+
<button class="jam-toggle-btn" data-value="LM">{ui.pectinLM}</button>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="jam-input-row">
|
|
28
|
+
<label class="jam-input-label">{ui.sugarModeLabel}</label>
|
|
29
|
+
<div class="jam-toggle-wrap" id="sugar-mode-toggle">
|
|
30
|
+
<button class="jam-toggle-btn active" data-value="auto">{ui.sugarModeAuto}</button>
|
|
31
|
+
<button class="jam-toggle-btn" data-value="manual">{ui.sugarModeManual}</button>
|
|
32
|
+
</div>
|
|
33
|
+
<span class="jam-sugar-auto-hint" id="sugar-auto-hint">{ui.sugarAutoHint}</span>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="jam-input-row jam-hidden" id="sugar-manual-row">
|
|
37
|
+
<label class="jam-input-label" for="sugar-weight">{ui.sugarLabel}</label>
|
|
38
|
+
<div class="jam-weight-input-wrap">
|
|
39
|
+
<input type="number" id="sugar-weight" class="jam-weight-input" value="650" min="0" max="99999"
|
|
40
|
+
placeholder={ui.sugarPlaceholder} />
|
|
41
|
+
<span class="jam-weight-unit">{ui.weightUnitMetric}</span>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
ui: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { ui } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div class="jam-visual-section">
|
|
10
|
+
<div class="jam-section-label">{ui.dropTestTitle}</div>
|
|
11
|
+
<div class="jam-drop-hint">{ui.dropTestHow}</div>
|
|
12
|
+
<div class="jam-plate-stage" id="plate-stage">
|
|
13
|
+
<svg class="jam-plate-svg" viewBox="0 0 240 160" xmlns="http://www.w3.org/2000/svg">
|
|
14
|
+
<line x1="20" y1="135" x2="220" y2="45" stroke="currentColor" stroke-width="3" stroke-linecap="round"
|
|
15
|
+
opacity="0.35" class="jam-plate-line" />
|
|
16
|
+
<line x1="20" y1="130" x2="220" y2="40" stroke="currentColor" stroke-width="1" stroke-linecap="round"
|
|
17
|
+
opacity="0.12" class="jam-plate-line-bottom" />
|
|
18
|
+
|
|
19
|
+
<defs>
|
|
20
|
+
<radialGradient id="drop-grad" cx="40%" cy="35%">
|
|
21
|
+
<stop offset="0%" stop-color="var(--jam-fruit-color-light)" />
|
|
22
|
+
<stop offset="60%" stop-color="var(--jam-fruit-color)" />
|
|
23
|
+
<stop offset="100%" stop-color="var(--jam-fruit-color-dark)" />
|
|
24
|
+
</radialGradient>
|
|
25
|
+
<filter id="drop-shadow">
|
|
26
|
+
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.3" />
|
|
27
|
+
</filter>
|
|
28
|
+
</defs>
|
|
29
|
+
|
|
30
|
+
<ellipse id="jam-drop" class="jam-drop" cx="120" cy="30" rx="18" ry="15" fill="url(#drop-grad)"
|
|
31
|
+
filter="url(#drop-shadow)" />
|
|
32
|
+
<ellipse id="jam-drop-highlight" class="jam-drop-highlight" cx="114" cy="22" rx="6" ry="3.5" fill="white"
|
|
33
|
+
opacity="0.45" />
|
|
34
|
+
</svg>
|
|
35
|
+
<div class="jam-drop-status visible perfect" id="drop-status">
|
|
36
|
+
<span class="jam-drop-status-icon" id="drop-status-icon"></span>
|
|
37
|
+
<span class="jam-drop-status-text" id="drop-status-text"></span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Fruit {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
pectin: string;
|
|
6
|
+
acidity: string;
|
|
7
|
+
color: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
ui: Record<string, string>;
|
|
12
|
+
fruits: Fruit[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { ui, fruits } = Astro.props;
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<div class="jam-selector-section">
|
|
19
|
+
<div class="jam-section-label">{ui.fruitLabel}</div>
|
|
20
|
+
<div class="jam-fruit-scroll" id="fruit-scroll">
|
|
21
|
+
{fruits.map((fruit) => (
|
|
22
|
+
<button class="jam-fruit-pill"
|
|
23
|
+
data-fruit={fruit.id} data-pectin={fruit.pectin} data-acidity={fruit.acidity}
|
|
24
|
+
data-color={fruit.color}
|
|
25
|
+
style={`--fruit-color:${fruit.color}`}
|
|
26
|
+
aria-label={fruit.name}>
|
|
27
|
+
<span class="jam-fruit-dot" style={`background:${fruit.color}`}></span>
|
|
28
|
+
<span class="jam-fruit-name">{fruit.name}</span>
|
|
29
|
+
</button>
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
<div class="jam-filter-pills" id="filter-pills">
|
|
33
|
+
<button class="jam-filter-pill active" data-filter="all">{ui.allFruits}</button>
|
|
34
|
+
<button class="jam-filter-pill" data-filter="high">{ui.highPectin}</button>
|
|
35
|
+
<button class="jam-filter-pill" data-filter="medium">{ui.mediumPectin}</button>
|
|
36
|
+
<button class="jam-filter-pill" data-filter="low">{ui.lowPectin}</button>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
ui: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { ui } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div class="jam-recipe-section" id="recipe-section">
|
|
10
|
+
<div class="jam-section-label">{ui.recipeTitle}</div>
|
|
11
|
+
<div class="jam-recipe-grid">
|
|
12
|
+
<div class="jam-recipe-item">
|
|
13
|
+
<div class="jam-recipe-icon">
|
|
14
|
+
<svg width="20" height="20" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="none"
|
|
15
|
+
stroke="currentColor" stroke-width="2" /><circle cx="12" cy="12" r="3" fill="currentColor" /></svg>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="jam-recipe-info">
|
|
18
|
+
<div class="jam-recipe-val"><span id="pectin-val">0.0</span><span class="jam-recipe-unit"> g</span></div>
|
|
19
|
+
<div class="jam-recipe-label">{ui.pectinNeeded}</div>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="jam-recipe-item">
|
|
23
|
+
<div class="jam-recipe-icon">
|
|
24
|
+
<svg width="20" height="20" viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"
|
|
25
|
+
fill="none" stroke="currentColor" stroke-width="2" /></svg>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="jam-recipe-info">
|
|
28
|
+
<div class="jam-recipe-val"><span id="citric-val">0.0</span><span class="jam-recipe-unit"> g</span></div>
|
|
29
|
+
<div class="jam-recipe-label">{ui.citricAcidNeeded}</div>
|
|
30
|
+
<div class="jam-recipe-sub"><span id="lemon-val">{ui.lemonJuiceNeeded}: 0 ml</span></div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="jam-recipe-item">
|
|
34
|
+
<div class="jam-recipe-icon">
|
|
35
|
+
<svg width="20" height="20" viewBox="0 0 24 24"><rect x="4" y="8" width="16" height="12" rx="2" fill="none"
|
|
36
|
+
stroke="currentColor" stroke-width="2" /><circle cx="12" cy="5" r="3" fill="none" stroke="currentColor"
|
|
37
|
+
stroke-width="2" /></svg>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="jam-recipe-info">
|
|
40
|
+
<div class="jam-recipe-val"><span id="sugar-val">650</span><span class="jam-recipe-unit"> g</span></div>
|
|
41
|
+
<div class="jam-recipe-label">{ui.sugarNeeded} <span class="jam-sugar-hint" id="sugar-hint"></span></div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="jam-recipe-item">
|
|
45
|
+
<div class="jam-recipe-icon">
|
|
46
|
+
<svg width="20" height="20" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="none"
|
|
47
|
+
stroke="currentColor" stroke-width="2" /><circle cx="12" cy="12" r="6" fill="none" stroke="currentColor"
|
|
48
|
+
stroke-width="2" /><circle cx="12" cy="12" r="2" fill="currentColor" /></svg>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="jam-recipe-info">
|
|
51
|
+
<div class="jam-recipe-val"><span id="yield-val">1650</span><span class="jam-recipe-unit"> g</span></div>
|
|
52
|
+
<div class="jam-recipe-label">{ui.totalYield}</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="jam-sugar-bar-wrap">
|
|
57
|
+
<div class="jam-sugar-bar-header">
|
|
58
|
+
<span>{ui.sugarPercent}</span>
|
|
59
|
+
<span class="jam-sugar-bar-val"><span id="sugar-pct-val">39</span>% <span id="sugar-pct-context"></span></span>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="jam-sugar-bar">
|
|
62
|
+
<div class="jam-sugar-bar-fill" id="sugar-bar-fill" style="width:39%"></div>
|
|
63
|
+
<div class="jam-sugar-bar-marker" style="left:60%"></div>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="jam-sugar-bar-labels">
|
|
66
|
+
<span>0%</span>
|
|
67
|
+
<span>{ui.sugarLow}</span>
|
|
68
|
+
<span class="jam-sugar-bar-ideal">{ui.sugarIdeal}</span>
|
|
69
|
+
<span>{ui.sugarHigh}</span>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CookingToolEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const pectinJam: CookingToolEntry = {
|
|
4
|
+
id: 'pectin-jam-setting-calculator',
|
|
5
|
+
icons: {
|
|
6
|
+
bg: 'mdi:fruit-cherries',
|
|
7
|
+
fg: 'mdi:beaker-outline',
|
|
8
|
+
},
|
|
9
|
+
i18n: {
|
|
10
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
11
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
12
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
13
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
14
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
15
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
16
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
17
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
18
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
19
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
20
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
21
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
22
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
23
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
24
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
25
|
+
},
|
|
26
|
+
};
|