@jjlmoya/utils-cooking 1.24.0 → 1.25.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.
Files changed (249) hide show
  1. package/package.json +1 -1
  2. package/src/entries.ts +3 -1
  3. package/src/index.ts +1 -0
  4. package/src/pages/[locale]/[slug].astro +56 -19
  5. package/src/tests/i18n-titles.test.ts +5 -17
  6. package/src/tests/locale_completeness.test.ts +2 -13
  7. package/src/tests/shared-test-helpers.ts +56 -0
  8. package/src/tests/tool_exports.test.ts +34 -0
  9. package/src/tests/tool_validation.test.ts +2 -2
  10. package/src/tool/american-kitchen-converter/bibliography.astro +3 -3
  11. package/src/tool/american-kitchen-converter/bibliography.ts +10 -0
  12. package/src/tool/american-kitchen-converter/i18n/de.ts +2 -11
  13. package/src/tool/american-kitchen-converter/i18n/en.ts +2 -11
  14. package/src/tool/american-kitchen-converter/i18n/es.ts +40 -49
  15. package/src/tool/american-kitchen-converter/i18n/fr.ts +2 -21
  16. package/src/tool/american-kitchen-converter/i18n/id.ts +2 -11
  17. package/src/tool/american-kitchen-converter/i18n/it.ts +2 -11
  18. package/src/tool/american-kitchen-converter/i18n/ja.ts +2 -11
  19. package/src/tool/american-kitchen-converter/i18n/ko.ts +2 -11
  20. package/src/tool/american-kitchen-converter/i18n/nl.ts +2 -11
  21. package/src/tool/american-kitchen-converter/i18n/pl.ts +2 -11
  22. package/src/tool/american-kitchen-converter/i18n/pt.ts +2 -11
  23. package/src/tool/american-kitchen-converter/i18n/ru.ts +2 -11
  24. package/src/tool/american-kitchen-converter/i18n/sv.ts +2 -11
  25. package/src/tool/american-kitchen-converter/i18n/tr.ts +2 -11
  26. package/src/tool/american-kitchen-converter/i18n/zh.ts +2 -11
  27. package/src/tool/american-kitchen-converter/seo.astro +11 -4
  28. package/src/tool/banana-ripeness/bibliography.astro +3 -3
  29. package/src/tool/banana-ripeness/bibliography.ts +10 -0
  30. package/src/tool/banana-ripeness/i18n/de.ts +2 -15
  31. package/src/tool/banana-ripeness/i18n/en.ts +36 -49
  32. package/src/tool/banana-ripeness/i18n/es.ts +36 -49
  33. package/src/tool/banana-ripeness/i18n/fr.ts +36 -49
  34. package/src/tool/banana-ripeness/i18n/id.ts +2 -15
  35. package/src/tool/banana-ripeness/i18n/it.ts +2 -15
  36. package/src/tool/banana-ripeness/i18n/ja.ts +2 -15
  37. package/src/tool/banana-ripeness/i18n/ko.ts +2 -15
  38. package/src/tool/banana-ripeness/i18n/nl.ts +2 -15
  39. package/src/tool/banana-ripeness/i18n/pl.ts +2 -15
  40. package/src/tool/banana-ripeness/i18n/pt.ts +2 -15
  41. package/src/tool/banana-ripeness/i18n/ru.ts +2 -15
  42. package/src/tool/banana-ripeness/i18n/sv.ts +2 -15
  43. package/src/tool/banana-ripeness/i18n/tr.ts +2 -15
  44. package/src/tool/banana-ripeness/i18n/zh.ts +2 -15
  45. package/src/tool/banana-ripeness/seo.astro +10 -3
  46. package/src/tool/brine/bibliography.astro +3 -3
  47. package/src/tool/brine/bibliography.ts +6 -0
  48. package/src/tool/brine/i18n/de.ts +4 -19
  49. package/src/tool/brine/i18n/en.ts +3 -17
  50. package/src/tool/brine/i18n/es.ts +4 -19
  51. package/src/tool/brine/i18n/fr.ts +3 -17
  52. package/src/tool/brine/i18n/id.ts +4 -19
  53. package/src/tool/brine/i18n/it.ts +4 -19
  54. package/src/tool/brine/i18n/ja.ts +4 -19
  55. package/src/tool/brine/i18n/ko.ts +4 -19
  56. package/src/tool/brine/i18n/nl.ts +4 -19
  57. package/src/tool/brine/i18n/pl.ts +4 -19
  58. package/src/tool/brine/i18n/pt.ts +4 -19
  59. package/src/tool/brine/i18n/ru.ts +4 -19
  60. package/src/tool/brine/i18n/sv.ts +4 -19
  61. package/src/tool/brine/i18n/tr.ts +4 -19
  62. package/src/tool/brine/i18n/zh.ts +4 -19
  63. package/src/tool/brine/seo.astro +11 -4
  64. package/src/tool/cookware-guide/bibliography.astro +2 -2
  65. package/src/tool/cookware-guide/bibliography.ts +10 -0
  66. package/src/tool/cookware-guide/i18n/de.ts +3 -17
  67. package/src/tool/cookware-guide/i18n/en.ts +3 -17
  68. package/src/tool/cookware-guide/i18n/es.ts +3 -17
  69. package/src/tool/cookware-guide/i18n/fr.ts +3 -17
  70. package/src/tool/cookware-guide/i18n/id.ts +3 -17
  71. package/src/tool/cookware-guide/i18n/it.ts +3 -17
  72. package/src/tool/cookware-guide/i18n/ja.ts +3 -17
  73. package/src/tool/cookware-guide/i18n/ko.ts +3 -17
  74. package/src/tool/cookware-guide/i18n/nl.ts +3 -17
  75. package/src/tool/cookware-guide/i18n/pl.ts +3 -17
  76. package/src/tool/cookware-guide/i18n/pt.ts +3 -17
  77. package/src/tool/cookware-guide/i18n/ru.ts +3 -17
  78. package/src/tool/cookware-guide/i18n/sv.ts +3 -17
  79. package/src/tool/cookware-guide/i18n/tr.ts +3 -17
  80. package/src/tool/cookware-guide/i18n/zh.ts +3 -17
  81. package/src/tool/cookware-guide/seo.astro +11 -4
  82. package/src/tool/egg-timer/bibliography.astro +3 -11
  83. package/src/tool/egg-timer/bibliography.ts +10 -0
  84. package/src/tool/egg-timer/i18n/de.ts +3 -17
  85. package/src/tool/egg-timer/i18n/en.ts +2 -15
  86. package/src/tool/egg-timer/i18n/es.ts +127 -141
  87. package/src/tool/egg-timer/i18n/fr.ts +2 -15
  88. package/src/tool/egg-timer/i18n/id.ts +4 -18
  89. package/src/tool/egg-timer/i18n/it.ts +3 -17
  90. package/src/tool/egg-timer/i18n/ja.ts +3 -17
  91. package/src/tool/egg-timer/i18n/ko.ts +3 -17
  92. package/src/tool/egg-timer/i18n/nl.ts +3 -17
  93. package/src/tool/egg-timer/i18n/pl.ts +3 -17
  94. package/src/tool/egg-timer/i18n/pt.ts +3 -17
  95. package/src/tool/egg-timer/i18n/ru.ts +3 -17
  96. package/src/tool/egg-timer/i18n/sv.ts +3 -17
  97. package/src/tool/egg-timer/i18n/tr.ts +3 -17
  98. package/src/tool/egg-timer/i18n/zh.ts +3 -17
  99. package/src/tool/egg-timer/seo.astro +6 -30
  100. package/src/tool/ingredient-rescaler/bibliography.astro +3 -3
  101. package/src/tool/ingredient-rescaler/bibliography.ts +6 -0
  102. package/src/tool/ingredient-rescaler/i18n/de.ts +3 -17
  103. package/src/tool/ingredient-rescaler/i18n/en.ts +3 -18
  104. package/src/tool/ingredient-rescaler/i18n/es.ts +4 -19
  105. package/src/tool/ingredient-rescaler/i18n/fr.ts +2 -15
  106. package/src/tool/ingredient-rescaler/i18n/id.ts +3 -17
  107. package/src/tool/ingredient-rescaler/i18n/it.ts +3 -17
  108. package/src/tool/ingredient-rescaler/i18n/ja.ts +3 -17
  109. package/src/tool/ingredient-rescaler/i18n/ko.ts +3 -17
  110. package/src/tool/ingredient-rescaler/i18n/nl.ts +3 -17
  111. package/src/tool/ingredient-rescaler/i18n/pl.ts +3 -17
  112. package/src/tool/ingredient-rescaler/i18n/pt.ts +3 -17
  113. package/src/tool/ingredient-rescaler/i18n/ru.ts +3 -17
  114. package/src/tool/ingredient-rescaler/i18n/sv.ts +3 -17
  115. package/src/tool/ingredient-rescaler/i18n/tr.ts +3 -17
  116. package/src/tool/ingredient-rescaler/i18n/zh.ts +3 -17
  117. package/src/tool/ingredient-rescaler/seo.astro +11 -4
  118. package/src/tool/kitchen-timer/bibliography.astro +3 -3
  119. package/src/tool/kitchen-timer/bibliography.ts +10 -0
  120. package/src/tool/kitchen-timer/i18n/de.ts +3 -13
  121. package/src/tool/kitchen-timer/i18n/en.ts +6 -22
  122. package/src/tool/kitchen-timer/i18n/es.ts +6 -22
  123. package/src/tool/kitchen-timer/i18n/fr.ts +6 -22
  124. package/src/tool/kitchen-timer/i18n/id.ts +3 -13
  125. package/src/tool/kitchen-timer/i18n/it.ts +3 -13
  126. package/src/tool/kitchen-timer/i18n/ja.ts +3 -13
  127. package/src/tool/kitchen-timer/i18n/ko.ts +3 -13
  128. package/src/tool/kitchen-timer/i18n/nl.ts +3 -13
  129. package/src/tool/kitchen-timer/i18n/pl.ts +3 -13
  130. package/src/tool/kitchen-timer/i18n/pt.ts +3 -13
  131. package/src/tool/kitchen-timer/i18n/ru.ts +3 -13
  132. package/src/tool/kitchen-timer/i18n/sv.ts +3 -13
  133. package/src/tool/kitchen-timer/i18n/tr.ts +3 -13
  134. package/src/tool/kitchen-timer/i18n/zh.ts +3 -13
  135. package/src/tool/kitchen-timer/seo.astro +10 -3
  136. package/src/tool/meringue-peak/bibliography.astro +3 -3
  137. package/src/tool/meringue-peak/bibliography.ts +14 -0
  138. package/src/tool/meringue-peak/i18n/de.ts +3 -13
  139. package/src/tool/meringue-peak/i18n/en.ts +2 -15
  140. package/src/tool/meringue-peak/i18n/es.ts +135 -149
  141. package/src/tool/meringue-peak/i18n/fr.ts +2 -15
  142. package/src/tool/meringue-peak/i18n/id.ts +3 -13
  143. package/src/tool/meringue-peak/i18n/it.ts +3 -13
  144. package/src/tool/meringue-peak/i18n/ja.ts +3 -13
  145. package/src/tool/meringue-peak/i18n/ko.ts +3 -13
  146. package/src/tool/meringue-peak/i18n/nl.ts +3 -13
  147. package/src/tool/meringue-peak/i18n/pl.ts +3 -13
  148. package/src/tool/meringue-peak/i18n/pt.ts +3 -13
  149. package/src/tool/meringue-peak/i18n/ru.ts +3 -13
  150. package/src/tool/meringue-peak/i18n/sv.ts +3 -13
  151. package/src/tool/meringue-peak/i18n/tr.ts +3 -13
  152. package/src/tool/meringue-peak/i18n/zh.ts +3 -13
  153. package/src/tool/meringue-peak/seo.astro +10 -3
  154. package/src/tool/mold-scaler/bibliography.astro +3 -3
  155. package/src/tool/mold-scaler/bibliography.ts +10 -0
  156. package/src/tool/mold-scaler/i18n/de.ts +5 -17
  157. package/src/tool/mold-scaler/i18n/en.ts +5 -20
  158. package/src/tool/mold-scaler/i18n/es.ts +5 -20
  159. package/src/tool/mold-scaler/i18n/fr.ts +5 -20
  160. package/src/tool/mold-scaler/i18n/id.ts +5 -17
  161. package/src/tool/mold-scaler/i18n/it.ts +5 -17
  162. package/src/tool/mold-scaler/i18n/ja.ts +5 -17
  163. package/src/tool/mold-scaler/i18n/ko.ts +5 -17
  164. package/src/tool/mold-scaler/i18n/nl.ts +5 -17
  165. package/src/tool/mold-scaler/i18n/pl.ts +5 -17
  166. package/src/tool/mold-scaler/i18n/pt.ts +5 -17
  167. package/src/tool/mold-scaler/i18n/ru.ts +5 -17
  168. package/src/tool/mold-scaler/i18n/sv.ts +5 -17
  169. package/src/tool/mold-scaler/i18n/tr.ts +5 -17
  170. package/src/tool/mold-scaler/i18n/zh.ts +5 -17
  171. package/src/tool/mold-scaler/seo.astro +10 -3
  172. package/src/tool/pizza/bibliography.astro +3 -3
  173. package/src/tool/pizza/bibliography.ts +18 -0
  174. package/src/tool/pizza/i18n/de.ts +3 -13
  175. package/src/tool/pizza/i18n/en.ts +58 -75
  176. package/src/tool/pizza/i18n/es.ts +2 -20
  177. package/src/tool/pizza/i18n/fr.ts +58 -75
  178. package/src/tool/pizza/i18n/id.ts +3 -9
  179. package/src/tool/pizza/i18n/it.ts +3 -13
  180. package/src/tool/pizza/i18n/ja.ts +3 -9
  181. package/src/tool/pizza/i18n/ko.ts +3 -9
  182. package/src/tool/pizza/i18n/nl.ts +3 -9
  183. package/src/tool/pizza/i18n/pl.ts +3 -9
  184. package/src/tool/pizza/i18n/pt.ts +3 -9
  185. package/src/tool/pizza/i18n/ru.ts +28 -34
  186. package/src/tool/pizza/i18n/sv.ts +3 -9
  187. package/src/tool/pizza/i18n/tr.ts +3 -9
  188. package/src/tool/pizza/i18n/zh.ts +3 -9
  189. package/src/tool/pizza/seo.astro +11 -4
  190. package/src/tool/roux-guide/bibliography.astro +3 -3
  191. package/src/tool/roux-guide/bibliography.ts +14 -0
  192. package/src/tool/roux-guide/i18n/de.ts +3 -9
  193. package/src/tool/roux-guide/i18n/en.ts +2 -15
  194. package/src/tool/roux-guide/i18n/es.ts +3 -17
  195. package/src/tool/roux-guide/i18n/fr.ts +2 -15
  196. package/src/tool/roux-guide/i18n/id.ts +3 -9
  197. package/src/tool/roux-guide/i18n/it.ts +3 -9
  198. package/src/tool/roux-guide/i18n/ja.ts +3 -9
  199. package/src/tool/roux-guide/i18n/ko.ts +3 -9
  200. package/src/tool/roux-guide/i18n/nl.ts +3 -9
  201. package/src/tool/roux-guide/i18n/pl.ts +3 -9
  202. package/src/tool/roux-guide/i18n/pt.ts +3 -9
  203. package/src/tool/roux-guide/i18n/ru.ts +3 -9
  204. package/src/tool/roux-guide/i18n/sv.ts +3 -9
  205. package/src/tool/roux-guide/i18n/tr.ts +3 -9
  206. package/src/tool/roux-guide/i18n/zh.ts +3 -9
  207. package/src/tool/roux-guide/seo.astro +11 -4
  208. package/src/tool/sourdough-calculator/bibliography.astro +3 -3
  209. package/src/tool/sourdough-calculator/bibliography.ts +14 -0
  210. package/src/tool/sourdough-calculator/i18n/de.ts +3 -9
  211. package/src/tool/sourdough-calculator/i18n/en.ts +3 -17
  212. package/src/tool/sourdough-calculator/i18n/es.ts +4 -18
  213. package/src/tool/sourdough-calculator/i18n/fr.ts +2 -15
  214. package/src/tool/sourdough-calculator/i18n/id.ts +3 -9
  215. package/src/tool/sourdough-calculator/i18n/it.ts +3 -9
  216. package/src/tool/sourdough-calculator/i18n/ja.ts +3 -9
  217. package/src/tool/sourdough-calculator/i18n/ko.ts +3 -9
  218. package/src/tool/sourdough-calculator/i18n/nl.ts +3 -9
  219. package/src/tool/sourdough-calculator/i18n/pl.ts +3 -9
  220. package/src/tool/sourdough-calculator/i18n/pt.ts +3 -9
  221. package/src/tool/sourdough-calculator/i18n/ru.ts +3 -9
  222. package/src/tool/sourdough-calculator/i18n/sv.ts +3 -9
  223. package/src/tool/sourdough-calculator/i18n/tr.ts +3 -9
  224. package/src/tool/sourdough-calculator/i18n/zh.ts +3 -9
  225. package/src/tool/sourdough-calculator/seo.astro +11 -4
  226. package/src/tool/yeast-converter/bibliography.astro +6 -0
  227. package/src/tool/yeast-converter/bibliography.ts +6 -0
  228. package/src/tool/yeast-converter/component.astro +96 -0
  229. package/src/tool/yeast-converter/entry.ts +26 -0
  230. package/src/tool/yeast-converter/i18n/de.ts +235 -0
  231. package/src/tool/yeast-converter/i18n/en.ts +234 -0
  232. package/src/tool/yeast-converter/i18n/es.ts +235 -0
  233. package/src/tool/yeast-converter/i18n/fr.ts +235 -0
  234. package/src/tool/yeast-converter/i18n/id.ts +235 -0
  235. package/src/tool/yeast-converter/i18n/it.ts +235 -0
  236. package/src/tool/yeast-converter/i18n/ja.ts +235 -0
  237. package/src/tool/yeast-converter/i18n/ko.ts +235 -0
  238. package/src/tool/yeast-converter/i18n/nl.ts +235 -0
  239. package/src/tool/yeast-converter/i18n/pl.ts +235 -0
  240. package/src/tool/yeast-converter/i18n/pt.ts +235 -0
  241. package/src/tool/yeast-converter/i18n/ru.ts +235 -0
  242. package/src/tool/yeast-converter/i18n/sv.ts +235 -0
  243. package/src/tool/yeast-converter/i18n/tr.ts +235 -0
  244. package/src/tool/yeast-converter/i18n/zh.ts +235 -0
  245. package/src/tool/yeast-converter/index.ts +11 -0
  246. package/src/tool/yeast-converter/init.ts +277 -0
  247. package/src/tool/yeast-converter/seo.astro +15 -0
  248. package/src/tool/yeast-converter/yeast-converter-fresh-dry-sourdough-starter.css +388 -0
  249. package/src/tools.ts +2 -0
