@recipe-book/react 0.0.3 → 0.1.1

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 (167) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -0
  3. package/README.md +48 -15
  4. package/core/package.json +5 -0
  5. package/core/schema-org/package.json +5 -0
  6. package/dist/core/schema-org.cjs +25 -0
  7. package/dist/core/schema-org.cjs.map +1 -0
  8. package/dist/core/schema-org.d.cts +1 -0
  9. package/dist/core/schema-org.d.ts +1 -0
  10. package/dist/core/schema-org.js +3 -0
  11. package/dist/core/schema-org.js.map +1 -0
  12. package/dist/core.cjs +25 -0
  13. package/dist/core.cjs.map +1 -0
  14. package/dist/core.d.cts +1 -0
  15. package/dist/core.d.ts +1 -0
  16. package/dist/core.js +3 -0
  17. package/dist/core.js.map +1 -0
  18. package/dist/index.cjs +1224 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.cts +1276 -0
  21. package/dist/index.d.ts +1276 -10
  22. package/dist/index.js +1207 -2
  23. package/dist/index.js.map +1 -0
  24. package/package.json +71 -15
  25. package/dist/chunk-2S2Z4HLL.mjs +0 -46
  26. package/dist/chunk-2VMRVR3X.mjs +0 -1
  27. package/dist/chunk-36H6LM2B.mjs +0 -14
  28. package/dist/chunk-4IWVZEXX.mjs +0 -20
  29. package/dist/chunk-4XBZKTOL.mjs +0 -1
  30. package/dist/chunk-6AGILC25.mjs +0 -20
  31. package/dist/chunk-6AXGEB5P.mjs +0 -14
  32. package/dist/chunk-6JEDN4VR.mjs +0 -20
  33. package/dist/chunk-6KLLMLNF.mjs +0 -33
  34. package/dist/chunk-6LTLY5K7.mjs +0 -20
  35. package/dist/chunk-6RA4EVIO.mjs +0 -46
  36. package/dist/chunk-75DF3E6L.mjs +0 -50
  37. package/dist/chunk-7BWMULSB.mjs +0 -55
  38. package/dist/chunk-7PPB3PM6.mjs +0 -40
  39. package/dist/chunk-7SBP7DC5.mjs +0 -42
  40. package/dist/chunk-7XJBP24U.mjs +0 -20
  41. package/dist/chunk-A5JAKCMG.mjs +0 -44
  42. package/dist/chunk-ATOAYKI3.mjs +0 -26
  43. package/dist/chunk-B5IXKKFC.mjs +0 -38
  44. package/dist/chunk-BEMMFA52.mjs +0 -49
  45. package/dist/chunk-BOG5DA6M.mjs +0 -38
  46. package/dist/chunk-BW4DYFBO.mjs +0 -36
  47. package/dist/chunk-CDQFOSWR.mjs +0 -34
  48. package/dist/chunk-CRQVP6PW.mjs +0 -20
  49. package/dist/chunk-CY27OHOG.mjs +0 -1
  50. package/dist/chunk-DACFJWGN.mjs +0 -14
  51. package/dist/chunk-DE5GUZKO.mjs +0 -46
  52. package/dist/chunk-DNZAFS5G.mjs +0 -48
  53. package/dist/chunk-DS3O5PAR.mjs +0 -20
  54. package/dist/chunk-ENGJXHYU.mjs +0 -42
  55. package/dist/chunk-FIFGRJKD.mjs +0 -26
  56. package/dist/chunk-FWOMIZEF.mjs +0 -46
  57. package/dist/chunk-G7XD4VBW.mjs +0 -44
  58. package/dist/chunk-GBU6MVZL.mjs +0 -14
  59. package/dist/chunk-GM5F7IUT.mjs +0 -2
  60. package/dist/chunk-GSGRXRJP.mjs +0 -38
  61. package/dist/chunk-GZRNU24M.mjs +0 -46
  62. package/dist/chunk-HNDXCSZY.mjs +0 -44
  63. package/dist/chunk-HXTUDCO2.mjs +0 -49
  64. package/dist/chunk-I5TR6PEA.mjs +0 -0
  65. package/dist/chunk-IXRYSEMS.mjs +0 -30
  66. package/dist/chunk-J5Q2Z2JG.mjs +0 -38
  67. package/dist/chunk-JBAAWEV6.mjs +0 -1
  68. package/dist/chunk-JCPPNN26.mjs +0 -44
  69. package/dist/chunk-JR3OTSZB.mjs +0 -32
  70. package/dist/chunk-JWS5RTU3.mjs +0 -46
  71. package/dist/chunk-K6EZ773Q.mjs +0 -26
  72. package/dist/chunk-KNXMYQVO.mjs +0 -36
  73. package/dist/chunk-KRUZ7OA7.mjs +0 -44
  74. package/dist/chunk-KXCIJBR6.mjs +0 -14
  75. package/dist/chunk-M5D3UUZF.mjs +0 -19
  76. package/dist/chunk-MASAOP7L.mjs +0 -55
  77. package/dist/chunk-MGBEDCLT.mjs +0 -34
  78. package/dist/chunk-MKBCBPVR.mjs +0 -53
  79. package/dist/chunk-N53TONLF.mjs +0 -39
  80. package/dist/chunk-NYDGFRPM.mjs +0 -46
  81. package/dist/chunk-OPOABKD6.mjs +0 -23
  82. package/dist/chunk-OX6ITS5C.mjs +0 -20
  83. package/dist/chunk-PAAVW47I.mjs +0 -50
  84. package/dist/chunk-PAF5HSSV.mjs +0 -53
  85. package/dist/chunk-PBENQR7Y.mjs +0 -1
  86. package/dist/chunk-PH3LQIHZ.mjs +0 -26
  87. package/dist/chunk-PIB3RAOO.mjs +0 -24
  88. package/dist/chunk-PIXLJD7Y.mjs +0 -37
  89. package/dist/chunk-PTS3BZHK.mjs +0 -1
  90. package/dist/chunk-Q6I5J3IJ.mjs +0 -1
  91. package/dist/chunk-QOCKR2HO.mjs +0 -1
  92. package/dist/chunk-QOZCSS3B.mjs +0 -44
  93. package/dist/chunk-QSFPUODQ.mjs +0 -34
  94. package/dist/chunk-RY7QUBJI.mjs +0 -34
  95. package/dist/chunk-RZJRUG2I.mjs +0 -55
  96. package/dist/chunk-S3GGSQB5.mjs +0 -1
  97. package/dist/chunk-S3HRT5Z5.mjs +0 -14
  98. package/dist/chunk-SB7HBCCZ.mjs +0 -49
  99. package/dist/chunk-SEXTT3NJ.mjs +0 -25
  100. package/dist/chunk-SKEQCOPX.mjs +0 -43
  101. package/dist/chunk-TLBRVWD2.mjs +0 -1
  102. package/dist/chunk-TOOCHDUY.mjs +0 -36
  103. package/dist/chunk-TSKANLT4.mjs +0 -37
  104. package/dist/chunk-UAMZEZUI.mjs +0 -36
  105. package/dist/chunk-UQJ4XNWN.mjs +0 -26
  106. package/dist/chunk-VDSUUXCH.mjs +0 -34
  107. package/dist/chunk-VO7RY2ZK.mjs +0 -44
  108. package/dist/chunk-WJKDFBJ2.mjs +0 -34
  109. package/dist/chunk-WTC7PY7U.mjs +0 -54
  110. package/dist/chunk-WXDV7CMB.mjs +0 -14
  111. package/dist/chunk-X5FG3X6T.mjs +0 -1
  112. package/dist/chunk-X66DY4AZ.mjs +0 -1
  113. package/dist/chunk-XXSJXKWQ.mjs +0 -44
  114. package/dist/chunk-YZMVAQQG.mjs +0 -1
  115. package/dist/chunk-ZCXPZU7W.mjs +0 -20
  116. package/dist/chunk-ZJ4PNVYO.mjs +0 -52
  117. package/dist/chunk-ZU4QJLEY.mjs +0 -53
  118. package/dist/components/ingredients/ingredients-list.d.mts +0 -13
  119. package/dist/components/ingredients/ingredients-list.d.ts +0 -13
  120. package/dist/components/ingredients/ingredients-list.js +0 -2
  121. package/dist/components/ingredients/ingredients-list.mjs +0 -1
  122. package/dist/components/instructions/instruction-list.d.mts +0 -13
  123. package/dist/components/instructions/instruction-list.d.ts +0 -13
  124. package/dist/components/instructions/instruction-list.js +0 -1
  125. package/dist/components/instructions/instruction-list.mjs +0 -1
  126. package/dist/components/layouts/layout-default.d.mts +0 -7
  127. package/dist/components/layouts/layout-default.d.ts +0 -7
  128. package/dist/components/layouts/layout-default.js +0 -1
  129. package/dist/components/layouts/layout-default.mjs +0 -1
  130. package/dist/components/layouts/layout-types.d.mts +0 -8
  131. package/dist/components/layouts/layout-types.d.ts +0 -8
  132. package/dist/components/layouts/layout-types.js +0 -1
  133. package/dist/components/layouts/layout-types.mjs +0 -0
  134. package/dist/components/measurement-select.d.mts +0 -13
  135. package/dist/components/measurement-select.d.ts +0 -13
  136. package/dist/components/measurement-select.js +0 -1
  137. package/dist/components/measurement-select.mjs +0 -1
  138. package/dist/components/root.d.mts +0 -13
  139. package/dist/components/root.d.ts +0 -13
  140. package/dist/components/root.js +0 -1
  141. package/dist/components/root.mjs +0 -1
  142. package/dist/components/servings-select.d.mts +0 -11
  143. package/dist/components/servings-select.d.ts +0 -11
  144. package/dist/components/servings-select.js +0 -1
  145. package/dist/components/servings-select.mjs +0 -1
  146. package/dist/components/thumbnail.d.mts +0 -18
  147. package/dist/components/thumbnail.d.ts +0 -18
  148. package/dist/components/thumbnail.js +0 -1
  149. package/dist/components/thumbnail.mjs +0 -1
  150. package/dist/global-context.d.mts +0 -29
  151. package/dist/global-context.d.ts +0 -29
  152. package/dist/global-context.js +0 -1
  153. package/dist/global-context.mjs +0 -1
  154. package/dist/hooks/use-controllable.d.mts +0 -15
  155. package/dist/hooks/use-controllable.d.ts +0 -15
  156. package/dist/hooks/use-controllable.js +0 -1
  157. package/dist/hooks/use-controllable.mjs +0 -1
  158. package/dist/index.d.mts +0 -10
  159. package/dist/index.mjs +0 -1
  160. package/dist/recipe-context.d.mts +0 -29
  161. package/dist/recipe-context.d.ts +0 -29
  162. package/dist/recipe-context.js +0 -1
  163. package/dist/recipe-context.mjs +0 -1
  164. package/dist/types-from-core.d.mts +0 -1
  165. package/dist/types-from-core.d.ts +0 -1
  166. package/dist/types-from-core.js +0 -1
  167. package/dist/types-from-core.mjs +0 -1
