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