@@ -0,0 +1,11 @@
1
+ import type { ToolDefinition } from '../../types';
2
+ import { yeastConverter } from './entry';
3
+
4
+ export * from './entry';
5
+
6
+ export const YEAST_CONVERTER_TOOL: ToolDefinition = {
7
+ entry: yeastConverter,
8
+ Component: () => import('./component.astro'),
9
+ SEOComponent: () => import('./seo.astro'),
10
+ BibliographyComponent: () => import('./bibliography.astro'),
11
+ };
@@ -0,0 +1,277 @@
1
+ type YeastType = 'fresh' | 'dry' | 'sourdough';
2
+ type UnitType = 'g' | 'oz' | 'lb' | 'mg';
3
+
4
+ interface ConversionResult {
5
+ fresh: number | null;
6
+ dry: number | null;
7
+ sourdough: number | null;
8
+ flourAdjustment: number | null;
9
+ waterAdjustment: number | null;
10
+ }
11
+
12
+ const UNIT_TO_GRAMS: Record<UnitType, number> = {
13
+ g: 1,
14
+ oz: 28.3495,
15
+ lb: 453.592,
16
+ mg: 0.001,
17
+ };
18
+
19
+ function toGrams(amount: number, unit: UnitType): number {
20
+ return amount * UNIT_TO_GRAMS[unit];
21
+ }
22
+
23
+ function fromGrams(amount: number, unit: UnitType): number {
24
+ return amount / UNIT_TO_GRAMS[unit];
25
+ }
26
+
27
+ function calculateFreshYeast(amount: number): ConversionResult {
28
+ return {
29
+ fresh: amount,
30
+ dry: amount / 3,
31
+ sourdough: amount * 5,
32
+ flourAdjustment: (amount * 5) / 2,
33
+ waterAdjustment: (amount * 5) / 2,
34
+ };
35
+ }
36
+
37
+ function calculateDryYeast(amount: number): ConversionResult {
38
+ return {
39
+ dry: amount,
40
+ fresh: amount * 3,
41
+ sourdough: amount * 15,
42
+ flourAdjustment: (amount * 15) / 2,
43
+ waterAdjustment: (amount * 15) / 2,
44
+ };
45
+ }
46
+
47
+ function calculateSourdough(amount: number): ConversionResult {
48
+ return {
49
+ sourdough: amount,
50
+ fresh: amount / 5,
51
+ dry: amount / 15,
52
+ flourAdjustment: amount / 2,
53
+ waterAdjustment: amount / 2,
54
+ };
55
+ }
56
+
57
+ function calculateConversions(amount: number, sourceType: YeastType): ConversionResult {
58
+ if (amount <= 0) {
59
+ return { fresh: null, dry: null, sourdough: null, flourAdjustment: null, waterAdjustment: null };
60
+ }
61
+
62
+ if (sourceType === 'fresh') return calculateFreshYeast(amount);
63
+ if (sourceType === 'dry') return calculateDryYeast(amount);
64
+ return calculateSourdough(amount);
65
+ }
66
+
67
+ interface ConverterContext {
68
+ amountInput: HTMLInputElement;
69
+ sourceSelect: HTMLSelectElement;
70
+ unitSelect: HTMLSelectElement;
71
+ resultsContainer: HTMLDivElement;
72
+ ui: Record<string, string>;
73
+ }
74
+
75
+ const STORAGE_KEYS = {
76
+ unit: 'yeast-converter-unit',
77
+ amount: 'yeast-converter-amount',
78
+ sourceType: 'yeast-converter-source',
79
+ };
80
+
81
+ function saveToStorage(key: string, value: string): void {
82
+ try {
83
+ localStorage.setItem(key, value);
84
+ } catch {
85
+ }
86
+ }
87
+
88
+ function loadFromStorage(key: string, defaultValue: string): string {
89
+ try {
90
+ return localStorage.getItem(key) ?? defaultValue;
91
+ } catch {
92
+ return defaultValue;
93
+ }
94
+ }
95
+
96
+ function formatAmount(amount: number | null): string {
97
+ if (amount === null || isNaN(amount)) return '—';
98
+ if (Number.isInteger(amount)) return amount.toString();
99
+ const rounded = Math.round(amount * 100) / 100;
100
+ return rounded.toString();
101
+ }
102
+
103
+ function getResultItems(
104
+ result: ConversionResult,
105
+ sourceType: YeastType,
106
+ unit: UnitType,
107
+ ui: Record<string, string>,
108
+ ): Array<{ label: string; value: number | null; unit: UnitType }> {
109
+ const items: Array<{ label: string; value: number | null; unit: UnitType }> = [];
110
+ if (sourceType !== 'fresh') {
111
+ const value = result.fresh !== null ? fromGrams(result.fresh, unit) : null;
112
+ items.push({ label: ui.freshYeast, value, unit });
113
+ }
114
+ if (sourceType !== 'dry') {
115
+ const value = result.dry !== null ? fromGrams(result.dry, unit) : null;
116
+ items.push({ label: ui.dryYeast, value, unit });
117
+ }
118
+ if (sourceType !== 'sourdough') {
119
+ const value = result.sourdough !== null ? fromGrams(result.sourdough, unit) : null;
120
+ items.push({ label: ui.sourdough, value, unit });
121
+ }
122
+ return items;
123
+ }
124
+
125
+ function renderResultRows(container: HTMLDivElement, items: Array<{ label: string; value: number | null; unit: string }>): void {
126
+ items.forEach((item) => {
127
+ const row = document.createElement('div');
128
+ row.className = 'yc-result-row';
129
+ row.innerHTML = `<span class="yc-result-label">${item.label}</span><span class="yc-result-value">${formatAmount(item.value)} ${item.unit}</span>`;
130
+ container.appendChild(row);
131
+ });
132
+ }
133
+
134
+ function renderAdjustmentCard(container: HTMLDivElement, result: ConversionResult, unit: UnitType, ui: Record<string, string>): void {
135
+ const adjustmentCard = document.createElement('div');
136
+ adjustmentCard.className = 'yc-adjustment-card';
137
+ const flourValue = result.flourAdjustment !== null ? fromGrams(result.flourAdjustment, unit) : null;
138
+ const waterValue = result.waterAdjustment !== null ? fromGrams(result.waterAdjustment, unit) : null;
139
+ adjustmentCard.innerHTML = `<h4 class="yc-adjustment-title">${ui.recipeAdjustment}</h4><div class="yc-adjustment-items"><div class="yc-adjustment-item"><span class="yc-adjustment-label">${ui.flourSubtract}</span><span class="yc-adjustment-value">${formatAmount(flourValue)} ${unit}</span></div><div class="yc-adjustment-item"><span class="yc-adjustment-label">${ui.waterSubtract}</span><span class="yc-adjustment-value">${formatAmount(waterValue)} ${unit}</span></div></div>`;
140
+ container.appendChild(adjustmentCard);
141
+ }
142
+
143
+ interface RenderState {
144
+ result: ConversionResult;
145
+ sourceType: YeastType;
146
+ unit: UnitType;
147
+ }
148
+
149
+ function renderResults(container: HTMLDivElement, state: RenderState, ctx: ConverterContext): void {
150
+ container.innerHTML = '';
151
+
152
+ if (state.result.fresh === null) {
153
+ container.innerHTML = `<div class="yc-empty-state"><p>${ctx.ui.enterAmount}</p></div>`;
154
+ return;
155
+ }
156
+
157
+ const items = getResultItems(state.result, state.sourceType, state.unit, ctx.ui);
158
+ renderResultRows(container, items);
159
+
160
+ if (state.sourceType === 'sourdough') {
161
+ renderAdjustmentCard(container, state.result, state.unit, ctx.ui);
162
+ }
163
+ }
164
+
165
+ function updateState(ctx: ConverterContext): void {
166
+ const amount = parseFloat(ctx.amountInput.value) || 0;
167
+ const sourceType = (ctx.sourceSelect.value || 'fresh') as YeastType;
168
+ const unit = (ctx.unitSelect.value || 'g') as UnitType;
169
+ const amountInGrams = toGrams(amount, unit);
170
+ const result = calculateConversions(amountInGrams, sourceType);
171
+ const state: RenderState = { result, sourceType, unit };
172
+ renderResults(ctx.resultsContainer, state, ctx);
173
+ }
174
+
175
+ function buildYeastLines(result: ConversionResult, unit: UnitType): string {
176
+ const lines: string[] = [];
177
+ if (result.fresh !== null) lines.push(`Fresh Yeast: ${formatAmount(fromGrams(result.fresh, unit))}${unit}`);
178
+ if (result.dry !== null) lines.push(`Dry Yeast: ${formatAmount(fromGrams(result.dry, unit))}${unit}`);
179
+ if (result.sourdough !== null) lines.push(`Sourdough Starter: ${formatAmount(fromGrams(result.sourdough, unit))}${unit}`);
180
+ return lines.join('\n');
181
+ }
182
+
183
+ function buildCopyText(result: ConversionResult, sourceType: YeastType, unit: UnitType): string {
184
+ let text = buildYeastLines(result, unit);
185
+
186
+ if (sourceType === 'sourdough' && result.flourAdjustment !== null && result.waterAdjustment !== null) {
187
+ text += '\n\nRecipe Adjustment:\n';
188
+ const flourVal = formatAmount(fromGrams(result.flourAdjustment, unit));
189
+ const waterVal = formatAmount(fromGrams(result.waterAdjustment, unit));
190
+ text += `Flour to subtract: ${flourVal}${unit}\n`;
191
+ text += `Water to subtract: ${waterVal}${unit}`;
192
+ }
193
+ return text;
194
+ }
195
+
196
+ function setupCopyButton(copyBtn: HTMLButtonElement, ctx: ConverterContext): void {
197
+ copyBtn.addEventListener('click', () => {
198
+ const amount = parseFloat(ctx.amountInput.value) || 0;
199
+ const sourceType = (ctx.sourceSelect.value || 'fresh') as YeastType;
200
+ const unit = (ctx.unitSelect.value || 'g') as UnitType;
201
+ const amountInGrams = toGrams(amount, unit);
202
+ const result = calculateConversions(amountInGrams, sourceType);
203
+ const textToCopy = buildCopyText(result, sourceType, unit);
204
+
205
+ navigator.clipboard.writeText(textToCopy).then(() => {
206
+ const originalText = copyBtn.innerHTML;
207
+ copyBtn.textContent = ctx.ui.copied ?? 'Copied!';
208
+ copyBtn.classList.add('yc-copied');
209
+ setTimeout(() => {
210
+ copyBtn.innerHTML = originalText;
211
+ copyBtn.classList.remove('yc-copied');
212
+ }, 2000);
213
+ });
214
+ });
215
+ }
216
+
217
+ function setupResetButton(resetBtn: HTMLButtonElement, ctx: ConverterContext, update: () => void): void {
218
+ resetBtn.addEventListener('click', () => {
219
+ ctx.amountInput.value = '';
220
+ ctx.sourceSelect.value = 'fresh';
221
+ update();
222
+ });
223
+ }
224
+
225
+ function restoreSavedState(
226
+ amountInput: HTMLInputElement,
227
+ sourceSelect: HTMLSelectElement,
228
+ unitSelect: HTMLSelectElement,
229
+ ): void {
230
+ unitSelect.value = loadFromStorage(STORAGE_KEYS.unit, 'g');
231
+ amountInput.value = loadFromStorage(STORAGE_KEYS.amount, '');
232
+ sourceSelect.value = loadFromStorage(STORAGE_KEYS.sourceType, 'fresh');
233
+ }
234
+
235
+ function setupEventListeners(
236
+ ctx: ConverterContext,
237
+ resetBtn: HTMLButtonElement,
238
+ copyBtn: HTMLButtonElement,
239
+ update: () => void,
240
+ ): void {
241
+ ctx.amountInput.addEventListener('input', () => {
242
+ saveToStorage(STORAGE_KEYS.amount, ctx.amountInput.value);
243
+ update();
244
+ });
245
+
246
+ ctx.sourceSelect.addEventListener('change', () => {
247
+ saveToStorage(STORAGE_KEYS.sourceType, ctx.sourceSelect.value);
248
+ update();
249
+ });
250
+
251
+ ctx.unitSelect.addEventListener('change', () => {
252
+ saveToStorage(STORAGE_KEYS.unit, ctx.unitSelect.value);
253
+ update();
254
+ });
255
+
256
+ setupResetButton(resetBtn, ctx, update);
257
+ setupCopyButton(copyBtn, ctx);
258
+ }
259
+
260
+ export function initYeastConverter(ui: Record<string, string>): void {
261
+ const amountInput = document.getElementById('yeast-amount') as HTMLInputElement | null;
262
+ const sourceSelect = document.getElementById('yeast-source') as HTMLSelectElement | null;
263
+ const unitSelect = document.getElementById('yeast-unit') as HTMLSelectElement | null;
264
+ const resetBtn = document.getElementById('reset-btn') as HTMLButtonElement | null;
265
+ const resultsContainer = document.getElementById('results-container') as HTMLDivElement | null;
266
+ const copyBtn = document.getElementById('copy-btn') as HTMLButtonElement | null;
267
+
268
+ if (!amountInput || !sourceSelect || !unitSelect || !resultsContainer || !resetBtn || !copyBtn) {
269
+ return;
270
+ }
271
+
272
+ restoreSavedState(amountInput, sourceSelect, unitSelect);
273
+ const ctx: ConverterContext = { amountInput, sourceSelect, unitSelect, resultsContainer, ui };
274
+ const update = () => updateState(ctx);
275
+ setupEventListeners(ctx, resetBtn, copyBtn, update);
276
+ update();
277
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { YEAST_CONVERTER_TOOL } from './index';
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 YEAST_CONVERTER_TOOL.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,388 @@
1
+ :root {
2
+ --yc-primary: hsl(220deg, 90%, 56%);
3
+ --yc-primary-light: hsl(220deg, 90%, 92%);
4
+ --yc-bg-card: hsl(0deg, 0%, 100%);
5
+ --yc-bg-app: hsl(210deg, 20%, 98%);
6
+ --yc-border: hsl(210deg, 20%, 90%);
7
+ --yc-text-main: hsl(210deg, 30%, 20%);
8
+ --yc-text-muted: hsl(210deg, 15%, 50%);
9
+ --yc-shadow-lg: 0 10px 15px -3px rgb(0, 0, 0, 0.1);
10
+ --yc-radius: 1rem;
11
+ --yc-white: hsl(0deg, 0%, 100%);
12
+ }
13
+
14
+ .theme-dark {
15
+ --yc-bg-card: hsl(220deg, 25%, 12%);
16
+ --yc-bg-app: hsl(220deg, 30%, 7%);
17
+ --yc-border: hsl(220deg, 20%, 20%);
18
+ --yc-text-main: hsl(210deg, 20%, 95%);
19
+ --yc-text-muted: hsl(210deg, 15%, 70%);
20
+ --yc-primary-light: hsl(220deg, 90%, 12%);
21
+ }
22
+
23
+ .yeast-converter-container {
24
+ max-width: 100%;
25
+ padding: 1.5rem;
26
+ background: var(--yc-bg-card);
27
+ border: 1px solid var(--yc-border);
28
+ border-radius: var(--yc-radius);
29
+ box-shadow: var(--yc-shadow-lg);
30
+ }
31
+
32
+ .yc-grid {
33
+ display: grid;
34
+ grid-template-columns: 1fr;
35
+ gap: 1.5rem;
36
+ margin-bottom: 2rem;
37
+ }
38
+
39
+ @media (min-width: 768px) {
40
+ .yc-grid {
41
+ grid-template-columns: 1fr 1fr;
42
+ }
43
+ }
44
+
45
+ .yc-section {
46
+ background: var(--yc-bg-app);
47
+ padding: 1.5rem;
48
+ border-radius: 1.25rem;
49
+ border: 1px solid var(--yc-border);
50
+ }
51
+
52
+ .yc-section-primary {
53
+ background: var(--yc-bg-card);
54
+ border: 2px solid var(--yc-primary);
55
+ }
56
+
57
+ .yc-section-title {
58
+ font-size: 1.125rem;
59
+ font-weight: 700;
60
+ color: var(--yc-text-main);
61
+ margin-bottom: 1rem;
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 0.5rem;
65
+ }
66
+
67
+ .yc-section-primary .yc-section-title {
68
+ color: var(--yc-primary);
69
+ }
70
+
71
+ .yc-type-selector {
72
+ position: relative;
73
+ margin-bottom: 1rem;
74
+ }
75
+
76
+ .yc-select {
77
+ width: 100%;
78
+ padding: 0.75rem 2.5rem 0.75rem 1rem;
79
+ border-radius: 0.75rem;
80
+ border: 1px solid var(--yc-border);
81
+ background: var(--yc-bg-card);
82
+ color: var(--yc-text-main);
83
+ font-weight: 600;
84
+ font-size: 1rem;
85
+ appearance: none;
86
+ cursor: pointer;
87
+ transition: border-color 0.2s;
88
+ }
89
+
90
+ .yc-select:hover {
91
+ border-color: var(--yc-primary);
92
+ }
93
+
94
+ .yc-select:focus {
95
+ outline: none;
96
+ border-color: var(--yc-primary);
97
+ box-shadow: 0 0 0 3px var(--yc-primary-light);
98
+ }
99
+
100
+ .yc-select-icon {
101
+ position: absolute;
102
+ right: 0.75rem;
103
+ top: 50%;
104
+ transform: translateY(-50%);
105
+ pointer-events: none;
106
+ color: var(--yc-text-muted);
107
+ width: 1.25rem;
108
+ height: 1.25rem;
109
+ }
110
+
111
+ .yc-field {
112
+ display: flex;
113
+ flex-direction: column;
114
+ gap: 0.5rem;
115
+ }
116
+
117
+ .yc-label {
118
+ font-size: 0.75rem;
119
+ font-weight: 700;
120
+ color: var(--yc-text-muted);
121
+ text-transform: uppercase;
122
+ letter-spacing: 0.05em;
123
+ }
124
+
125
+ .yc-input-wrapper {
126
+ position: relative;
127
+ display: flex;
128
+ align-items: center;
129
+ }
130
+
131
+ .yc-input {
132
+ width: 100%;
133
+ padding: 0.75rem 2.5rem 0.75rem 1rem;
134
+ border-radius: 0.75rem;
135
+ border: 1px solid var(--yc-border);
136
+ background: var(--yc-bg-card);
137
+ color: var(--yc-text-main);
138
+ font-weight: 600;
139
+ font-size: 1.125rem;
140
+ text-align: left;
141
+ transition: border-color 0.2s;
142
+ }
143
+
144
+ .yc-input:focus {
145
+ outline: none;
146
+ border-color: var(--yc-primary);
147
+ box-shadow: 0 0 0 3px var(--yc-primary-light);
148
+ }
149
+
150
+ .yc-input::placeholder {
151
+ color: var(--yc-text-muted);
152
+ }
153
+
154
+ .yc-unit-select {
155
+ position: absolute;
156
+ right: 0;
157
+ top: 50%;
158
+ transform: translateY(-50%);
159
+ background: var(--yc-bg-card);
160
+ border: 1px solid var(--yc-border);
161
+ color: var(--yc-text-main);
162
+ font-weight: 600;
163
+ font-size: 0.875rem;
164
+ cursor: pointer;
165
+ padding: 0.5rem 0.75rem;
166
+ appearance: none;
167
+ padding-right: 1.75rem;
168
+ border-radius: 0.5rem;
169
+ }
170
+
171
+ .yc-unit-select:hover {
172
+ border-color: var(--yc-primary);
173
+ background: var(--yc-primary-light);
174
+ }
175
+
176
+ .yc-unit-select:focus {
177
+ outline: none;
178
+ border-color: var(--yc-primary);
179
+ box-shadow: 0 0 0 3px var(--yc-primary-light);
180
+ }
181
+
182
+ .yc-reset-btn {
183
+ width: 100%;
184
+ padding: 0.75rem;
185
+ margin-top: 1rem;
186
+ border: 1px solid var(--yc-border);
187
+ background: transparent;
188
+ color: var(--yc-text-muted);
189
+ border-radius: 0.75rem;
190
+ font-weight: 600;
191
+ cursor: pointer;
192
+ transition: all 0.2s;
193
+ display: flex;
194
+ align-items: center;
195
+ justify-content: center;
196
+ gap: 0.5rem;
197
+ }
198
+
199
+ .yc-reset-btn:hover {
200
+ border-color: var(--yc-primary);
201
+ color: var(--yc-primary);
202
+ background: var(--yc-primary-light);
203
+ }
204
+
205
+ .yc-reset-btn svg {
206
+ width: 1.125rem;
207
+ height: 1.125rem;
208
+ }
209
+
210
+ .yc-results-card {
211
+ padding: 1.5rem;
212
+ background: var(--yc-bg-app);
213
+ border: 1px solid var(--yc-border);
214
+ border-radius: 1.25rem;
215
+ }
216
+
217
+ .yc-results-header {
218
+ display: flex;
219
+ align-items: center;
220
+ justify-content: space-between;
221
+ margin-bottom: 1.5rem;
222
+ gap: 1rem;
223
+ }
224
+
225
+ .yc-results-title {
226
+ font-size: 1.125rem;
227
+ font-weight: 700;
228
+ color: var(--yc-text-main);
229
+ display: flex;
230
+ align-items: center;
231
+ gap: 0.5rem;
232
+ margin: 0;
233
+ }
234
+
235
+ .yc-results-title svg {
236
+ width: 1.5rem;
237
+ height: 1.5rem;
238
+ color: var(--yc-primary);
239
+ }
240
+
241
+ .yc-copy-btn {
242
+ display: flex;
243
+ align-items: center;
244
+ gap: 0.5rem;
245
+ padding: 0.625rem 1rem;
246
+ background: var(--yc-primary);
247
+ color: var(--yc-white);
248
+ border: none;
249
+ border-radius: 0.75rem;
250
+ font-weight: 600;
251
+ cursor: pointer;
252
+ transition: all 0.2s;
253
+ white-space: nowrap;
254
+ }
255
+
256
+ .yc-copy-btn:hover {
257
+ background: hsl(220deg, 90%, 50%);
258
+ transform: translateY(-2px);
259
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
260
+ }
261
+
262
+ .yc-copy-btn.yc-copied {
263
+ background: hsl(120deg, 50%, 50%);
264
+ }
265
+
266
+ .yc-copy-btn svg {
267
+ width: 1.125rem;
268
+ height: 1.125rem;
269
+ }
270
+
271
+ .yc-results-container {
272
+ display: flex;
273
+ flex-direction: column;
274
+ gap: 1rem;
275
+ }
276
+
277
+ .yc-empty-state {
278
+ display: flex;
279
+ flex-direction: column;
280
+ align-items: center;
281
+ justify-content: center;
282
+ gap: 1rem;
283
+ padding: 2rem 1rem;
284
+ text-align: center;
285
+ color: var(--yc-text-muted);
286
+ }
287
+
288
+ .yc-empty-icon {
289
+ width: 3rem;
290
+ height: 3rem;
291
+ opacity: 0.5;
292
+ }
293
+
294
+ .yc-empty-state p {
295
+ margin: 0;
296
+ font-size: 0.95rem;
297
+ }
298
+
299
+ .yc-result-row {
300
+ display: flex;
301
+ justify-content: space-between;
302
+ align-items: center;
303
+ padding: 1rem;
304
+ background: var(--yc-bg-card);
305
+ border: 1px solid var(--yc-border);
306
+ border-radius: 0.75rem;
307
+ transition: all 0.2s;
308
+ }
309
+
310
+ .yc-result-row:hover {
311
+ border-color: var(--yc-primary);
312
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
313
+ }
314
+
315
+ .yc-result-label {
316
+ font-weight: 600;
317
+ color: var(--yc-text-main);
318
+ }
319
+
320
+ .yc-result-value {
321
+ font-weight: 700;
322
+ color: var(--yc-primary);
323
+ font-size: 1.125rem;
324
+ }
325
+
326
+ .yc-adjustment-card {
327
+ padding: 1.5rem;
328
+ background: hsl(120deg, 50%, 95%);
329
+ border: 1px solid hsl(120deg, 50%, 80%);
330
+ border-radius: 0.75rem;
331
+ margin-top: 1rem;
332
+ }
333
+
334
+ .theme-dark .yc-adjustment-card {
335
+ background: hsl(120deg, 30%, 20%);
336
+ border-color: hsl(120deg, 30%, 35%);
337
+ }
338
+
339
+ .yc-adjustment-title {
340
+ font-size: 0.95rem;
341
+ font-weight: 700;
342
+ color: hsl(120deg, 60%, 30%);
343
+ margin: 0 0 1rem;
344
+ display: flex;
345
+ align-items: center;
346
+ gap: 0.5rem;
347
+ }
348
+
349
+ .theme-dark .yc-adjustment-title {
350
+ color: hsl(120deg, 60%, 70%);
351
+ }
352
+
353
+ .yc-adjustment-items {
354
+ display: flex;
355
+ flex-direction: column;
356
+ gap: 0.75rem;
357
+ }
358
+
359
+ .yc-adjustment-item {
360
+ display: flex;
361
+ justify-content: space-between;
362
+ align-items: center;
363
+ padding: 0.5rem 0;
364
+ border-bottom: 1px solid hsl(120deg, 50%, 85%);
365
+ }
366
+
367
+ .theme-dark .yc-adjustment-item {
368
+ border-bottom-color: hsl(120deg, 30%, 40%);
369
+ }
370
+
371
+ .yc-adjustment-item:last-child {
372
+ border-bottom: none;
373
+ }
374
+
375
+ .yc-adjustment-label {
376
+ color: var(--yc-text-main);
377
+ font-weight: 500;
378
+ }
379
+
380
+ .yc-adjustment-value {
381
+ color: hsl(120deg, 60%, 30%);
382
+ font-weight: 700;
383
+ font-size: 1.05rem;
384
+ }
385
+
386
+ .theme-dark .yc-adjustment-value {
387
+ color: hsl(120deg, 60%, 70%);
388
+ }
package/src/tools.ts CHANGED
@@ -12,6 +12,7 @@ import { INGREDIENT_RESCALER_TOOL } from './tool/ingredient-rescaler';
12
12
  import { SOURDOUGH_CALCULATOR_TOOL } from './tool/sourdough-calculator';
13
13
  import { ROUX_GUIDE_TOOL } from './tool/roux-guide';
14
14
  import { COOKWARE_GUIDE_TOOL } from './tool/cookware-guide';
15
+ import { YEAST_CONVERTER_TOOL } from './tool/yeast-converter';
15
16
 
16
17
  export const ALL_TOOLS: ToolDefinition[] = [
17
18
  AMERICAN_KITCHEN_CONVERTER_TOOL,
@@ -26,6 +27,7 @@ export const ALL_TOOLS: ToolDefinition[] = [
26
27
  SOURDOUGH_CALCULATOR_TOOL,
27
28
  ROUX_GUIDE_TOOL,
28
29
  COOKWARE_GUIDE_TOOL,
30
+ YEAST_CONVERTER_TOOL,
29
31
  ];
30
32
 
31
33