@jjlmoya/utils-cooking 1.35.0 → 1.37.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 +6 -0
- package/src/entries.ts +7 -1
- package/src/index.ts +3 -0
- package/src/tests/brix-sorbet-density-calculator.test.ts +53 -0
- package/src/tests/i18n-titles.test.ts +2 -2
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/brix-sorbet-density-calculator/bibliography.astro +6 -0
- package/src/tool/brix-sorbet-density-calculator/bibliography.ts +10 -0
- package/src/tool/brix-sorbet-density-calculator/brix-sorbet-density-calculator.css +878 -0
- package/src/tool/brix-sorbet-density-calculator/component.astro +220 -0
- package/src/tool/brix-sorbet-density-calculator/entry.ts +26 -0
- package/src/tool/brix-sorbet-density-calculator/helpers.ts +102 -0
- package/src/tool/brix-sorbet-density-calculator/i18n/de.ts +295 -0
- package/src/tool/brix-sorbet-density-calculator/i18n/en.ts +295 -0
- package/src/tool/brix-sorbet-density-calculator/i18n/es.ts +295 -0
- package/src/tool/brix-sorbet-density-calculator/i18n/fr.ts +295 -0
- package/src/tool/brix-sorbet-density-calculator/i18n/id.ts +295 -0
- package/src/tool/brix-sorbet-density-calculator/i18n/it.ts +295 -0
- package/src/tool/brix-sorbet-density-calculator/i18n/ja.ts +295 -0
- package/src/tool/brix-sorbet-density-calculator/i18n/ko.ts +295 -0
- package/src/tool/brix-sorbet-density-calculator/i18n/nl.ts +295 -0
- package/src/tool/brix-sorbet-density-calculator/i18n/pl.ts +295 -0
- package/src/tool/brix-sorbet-density-calculator/i18n/pt.ts +295 -0
- package/src/tool/brix-sorbet-density-calculator/i18n/ru.ts +295 -0
- package/src/tool/brix-sorbet-density-calculator/i18n/sv.ts +295 -0
- package/src/tool/brix-sorbet-density-calculator/i18n/tr.ts +295 -0
- package/src/tool/brix-sorbet-density-calculator/i18n/zh.ts +295 -0
- package/src/tool/brix-sorbet-density-calculator/index.ts +11 -0
- package/src/tool/brix-sorbet-density-calculator/logic.ts +180 -0
- package/src/tool/brix-sorbet-density-calculator/script.ts +114 -0
- package/src/tool/brix-sorbet-density-calculator/seo.astro +15 -0
- package/src/tool/macaron-drying-predictor/bibliography.astro +6 -0
- package/src/tool/macaron-drying-predictor/bibliography.ts +14 -0
- package/src/tool/macaron-drying-predictor/component.astro +319 -0
- package/src/tool/macaron-drying-predictor/entry.ts +26 -0
- package/src/tool/macaron-drying-predictor/i18n/de.ts +245 -0
- package/src/tool/macaron-drying-predictor/i18n/en.ts +247 -0
- package/src/tool/macaron-drying-predictor/i18n/es.ts +245 -0
- package/src/tool/macaron-drying-predictor/i18n/fr.ts +245 -0
- package/src/tool/macaron-drying-predictor/i18n/id.ts +245 -0
- package/src/tool/macaron-drying-predictor/i18n/it.ts +245 -0
- package/src/tool/macaron-drying-predictor/i18n/ja.ts +245 -0
- package/src/tool/macaron-drying-predictor/i18n/ko.ts +245 -0
- package/src/tool/macaron-drying-predictor/i18n/nl.ts +245 -0
- package/src/tool/macaron-drying-predictor/i18n/pl.ts +245 -0
- package/src/tool/macaron-drying-predictor/i18n/pt.ts +245 -0
- package/src/tool/macaron-drying-predictor/i18n/ru.ts +245 -0
- package/src/tool/macaron-drying-predictor/i18n/sv.ts +245 -0
- package/src/tool/macaron-drying-predictor/i18n/tr.ts +245 -0
- package/src/tool/macaron-drying-predictor/i18n/zh.ts +245 -0
- package/src/tool/macaron-drying-predictor/index.ts +11 -0
- package/src/tool/macaron-drying-predictor/logic.ts +58 -0
- package/src/tool/macaron-drying-predictor/macaron-drying-predictor.css +551 -0
- package/src/tool/macaron-drying-predictor/seo.astro +15 -0
- package/src/tool/oil-smoke-point-tracker/bibliography.astro +6 -0
- package/src/tool/oil-smoke-point-tracker/bibliography.ts +10 -0
- package/src/tool/oil-smoke-point-tracker/component.astro +445 -0
- package/src/tool/oil-smoke-point-tracker/entry.ts +26 -0
- package/src/tool/oil-smoke-point-tracker/i18n/de.ts +285 -0
- package/src/tool/oil-smoke-point-tracker/i18n/en.ts +285 -0
- package/src/tool/oil-smoke-point-tracker/i18n/es.ts +285 -0
- package/src/tool/oil-smoke-point-tracker/i18n/fr.ts +285 -0
- package/src/tool/oil-smoke-point-tracker/i18n/id.ts +244 -0
- package/src/tool/oil-smoke-point-tracker/i18n/it.ts +244 -0
- package/src/tool/oil-smoke-point-tracker/i18n/ja.ts +244 -0
- package/src/tool/oil-smoke-point-tracker/i18n/ko.ts +244 -0
- package/src/tool/oil-smoke-point-tracker/i18n/nl.ts +244 -0
- package/src/tool/oil-smoke-point-tracker/i18n/pl.ts +244 -0
- package/src/tool/oil-smoke-point-tracker/i18n/pt.ts +244 -0
- package/src/tool/oil-smoke-point-tracker/i18n/ru.ts +244 -0
- package/src/tool/oil-smoke-point-tracker/i18n/sv.ts +244 -0
- package/src/tool/oil-smoke-point-tracker/i18n/tr.ts +244 -0
- package/src/tool/oil-smoke-point-tracker/i18n/zh.ts +244 -0
- package/src/tool/oil-smoke-point-tracker/index.ts +11 -0
- package/src/tool/oil-smoke-point-tracker/logic.ts +70 -0
- package/src/tool/oil-smoke-point-tracker/oil-smoke-point-tracker.css +937 -0
- package/src/tool/oil-smoke-point-tracker/seo.astro +15 -0
- package/src/tools.ts +6 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { BrixState } from './helpers';
|
|
2
|
+
import { getUnitLabel, formatWeight, formatWeightVal, cToF, getTempStatusDash } from './helpers';
|
|
3
|
+
|
|
4
|
+
export type { BrixState, SorbetInputs, SorbetResult } from './helpers';
|
|
5
|
+
export { SorbetLogic } from './helpers';
|
|
6
|
+
|
|
7
|
+
const STORAGE_KEY = 'bsdc-v1';
|
|
8
|
+
|
|
9
|
+
const LOAD_FIELDS = [
|
|
10
|
+
{ key: 'fw', elId: 'bsdc-fruit-weight' },
|
|
11
|
+
{ key: 'fb', elId: 'bsdc-fruit-brix' },
|
|
12
|
+
{ key: 'sw', elId: 'bsdc-sugar-weight' },
|
|
13
|
+
{ key: 'dw', elId: 'bsdc-dextrose-weight' },
|
|
14
|
+
{ key: 'ww', elId: 'bsdc-water-weight' },
|
|
15
|
+
{ key: 'tb', elId: 'bsdc-target-brix' },
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
function updateDisplayValues(state: BrixState): void {
|
|
19
|
+
const { elements: el, activeUnit } = state;
|
|
20
|
+
el.fruitWeightVal.textContent = String(formatWeightVal(parseFloat(el.fruitWeight.value) || 0, state));
|
|
21
|
+
el.fruitBrixVal.textContent = el.fruitBrix.value;
|
|
22
|
+
el.sugarWeightVal.textContent = String(formatWeightVal(parseFloat(el.sugarWeight.value) || 0, state));
|
|
23
|
+
el.dextroseWeightVal.textContent = String(formatWeightVal(parseFloat(el.dextroseWeight.value) || 0, state));
|
|
24
|
+
el.waterWeightVal.textContent = String(formatWeightVal(parseFloat(el.waterWeight.value) || 0, state));
|
|
25
|
+
el.targetBrixVal.textContent = el.targetBrix.value;
|
|
26
|
+
|
|
27
|
+
const labelText = getUnitLabel(state);
|
|
28
|
+
document.querySelectorAll('.bsdc-weight-unit').forEach((el2) => { el2.textContent = labelText; });
|
|
29
|
+
|
|
30
|
+
const labelsEl = document.getElementById('bsdc-temp-labels');
|
|
31
|
+
if (labelsEl) {
|
|
32
|
+
labelsEl.innerHTML = activeUnit === 'imperial'
|
|
33
|
+
? '<span>-13°F</span><span>-4°F</span><span>5°F</span><span>14°F</span><span>23°F</span>'
|
|
34
|
+
: '<span>-25°C</span><span>-20°C</span><span>-15°C</span><span>-10°C</span><span>-5°C</span>';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function updatePresetDropdownUI(state: BrixState): void {
|
|
39
|
+
const { selectOptions, selectedIcon, selectedName } = state.elements;
|
|
40
|
+
selectOptions.forEach((opt) => {
|
|
41
|
+
const isSelected = opt.getAttribute('data-value') === state.activePreset;
|
|
42
|
+
opt.classList.toggle('selected', isSelected);
|
|
43
|
+
if (!isSelected) return;
|
|
44
|
+
selectedIcon.innerHTML = (opt.querySelector('.bsdc-preset-icon') as HTMLElement).innerHTML;
|
|
45
|
+
const nameSpan = opt.querySelector('span:not(.bsdc-preset-icon)');
|
|
46
|
+
if (nameSpan) selectedName.textContent = nameSpan.textContent;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function updateBadge(brix: number, state: BrixState): void {
|
|
51
|
+
const card = document.querySelector('.bsdc-card');
|
|
52
|
+
if (card) card.classList.remove('status-hard', 'status-soft', 'status-optimal');
|
|
53
|
+
const { statusBadge } = state.elements;
|
|
54
|
+
const s = state.ui;
|
|
55
|
+
if (brix < 25) {
|
|
56
|
+
statusBadge.textContent = s.statusHard;
|
|
57
|
+
statusBadge.className = 'bsdc-badge hard';
|
|
58
|
+
if (card) card.classList.add('status-hard');
|
|
59
|
+
} else if (brix > 30) {
|
|
60
|
+
statusBadge.textContent = s.statusSoft;
|
|
61
|
+
statusBadge.className = 'bsdc-badge soft';
|
|
62
|
+
if (card) card.classList.add('status-soft');
|
|
63
|
+
} else {
|
|
64
|
+
statusBadge.textContent = s.statusOptimal;
|
|
65
|
+
statusBadge.className = 'bsdc-badge optimal';
|
|
66
|
+
if (card) card.classList.add('status-optimal');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function updateTempMarker(tMin: number, tMax: number): void {
|
|
71
|
+
const marker = document.getElementById('bsdc-temp-marker');
|
|
72
|
+
if (!marker) return;
|
|
73
|
+
marker.classList.remove('out-of-range-cold', 'out-of-range-warm');
|
|
74
|
+
if (tMin < -25) {
|
|
75
|
+
marker.style.left = '0%';
|
|
76
|
+
marker.style.width = '12%';
|
|
77
|
+
marker.classList.add('out-of-range-cold');
|
|
78
|
+
} else if (tMax > -5) {
|
|
79
|
+
marker.style.left = '88%';
|
|
80
|
+
marker.style.width = '12%';
|
|
81
|
+
marker.classList.add('out-of-range-warm');
|
|
82
|
+
} else {
|
|
83
|
+
const pMin = Math.max(0, Math.min(100, ((tMin + 25) / 20) * 100));
|
|
84
|
+
const pMax = Math.max(0, Math.min(100, ((tMax + 25) / 20) * 100));
|
|
85
|
+
marker.style.left = pMin.toFixed(1) + '%';
|
|
86
|
+
marker.style.width = Math.max(8, pMax - pMin).toFixed(1) + '%';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function updateTempGauge(totalPAC: number, state: BrixState): void {
|
|
91
|
+
const tIdeal = -(totalPAC / 18);
|
|
92
|
+
const tMin = tIdeal - 2.5;
|
|
93
|
+
const tMax = tIdeal + 2.5;
|
|
94
|
+
updateTempMarker(tMin, tMax);
|
|
95
|
+
|
|
96
|
+
const rangeVal = document.getElementById('bsdc-temp-range-val');
|
|
97
|
+
if (rangeVal) {
|
|
98
|
+
if (state.activeUnit === 'imperial') {
|
|
99
|
+
const fIdeal = cToF(tIdeal);
|
|
100
|
+
rangeVal.textContent = (fIdeal - 4.5).toFixed(0) + '°F to ' + (fIdeal + 4.5).toFixed(0) + '°F';
|
|
101
|
+
} else {
|
|
102
|
+
rangeVal.textContent = tMin.toFixed(0) + '°C to ' + tMax.toFixed(0) + '°C';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const tempStatus = document.getElementById('bsdc-temp-status');
|
|
107
|
+
if (tempStatus) {
|
|
108
|
+
const st = getTempStatusDash(tIdeal);
|
|
109
|
+
tempStatus.textContent = st.text;
|
|
110
|
+
tempStatus.className = st.className;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function calculate(state: BrixState): void {
|
|
115
|
+
const { elements: el } = state;
|
|
116
|
+
const fw = parseFloat(el.fruitWeight.value) || 0;
|
|
117
|
+
const fb = parseFloat(el.fruitBrix.value) || 0;
|
|
118
|
+
const sw = parseFloat(el.sugarWeight.value) || 0;
|
|
119
|
+
const dw = parseFloat(el.dextroseWeight.value) || 0;
|
|
120
|
+
const ww = parseFloat(el.waterWeight.value) || 0;
|
|
121
|
+
const tb = parseFloat(el.targetBrix.value) || 28;
|
|
122
|
+
const fruitSugar = fw * (fb / 100);
|
|
123
|
+
const totalWeight = fw + ww + sw + dw;
|
|
124
|
+
const brix = totalWeight > 0 ? ((fruitSugar + sw + dw) / totalWeight) * 100 : 0;
|
|
125
|
+
const totalPAC = fruitSugar * 1.9 + sw + dw * 1.9;
|
|
126
|
+
el.totalBrixDisp.textContent = brix.toFixed(1) + '%';
|
|
127
|
+
el.totalWeightDisp.textContent = formatWeight(totalWeight, state);
|
|
128
|
+
el.totalPacDisp.textContent = totalPAC.toFixed(1);
|
|
129
|
+
el.lens.style.transform = `translateY(-${Math.min(50, (brix / 50) * 50) * 0.5}%)`;
|
|
130
|
+
updateBadge(brix, state);
|
|
131
|
+
updateTempGauge(totalPAC, state);
|
|
132
|
+
const recSugar = Math.max(0, fw * (2 * (tb / 100) - fb / 100));
|
|
133
|
+
const recWater = Math.max(0, fw - recSugar);
|
|
134
|
+
el.recSugarDisp.textContent = formatWeight(recSugar, state);
|
|
135
|
+
el.recWaterDisp.textContent = formatWeight(recWater, state);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function save(state: BrixState): void {
|
|
139
|
+
try {
|
|
140
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
|
141
|
+
fw: state.elements.fruitWeight.value,
|
|
142
|
+
fb: state.elements.fruitBrix.value,
|
|
143
|
+
sw: state.elements.sugarWeight.value,
|
|
144
|
+
dw: state.elements.dextroseWeight.value,
|
|
145
|
+
ww: state.elements.waterWeight.value,
|
|
146
|
+
tb: state.elements.targetBrix.value,
|
|
147
|
+
pr: state.activePreset,
|
|
148
|
+
un: state.activeUnit,
|
|
149
|
+
}));
|
|
150
|
+
} catch {}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function load(state: BrixState): void {
|
|
154
|
+
try {
|
|
155
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
156
|
+
if (!raw) return;
|
|
157
|
+
const data = JSON.parse(raw) as Record<string, string>;
|
|
158
|
+
if (data.un) state.activeUnit = data.un;
|
|
159
|
+
if (data.pr) state.activePreset = data.pr;
|
|
160
|
+
LOAD_FIELDS.forEach(({ key, elId }) => {
|
|
161
|
+
if (data[key] !== undefined) {
|
|
162
|
+
const el = document.getElementById(elId) as HTMLInputElement;
|
|
163
|
+
if (el) el.value = data[key];
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
} catch {}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function refresh(state: BrixState): void {
|
|
170
|
+
updateDisplayValues(state);
|
|
171
|
+
calculate(state);
|
|
172
|
+
save(state);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function toggleUnit(unit: string, state: BrixState): void {
|
|
176
|
+
state.activeUnit = unit;
|
|
177
|
+
state.elements.unitMetricBtn.classList.toggle('active', unit === 'metric');
|
|
178
|
+
state.elements.unitImperialBtn.classList.toggle('active', unit === 'imperial');
|
|
179
|
+
refresh(state);
|
|
180
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { BrixState } from './logic';
|
|
2
|
+
import { toggleUnit, load, updatePresetDropdownUI, refresh } from './logic';
|
|
3
|
+
|
|
4
|
+
function createElements() {
|
|
5
|
+
const g = (id: string) => document.getElementById(id);
|
|
6
|
+
return {
|
|
7
|
+
selectTrigger: g('bsdc-select-trigger')!,
|
|
8
|
+
selectDropdown: g('bsdc-select-dropdown')!,
|
|
9
|
+
selectOptions: document.querySelectorAll('.bsdc-select-option'),
|
|
10
|
+
selectedIcon: g('bsdc-selected-icon')!,
|
|
11
|
+
selectedName: g('bsdc-selected-name')!,
|
|
12
|
+
fruitWeight: g('bsdc-fruit-weight') as HTMLInputElement,
|
|
13
|
+
fruitBrix: g('bsdc-fruit-brix') as HTMLInputElement,
|
|
14
|
+
sugarWeight: g('bsdc-sugar-weight') as HTMLInputElement,
|
|
15
|
+
dextroseWeight: g('bsdc-dextrose-weight') as HTMLInputElement,
|
|
16
|
+
waterWeight: g('bsdc-water-weight') as HTMLInputElement,
|
|
17
|
+
targetBrix: g('bsdc-target-brix') as HTMLInputElement,
|
|
18
|
+
fruitWeightVal: g('bsdc-fruit-weight-val')!,
|
|
19
|
+
fruitBrixVal: g('bsdc-fruit-brix-val')!,
|
|
20
|
+
sugarWeightVal: g('bsdc-sugar-weight-val')!,
|
|
21
|
+
dextroseWeightVal: g('bsdc-dextrose-weight-val')!,
|
|
22
|
+
waterWeightVal: g('bsdc-water-weight-val')!,
|
|
23
|
+
targetBrixVal: g('bsdc-target-brix-val')!,
|
|
24
|
+
totalBrixDisp: g('bsdc-total-brix')!,
|
|
25
|
+
totalWeightDisp: g('bsdc-total-weight')!,
|
|
26
|
+
totalPacDisp: g('bsdc-total-pac')!,
|
|
27
|
+
statusBadge: g('bsdc-status-badge')!,
|
|
28
|
+
lens: g('bsdc-lens')!,
|
|
29
|
+
recSugarDisp: g('bsdc-rec-sugar')!,
|
|
30
|
+
recWaterDisp: g('bsdc-rec-water')!,
|
|
31
|
+
unitMetricBtn: g('bsdc-unit-metric')!, unitImperialBtn: g('bsdc-unit-imperial')!,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function bindSelectDropdown(el: BrixState['elements']): void {
|
|
36
|
+
el.selectTrigger.addEventListener('click', (e) => {
|
|
37
|
+
e.stopPropagation();
|
|
38
|
+
el.selectTrigger.classList.toggle('open');
|
|
39
|
+
el.selectDropdown.classList.toggle('open');
|
|
40
|
+
});
|
|
41
|
+
document.addEventListener('click', (e) => {
|
|
42
|
+
if (!el.selectTrigger.contains(e.target as Node) && !el.selectDropdown.contains(e.target as Node)) {
|
|
43
|
+
el.selectTrigger.classList.remove('open');
|
|
44
|
+
el.selectDropdown.classList.remove('open');
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function bindSelectOptions(state: BrixState): void {
|
|
50
|
+
const { elements: el } = state;
|
|
51
|
+
el.selectOptions.forEach((opt) => {
|
|
52
|
+
opt.addEventListener('click', () => {
|
|
53
|
+
const val = opt.getAttribute('data-value')!;
|
|
54
|
+
state.activePreset = val;
|
|
55
|
+
if (val !== 'custom') el.fruitBrix.value = val;
|
|
56
|
+
el.selectTrigger.classList.remove('open');
|
|
57
|
+
el.selectDropdown.classList.remove('open');
|
|
58
|
+
updatePresetDropdownUI(state);
|
|
59
|
+
refresh(state);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function bindAdjusters(state: BrixState): void {
|
|
65
|
+
document.querySelectorAll('.bsdc-adjuster-btn').forEach((btn) => {
|
|
66
|
+
btn.addEventListener('click', () => {
|
|
67
|
+
const targetId = btn.getAttribute('data-target')!;
|
|
68
|
+
const slider = document.getElementById(targetId) as HTMLInputElement;
|
|
69
|
+
if (!slider) return;
|
|
70
|
+
const step = parseFloat(slider.getAttribute('step') || '1');
|
|
71
|
+
const cur = parseFloat(slider.value) || 0;
|
|
72
|
+
const dir = btn.getAttribute('data-dir');
|
|
73
|
+
slider.value = String(Math.max(parseFloat(slider.min), Math.min(parseFloat(slider.max), cur + (dir === 'up' ? step : -step))));
|
|
74
|
+
if (targetId === 'bsdc-fruit-brix') {
|
|
75
|
+
state.activePreset = 'custom';
|
|
76
|
+
updatePresetDropdownUI(state);
|
|
77
|
+
}
|
|
78
|
+
refresh(state);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function bindSliderInputs(state: BrixState): void {
|
|
84
|
+
const { elements: el } = state;
|
|
85
|
+
el.fruitWeight.addEventListener('input', () => refresh(state));
|
|
86
|
+
el.fruitBrix.addEventListener('input', () => {
|
|
87
|
+
state.activePreset = 'custom';
|
|
88
|
+
updatePresetDropdownUI(state);
|
|
89
|
+
refresh(state);
|
|
90
|
+
});
|
|
91
|
+
el.sugarWeight.addEventListener('input', () => refresh(state));
|
|
92
|
+
el.dextroseWeight.addEventListener('input', () => refresh(state));
|
|
93
|
+
el.waterWeight.addEventListener('input', () => refresh(state));
|
|
94
|
+
el.targetBrix.addEventListener('input', () => refresh(state));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function bindEvents(state: BrixState): void {
|
|
98
|
+
const { elements: el } = state;
|
|
99
|
+
el.unitMetricBtn.addEventListener('click', () => toggleUnit('metric', state));
|
|
100
|
+
el.unitImperialBtn.addEventListener('click', () => toggleUnit('imperial', state));
|
|
101
|
+
bindSelectDropdown(el);
|
|
102
|
+
bindSelectOptions(state);
|
|
103
|
+
bindAdjusters(state);
|
|
104
|
+
bindSliderInputs(state);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function initBrixCalculator(ui: Record<string, string>): void {
|
|
108
|
+
const elements = createElements();
|
|
109
|
+
const state: BrixState = { activeUnit: 'metric', activePreset: '14', ui, elements };
|
|
110
|
+
bindEvents(state);
|
|
111
|
+
load(state);
|
|
112
|
+
updatePresetDropdownUI(state);
|
|
113
|
+
toggleUnit(state.activeUnit, state);
|
|
114
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { brixSorbetDensity } from './entry';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const content = await brixSorbetDensity.i18n[locale]?.();
|
|
12
|
+
if (!content) return null;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const bibliography = [
|
|
2
|
+
{
|
|
3
|
+
name: 'Les Petits Macarons: Colorful French Confections to Make at Home - Kathryn Gordon, Anne E. McBride',
|
|
4
|
+
url: 'https://www.scribd.com/document/484527225/Les-Petits-Macarons-Colorful-French-Confections-to-Make-at-Home-PDFDrive-pdf',
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
name: 'Macarons: The definitive guide to mastering the basics',
|
|
8
|
+
url: 'https://michellesmacarons.com/french-macaron-recipe/',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: 'The Science Behind Macarons',
|
|
12
|
+
url: 'https://docmacaron.com/492/science-behind-macarons/',
|
|
13
|
+
},
|
|
14
|
+
];
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
ui: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { ui } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div class="md">
|
|
10
|
+
<div class="md-card">
|
|
11
|
+
<div class="md-unit-row">
|
|
12
|
+
<button type="button" id="md-unit-metric" class="md-unit-btn active">Metric</button>
|
|
13
|
+
<button type="button" id="md-unit-imperial" class="md-unit-btn">Imperial</button>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="md-body">
|
|
16
|
+
<div class="md-controls">
|
|
17
|
+
<div class="md-input-row">
|
|
18
|
+
<div class="md-input-group">
|
|
19
|
+
<label class="md-label" for="md-humidity">{ui.humidityLabel}</label>
|
|
20
|
+
<div class="md-slider-wrap">
|
|
21
|
+
<input type="range" id="md-humidity" class="md-slider" min="20" max="90" value="50" />
|
|
22
|
+
<span class="md-value"><span id="md-humidity-val">50</span><span class="md-unit">{ui.humidityUnit}</span></span>
|
|
23
|
+
</div>
|
|
24
|
+
<span id="md-humidity-tag" class="md-tag md-tag-humid-normal">{ui.humidityTagNormal}</span>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="md-input-row">
|
|
28
|
+
<div class="md-input-group">
|
|
29
|
+
<label class="md-label" for="md-temp">{ui.tempLabel}</label>
|
|
30
|
+
<div class="md-slider-wrap">
|
|
31
|
+
<input type="range" id="md-temp" class="md-slider" min="15" max="35" value="22" />
|
|
32
|
+
<span class="md-value"><span id="md-temp-val">22</span><span class="md-unit" id="md-temp-unit">{ui.tempUnit}</span></span>
|
|
33
|
+
</div>
|
|
34
|
+
<span id="md-temp-tag" class="md-tag md-tag-temp-ideal">{ui.tempTagIdeal}</span>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="md-input-row">
|
|
38
|
+
<div class="md-input-group">
|
|
39
|
+
<label class="md-label" for="md-size">{ui.sizeLabel}</label>
|
|
40
|
+
<div class="md-slider-wrap">
|
|
41
|
+
<input type="range" id="md-size" class="md-slider" min="2" max="6" step="0.5" value="3" />
|
|
42
|
+
<span class="md-value"><span id="md-size-val">3</span><span class="md-unit" id="md-size-unit">{ui.sizeUnit}</span></span>
|
|
43
|
+
</div>
|
|
44
|
+
<span id="md-size-tag" class="md-tag md-tag-size-standard">{ui.sizeTagStandard}</span>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="md-result" id="md-result">
|
|
50
|
+
<div class="md-macaron-visual">
|
|
51
|
+
<svg viewBox="0 0 200 160" class="md-macaron-svg">
|
|
52
|
+
<defs>
|
|
53
|
+
<radialGradient id="md-shell-top" cx="50%" cy="40%" r="50%">
|
|
54
|
+
<stop offset="0%" stop-color="#f9a8d4" />
|
|
55
|
+
<stop offset="100%" stop-color="#ec4899" />
|
|
56
|
+
</radialGradient>
|
|
57
|
+
<radialGradient id="md-shell-top-dry" cx="50%" cy="40%" r="50%">
|
|
58
|
+
<stop offset="0%" stop-color="#fce7f3" />
|
|
59
|
+
<stop offset="100%" stop-color="#f9a8d4" />
|
|
60
|
+
</radialGradient>
|
|
61
|
+
<radialGradient id="md-shell-top-overdry" cx="50%" cy="40%" r="50%">
|
|
62
|
+
<stop offset="0%" stop-color="#f1f5f9" />
|
|
63
|
+
<stop offset="100%" stop-color="#e2e8f0" />
|
|
64
|
+
</radialGradient>
|
|
65
|
+
<filter id="md-shell-glow">
|
|
66
|
+
<feGaussianBlur stdDeviation="3" result="blur" />
|
|
67
|
+
<feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge>
|
|
68
|
+
</filter>
|
|
69
|
+
</defs>
|
|
70
|
+
<ellipse id="md-shell-bottom" cx="100" cy="100" rx="70" ry="22" fill="#be185d" />
|
|
71
|
+
<ellipse id="md-shell-bottom-inner" cx="100" cy="96" rx="60" ry="16" fill="#fce7f3" />
|
|
72
|
+
<rect id="md-shell-feet" x="25" y="88" width="150" height="14" rx="7" fill="#9d174d" opacity="0.3" />
|
|
73
|
+
<ellipse id="md-shell-top" cx="100" cy="68" rx="65" ry="36" fill="url(#md-shell-top)" filter="url(#md-shell-glow)" />
|
|
74
|
+
<text id="md-minutes-display" x="100" y="74" text-anchor="middle" class="md-macaron-minutes">30</text>
|
|
75
|
+
<text x="100" y="92" text-anchor="middle" class="md-macaron-unit">{ui.minutesUnit}</text>
|
|
76
|
+
</svg>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div class="md-stats-row">
|
|
80
|
+
<div class="md-stat">
|
|
81
|
+
<span class="md-stat-label">{ui.readinessLabel}</span>
|
|
82
|
+
<span id="md-readiness-val" class="md-stat-value">75%</span>
|
|
83
|
+
</div>
|
|
84
|
+
<div id="md-skin-badge" class="md-skin-badge ready">{ui.skinStatusReady}</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div id="md-humidity-desc" class="md-info-card"></div>
|
|
88
|
+
<div id="md-humidity-note" class="md-info-note"></div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="md-footer" id="md-footer">
|
|
93
|
+
<div class="md-footer-row">
|
|
94
|
+
<div class="md-footer-chip md-footer-chip-time">
|
|
95
|
+
<svg width="16" height="16" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/><polyline points="12,6 12,12 16,14" fill="none" stroke="currentColor" stroke-width="2"/></svg>
|
|
96
|
+
<span id="md-footer-time">30 min</span>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="md-footer-chip md-footer-chip-humidity">
|
|
99
|
+
<svg width="16" height="16" viewBox="0 0 24 24"><path d="M12 2C12 2 4 12 4 16a8 8 0 0 0 16 0C20 12 12 2 12 2z" fill="currentColor"/></svg>
|
|
100
|
+
<span id="md-footer-humidity">50%</span>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="md-footer-chip md-footer-chip-temp">
|
|
103
|
+
<svg width="16" height="16" viewBox="0 0 24 24"><path d="M12 2v6a4 4 0 1 0 0 8v6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
|
104
|
+
<span id="md-footer-temp">22°C</span>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="md-footer-chip md-footer-chip-size">
|
|
107
|
+
<svg width="16" height="16" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3" fill="currentColor"/><circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
|
|
108
|
+
<span id="md-footer-size">3cm</span>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<script is:inline define:vars={{ ui }}>
|
|
116
|
+
const $ = (id) => document.getElementById(id);
|
|
117
|
+
|
|
118
|
+
const humidity = $('md-humidity');
|
|
119
|
+
const temp = $('md-temp');
|
|
120
|
+
const size = $('md-size');
|
|
121
|
+
const hVal = $('md-humidity-val');
|
|
122
|
+
const tVal = $('md-temp-val');
|
|
123
|
+
const sVal = $('md-size-val');
|
|
124
|
+
const hTag = $('md-humidity-tag');
|
|
125
|
+
const tTag = $('md-temp-tag');
|
|
126
|
+
const sTag = $('md-size-tag');
|
|
127
|
+
const result = $('md-result');
|
|
128
|
+
const minutesDisp = $('md-minutes-display');
|
|
129
|
+
const readinessVal = $('md-readiness-val');
|
|
130
|
+
const skinBadge = $('md-skin-badge');
|
|
131
|
+
const humidityDesc = $('md-humidity-desc');
|
|
132
|
+
const humidityNote = $('md-humidity-note');
|
|
133
|
+
const footerTime = $('md-footer-time');
|
|
134
|
+
const footerHumidity = $('md-footer-humidity');
|
|
135
|
+
const footerTemp = $('md-footer-temp');
|
|
136
|
+
const footerSize = $('md-footer-size');
|
|
137
|
+
const footer = $('md-footer');
|
|
138
|
+
const shellTop = $('md-shell-top');
|
|
139
|
+
const unitMetric = $('md-unit-metric');
|
|
140
|
+
const unitImperial = $('md-unit-imperial');
|
|
141
|
+
const tUnit = $('md-temp-unit');
|
|
142
|
+
const sUnit = $('md-size-unit');
|
|
143
|
+
|
|
144
|
+
let unit = 'metric';
|
|
145
|
+
|
|
146
|
+
function cToF(c) { return c * 9/5 + 32; }
|
|
147
|
+
function fToC(f) { return (f - 32) * 5/9; }
|
|
148
|
+
function cmToIn(cm) { return cm / 2.54; }
|
|
149
|
+
function inToCm(inch) { return inch * 2.54; }
|
|
150
|
+
|
|
151
|
+
function getTempUnit() { return unit === 'imperial' ? ui.tempUnitF : ui.tempUnit; }
|
|
152
|
+
function getSizeUnit() { return unit === 'imperial' ? ui.sizeUnitIn : ui.sizeUnit; }
|
|
153
|
+
|
|
154
|
+
function setTempBounds(isImp) {
|
|
155
|
+
temp.setAttribute('min', isImp ? '59' : '15');
|
|
156
|
+
temp.setAttribute('max', isImp ? '95' : '35');
|
|
157
|
+
temp.setAttribute('step', '1');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function setSizeBounds(isImp) {
|
|
161
|
+
size.setAttribute('min', isImp ? '0.8' : '2');
|
|
162
|
+
size.setAttribute('max', isImp ? '2.4' : '6');
|
|
163
|
+
size.setAttribute('step', isImp ? '0.2' : '0.5');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function switchUnit(to) {
|
|
167
|
+
if (unit === to) return;
|
|
168
|
+
const tv = parseInt(temp.value) || 22;
|
|
169
|
+
const sv = parseFloat(size.value) || 3;
|
|
170
|
+
unit = to;
|
|
171
|
+
unitMetric.classList.toggle('active', to === 'metric');
|
|
172
|
+
unitImperial.classList.toggle('active', to === 'imperial');
|
|
173
|
+
|
|
174
|
+
const isImp = to === 'imperial';
|
|
175
|
+
setTempBounds(isImp);
|
|
176
|
+
setSizeBounds(isImp);
|
|
177
|
+
temp.value = isImp ? Math.round(cToF(tv)).toString() : tv.toString();
|
|
178
|
+
size.value = isImp ? cmToIn(sv).toFixed(1) : sv.toString();
|
|
179
|
+
|
|
180
|
+
tUnit.textContent = getTempUnit();
|
|
181
|
+
sUnit.textContent = getSizeUnit();
|
|
182
|
+
refresh();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function getTempC() {
|
|
186
|
+
const tv = parseInt(temp.value) || 22;
|
|
187
|
+
return unit === 'imperial' ? fToC(tv) : tv;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getSizeCm() {
|
|
191
|
+
const sv = parseFloat(size.value) || 3;
|
|
192
|
+
return unit === 'imperial' ? inToCm(sv) : sv;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function formatTemp(tc) { return unit === 'imperial' ? Math.round(cToF(tc)).toString() : tc.toString(); }
|
|
196
|
+
function formatSize(sc) { return unit === 'imperial' ? cmToIn(sc).toFixed(1) : sc.toString(); }
|
|
197
|
+
|
|
198
|
+
function updateHumidityTag(hv) {
|
|
199
|
+
if (hv >= 65) {
|
|
200
|
+
hTag.textContent = ui.humidityTagHigh; hTag.className = 'md-tag md-tag-humid-high';
|
|
201
|
+
humidityDesc.className = 'md-info-card md-info-high';
|
|
202
|
+
humidityDesc.innerHTML = '<svg class="md-info-svg" viewBox="0 0 24 24"><path d="M12 2C12 2 4 12 4 16a8 8 0 0 0 16 0C20 12 12 2 12 2z" fill="currentColor"/><path d="M12 6v4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg><span>' + ui.humidityDescHigh + '</span>';
|
|
203
|
+
} else if (hv <= 35) {
|
|
204
|
+
hTag.textContent = ui.humidityTagLow; hTag.className = 'md-tag md-tag-humid-low';
|
|
205
|
+
humidityDesc.className = 'md-info-card md-info-low';
|
|
206
|
+
humidityDesc.innerHTML = '<svg class="md-info-svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="5" fill="currentColor"/><g stroke="currentColor" stroke-width="1.5"><line x1="12" y1="1" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="23"/><line x1="1" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="23" y2="12"/><line x1="4.2" y1="4.2" x2="6.3" y2="6.3"/><line x1="17.7" y1="17.7" x2="19.8" y2="19.8"/><line x1="4.2" y1="19.8" x2="6.3" y2="17.7"/><line x1="17.7" y1="6.3" x2="19.8" y2="4.2"/></g></svg><span>' + ui.humidityDescLow + '</span>';
|
|
207
|
+
} else {
|
|
208
|
+
hTag.textContent = ui.humidityTagNormal; hTag.className = 'md-tag md-tag-humid-normal';
|
|
209
|
+
humidityDesc.className = 'md-info-card md-info-ok';
|
|
210
|
+
humidityDesc.innerHTML = '<svg class="md-info-svg" viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>' + ui.humidityDescNormal + '</span>';
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function updateTempTag(tv) {
|
|
215
|
+
if (tv >= 28) { tTag.textContent = ui.tempTagHot; tTag.className = 'md-tag md-tag-temp-hot'; }
|
|
216
|
+
else if (tv <= 18) { tTag.textContent = ui.tempTagCool; tTag.className = 'md-tag md-tag-temp-cool'; }
|
|
217
|
+
else { tTag.textContent = ui.tempTagIdeal; tTag.className = 'md-tag md-tag-temp-ideal'; }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function updateSizeTag(sv) {
|
|
221
|
+
if (sv <= 2.5) { sTag.textContent = ui.sizeTagMini; sTag.className = 'md-tag md-tag-size-mini'; }
|
|
222
|
+
else if (sv >= 4.5) { sTag.textContent = ui.sizeTagLarge; sTag.className = 'md-tag md-tag-size-large'; }
|
|
223
|
+
else { sTag.textContent = ui.sizeTagStandard; sTag.className = 'md-tag md-tag-size-standard'; }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function updateTags() {
|
|
227
|
+
const hv = parseInt(humidity.value);
|
|
228
|
+
const tv = parseInt(temp.value);
|
|
229
|
+
const sv = parseFloat(size.value);
|
|
230
|
+
hVal.textContent = hv.toString();
|
|
231
|
+
tVal.textContent = tv.toString();
|
|
232
|
+
sVal.textContent = sv.toString();
|
|
233
|
+
updateHumidityTag(hv);
|
|
234
|
+
updateTempTag(tv);
|
|
235
|
+
updateSizeTag(sv);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function updateSkinBadge(readiness) {
|
|
239
|
+
readinessVal.textContent = readiness + '%';
|
|
240
|
+
if (readiness >= 90) {
|
|
241
|
+
readinessVal.style.color = 'var(--md-green)';
|
|
242
|
+
skinBadge.textContent = ui.skinStatusReady;
|
|
243
|
+
skinBadge.className = 'md-skin-badge ready';
|
|
244
|
+
shellTop.setAttribute('fill', 'url(#md-shell-top)');
|
|
245
|
+
} else if (readiness >= 60) {
|
|
246
|
+
readinessVal.style.color = 'var(--md-amber)';
|
|
247
|
+
skinBadge.textContent = ui.skinStatusForming;
|
|
248
|
+
skinBadge.className = 'md-skin-badge forming';
|
|
249
|
+
shellTop.setAttribute('fill', 'url(#md-shell-top-dry)');
|
|
250
|
+
} else if (readiness >= 20) {
|
|
251
|
+
readinessVal.style.color = 'var(--md-pink)';
|
|
252
|
+
skinBadge.textContent = ui.skinStatusSticky;
|
|
253
|
+
skinBadge.className = 'md-skin-badge sticky';
|
|
254
|
+
shellTop.setAttribute('fill', 'url(#md-shell-top)');
|
|
255
|
+
} else {
|
|
256
|
+
readinessVal.style.color = 'var(--md-gray)';
|
|
257
|
+
skinBadge.textContent = ui.skinStatusOverDry;
|
|
258
|
+
skinBadge.className = 'md-skin-badge over-dry';
|
|
259
|
+
shellTop.setAttribute('fill', 'url(#md-shell-top-overdry)');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function compute() {
|
|
264
|
+
const hv = parseInt(humidity.value);
|
|
265
|
+
const tc = getTempC();
|
|
266
|
+
const sc = getSizeCm();
|
|
267
|
+
const hc = Math.max(20, Math.min(90, hv));
|
|
268
|
+
const tcc = Math.max(15, Math.min(35, tc));
|
|
269
|
+
const dcc = Math.max(2, Math.min(6, sc));
|
|
270
|
+
|
|
271
|
+
const hf = 1 + Math.pow((hc - 30) / 30, 2) * 2;
|
|
272
|
+
const tf = 1 + (25 - tcc) * 0.04;
|
|
273
|
+
const sf = Math.pow(dcc / 3, 1.5);
|
|
274
|
+
const minutes = Math.round(30 * hf * tf * sf);
|
|
275
|
+
const readiness = Math.max(0, Math.min(100, Math.round(100 - (hf - 1) * 50 - (tf - 1) * 30)));
|
|
276
|
+
|
|
277
|
+
minutesDisp.textContent = minutes.toString();
|
|
278
|
+
updateSkinBadge(readiness);
|
|
279
|
+
humidityNote.textContent = Math.round(hf * 100) / 100 !== 1 ? hf.toFixed(1) + 'x factor' : '';
|
|
280
|
+
|
|
281
|
+
footerTime.textContent = minutes + ' ' + ui.minutesUnit;
|
|
282
|
+
footerHumidity.textContent = hv + ui.humidityUnit;
|
|
283
|
+
footerTemp.textContent = formatTemp(tc) + getTempUnit();
|
|
284
|
+
footerSize.textContent = formatSize(sc) + ' ' + getSizeUnit();
|
|
285
|
+
footer.style.display = 'flex';
|
|
286
|
+
result.style.display = 'flex';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const STORAGE_KEY = 'md-v1';
|
|
290
|
+
|
|
291
|
+
function save() {
|
|
292
|
+
try {
|
|
293
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify({ h: humidity.value, t: temp.value, s: size.value, u: unit }));
|
|
294
|
+
} catch {}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function load() {
|
|
298
|
+
try {
|
|
299
|
+
const d = localStorage.getItem(STORAGE_KEY);
|
|
300
|
+
if (!d) return;
|
|
301
|
+
const v = JSON.parse(d);
|
|
302
|
+
if (v.u === 'imperial') switchUnit('imperial');
|
|
303
|
+
if (v.h) humidity.value = v.h;
|
|
304
|
+
if (v.t) temp.value = v.t;
|
|
305
|
+
if (v.s) size.value = v.s;
|
|
306
|
+
} catch {}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function refresh() { updateTags(); compute(); save(); }
|
|
310
|
+
|
|
311
|
+
unitMetric.addEventListener('click', function() { switchUnit('metric'); });
|
|
312
|
+
unitImperial.addEventListener('click', function() { switchUnit('imperial'); });
|
|
313
|
+
humidity.addEventListener('input', refresh);
|
|
314
|
+
temp.addEventListener('input', refresh);
|
|
315
|
+
size.addEventListener('input', refresh);
|
|
316
|
+
|
|
317
|
+
load();
|
|
318
|
+
refresh();
|
|
319
|
+
</script>
|