package/dist/index.cjs ADDED
@@ -0,0 +1,1224 @@
1
+ "use client";
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ Image: () => Image,
25
+ Ingredient: () => Ingredient,
26
+ IngredientList: () => IngredientList,
27
+ IngredientName: () => IngredientName,
28
+ IngredientNote: () => IngredientNote,
29
+ IngredientOptional: () => IngredientOptional,
30
+ IngredientQuantity: () => IngredientQuantity,
31
+ Ingredients: () => Ingredients,
32
+ JsonLd: () => JsonLd,
33
+ RecipeBookProvider: () => RecipeBookProvider,
34
+ RecipeImage: () => Image,
35
+ RecipeIngredient: () => Ingredient,
36
+ RecipeIngredientList: () => IngredientList,
37
+ RecipeIngredientName: () => IngredientName,
38
+ RecipeIngredientNote: () => IngredientNote,
39
+ RecipeIngredientOptional: () => IngredientOptional,
40
+ RecipeIngredientQuantity: () => IngredientQuantity,
41
+ RecipeIngredients: () => Ingredients,
42
+ RecipeJsonLd: () => JsonLd,
43
+ RecipeRoot: () => Root,
44
+ RecipeServingsInput: () => ServingsInput,
45
+ RecipeServingsSelect: () => ServingsSelect,
46
+ RecipeStep: () => Step,
47
+ RecipeStepList: () => StepList,
48
+ RecipeStepName: () => StepName,
49
+ RecipeStepText: () => StepText,
50
+ RecipeSteps: () => Steps,
51
+ RecipeSystemOption: () => SystemOption,
52
+ RecipeSystemToggle: () => SystemToggle,
53
+ Root: () => Root,
54
+ ServingsInput: () => ServingsInput,
55
+ ServingsSelect: () => ServingsSelect,
56
+ Step: () => Step,
57
+ StepList: () => StepList,
58
+ StepName: () => StepName,
59
+ StepText: () => StepText,
60
+ Steps: () => Steps,
61
+ SystemOption: () => SystemOption,
62
+ SystemToggle: () => SystemToggle,
63
+ useFormatIngredient: () => useFormatIngredient,
64
+ useRecipe: () => useRecipe,
65
+ useRecipeState: () => useRecipeState
66
+ });
67
+ module.exports = __toCommonJS(index_exports);
68
+
69
+ // src/context.tsx
70
+ var import_core3 = require("@recipe-book/core");
71
+ var import_react4 = require("react");
72
+
73
+ // src/labels.ts
74
+ var defaultLabels = {
75
+ servings: "Servings",
76
+ servingUnit: { one: "serving", other: "servings" },
77
+ system: "Measurement system",
78
+ metric: "Metric",
79
+ us: "US",
80
+ fan: "fan",
81
+ optional: "(optional)",
82
+ to: "to"
83
+ };
84
+
85
+ // src/react.ts
86
+ var import_react = require("react");
87
+
88
+ // src/state/use-recipe-state.ts
89
+ var import_core2 = require("@recipe-book/core");
90
+ var import_react3 = require("react");
91
+
92
+ // src/is-parsed-recipe.ts
93
+ var import_core = require("@recipe-book/core");
94
+ function assertParsedRecipe(value, caller, fix) {
95
+ if (isFields(value) && typeof value.id === "string" && typeof value.name === "string" && isFields(value.times) && Array.isArray(value.times.custom) && Array.isArray(value.notes) && Array.isArray(value.equipment) && (value.servings === void 0 || isFields(value.servings) && typeof value.servings.amount === "number") && (value.yield === void 0 || isFields(value.yield) && typeof value.yield.amount === "number" && typeof value.yield.unit === "string") && isSections(
96
+ value.ingredients,
97
+ (item) => (
98
+ // Parts format each line with formatIngredient, which checks the name.
99
+ (item.quantity === void 0 || isFields(item.quantity) && (typeof item.quantity.amount === "number" || isFields(item.quantity.amount))) && Array.isArray(item.equivalents) && typeof item.optional === "boolean"
100
+ )
101
+ ) && isSections(value.steps, (item) => typeof item.text === "string")) {
102
+ return;
103
+ }
104
+ throw new import_core.RecipeBookError(
105
+ "invalid_argument",
106
+ `${caller}: recipe must be a parsed recipe, received ${value === void 0 || value === null ? String(value) : Array.isArray(value) ? "an array" : typeof value === "object" ? "an object that parseRecipe hasn't normalized" : `a ${typeof value}`}. ${fix}`
107
+ );
108
+ }
109
+ function isSections(value, isItem) {
110
+ return Array.isArray(value) && value.every(
111
+ (section) => isFields(section) && typeof section.id === "string" && Array.isArray(section.items) && section.items.every(
112
+ (item) => isFields(item) && typeof item.id === "string" && isItem(item)
113
+ )
114
+ );
115
+ }
116
+ function isFields(value) {
117
+ return typeof value === "object" && value !== null && !Array.isArray(value);
118
+ }
119
+
120
+ // src/quote.ts
121
+ var UNESCAPED_BY_JSON = /[\x80-\x9f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/g;
122
+ function quote(text) {
123
+ const shown = text.length > 60 ? `${text.slice(0, 59).replace(/[\ud800-\udbff]$/, "")}\u2026` : text;
124
+ return JSON.stringify(shown).replace(
125
+ UNESCAPED_BY_JSON,
126
+ (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`
127
+ );
128
+ }
129
+
130
+ // src/state/use-controllable-state.ts
131
+ var import_react2 = require("react");
132
+ function useControllableState(options) {
133
+ const { value, defaultValue, onChange, resetKey } = options;
134
+ const [internal, setInternal] = (0, import_react2.useState)(defaultValue);
135
+ const [internalResetKey, setInternalResetKey] = (0, import_react2.useState)(resetKey);
136
+ let uncontrolled = internal;
137
+ if (!Object.is(resetKey, internalResetKey)) {
138
+ setInternalResetKey(resetKey);
139
+ setInternal(defaultValue);
140
+ uncontrolled = defaultValue;
141
+ }
142
+ const current = value === void 0 ? uncontrolled : value;
143
+ const latest = (0, import_react2.useRef)({ value, current, onChange });
144
+ (0, import_react2.useInsertionEffect)(() => {
145
+ latest.current = { value, current, onChange };
146
+ });
147
+ const setValue = (0, import_react2.useCallback)((next) => {
148
+ const { value: value2, current: current2, onChange: onChange2 } = latest.current;
149
+ if (Object.is(next, current2)) return;
150
+ if (value2 === void 0) {
151
+ latest.current.current = next;
152
+ setInternal(next);
153
+ }
154
+ onChange2?.(next);
155
+ }, []);
156
+ if (process.env.NODE_ENV !== "production") {
157
+ const controlled = value !== void 0;
158
+ useModeWarnings(
159
+ options.prop,
160
+ controlled,
161
+ controlled && onChange === void 0
162
+ );
163
+ }
164
+ return [current, setValue];
165
+ }
166
+ function useModeWarnings(prop, controlled, readOnly) {
167
+ const initiallyControlled = (0, import_react2.useRef)(controlled);
168
+ const warned2 = (0, import_react2.useRef)({ readOnly: false, modeSwitch: false });
169
+ (0, import_react2.useEffect)(() => {
170
+ const name = prop.charAt(0).toUpperCase() + prop.slice(1);
171
+ if (readOnly && !warned2.current.readOnly) {
172
+ warned2.current.readOnly = true;
173
+ console.warn(
174
+ `Recipe Book: \`${prop}\` was passed without \`on${name}Change\`, so it is read-only. Pass \`on${name}Change\` to update it, or \`default${name}\` instead of \`${prop}\` for uncontrolled state.`
175
+ );
176
+ }
177
+ if (controlled !== initiallyControlled.current && !warned2.current.modeSwitch) {
178
+ warned2.current.modeSwitch = true;
179
+ const [from, to] = controlled ? ["undefined to a value", "uncontrolled to controlled"] : ["a value to undefined", "controlled to uncontrolled"];
180
+ console.warn(
181
+ `Recipe Book: \`${prop}\` changed from ${from}, which switches it from ${to}. Decide between controlled and uncontrolled for the component's lifetime: pass \`${prop}\` on every render (never undefined), or pass only \`default${name}\`.`
182
+ );
183
+ }
184
+ }, [prop, controlled, readOnly]);
185
+ }
186
+
187
+ // src/state/use-recipe-state.ts
188
+ function useRecipeState(options) {
189
+ const { recipe } = options;
190
+ if (process.env.NODE_ENV !== "production") {
191
+ (0, import_react3.useMemo)(() => {
192
+ assertParsedRecipe(
193
+ recipe,
194
+ "useRecipeState()",
195
+ "Call parseRecipe() from @recipe-book/react/core and pass result.data."
196
+ );
197
+ }, [recipe]);
198
+ }
199
+ const base = servingsBase(recipe);
200
+ const defaultServings = isServingsFor(options.defaultServings, base) ? options.defaultServings : base;
201
+ const defaultSystem = [options.defaultSystem, recipe.system].find(isSystem) ?? "metric";
202
+ const [rawServings, updateServings] = useControllableState({
203
+ prop: "servings",
204
+ value: options.servings,
205
+ defaultValue: defaultServings,
206
+ onChange: options.onServingsChange,
207
+ resetKey: recipe.id
208
+ });
209
+ const [rawSystem, updateSystem] = useControllableState({
210
+ prop: "system",
211
+ value: options.system,
212
+ defaultValue: defaultSystem,
213
+ onChange: options.onSystemChange,
214
+ resetKey: recipe.id
215
+ });
216
+ const servings = isServingsFor(rawServings, base) ? rawServings : defaultServings;
217
+ const system = isSystem(rawSystem) ? rawSystem : defaultSystem;
218
+ if (process.env.NODE_ENV !== "production") {
219
+ useWarning(
220
+ options.defaultServings !== void 0 && options.defaultServings !== defaultServings && `Recipe Book: \`defaultServings\` must be ${describeRange(base)}, received ${describe(options.defaultServings)}. Using ${defaultServings} instead.`
221
+ );
222
+ useWarning(
223
+ servings !== rawServings && `Recipe Book: \`servings\` must be ${describeRange(base)}, received ${describe(rawServings)}. Showing ${servings} instead.`
224
+ );
225
+ useWarning(
226
+ system !== rawSystem && `Recipe Book: \`system\` must be "metric" or "us", received ${describe(rawSystem)}. Showing "${system}" instead.`
227
+ );
228
+ }
229
+ const setServings = (0, import_react3.useCallback)(
230
+ (next) => {
231
+ if (isServingsFor(next, base)) {
232
+ updateServings(next);
233
+ } else if (process.env.NODE_ENV !== "production") {
234
+ console.warn(
235
+ `Recipe Book: setServings() expects ${describeRange(base)}, but received ${describe(next)}. The value was ignored.`
236
+ );
237
+ }
238
+ },
239
+ [updateServings, base]
240
+ );
241
+ const setSystem = (0, import_react3.useCallback)(
242
+ (next) => {
243
+ if (isSystem(next)) {
244
+ updateSystem(next);
245
+ } else if (process.env.NODE_ENV !== "production") {
246
+ console.warn(
247
+ `Recipe Book: setSystem() expects "metric" or "us", but received ${describe(next)}. The value was ignored.`
248
+ );
249
+ }
250
+ },
251
+ [updateSystem]
252
+ );
253
+ const steps = (0, import_react3.useMemo)(() => (0, import_core2.resolveRecipe)(recipe).steps, [recipe]);
254
+ const resolved = (0, import_react3.useMemo)(
255
+ () => (0, import_core2.resolveRecipe)(recipe, { servings, system }),
256
+ [recipe, servings, system]
257
+ );
258
+ return (0, import_react3.useMemo)(
259
+ () => ({
260
+ recipe,
261
+ servings,
262
+ setServings,
263
+ scale: resolved.scale,
264
+ system,
265
+ setSystem,
266
+ canConvert: resolved.canConvert,
267
+ ingredients: resolved.ingredients,
268
+ steps
269
+ }),
270
+ [recipe, servings, setServings, resolved, system, setSystem, steps]
271
+ );
272
+ }
273
+ function useWarning(message) {
274
+ const warned2 = (0, import_react3.useRef)(false);
275
+ (0, import_react3.useEffect)(() => {
276
+ if (message !== warned2.current) {
277
+ warned2.current = message;
278
+ if (message) console.warn(message);
279
+ }
280
+ }, [message]);
281
+ }
282
+ function isServings(value) {
283
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
284
+ }
285
+ function servingsBase(recipe) {
286
+ return [recipe.servings?.amount, recipe.yield?.amount].find(isServings) ?? 1;
287
+ }
288
+ function servingsWindow(base) {
289
+ const [mantissa, exponent] = base.toExponential().split("e");
290
+ return [
291
+ Number(`${mantissa}e${Number(exponent) - 6}`),
292
+ Number(`${mantissa}e${Number(exponent) + 6}`)
293
+ ];
294
+ }
295
+ function isServingsFor(value, base) {
296
+ const [min, max] = servingsWindow(base);
297
+ return isServings(value) && value >= min && value <= max;
298
+ }
299
+ function describeRange(base) {
300
+ const [min, max] = servingsWindow(base);
301
+ return `a number from ${roundInward(min, true)} to ${roundInward(max, false)} for this recipe`;
302
+ }
303
+ function roundInward(bound, up) {
304
+ const nearest = Number(bound.toPrecision(6));
305
+ if (up ? nearest >= bound : nearest <= bound) {
306
+ return nearest;
307
+ }
308
+ const [mantissa, exponent] = bound.toExponential().split("e");
309
+ const digits = Number(mantissa.replace(".", "").slice(0, 6));
310
+ return Number(`${up ? digits + 1 : digits}e${Number(exponent) - 5}`);
311
+ }
312
+ function isSystem(value) {
313
+ return value === "metric" || value === "us";
314
+ }
315
+ function describe(value) {
316
+ if (typeof value === "string") {
317
+ return quote(value);
318
+ }
319
+ if (typeof value === "function") {
320
+ return "a function";
321
+ }
322
+ if (value === null || typeof value !== "object") {
323
+ return quote(String(value)).slice(1, -1);
324
+ }
325
+ return Array.isArray(value) ? "an array" : "an object";
326
+ }
327
+
328
+ // src/context.tsx
329
+ var import_jsx_runtime = require("react/jsx-runtime");
330
+ var ConfigContext = /* @__PURE__ */ (0, import_react.createContext)({
331
+ locale: "en-US",
332
+ labels: defaultLabels
333
+ });
334
+ function RecipeBookProvider({
335
+ children,
336
+ ...config
337
+ }) {
338
+ const { locale, labels, defaultSystem } = useConfig(config);
339
+ const value = (0, import_react4.useMemo)(
340
+ () => ({ locale, labels, defaultSystem }),
341
+ [locale, labels, defaultSystem]
342
+ );
343
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ConfigContext.Provider, { value, children });
344
+ }
345
+ function useConfig({
346
+ locale,
347
+ labels,
348
+ defaultSystem
349
+ }) {
350
+ const outer = (0, import_react4.useContext)(ConfigContext);
351
+ return {
352
+ locale: locale ?? outer.locale,
353
+ labels: (0, import_react4.useMemo)(
354
+ () => ({
355
+ ...outer.labels,
356
+ ...Object.fromEntries(
357
+ Object.entries({ ...labels }).filter(
358
+ ([, value]) => value !== void 0
359
+ )
360
+ )
361
+ }),
362
+ [outer.labels, labels]
363
+ ),
364
+ defaultSystem: [defaultSystem, outer.defaultSystem].find(isSystem)
365
+ };
366
+ }
367
+ var RecipeDataContext = /* @__PURE__ */ (0, import_react.createContext)(null);
368
+ var RecipeStateContext = /* @__PURE__ */ (0, import_react.createContext)(null);
369
+ function useRequired(context, message) {
370
+ const value = (0, import_react4.useContext)(context);
371
+ if (!value) {
372
+ throw new import_core3.RecipeBookError("missing_parent", message);
373
+ }
374
+ return value;
375
+ }
376
+ function useRecipeData(part) {
377
+ return useRequired(
378
+ RecipeDataContext,
379
+ `<${part}> must be rendered inside <Recipe.Root>.`
380
+ );
381
+ }
382
+ function usePartState(part) {
383
+ return useRequired(
384
+ RecipeStateContext,
385
+ `<${part}> must be rendered inside <Recipe.Root>.`
386
+ );
387
+ }
388
+ function useRecipe() {
389
+ return useRequired(
390
+ RecipeStateContext,
391
+ "useRecipe() must be called inside <Recipe.Root>. To use recipe state without a Root, call useRecipeState()."
392
+ );
393
+ }
394
+
395
+ // src/parts/image.tsx
396
+ var import_jsx_runtime2 = require("react/jsx-runtime");
397
+ var Image = /* @__PURE__ */ (0, import_react.forwardRef)(function Image2({
398
+ as: Comp = "img",
399
+ src,
400
+ alt,
401
+ ...props
402
+ }, ref) {
403
+ const { recipe } = useRecipeData("Recipe.Image");
404
+ return recipe.image !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
405
+ Comp,
406
+ {
407
+ ...props,
408
+ src: src ?? recipe.image,
409
+ alt: alt ?? recipe.name,
410
+ "data-recipe-image": "",
411
+ ref
412
+ }
413
+ );
414
+ });
415
+
416
+ // src/parts/ingredients.tsx
417
+ var import_react12 = require("react");
418
+
419
+ // src/slot.tsx
420
+ var import_react7 = require("react");
421
+ function resolveAsChild(part, tag, asChild, children, defaults) {
422
+ if (asChild) {
423
+ if ((0, import_react.isValidElement)(children) && children.type !== import_react.Fragment) {
424
+ return [
425
+ Slot,
426
+ isEmpty(children.props.children) ? (0, import_react.cloneElement)(children, void 0, defaults) : children
427
+ ];
428
+ }
429
+ if (process.env.NODE_ENV !== "production" && !warned.has(part)) {
430
+ warned.add(part);
431
+ console.warn(
432
+ `Recipe Book: <${part}> has asChild but no single element child, received ${describeChildren(children)}, so it renders ${tag === "li" ? "an" : "a"} <${tag}>. Pass one element, such as <${tag}>.`
433
+ );
434
+ }
435
+ }
436
+ return [tag, isEmpty(children) ? defaults : children];
437
+ }
438
+ var Slot = /* @__PURE__ */ (0, import_react.forwardRef)(function Slot2({ children, ...props }, ref) {
439
+ const child = children;
440
+ const merged = { ...props, ...child.props };
441
+ for (const name in child.props) {
442
+ const ours = props[name];
443
+ const theirs = child.props[name];
444
+ if (name === "tabIndex" && ours !== void 0) {
445
+ merged[name] = ours;
446
+ } else if (name === "className") {
447
+ merged[name] = [ours, theirs].filter(Boolean).join(" ") || void 0;
448
+ } else if (name === "style") {
449
+ merged[name] = { ...ours, ...theirs };
450
+ } else if (/^on/.test(name) && typeof ours === "function") {
451
+ merged[name] = typeof theirs === "function" ? (...args) => {
452
+ const [first, second] = [theirs, ours];
453
+ first(...args);
454
+ second(...args);
455
+ } : ours;
456
+ }
457
+ }
458
+ const childRef = Object.getOwnPropertyDescriptor(child.props, "ref")?.value ?? child.ref;
459
+ merged.ref = (0, import_react7.useCallback)(
460
+ (node) => {
461
+ const refs = [ref, childRef];
462
+ const cleanups = refs.map((each) => setRef(each, node));
463
+ if (cleanups.some((cleanup) => typeof cleanup === "function")) {
464
+ return () => refs.forEach((each, index) => {
465
+ const cleanup = cleanups[index];
466
+ if (typeof cleanup === "function") {
467
+ cleanup();
468
+ } else {
469
+ setRef(each, null);
470
+ }
471
+ });
472
+ }
473
+ return void 0;
474
+ },
475
+ [ref, childRef]
476
+ );
477
+ return (0, import_react.cloneElement)(child, merged);
478
+ });
479
+ function setRef(ref, node) {
480
+ if (typeof ref === "function") {
481
+ return ref(node);
482
+ }
483
+ if (ref) {
484
+ ref.current = node;
485
+ }
486
+ return void 0;
487
+ }
488
+ var warned = /* @__PURE__ */ new Set();
489
+ function describeChildren(children) {
490
+ if ((0, import_react.isValidElement)(children)) {
491
+ return "a Fragment";
492
+ }
493
+ if (Array.isArray(children)) {
494
+ const count = children.length;
495
+ return `an array of ${count} ${count === 1 ? "child" : "children"}`;
496
+ }
497
+ if (typeof children === "string" || typeof children === "number") {
498
+ return `text ${quote(String(children))}`;
499
+ }
500
+ return isEmpty(children) ? "no children" : "a child that is not an element";
501
+ }
502
+ function isEmpty(children) {
503
+ return children == null || typeof children === "boolean";
504
+ }
505
+
506
+ // src/use-format-ingredient.ts
507
+ var import_core4 = require("@recipe-book/core");
508
+ var import_react9 = require("react");
509
+ function useFormatIngredient() {
510
+ const {
511
+ locale,
512
+ labels: { optional }
513
+ } = useRequired(
514
+ RecipeDataContext,
515
+ "useFormatIngredient() must be called inside <Recipe.Root>. To format ingredients without a Root, call formatIngredient() from @recipe-book/react/core."
516
+ );
517
+ return (0, import_react9.useCallback)(
518
+ (item) => formatLine(item, locale, optional),
519
+ [locale, optional]
520
+ );
521
+ }
522
+ function formatLine(item, locale, optional) {
523
+ const options = { locale };
524
+ const formatted = (0, import_core4.formatIngredient)(item, options);
525
+ const quantity = lineQuantity(item);
526
+ return {
527
+ ...formatted,
528
+ quantityParts: quantity ? (0, import_core4.formatQuantityToParts)(quantity, options) : [],
529
+ ...item.ingredient.optional && { optional }
530
+ };
531
+ }
532
+ function lineQuantity(item) {
533
+ return item.quantity ?? item.ingredient.quantity;
534
+ }
535
+
536
+ // src/parts/sections.tsx
537
+ var import_react10 = require("react");
538
+ var import_jsx_runtime3 = require("react/jsx-runtime");
539
+ function renderKeyed(items, render) {
540
+ return items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react.Fragment, { children: render(item) }, item.id));
541
+ }
542
+ function SectionBlock({
543
+ kind,
544
+ title,
545
+ headingLevel,
546
+ children
547
+ }) {
548
+ const headingId = (0, import_react10.useId)();
549
+ const marker = `data-recipe-${kind}-section`;
550
+ const Heading = `h${headingLevel}`;
551
+ return title ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { ...{ [marker]: "" }, "aria-labelledby": headingId, children: [
552
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Heading, { id: headingId, children: title }),
553
+ children
554
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ...{ [marker]: "" }, children });
555
+ }
556
+
557
+ // src/parts/ingredients.tsx
558
+ var import_jsx_runtime4 = require("react/jsx-runtime");
559
+ var Ingredients = /* @__PURE__ */ (0, import_react.forwardRef)(
560
+ function Ingredients2({ headingLevel = 3, children, ...props }, ref) {
561
+ const { ingredients } = usePartState("Recipe.Ingredients");
562
+ const renderSection = children ?? ((section) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
563
+ SectionBlock,
564
+ {
565
+ kind: "ingredient",
566
+ title: section.title,
567
+ headingLevel,
568
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(IngredientList, { section })
569
+ }
570
+ ));
571
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ...props, "data-recipe-ingredients": "", ref, children: renderKeyed(ingredients, renderSection) });
572
+ }
573
+ );
574
+ var IngredientList = /* @__PURE__ */ (0, import_react.forwardRef)(
575
+ function IngredientList2({ section, children = (item) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Ingredient, { item }), ...props }, ref) {
576
+ const { ingredients } = usePartState("Recipe.IngredientList");
577
+ const items = section?.items ?? ingredients.flatMap((s) => s.items);
578
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { ...props, "data-recipe-ingredient-list": "", ref, children: renderKeyed(items, children) });
579
+ }
580
+ );
581
+ var LineContext = /* @__PURE__ */ (0, import_react.createContext)(null);
582
+ var Ingredient = /* @__PURE__ */ (0, import_react.forwardRef)(
583
+ function Ingredient2({ item, asChild = false, children, ...props }, ref) {
584
+ const {
585
+ labels: { optional, to },
586
+ locale
587
+ } = useRecipeData("Recipe.Ingredient");
588
+ const line = (0, import_react12.useMemo)(
589
+ () => ({ ...formatLine(item, locale, optional), to }),
590
+ [item, locale, optional, to]
591
+ );
592
+ const quantity = lineQuantity(item);
593
+ const [Comp, content] = resolveAsChild(
594
+ "Recipe.Ingredient",
595
+ "li",
596
+ asChild,
597
+ children,
598
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
599
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(IngredientQuantity, {}),
600
+ quantity && (quantity.unit === void 0 ? "\xA0" : " "),
601
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(IngredientName, {}),
602
+ line.note !== void 0 && ", ",
603
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(IngredientNote, {}),
604
+ line.optional && " ",
605
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(IngredientOptional, {})
606
+ ] })
607
+ );
608
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(LineContext.Provider, { value: line, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
609
+ Comp,
610
+ {
611
+ ...props,
612
+ "data-recipe-ingredient": "",
613
+ "data-origin": quantity && item.origin,
614
+ "data-unquantified": quantity ? void 0 : "",
615
+ "data-optional": item.ingredient.optional ? "" : void 0,
616
+ ref,
617
+ children: content
618
+ }
619
+ ) });
620
+ }
621
+ );
622
+ function ingredientText(name, attribute, read) {
623
+ const part = `Recipe.${name}`;
624
+ function IngredientText({ asChild = false, children, ...props }, ref) {
625
+ const line = useRequired(
626
+ LineContext,
627
+ `<${part}> must be rendered inside <Recipe.Ingredient>.`
628
+ );
629
+ const text = read(line, children);
630
+ if (text === void 0) {
631
+ return null;
632
+ }
633
+ const [Comp, child] = resolveAsChild(part, "span", asChild, children, null);
634
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Comp, { ...props, ...{ [attribute]: "" }, ref, children: Comp === "span" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
635
+ children,
636
+ text
637
+ ] }) : (0, import_react.cloneElement)(child, void 0, child.props.children, text) });
638
+ }
639
+ if (process.env.NODE_ENV !== "production") {
640
+ Object.defineProperty(IngredientText, "name", { value: name });
641
+ }
642
+ return (0, import_react.forwardRef)(IngredientText);
643
+ }
644
+ var IngredientQuantity = /* @__PURE__ */ ingredientText(
645
+ "IngredientQuantity",
646
+ "data-recipe-quantity",
647
+ ({ quantity, quantityParts, to }) => quantity === void 0 ? void 0 : renderQuantity(quantityParts, to)
648
+ );
649
+ var hiddenStyle = {
650
+ position: "absolute",
651
+ border: 0,
652
+ width: 1,
653
+ height: 1,
654
+ padding: 0,
655
+ margin: -1,
656
+ overflow: "hidden",
657
+ clip: "rect(0, 0, 0, 0)",
658
+ whiteSpace: "nowrap",
659
+ wordWrap: "normal",
660
+ userSelect: "none",
661
+ WebkitUserSelect: "none"
662
+ };
663
+ function renderQuantity(parts, to) {
664
+ const join = (from) => from.map((part) => part.value).join("");
665
+ const dash = parts.findIndex((part) => part.type === "rangeSeparator");
666
+ const separator = parts[dash];
667
+ if (!separator) {
668
+ return join(parts);
669
+ }
670
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
671
+ join(parts.slice(0, dash)),
672
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { "data-recipe-range-separator": "", "aria-hidden": "true", children: separator.value }),
673
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: hiddenStyle, children: ` ${to} ` }),
674
+ join(parts.slice(dash + 1))
675
+ ] });
676
+ }
677
+ var IngredientName = /* @__PURE__ */ ingredientText(
678
+ "IngredientName",
679
+ "data-recipe-name",
680
+ ({ name }) => name
681
+ );
682
+ var IngredientNote = /* @__PURE__ */ ingredientText(
683
+ "IngredientNote",
684
+ "data-recipe-note",
685
+ ({ note }) => note
686
+ );
687
+ var IngredientOptional = /* @__PURE__ */ ingredientText(
688
+ "IngredientOptional",
689
+ "data-recipe-optional",
690
+ // An empty label shows only the part's own children, such as an icon.
691
+ ({ optional }, children) => optional === "" && isEmpty(children) ? void 0 : optional
692
+ );
693
+
694
+ // src/parts/json-ld.tsx
695
+ var import_schema_org = require("@recipe-book/core/schema-org");
696
+ var import_react14 = require("react");
697
+ var import_jsx_runtime5 = require("react/jsx-runtime");
698
+ var JsonLd = /* @__PURE__ */ (0, import_react.forwardRef)(
699
+ function JsonLd2({
700
+ servings,
701
+ system,
702
+ url,
703
+ extra,
704
+ // Off the props type, but a JavaScript caller can still pass them.
705
+ children,
706
+ src,
707
+ ...props
708
+ }, ref) {
709
+ const { recipe } = useRecipeData("Recipe.JsonLd");
710
+ const result = (0, import_react14.useMemo)(
711
+ () => (0, import_schema_org.toJsonLd)(recipe, { servings, system, url, extra }),
712
+ [recipe, servings, system, url, extra]
713
+ );
714
+ if (process.env.NODE_ENV !== "production") {
715
+ useWarningsReport(recipe.name, result.warnings);
716
+ }
717
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
718
+ "script",
719
+ {
720
+ ...props,
721
+ type: "application/ld+json",
722
+ "data-recipe-json-ld": "",
723
+ dangerouslySetInnerHTML: {
724
+ __html: JSON.stringify(result.data).replace(/</g, "\\u003c")
725
+ },
726
+ ref
727
+ }
728
+ );
729
+ }
730
+ );
731
+ function useWarningsReport(name, warnings) {
732
+ const reported = (0, import_react14.useRef)(void 0);
733
+ (0, import_react14.useEffect)(() => {
734
+ const key = JSON.stringify(warnings);
735
+ const count = warnings.length;
736
+ if (count > 0 && key !== reported.current) {
737
+ console.warn(
738
+ `Recipe Book: <Recipe.JsonLd> for ${quote(name)} has ${count} ${count === 1 ? "warning" : "warnings"}. The script still renders, but without the fields they name.`,
739
+ warnings
740
+ );
741
+ }
742
+ reported.current = key;
743
+ }, [name, warnings]);
744
+ }
745
+
746
+ // src/parts/root.tsx
747
+ var import_react16 = require("react");
748
+ var import_jsx_runtime6 = require("react/jsx-runtime");
749
+ function Root({
750
+ recipe,
751
+ parse,
752
+ fallback = null,
753
+ children,
754
+ ...options
755
+ }) {
756
+ const result = (0, import_react16.useMemo)(() => {
757
+ if (parse !== void 0) {
758
+ return parse(recipe);
759
+ }
760
+ assertParsedRecipe(
761
+ recipe,
762
+ "<Recipe.Root>",
763
+ "Call parseRecipe() from @recipe-book/react/core on the server, in a loader, or at build time and pass result.data, or pass parse={parseRecipe} to parse it in the browser."
764
+ );
765
+ return { success: true, data: recipe, warnings: [] };
766
+ }, [parse, recipe]);
767
+ if (process.env.NODE_ENV !== "production") {
768
+ useParseReport(result);
769
+ }
770
+ if (!result.success) {
771
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: fallback });
772
+ }
773
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RecipeProvider, { ...options, recipe: result.data, children });
774
+ }
775
+ function RecipeProvider({ children, ...options }) {
776
+ const { locale, labels, defaultSystem } = useConfig(options);
777
+ const state = useRecipeState({ ...options, defaultSystem });
778
+ const [temperatures, setTemperatures] = (0, import_react16.useState)(0);
779
+ const reportTemperature = (0, import_react16.useCallback)(() => {
780
+ setTemperatures((count) => count + 1);
781
+ return () => setTemperatures((count) => count - 1);
782
+ }, []);
783
+ const canConvert = state.canConvert || temperatures > 0;
784
+ const value = (0, import_react16.useMemo)(() => ({ ...state, canConvert }), [state, canConvert]);
785
+ const { recipe, steps } = state;
786
+ const data = (0, import_react16.useMemo)(
787
+ () => ({ recipe, steps, labels, locale, reportTemperature }),
788
+ [recipe, steps, labels, locale, reportTemperature]
789
+ );
790
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RecipeDataContext.Provider, { value: data, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RecipeStateContext.Provider, { value, children }) });
791
+ }
792
+ function useParseReport(result) {
793
+ const reported = (0, import_react16.useRef)(void 0);
794
+ (0, import_react16.useEffect)(() => {
795
+ const key = result.success ? result.warnings.length > 0 ? `warnings ${JSON.stringify(result.warnings)}` : void 0 : `error ${JSON.stringify([result.error.message, result.error.issues])}`;
796
+ if (key === reported.current) {
797
+ return;
798
+ }
799
+ reported.current = key;
800
+ if (key === void 0) {
801
+ return;
802
+ }
803
+ if (result.success) {
804
+ const count = result.warnings.length;
805
+ console.warn(
806
+ `Recipe Book: ${quote(result.data.name)} has ${count} ${count === 1 ? "warning" : "warnings"}. It still renders, but a warning can mean a misspelled field.`,
807
+ result.warnings
808
+ );
809
+ } else {
810
+ console.error(result.error);
811
+ }
812
+ }, [result]);
813
+ }
814
+
815
+ // src/parts/servings.tsx
816
+ var import_core5 = require("@recipe-book/core");
817
+ var import_react17 = require("react");
818
+
819
+ // src/compose-event-handlers.ts
820
+ function composeEventHandlers(theirs, ours) {
821
+ return (event) => {
822
+ theirs?.(event);
823
+ if (!event.defaultPrevented) {
824
+ ours(event);
825
+ }
826
+ };
827
+ }
828
+
829
+ // src/parts/servings.tsx
830
+ var import_jsx_runtime7 = require("react/jsx-runtime");
831
+ var ServingsSelect = /* @__PURE__ */ (0, import_react.forwardRef)(
832
+ function ServingsSelect2({ options, "aria-label": ariaLabel, onChange, ...props }, ref) {
833
+ const { recipe, labels, locale } = useRecipeData("Recipe.ServingsSelect");
834
+ const { servings, setServings } = usePartState("Recipe.ServingsSelect");
835
+ const base = servingsBase(recipe);
836
+ const choices = [
837
+ .../* @__PURE__ */ new Set([
838
+ ...(options ?? [0.5, 1, 2, 3].map((factor) => Math.max(1, Math.round(base * factor)))).filter((choice) => isServingsFor(choice, base)),
839
+ servings
840
+ ])
841
+ ].sort((a, b) => a - b);
842
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
843
+ "select",
844
+ {
845
+ autoComplete: "off",
846
+ ...props,
847
+ "aria-label": (ariaLabel ?? labels.servings) || void 0,
848
+ value: servings,
849
+ "data-recipe-servings-select": "",
850
+ ref,
851
+ onChange: composeEventHandlers(onChange, (event) => {
852
+ setServings(Number(event.target.value));
853
+ }),
854
+ children: choices.map((choice) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: choice, children: optionText(choice, recipe, base, labels, locale) }, choice))
855
+ }
856
+ );
857
+ }
858
+ );
859
+ var ServingsInput = /* @__PURE__ */ (0, import_react.forwardRef)(
860
+ function ServingsInput2({
861
+ min = 1,
862
+ max,
863
+ step,
864
+ inputMode,
865
+ "aria-label": ariaLabel,
866
+ onChange,
867
+ onBlur,
868
+ ...props
869
+ }, ref) {
870
+ const { recipe, labels } = useRecipeData("Recipe.ServingsInput");
871
+ const { servings, setServings } = usePartState("Recipe.ServingsInput");
872
+ const base = servingsBase(recipe);
873
+ const [draft, setDraft] = (0, import_react17.useState)({ text: String(servings), servings });
874
+ if (draft.servings !== servings) {
875
+ setDraft({
876
+ text: Number(draft.text) === servings ? draft.text : String(servings),
877
+ servings
878
+ });
879
+ }
880
+ const whole = Number.isInteger(min) && Number.isInteger(step ?? 1);
881
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
882
+ "input",
883
+ {
884
+ autoComplete: "off",
885
+ ...props,
886
+ type: "number",
887
+ inputMode: inputMode ?? (whole ? "numeric" : "decimal"),
888
+ "aria-label": (ariaLabel ?? labels.servings) || void 0,
889
+ min,
890
+ max,
891
+ step,
892
+ value: draft.text,
893
+ "data-recipe-servings-input": "",
894
+ ref,
895
+ onChange: composeEventHandlers(onChange, (event) => {
896
+ const text = event.target.value;
897
+ const next = Number(text);
898
+ setDraft({ text, servings });
899
+ if (isServingsFor(next, base) && next >= min && (max === void 0 || next <= max)) {
900
+ const steps = step === "any" ? 0 : (next - min) / (step && step > 0 ? step : 1);
901
+ if (Math.abs(steps - Math.round(steps)) <= (Math.round(steps) && 1e-15 * (steps + steps / (1 - min / next)))) {
902
+ setServings(next);
903
+ }
904
+ }
905
+ }),
906
+ onBlur: composeEventHandlers(onBlur, () => {
907
+ setDraft({ text: String(servings), servings });
908
+ })
909
+ }
910
+ );
911
+ }
912
+ );
913
+ function optionText(choice, { servings, yield: makes }, base, labels, locale) {
914
+ const format = {
915
+ locale,
916
+ rounding: Number.isInteger(choice * 4) ? "kitchen" : "none"
917
+ };
918
+ if (base === servings?.amount) {
919
+ return (0, import_core5.formatYield)(
920
+ {
921
+ amount: choice,
922
+ unit: labels.servingUnit.one,
923
+ plural: labels.servingUnit.other
924
+ },
925
+ format
926
+ );
927
+ }
928
+ if (base === makes?.amount) {
929
+ return (0, import_core5.formatYield)(
930
+ { amount: choice, unit: makes.unit, plural: makes.plural },
931
+ format
932
+ );
933
+ }
934
+ return `${(0, import_core5.formatServings)({ amount: choice }, format)}\xD7`;
935
+ }
936
+
937
+ // src/parts/step-text.tsx
938
+ var import_core6 = require("@recipe-book/core");
939
+ var import_react20 = require("react");
940
+
941
+ // src/parts/steps.tsx
942
+ var import_jsx_runtime8 = require("react/jsx-runtime");
943
+ var Steps = /* @__PURE__ */ (0, import_react.forwardRef)(function Steps2({ headingLevel = 3, children, ...props }, ref) {
944
+ const { steps } = useRecipeData("Recipe.Steps");
945
+ const renderSection = children ?? ((section) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
946
+ SectionBlock,
947
+ {
948
+ kind: "step",
949
+ title: section.title,
950
+ headingLevel,
951
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(StepList, { section })
952
+ }
953
+ ));
954
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { ...props, "data-recipe-steps": "", ref, children: renderKeyed(steps, renderSection) });
955
+ });
956
+ var StepList = /* @__PURE__ */ (0, import_react.forwardRef)(
957
+ function StepList2({ section, children = (step) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Step, { item: step }), start, ...props }, ref) {
958
+ const { steps } = useRecipeData("Recipe.StepList");
959
+ const items = section?.items ?? steps.flatMap((s) => s.items);
960
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
961
+ "ol",
962
+ {
963
+ ...props,
964
+ start: start ?? items[0]?.number,
965
+ "data-recipe-step-list": "",
966
+ ref,
967
+ children: renderKeyed(items, children)
968
+ }
969
+ );
970
+ }
971
+ );
972
+ var StepContext = /* @__PURE__ */ (0, import_react.createContext)(null);
973
+ var Step = /* @__PURE__ */ (0, import_react.forwardRef)(function Step2({ item, asChild = false, children, ...props }, ref) {
974
+ useRecipeData("Recipe.Step");
975
+ const [Comp, content] = resolveAsChild(
976
+ "Recipe.Step",
977
+ "li",
978
+ asChild,
979
+ children,
980
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
981
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(StepName, {}),
982
+ item.name !== void 0 && " ",
983
+ item.text
984
+ ] })
985
+ );
986
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(StepContext.Provider, { value: item, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Comp, { ...props, "data-recipe-step": "", ref, children: content }) });
987
+ });
988
+ var StepName = /* @__PURE__ */ (0, import_react.forwardRef)(
989
+ function StepName2(props, ref) {
990
+ const step = useRequired(
991
+ StepContext,
992
+ "<Recipe.StepName> must be rendered inside <Recipe.Step>."
993
+ );
994
+ return step.name === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("b", { ...props, "data-recipe-name": "", ref, children: step.name });
995
+ }
996
+ );
997
+
998
+ // src/parts/step-text.tsx
999
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1000
+ var StepText = /* @__PURE__ */ (0, import_react.forwardRef)(
1001
+ function StepText2(props, ref) {
1002
+ const step = useRequired(
1003
+ StepContext,
1004
+ "<Recipe.StepText> must be rendered inside <Recipe.Step>."
1005
+ );
1006
+ const segments = (0, import_react20.useMemo)(() => (0, import_core6.segmentStepText)(step.text), [step.text]);
1007
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { ...props, "data-recipe-step-text": "", ref, children: segments.map(
1008
+ (segment, index) => segment.type === "temperature" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Temperature, { segment }, index) : (
1009
+ // Core may add segment types in a minor release; show those as authored.
1010
+ segment.text
1011
+ )
1012
+ ) });
1013
+ }
1014
+ );
1015
+ function Temperature({ segment }) {
1016
+ const { labels, locale, reportTemperature } = useRecipeData("Recipe.StepText");
1017
+ const unit = usePartState("Recipe.StepText").system === "us" ? "F" : "C";
1018
+ const readings = [segment, ...segment.alternatives];
1019
+ (0, import_react20.useEffect)(reportTemperature, [reportTemperature]);
1020
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { "data-recipe-temperature": "", children: [false, true].map((fan) => {
1021
+ const kind = readings.filter((reading2) => reading2.fan === fan);
1022
+ const reading = kind.find((each) => each.temperature.unit === unit) ?? kind[0];
1023
+ return reading && (0, import_core6.formatTemperature)((0, import_core6.convertTemperature)(reading.temperature, unit), {
1024
+ locale,
1025
+ rounding: reading.temperature.unit === unit ? "none" : void 0
1026
+ }) + (fan && labels.fan ? ` ${labels.fan}` : "");
1027
+ }).filter(Boolean).join(" / ") });
1028
+ }
1029
+
1030
+ // src/parts/system-toggle.tsx
1031
+ var import_react22 = require("react");
1032
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1033
+ var ToggleContext = /* @__PURE__ */ (0, import_react.createContext)(
1034
+ null
1035
+ );
1036
+ var SystemToggle = /* @__PURE__ */ (0, import_react.forwardRef)(
1037
+ function SystemToggle2({ asChild = false, "aria-label": ariaLabel, children, ...props }, ref) {
1038
+ const { labels } = useRecipeData("Recipe.SystemToggle");
1039
+ const { system, setSystem, canConvert } = usePartState(
1040
+ "Recipe.SystemToggle"
1041
+ );
1042
+ const disabled = !canConvert;
1043
+ const [offers, setOffers] = (0, import_react22.useState)([]);
1044
+ const [chosen, choose] = (0, import_react22.useState)();
1045
+ const offer = (0, import_react22.useCallback)((id, value) => {
1046
+ const entry = { id, value };
1047
+ setOffers((current) => [...current, entry]);
1048
+ return () => setOffers((current) => current.filter((other) => other !== entry));
1049
+ }, []);
1050
+ const tabStop = (offers.find((entry) => entry.value === system && entry.id === chosen) ?? offers.find((entry) => entry.value === system) ?? offers[0])?.id;
1051
+ const context = (0, import_react22.useMemo)(
1052
+ () => ({ system, setSystem, disabled, tabStop, offer, choose }),
1053
+ [system, setSystem, disabled, tabStop, offer]
1054
+ );
1055
+ const [Comp, content] = resolveAsChild(
1056
+ "Recipe.SystemToggle",
1057
+ "div",
1058
+ asChild,
1059
+ children,
1060
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
1061
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(SystemOption, { value: "metric" }),
1062
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(SystemOption, { value: "us" })
1063
+ ] })
1064
+ );
1065
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ToggleContext.Provider, { value: context, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1066
+ Comp,
1067
+ {
1068
+ ...props,
1069
+ "aria-label": ariaLabel ?? (props["aria-labelledby"] === void 0 ? labels.system : void 0),
1070
+ role: "radiogroup",
1071
+ "aria-disabled": disabled || void 0,
1072
+ "data-recipe-system-toggle": "",
1073
+ "data-disabled": disabled ? "" : void 0,
1074
+ ref,
1075
+ children: content
1076
+ }
1077
+ ) });
1078
+ }
1079
+ );
1080
+ var SystemOption = /* @__PURE__ */ (0, import_react.forwardRef)(
1081
+ function SystemOption2({
1082
+ value,
1083
+ asChild = false,
1084
+ type,
1085
+ disabled: ownDisabled = false,
1086
+ children,
1087
+ onClick,
1088
+ onKeyDown,
1089
+ ...props
1090
+ }, ref) {
1091
+ const {
1092
+ system,
1093
+ setSystem,
1094
+ disabled: groupDisabled,
1095
+ tabStop,
1096
+ offer,
1097
+ choose
1098
+ } = useRequired(
1099
+ ToggleContext,
1100
+ "<Recipe.SystemOption> must be rendered inside <Recipe.SystemToggle>."
1101
+ );
1102
+ const { labels } = useRecipeData("Recipe.SystemOption");
1103
+ const id = (0, import_react22.useId)();
1104
+ const disabled = groupDisabled || ownDisabled;
1105
+ const checked = value === system;
1106
+ (0, import_react22.useEffect)(
1107
+ () => disabled ? void 0 : offer(id, value),
1108
+ [disabled, offer, id, value]
1109
+ );
1110
+ const tabbable = !disabled && (tabStop === void 0 ? checked : tabStop === id);
1111
+ const [Comp, content] = resolveAsChild(
1112
+ "Recipe.SystemOption",
1113
+ "button",
1114
+ asChild,
1115
+ children,
1116
+ labels[value]
1117
+ );
1118
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1119
+ Comp,
1120
+ {
1121
+ autoComplete: "off",
1122
+ ...props,
1123
+ type: type ?? "button",
1124
+ role: "radio",
1125
+ "aria-checked": checked,
1126
+ tabIndex: tabbable ? 0 : -1,
1127
+ disabled,
1128
+ "data-recipe-system-option": "",
1129
+ "data-state": checked ? "checked" : "unchecked",
1130
+ "data-disabled": disabled ? "" : void 0,
1131
+ ref,
1132
+ onClick: composeEventHandlers(onClick, () => {
1133
+ if (!disabled) {
1134
+ setSystem(value);
1135
+ choose(id);
1136
+ }
1137
+ }),
1138
+ onKeyDown: composeEventHandlers(onKeyDown, moveOnArrowKey),
1139
+ children: content
1140
+ }
1141
+ );
1142
+ }
1143
+ );
1144
+ function moveOnArrowKey(event) {
1145
+ if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
1146
+ return;
1147
+ }
1148
+ const { key } = event;
1149
+ let step;
1150
+ if (key === "ArrowDown") {
1151
+ step = 1;
1152
+ } else if (key === "ArrowUp") {
1153
+ step = -1;
1154
+ } else if (key === "ArrowRight") {
1155
+ step = isRtl(event.currentTarget) ? -1 : 1;
1156
+ } else if (key === "ArrowLeft") {
1157
+ step = isRtl(event.currentTarget) ? 1 : -1;
1158
+ } else {
1159
+ return;
1160
+ }
1161
+ const group = event.currentTarget.closest("[data-recipe-system-toggle]");
1162
+ const options = Array.from(
1163
+ group?.querySelectorAll(
1164
+ "[data-recipe-system-option]:not([data-disabled])"
1165
+ ) ?? []
1166
+ );
1167
+ const index = options.indexOf(event.currentTarget);
1168
+ if (index === -1) {
1169
+ return;
1170
+ }
1171
+ event.preventDefault();
1172
+ const next = options[(index + step + options.length) % options.length];
1173
+ next?.focus();
1174
+ next?.click();
1175
+ }
1176
+ function isRtl(element) {
1177
+ return element.closest("[dir]")?.getAttribute("dir")?.toLowerCase() === "rtl";
1178
+ }
1179
+ // Annotate the CommonJS export names for ESM import in node:
1180
+ 0 && (module.exports = {
1181
+ Image,
1182
+ Ingredient,
1183
+ IngredientList,
1184
+ IngredientName,
1185
+ IngredientNote,
1186
+ IngredientOptional,
1187
+ IngredientQuantity,
1188
+ Ingredients,
1189
+ JsonLd,
1190
+ RecipeBookProvider,
1191
+ RecipeImage,
1192
+ RecipeIngredient,
1193
+ RecipeIngredientList,
1194
+ RecipeIngredientName,
1195
+ RecipeIngredientNote,
1196
+ RecipeIngredientOptional,
1197
+ RecipeIngredientQuantity,
1198
+ RecipeIngredients,
1199
+ RecipeJsonLd,
1200
+ RecipeRoot,
1201
+ RecipeServingsInput,
1202
+ RecipeServingsSelect,
1203
+ RecipeStep,
1204
+ RecipeStepList,
1205
+ RecipeStepName,
1206
+ RecipeStepText,
1207
+ RecipeSteps,
1208
+ RecipeSystemOption,
1209
+ RecipeSystemToggle,
1210
+ Root,
1211
+ ServingsInput,
1212
+ ServingsSelect,
1213
+ Step,
1214
+ StepList,
1215
+ StepName,
1216
+ StepText,
1217
+ Steps,
1218
+ SystemOption,
1219
+ SystemToggle,
1220
+ useFormatIngredient,
1221
+ useRecipe,
1222
+ useRecipeState
1223
+ });
1224
+ //# sourceMappingURL=index.cjs.map