@recipe-book/react 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -66,12 +66,16 @@ __export(index_exports, {
66
66
  });
67
67
  module.exports = __toCommonJS(index_exports);
68
68
 
69
- // src/parts/image.tsx
70
- var import_react4 = require("react");
71
-
72
69
  // src/context.tsx
73
70
  var import_core3 = require("@recipe-book/core");
74
- var import_react3 = require("react");
71
+ var import_react4 = require("react");
72
+
73
+ // src/display-name.ts
74
+ function setDisplayName(component, name) {
75
+ const target = component;
76
+ target.displayName = name;
77
+ Object.defineProperty(target.render ?? component, "name", { value: name });
78
+ }
75
79
 
76
80
  // src/labels.ts
77
81
  var defaultLabels = {
@@ -85,9 +89,12 @@ var defaultLabels = {
85
89
  to: "to"
86
90
  };
87
91
 
92
+ // src/react.ts
93
+ var import_react = require("react");
94
+
88
95
  // src/state/use-recipe-state.ts
89
96
  var import_core2 = require("@recipe-book/core");
90
- var import_react2 = require("react");
97
+ var import_react3 = require("react");
91
98
 
92
99
  // src/is-parsed-recipe.ts
93
100
  var import_core = require("@recipe-book/core");
@@ -128,46 +135,45 @@ function quote(text) {
128
135
  }
129
136
 
130
137
  // src/state/use-controllable-state.ts
131
- var import_react = require("react");
132
- function useControllableState({
133
- prop,
134
- value,
135
- defaultValue,
136
- onChange,
137
- resetKey
138
- }) {
139
- const controlled = value !== void 0;
140
- const [internal, setInternal] = (0, import_react.useState)(defaultValue);
141
- const [internalResetKey, setInternalResetKey] = (0, import_react.useState)(resetKey);
138
+ var import_react2 = require("react");
139
+ function useControllableState(options) {
140
+ const { value, defaultValue, onChange, resetKey } = options;
141
+ const [internal, setInternal] = (0, import_react2.useState)(defaultValue);
142
+ const [internalResetKey, setInternalResetKey] = (0, import_react2.useState)(resetKey);
142
143
  let uncontrolled = internal;
143
144
  if (!Object.is(resetKey, internalResetKey)) {
144
145
  setInternalResetKey(resetKey);
145
146
  setInternal(defaultValue);
146
147
  uncontrolled = defaultValue;
147
148
  }
148
- const current = controlled ? value : uncontrolled;
149
- const latest = (0, import_react.useRef)({ controlled, value: current, onChange });
150
- (0, import_react.useInsertionEffect)(() => {
151
- latest.current = { controlled, value: current, onChange };
149
+ const current = value === void 0 ? uncontrolled : value;
150
+ const latest = (0, import_react2.useRef)({ value, current, onChange });
151
+ (0, import_react2.useInsertionEffect)(() => {
152
+ latest.current = { value, current, onChange };
152
153
  });
153
- const setValue = (0, import_react.useCallback)((next) => {
154
- const { controlled: controlled2, value: value2, onChange: onChange2 } = latest.current;
155
- if (Object.is(next, value2)) return;
156
- if (!controlled2) {
157
- latest.current.value = next;
154
+ const setValue = (0, import_react2.useCallback)((next) => {
155
+ const { value: value2, current: current2, onChange: onChange2 } = latest.current;
156
+ if (Object.is(next, current2)) return;
157
+ if (value2 === void 0) {
158
+ latest.current.current = next;
158
159
  setInternal(next);
159
160
  }
160
161
  onChange2?.(next);
161
162
  }, []);
162
163
  if (process.env.NODE_ENV !== "production") {
163
- useModeWarnings(prop, controlled, controlled && onChange === void 0);
164
+ const controlled = value !== void 0;
165
+ useModeWarnings(
166
+ options.prop,
167
+ controlled,
168
+ controlled && onChange === void 0
169
+ );
164
170
  }
165
171
  return [current, setValue];
166
172
  }
167
173
  function useModeWarnings(prop, controlled, readOnly) {
168
- const initiallyControlled = (0, import_react.useRef)(controlled);
169
- const warned2 = (0, import_react.useRef)({ readOnly: false, modeSwitch: false });
170
- (0, import_react.useEffect)(() => {
174
+ const initiallyControlled = (0, import_react2.useRef)(controlled);
175
+ const warned2 = (0, import_react2.useRef)({ readOnly: false, modeSwitch: false });
176
+ (0, import_react2.useEffect)(() => {
171
177
  const name = prop.charAt(0).toUpperCase() + prop.slice(1);
172
178
  if (readOnly && !warned2.current.readOnly) {
173
179
  warned2.current.readOnly = true;
@@ -189,11 +195,11 @@ function useModeWarnings(prop, controlled, readOnly) {
189
195
  function useRecipeState(options) {
190
196
  const { recipe } = options;
191
197
  if (process.env.NODE_ENV !== "production") {
192
- (0, import_react2.useMemo)(() => {
198
+ (0, import_react3.useMemo)(() => {
193
199
  assertParsedRecipe(
194
200
  recipe,
195
201
  "useRecipeState()",
196
- "Call parseRecipe() from @recipe-book/core and pass result.data."
202
+ "Call parseRecipe() from @recipe-book/react/core and pass result.data."
197
203
  );
198
204
  }, [recipe]);
199
205
  }
@@ -227,7 +233,7 @@ function useRecipeState(options) {
227
233
  system !== rawSystem && `Recipe Book: \`system\` must be "metric" or "us", received ${describe(rawSystem)}. Showing "${system}" instead.`
228
234
  );
229
235
  }
230
- const setServings = (0, import_react2.useCallback)(
236
+ const setServings = (0, import_react3.useCallback)(
231
237
  (next) => {
232
238
  if (isServingsFor(next, base)) {
233
239
  updateServings(next);
@@ -239,7 +245,7 @@ function useRecipeState(options) {
239
245
  },
240
246
  [updateServings, base]
241
247
  );
242
- const setSystem = (0, import_react2.useCallback)(
248
+ const setSystem = (0, import_react3.useCallback)(
243
249
  (next) => {
244
250
  if (isSystem(next)) {
245
251
  updateSystem(next);
@@ -251,66 +257,64 @@ function useRecipeState(options) {
251
257
  },
252
258
  [updateSystem]
253
259
  );
254
- const steps = (0, import_react2.useMemo)(() => numberSteps(recipe), [recipe]);
255
- const { scale, canConvert, ingredients } = (0, import_react2.useMemo)(
260
+ const steps = (0, import_react3.useMemo)(() => (0, import_core2.resolveRecipe)(recipe).steps, [recipe]);
261
+ const resolved = (0, import_react3.useMemo)(
256
262
  () => (0, import_core2.resolveRecipe)(recipe, { servings, system }),
257
263
  [recipe, servings, system]
258
264
  );
259
- return (0, import_react2.useMemo)(
265
+ return (0, import_react3.useMemo)(
260
266
  () => ({
261
267
  recipe,
262
268
  servings,
263
269
  setServings,
264
- scale,
270
+ scale: resolved.scale,
265
271
  system,
266
272
  setSystem,
267
- canConvert,
268
- ingredients,
273
+ canConvert: resolved.canConvert,
274
+ ingredients: resolved.ingredients,
269
275
  steps
270
276
  }),
271
- [
272
- recipe,
273
- servings,
274
- setServings,
275
- scale,
276
- system,
277
- setSystem,
278
- canConvert,
279
- ingredients,
280
- steps
281
- ]
277
+ [recipe, servings, setServings, resolved, system, setSystem, steps]
282
278
  );
283
279
  }
284
280
  function useWarning(message) {
285
- const warned2 = (0, import_react2.useRef)(false);
286
- (0, import_react2.useEffect)(() => {
281
+ const warned2 = (0, import_react3.useRef)(false);
282
+ (0, import_react3.useEffect)(() => {
287
283
  if (message !== warned2.current) {
288
284
  warned2.current = message;
289
285
  if (message) console.warn(message);
290
286
  }
291
287
  }, [message]);
292
288
  }
293
- function numberSteps(recipe) {
294
- let number = 0;
295
- return recipe.steps.map((section) => ({
296
- ...section,
297
- items: section.items.map((step) => ({ ...step, number: ++number }))
298
- }));
299
- }
300
289
  function isServings(value) {
301
290
  return typeof value === "number" && Number.isFinite(value) && value > 0;
302
291
  }
303
292
  function servingsBase(recipe) {
304
293
  return [recipe.servings?.amount, recipe.yield?.amount].find(isServings) ?? 1;
305
294
  }
306
- var MIN_SCALE = 1e-6;
307
- var MAX_SCALE = 1e6;
295
+ function servingsWindow(base) {
296
+ const [mantissa, exponent] = base.toExponential().split("e");
297
+ return [
298
+ Number(`${mantissa}e${Number(exponent) - 6}`),
299
+ Number(`${mantissa}e${Number(exponent) + 6}`)
300
+ ];
301
+ }
308
302
  function isServingsFor(value, base) {
309
- return isServings(value) && value / base >= MIN_SCALE && value / base <= MAX_SCALE;
303
+ const [min, max] = servingsWindow(base);
304
+ return isServings(value) && value >= min && value <= max;
310
305
  }
311
306
  function describeRange(base) {
312
- const bound = (value) => String(Number(value.toPrecision(6)));
313
- return `a number from ${bound(base * MIN_SCALE)} to ${bound(base * MAX_SCALE)} for this recipe`;
307
+ const [min, max] = servingsWindow(base);
308
+ return `a number from ${roundInward(min, true)} to ${roundInward(max, false)} for this recipe`;
309
+ }
310
+ function roundInward(bound, up) {
311
+ const nearest = Number(bound.toPrecision(6));
312
+ if (up ? nearest >= bound : nearest <= bound) {
313
+ return nearest;
314
+ }
315
+ const [mantissa, exponent] = bound.toExponential().split("e");
316
+ const digits = Number(mantissa.replace(".", "").slice(0, 6));
317
+ return Number(`${up ? digits + 1 : digits}e${Number(exponent) - 5}`);
314
318
  }
315
319
  function isSystem(value) {
316
320
  return value === "metric" || value === "us";
@@ -330,7 +334,7 @@ function describe(value) {
330
334
 
331
335
  // src/context.tsx
332
336
  var import_jsx_runtime = require("react/jsx-runtime");
333
- var ConfigContext = /* @__PURE__ */ (0, import_react3.createContext)({
337
+ var ConfigContext = /* @__PURE__ */ (0, import_react.createContext)({
334
338
  locale: "en-US",
335
339
  labels: defaultLabels
336
340
  });
@@ -339,7 +343,7 @@ function RecipeBookProvider({
339
343
  ...config
340
344
  }) {
341
345
  const { locale, labels, defaultSystem } = useConfig(config);
342
- const value = (0, import_react3.useMemo)(
346
+ const value = (0, import_react4.useMemo)(
343
347
  () => ({ locale, labels, defaultSystem }),
344
348
  [locale, labels, defaultSystem]
345
349
  );
@@ -350,10 +354,10 @@ function useConfig({
350
354
  labels,
351
355
  defaultSystem
352
356
  }) {
353
- const outer = (0, import_react3.useContext)(ConfigContext);
357
+ const outer = (0, import_react4.useContext)(ConfigContext);
354
358
  return {
355
359
  locale: locale ?? outer.locale,
356
- labels: (0, import_react3.useMemo)(
360
+ labels: (0, import_react4.useMemo)(
357
361
  () => ({
358
362
  ...outer.labels,
359
363
  ...Object.fromEntries(
@@ -364,13 +368,13 @@ function useConfig({
364
368
  }),
365
369
  [outer.labels, labels]
366
370
  ),
367
- defaultSystem: isSystem(defaultSystem) ? defaultSystem : outer.defaultSystem
371
+ defaultSystem: [defaultSystem, outer.defaultSystem].find(isSystem)
368
372
  };
369
373
  }
370
- var RecipeDataContext = /* @__PURE__ */ (0, import_react3.createContext)(null);
371
- var RecipeStateContext = /* @__PURE__ */ (0, import_react3.createContext)(null);
374
+ var RecipeDataContext = /* @__PURE__ */ (0, import_react.createContext)(null);
375
+ var RecipeStateContext = /* @__PURE__ */ (0, import_react.createContext)(null);
372
376
  function useRequired(context, message) {
373
- const value = (0, import_react3.useContext)(context);
377
+ const value = (0, import_react4.useContext)(context);
374
378
  if (!value) {
375
379
  throw new import_core3.RecipeBookError("missing_parent", message);
376
380
  }
@@ -394,20 +398,20 @@ function useRecipe() {
394
398
  "useRecipe() must be called inside <Recipe.Root>. To use recipe state without a Root, call useRecipeState()."
395
399
  );
396
400
  }
401
+ if (process.env.NODE_ENV !== "production") {
402
+ setDisplayName(RecipeBookProvider, "Recipe.RecipeBookProvider");
403
+ }
397
404
 
398
405
  // src/parts/image.tsx
399
406
  var import_jsx_runtime2 = require("react/jsx-runtime");
400
- var Image = /* @__PURE__ */ (0, import_react4.forwardRef)(function Image2({
407
+ var Image = /* @__PURE__ */ (0, import_react.forwardRef)(function Image2({
401
408
  as: Comp = "img",
402
409
  src,
403
410
  alt,
404
411
  ...props
405
412
  }, ref) {
406
413
  const { recipe } = useRecipeData("Recipe.Image");
407
- if (recipe.image === void 0) {
408
- return null;
409
- }
410
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
414
+ return recipe.image !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
411
415
  Comp,
412
416
  {
413
417
  ...props,
@@ -418,18 +422,21 @@ var Image = /* @__PURE__ */ (0, import_react4.forwardRef)(function Image2({
418
422
  }
419
423
  );
420
424
  });
425
+ if (process.env.NODE_ENV !== "production") {
426
+ setDisplayName(Image, "Recipe.Image");
427
+ }
421
428
 
422
429
  // src/parts/ingredients.tsx
423
- var import_react8 = require("react");
430
+ var import_react12 = require("react");
424
431
 
425
432
  // src/slot.tsx
426
- var import_react5 = require("react");
433
+ var import_react7 = require("react");
427
434
  function resolveAsChild(part, tag, asChild, children, defaults) {
428
435
  if (asChild) {
429
- if ((0, import_react5.isValidElement)(children) && children.type !== import_react5.Fragment) {
436
+ if ((0, import_react.isValidElement)(children) && children.type !== import_react.Fragment) {
430
437
  return [
431
438
  Slot,
432
- isEmpty(children.props.children) ? (0, import_react5.cloneElement)(children, void 0, defaults) : children
439
+ isEmpty(children.props.children) ? (0, import_react.cloneElement)(children, void 0, defaults) : children
433
440
  ];
434
441
  }
435
442
  if (process.env.NODE_ENV !== "production" && !warned.has(part)) {
@@ -441,7 +448,7 @@ function resolveAsChild(part, tag, asChild, children, defaults) {
441
448
  }
442
449
  return [tag, isEmpty(children) ? defaults : children];
443
450
  }
444
- var Slot = /* @__PURE__ */ (0, import_react5.forwardRef)(function Slot2({ children, ...props }, ref) {
451
+ var Slot = /* @__PURE__ */ (0, import_react.forwardRef)(function Slot2({ children, ...props }, ref) {
445
452
  const child = children;
446
453
  const merged = { ...props, ...child.props };
447
454
  for (const name in child.props) {
@@ -462,7 +469,7 @@ var Slot = /* @__PURE__ */ (0, import_react5.forwardRef)(function Slot2({ childr
462
469
  }
463
470
  }
464
471
  const childRef = Object.getOwnPropertyDescriptor(child.props, "ref")?.value ?? child.ref;
465
- merged.ref = (0, import_react5.useCallback)(
472
+ const mergedRef = (0, import_react7.useCallback)(
466
473
  (node) => {
467
474
  const refs = [ref, childRef];
468
475
  const cleanups = refs.map((each) => setRef(each, node));
@@ -480,7 +487,10 @@ var Slot = /* @__PURE__ */ (0, import_react5.forwardRef)(function Slot2({ childr
480
487
  },
481
488
  [ref, childRef]
482
489
  );
483
- return (0, import_react5.cloneElement)(child, merged);
490
+ if (ref != null) {
491
+ merged.ref = mergedRef;
492
+ }
493
+ return (0, import_react.cloneElement)(child, merged);
484
494
  });
485
495
  function setRef(ref, node) {
486
496
  if (typeof ref === "function") {
@@ -493,7 +503,7 @@ function setRef(ref, node) {
493
503
  }
494
504
  var warned = /* @__PURE__ */ new Set();
495
505
  function describeChildren(children) {
496
- if ((0, import_react5.isValidElement)(children)) {
506
+ if ((0, import_react.isValidElement)(children)) {
497
507
  return "a Fragment";
498
508
  }
499
509
  if (Array.isArray(children)) {
@@ -508,28 +518,32 @@ function describeChildren(children) {
508
518
  function isEmpty(children) {
509
519
  return children == null || typeof children === "boolean";
510
520
  }
521
+ if (process.env.NODE_ENV !== "production") {
522
+ setDisplayName(Slot, "Slot");
523
+ }
511
524
 
512
525
  // src/use-format-ingredient.ts
513
526
  var import_core4 = require("@recipe-book/core");
514
- var import_react6 = require("react");
527
+ var import_react9 = require("react");
515
528
  function useFormatIngredient() {
516
529
  const {
517
530
  locale,
518
531
  labels: { optional }
519
532
  } = useRequired(
520
533
  RecipeDataContext,
521
- "useFormatIngredient() must be called inside <Recipe.Root>. To format ingredients without a Root, call formatIngredient() from @recipe-book/core."
534
+ "useFormatIngredient() must be called inside <Recipe.Root>. To format ingredients without a Root, call formatIngredient() from @recipe-book/react/core."
522
535
  );
523
- return (0, import_react6.useCallback)(
536
+ return (0, import_react9.useCallback)(
524
537
  (item) => formatLine(item, locale, optional),
525
538
  [locale, optional]
526
539
  );
527
540
  }
528
541
  function formatLine(item, locale, optional) {
529
542
  const options = { locale };
543
+ const formatted = (0, import_core4.formatIngredient)(item, options);
530
544
  const quantity = lineQuantity(item);
531
545
  return {
532
- ...(0, import_core4.formatIngredient)(item, options),
546
+ ...formatted,
533
547
  quantityParts: quantity ? (0, import_core4.formatQuantityToParts)(quantity, options) : [],
534
548
  ...item.ingredient.optional && { optional }
535
549
  };
@@ -539,10 +553,10 @@ function lineQuantity(item) {
539
553
  }
540
554
 
541
555
  // src/parts/sections.tsx
542
- var import_react7 = require("react");
556
+ var import_react10 = require("react");
543
557
  var import_jsx_runtime3 = require("react/jsx-runtime");
544
558
  function renderKeyed(items, render) {
545
- return items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react7.Fragment, { children: render(item) }, item.id));
559
+ return items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react.Fragment, { children: render(item) }, item.id));
546
560
  }
547
561
  function SectionBlock({
548
562
  kind,
@@ -550,21 +564,18 @@ function SectionBlock({
550
564
  headingLevel,
551
565
  children
552
566
  }) {
553
- const headingId = (0, import_react7.useId)();
567
+ const headingId = (0, import_react10.useId)();
554
568
  const marker = `data-recipe-${kind}-section`;
555
- if (!title) {
556
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ...{ [marker]: "" }, children });
557
- }
558
569
  const Heading = `h${headingLevel}`;
559
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { ...{ [marker]: "" }, "aria-labelledby": headingId, children: [
570
+ return title ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { ...{ [marker]: "" }, "aria-labelledby": headingId, children: [
560
571
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Heading, { id: headingId, children: title }),
561
572
  children
562
- ] });
573
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ...{ [marker]: "" }, children });
563
574
  }
564
575
 
565
576
  // src/parts/ingredients.tsx
566
577
  var import_jsx_runtime4 = require("react/jsx-runtime");
567
- var Ingredients = /* @__PURE__ */ (0, import_react8.forwardRef)(
578
+ var Ingredients = /* @__PURE__ */ (0, import_react.forwardRef)(
568
579
  function Ingredients2({ headingLevel = 3, children, ...props }, ref) {
569
580
  const { ingredients } = usePartState("Recipe.Ingredients");
570
581
  const renderSection = children ?? ((section) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -579,21 +590,21 @@ var Ingredients = /* @__PURE__ */ (0, import_react8.forwardRef)(
579
590
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ...props, "data-recipe-ingredients": "", ref, children: renderKeyed(ingredients, renderSection) });
580
591
  }
581
592
  );
582
- var IngredientList = /* @__PURE__ */ (0, import_react8.forwardRef)(
593
+ var IngredientList = /* @__PURE__ */ (0, import_react.forwardRef)(
583
594
  function IngredientList2({ section, children = (item) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Ingredient, { item }), ...props }, ref) {
584
595
  const { ingredients } = usePartState("Recipe.IngredientList");
585
596
  const items = section?.items ?? ingredients.flatMap((s) => s.items);
586
597
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { ...props, "data-recipe-ingredient-list": "", ref, children: renderKeyed(items, children) });
587
598
  }
588
599
  );
589
- var LineContext = /* @__PURE__ */ (0, import_react8.createContext)(null);
590
- var Ingredient = /* @__PURE__ */ (0, import_react8.forwardRef)(
600
+ var LineContext = /* @__PURE__ */ (0, import_react.createContext)(null);
601
+ var Ingredient = /* @__PURE__ */ (0, import_react.forwardRef)(
591
602
  function Ingredient2({ item, asChild = false, children, ...props }, ref) {
592
603
  const {
593
604
  labels: { optional, to },
594
605
  locale
595
606
  } = useRecipeData("Recipe.Ingredient");
596
- const line = (0, import_react8.useMemo)(
607
+ const line = (0, import_react12.useMemo)(
597
608
  () => ({ ...formatLine(item, locale, optional), to }),
598
609
  [item, locale, optional, to]
599
610
  );
@@ -642,12 +653,9 @@ function ingredientText(name, attribute, read) {
642
653
  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: [
643
654
  children,
644
655
  text
645
- ] }) : (0, import_react8.cloneElement)(child, void 0, child.props.children, text) });
646
- }
647
- if (process.env.NODE_ENV !== "production") {
648
- Object.defineProperty(IngredientText, "name", { value: name });
656
+ ] }) : (0, import_react.cloneElement)(child, void 0, child.props.children, text) });
649
657
  }
650
- return (0, import_react8.forwardRef)(IngredientText);
658
+ return (0, import_react.forwardRef)(IngredientText);
651
659
  }
652
660
  var IngredientQuantity = /* @__PURE__ */ ingredientText(
653
661
  "IngredientQuantity",
@@ -698,12 +706,21 @@ var IngredientOptional = /* @__PURE__ */ ingredientText(
698
706
  // An empty label shows only the part's own children, such as an icon.
699
707
  ({ optional }, children) => optional === "" && isEmpty(children) ? void 0 : optional
700
708
  );
709
+ if (process.env.NODE_ENV !== "production") {
710
+ setDisplayName(Ingredients, "Recipe.Ingredients");
711
+ setDisplayName(IngredientList, "Recipe.IngredientList");
712
+ setDisplayName(Ingredient, "Recipe.Ingredient");
713
+ setDisplayName(IngredientQuantity, "Recipe.IngredientQuantity");
714
+ setDisplayName(IngredientName, "Recipe.IngredientName");
715
+ setDisplayName(IngredientNote, "Recipe.IngredientNote");
716
+ setDisplayName(IngredientOptional, "Recipe.IngredientOptional");
717
+ }
701
718
 
702
719
  // src/parts/json-ld.tsx
703
720
  var import_schema_org = require("@recipe-book/core/schema-org");
704
- var import_react9 = require("react");
721
+ var import_react14 = require("react");
705
722
  var import_jsx_runtime5 = require("react/jsx-runtime");
706
- var JsonLd = /* @__PURE__ */ (0, import_react9.forwardRef)(
723
+ var JsonLd = /* @__PURE__ */ (0, import_react.forwardRef)(
707
724
  function JsonLd2({
708
725
  servings,
709
726
  system,
@@ -715,12 +732,12 @@ var JsonLd = /* @__PURE__ */ (0, import_react9.forwardRef)(
715
732
  ...props
716
733
  }, ref) {
717
734
  const { recipe } = useRecipeData("Recipe.JsonLd");
718
- const { data, warnings } = (0, import_react9.useMemo)(
735
+ const result = (0, import_react14.useMemo)(
719
736
  () => (0, import_schema_org.toJsonLd)(recipe, { servings, system, url, extra }),
720
737
  [recipe, servings, system, url, extra]
721
738
  );
722
739
  if (process.env.NODE_ENV !== "production") {
723
- useWarningsReport(recipe.name, warnings);
740
+ useWarningsReport(recipe.name, result.warnings);
724
741
  }
725
742
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
726
743
  "script",
@@ -729,7 +746,7 @@ var JsonLd = /* @__PURE__ */ (0, import_react9.forwardRef)(
729
746
  type: "application/ld+json",
730
747
  "data-recipe-json-ld": "",
731
748
  dangerouslySetInnerHTML: {
732
- __html: JSON.stringify(data).replace(/</g, "\\u003c")
749
+ __html: JSON.stringify(result.data).replace(/</g, "\\u003c")
733
750
  },
734
751
  ref
735
752
  }
@@ -737,8 +754,8 @@ var JsonLd = /* @__PURE__ */ (0, import_react9.forwardRef)(
737
754
  }
738
755
  );
739
756
  function useWarningsReport(name, warnings) {
740
- const reported = (0, import_react9.useRef)(void 0);
741
- (0, import_react9.useEffect)(() => {
757
+ const reported = (0, import_react14.useRef)(void 0);
758
+ (0, import_react14.useEffect)(() => {
742
759
  const key = JSON.stringify(warnings);
743
760
  const count = warnings.length;
744
761
  if (count > 0 && key !== reported.current) {
@@ -750,9 +767,12 @@ function useWarningsReport(name, warnings) {
750
767
  reported.current = key;
751
768
  }, [name, warnings]);
752
769
  }
770
+ if (process.env.NODE_ENV !== "production") {
771
+ setDisplayName(JsonLd, "Recipe.JsonLd");
772
+ }
753
773
 
754
774
  // src/parts/root.tsx
755
- var import_react10 = require("react");
775
+ var import_react16 = require("react");
756
776
  var import_jsx_runtime6 = require("react/jsx-runtime");
757
777
  function Root({
758
778
  recipe,
@@ -761,14 +781,14 @@ function Root({
761
781
  children,
762
782
  ...options
763
783
  }) {
764
- const result = (0, import_react10.useMemo)(() => {
784
+ const result = (0, import_react16.useMemo)(() => {
765
785
  if (parse !== void 0) {
766
786
  return parse(recipe);
767
787
  }
768
788
  assertParsedRecipe(
769
789
  recipe,
770
790
  "<Recipe.Root>",
771
- "Call parseRecipe() from @recipe-book/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."
791
+ "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."
772
792
  );
773
793
  return { success: true, data: recipe, warnings: [] };
774
794
  }, [parse, recipe]);
@@ -783,23 +803,23 @@ function Root({
783
803
  function RecipeProvider({ children, ...options }) {
784
804
  const { locale, labels, defaultSystem } = useConfig(options);
785
805
  const state = useRecipeState({ ...options, defaultSystem });
786
- const [temperatures, setTemperatures] = (0, import_react10.useState)(0);
787
- const reportTemperature = (0, import_react10.useCallback)(() => {
806
+ const [temperatures, setTemperatures] = (0, import_react16.useState)(0);
807
+ const reportTemperature = (0, import_react16.useCallback)(() => {
788
808
  setTemperatures((count) => count + 1);
789
809
  return () => setTemperatures((count) => count - 1);
790
810
  }, []);
791
811
  const canConvert = state.canConvert || temperatures > 0;
792
- const value = (0, import_react10.useMemo)(() => ({ ...state, canConvert }), [state, canConvert]);
812
+ const value = (0, import_react16.useMemo)(() => ({ ...state, canConvert }), [state, canConvert]);
793
813
  const { recipe, steps } = state;
794
- const data = (0, import_react10.useMemo)(
814
+ const data = (0, import_react16.useMemo)(
795
815
  () => ({ recipe, steps, labels, locale, reportTemperature }),
796
816
  [recipe, steps, labels, locale, reportTemperature]
797
817
  );
798
818
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RecipeDataContext.Provider, { value: data, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RecipeStateContext.Provider, { value, children }) });
799
819
  }
800
820
  function useParseReport(result) {
801
- const reported = (0, import_react10.useRef)(void 0);
802
- (0, import_react10.useEffect)(() => {
821
+ const reported = (0, import_react16.useRef)(void 0);
822
+ (0, import_react16.useEffect)(() => {
803
823
  const key = result.success ? result.warnings.length > 0 ? `warnings ${JSON.stringify(result.warnings)}` : void 0 : `error ${JSON.stringify([result.error.message, result.error.issues])}`;
804
824
  if (key === reported.current) {
805
825
  return;
@@ -811,7 +831,7 @@ function useParseReport(result) {
811
831
  if (result.success) {
812
832
  const count = result.warnings.length;
813
833
  console.warn(
814
- `Recipe Book: ${quote(result.data.name)} has ${count} ${count === 1 ? "warning" : "warnings"}. It still renders, but a warning can mean a misspelled field.`,
834
+ `Recipe Book: ${quote(result.data.name)} has ${count} ${count === 1 ? "warning" : "warnings"}. It still renders. https://recipe-book.siraj.page/errors/#issue-codes gives the fix for each warning's code.`,
815
835
  result.warnings
816
836
  );
817
837
  } else {
@@ -819,10 +839,13 @@ function useParseReport(result) {
819
839
  }
820
840
  }, [result]);
821
841
  }
842
+ if (process.env.NODE_ENV !== "production") {
843
+ setDisplayName(Root, "Recipe.Root");
844
+ }
822
845
 
823
846
  // src/parts/servings.tsx
824
847
  var import_core5 = require("@recipe-book/core");
825
- var import_react11 = require("react");
848
+ var import_react17 = require("react");
826
849
 
827
850
  // src/compose-event-handlers.ts
828
851
  function composeEventHandlers(theirs, ours) {
@@ -836,7 +859,7 @@ function composeEventHandlers(theirs, ours) {
836
859
 
837
860
  // src/parts/servings.tsx
838
861
  var import_jsx_runtime7 = require("react/jsx-runtime");
839
- var ServingsSelect = /* @__PURE__ */ (0, import_react11.forwardRef)(
862
+ var ServingsSelect = /* @__PURE__ */ (0, import_react.forwardRef)(
840
863
  function ServingsSelect2({ options, "aria-label": ariaLabel, onChange, ...props }, ref) {
841
864
  const { recipe, labels, locale } = useRecipeData("Recipe.ServingsSelect");
842
865
  const { servings, setServings } = usePartState("Recipe.ServingsSelect");
@@ -864,7 +887,7 @@ var ServingsSelect = /* @__PURE__ */ (0, import_react11.forwardRef)(
864
887
  );
865
888
  }
866
889
  );
867
- var ServingsInput = /* @__PURE__ */ (0, import_react11.forwardRef)(
890
+ var ServingsInput = /* @__PURE__ */ (0, import_react.forwardRef)(
868
891
  function ServingsInput2({
869
892
  min = 1,
870
893
  max,
@@ -878,7 +901,7 @@ var ServingsInput = /* @__PURE__ */ (0, import_react11.forwardRef)(
878
901
  const { recipe, labels } = useRecipeData("Recipe.ServingsInput");
879
902
  const { servings, setServings } = usePartState("Recipe.ServingsInput");
880
903
  const base = servingsBase(recipe);
881
- const [draft, setDraft] = (0, import_react11.useState)({ text: String(servings), servings });
904
+ const [draft, setDraft] = (0, import_react17.useState)({ text: String(servings), servings });
882
905
  if (draft.servings !== servings) {
883
906
  setDraft({
884
907
  text: Number(draft.text) === servings ? draft.text : String(servings),
@@ -941,15 +964,18 @@ function optionText(choice, { servings, yield: makes }, base, labels, locale) {
941
964
  }
942
965
  return `${(0, import_core5.formatServings)({ amount: choice }, format)}\xD7`;
943
966
  }
967
+ if (process.env.NODE_ENV !== "production") {
968
+ setDisplayName(ServingsSelect, "Recipe.ServingsSelect");
969
+ setDisplayName(ServingsInput, "Recipe.ServingsInput");
970
+ }
944
971
 
945
972
  // src/parts/step-text.tsx
946
973
  var import_core6 = require("@recipe-book/core");
947
- var import_react13 = require("react");
974
+ var import_react20 = require("react");
948
975
 
949
976
  // src/parts/steps.tsx
950
- var import_react12 = require("react");
951
977
  var import_jsx_runtime8 = require("react/jsx-runtime");
952
- var Steps = /* @__PURE__ */ (0, import_react12.forwardRef)(function Steps2({ headingLevel = 3, children, ...props }, ref) {
978
+ var Steps = /* @__PURE__ */ (0, import_react.forwardRef)(function Steps2({ headingLevel = 3, children, ...props }, ref) {
953
979
  const { steps } = useRecipeData("Recipe.Steps");
954
980
  const renderSection = children ?? ((section) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
955
981
  SectionBlock,
@@ -962,7 +988,7 @@ var Steps = /* @__PURE__ */ (0, import_react12.forwardRef)(function Steps2({ hea
962
988
  ));
963
989
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { ...props, "data-recipe-steps": "", ref, children: renderKeyed(steps, renderSection) });
964
990
  });
965
- var StepList = /* @__PURE__ */ (0, import_react12.forwardRef)(
991
+ var StepList = /* @__PURE__ */ (0, import_react.forwardRef)(
966
992
  function StepList2({ section, children = (step) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Step, { item: step }), start, ...props }, ref) {
967
993
  const { steps } = useRecipeData("Recipe.StepList");
968
994
  const items = section?.items ?? steps.flatMap((s) => s.items);
@@ -978,8 +1004,8 @@ var StepList = /* @__PURE__ */ (0, import_react12.forwardRef)(
978
1004
  );
979
1005
  }
980
1006
  );
981
- var StepContext = /* @__PURE__ */ (0, import_react12.createContext)(null);
982
- var Step = /* @__PURE__ */ (0, import_react12.forwardRef)(function Step2({ item, asChild = false, children, ...props }, ref) {
1007
+ var StepContext = /* @__PURE__ */ (0, import_react.createContext)(null);
1008
+ var Step = /* @__PURE__ */ (0, import_react.forwardRef)(function Step2({ item, asChild = false, children, ...props }, ref) {
983
1009
  useRecipeData("Recipe.Step");
984
1010
  const [Comp, content] = resolveAsChild(
985
1011
  "Recipe.Step",
@@ -994,25 +1020,31 @@ var Step = /* @__PURE__ */ (0, import_react12.forwardRef)(function Step2({ item,
994
1020
  );
995
1021
  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 }) });
996
1022
  });
997
- var StepName = /* @__PURE__ */ (0, import_react12.forwardRef)(
1023
+ var StepName = /* @__PURE__ */ (0, import_react.forwardRef)(
998
1024
  function StepName2(props, ref) {
999
1025
  const step = useRequired(
1000
1026
  StepContext,
1001
1027
  "<Recipe.StepName> must be rendered inside <Recipe.Step>."
1002
1028
  );
1003
- return step.name === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("b", { ...props, "data-recipe-name": "", ref, children: step.name });
1029
+ return step.name !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("b", { ...props, "data-recipe-name": "", ref, children: step.name });
1004
1030
  }
1005
1031
  );
1032
+ if (process.env.NODE_ENV !== "production") {
1033
+ setDisplayName(Steps, "Recipe.Steps");
1034
+ setDisplayName(StepList, "Recipe.StepList");
1035
+ setDisplayName(Step, "Recipe.Step");
1036
+ setDisplayName(StepName, "Recipe.StepName");
1037
+ }
1006
1038
 
1007
1039
  // src/parts/step-text.tsx
1008
1040
  var import_jsx_runtime9 = require("react/jsx-runtime");
1009
- var StepText = /* @__PURE__ */ (0, import_react13.forwardRef)(
1041
+ var StepText = /* @__PURE__ */ (0, import_react.forwardRef)(
1010
1042
  function StepText2(props, ref) {
1011
1043
  const step = useRequired(
1012
1044
  StepContext,
1013
1045
  "<Recipe.StepText> must be rendered inside <Recipe.Step>."
1014
1046
  );
1015
- const segments = (0, import_react13.useMemo)(() => (0, import_core6.segmentStepText)(step.text), [step.text]);
1047
+ const segments = (0, import_react20.useMemo)(() => (0, import_core6.segmentStepText)(step.text), [step.text]);
1016
1048
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { ...props, "data-recipe-step-text": "", ref, children: segments.map(
1017
1049
  (segment, index) => segment.type === "temperature" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Temperature, { segment }, index) : (
1018
1050
  // Core may add segment types in a minor release; show those as authored.
@@ -1025,44 +1057,42 @@ function Temperature({ segment }) {
1025
1057
  const { labels, locale, reportTemperature } = useRecipeData("Recipe.StepText");
1026
1058
  const unit = usePartState("Recipe.StepText").system === "us" ? "F" : "C";
1027
1059
  const readings = [segment, ...segment.alternatives];
1028
- (0, import_react13.useEffect)(reportTemperature, [reportTemperature]);
1029
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { "data-recipe-temperature": "", children: [false, true].flatMap((fan) => {
1060
+ (0, import_react20.useEffect)(reportTemperature, [reportTemperature]);
1061
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { "data-recipe-temperature": "", children: [false, true].map((fan) => {
1030
1062
  const kind = readings.filter((reading2) => reading2.fan === fan);
1031
1063
  const reading = kind.find((each) => each.temperature.unit === unit) ?? kind[0];
1032
- return reading ? [
1033
- (0, import_core6.formatTemperature)(
1034
- (0, import_core6.convertTemperature)(reading.temperature, unit),
1035
- {
1036
- locale,
1037
- rounding: reading.temperature.unit === unit ? "none" : void 0
1038
- }
1039
- ) + (fan && labels.fan ? ` ${labels.fan}` : "")
1040
- ] : [];
1041
- }).join(" / ") });
1064
+ return reading && (0, import_core6.formatTemperature)((0, import_core6.convertTemperature)(reading.temperature, unit), {
1065
+ locale,
1066
+ rounding: reading.temperature.unit === unit ? "none" : void 0
1067
+ }) + (fan && labels.fan ? ` ${labels.fan}` : "");
1068
+ }).filter(Boolean).join(" / ") });
1069
+ }
1070
+ if (process.env.NODE_ENV !== "production") {
1071
+ setDisplayName(StepText, "Recipe.StepText");
1042
1072
  }
1043
1073
 
1044
1074
  // src/parts/system-toggle.tsx
1045
- var import_react14 = require("react");
1075
+ var import_react22 = require("react");
1046
1076
  var import_jsx_runtime10 = require("react/jsx-runtime");
1047
- var ToggleContext = /* @__PURE__ */ (0, import_react14.createContext)(
1077
+ var ToggleContext = /* @__PURE__ */ (0, import_react.createContext)(
1048
1078
  null
1049
1079
  );
1050
- var SystemToggle = /* @__PURE__ */ (0, import_react14.forwardRef)(
1080
+ var SystemToggle = /* @__PURE__ */ (0, import_react.forwardRef)(
1051
1081
  function SystemToggle2({ asChild = false, "aria-label": ariaLabel, children, ...props }, ref) {
1052
1082
  const { labels } = useRecipeData("Recipe.SystemToggle");
1053
1083
  const { system, setSystem, canConvert } = usePartState(
1054
1084
  "Recipe.SystemToggle"
1055
1085
  );
1056
1086
  const disabled = !canConvert;
1057
- const [offers, setOffers] = (0, import_react14.useState)([]);
1058
- const [chosen, choose] = (0, import_react14.useState)();
1059
- const offer = (0, import_react14.useCallback)((id, value) => {
1087
+ const [offers, setOffers] = (0, import_react22.useState)([]);
1088
+ const [chosen, choose] = (0, import_react22.useState)();
1089
+ const offer = (0, import_react22.useCallback)((id, value) => {
1060
1090
  const entry = { id, value };
1061
1091
  setOffers((current) => [...current, entry]);
1062
1092
  return () => setOffers((current) => current.filter((other) => other !== entry));
1063
1093
  }, []);
1064
1094
  const tabStop = (offers.find((entry) => entry.value === system && entry.id === chosen) ?? offers.find((entry) => entry.value === system) ?? offers[0])?.id;
1065
- const context = (0, import_react14.useMemo)(
1095
+ const context = (0, import_react22.useMemo)(
1066
1096
  () => ({ system, setSystem, disabled, tabStop, offer, choose }),
1067
1097
  [system, setSystem, disabled, tabStop, offer]
1068
1098
  );
@@ -1091,7 +1121,7 @@ var SystemToggle = /* @__PURE__ */ (0, import_react14.forwardRef)(
1091
1121
  ) });
1092
1122
  }
1093
1123
  );
1094
- var SystemOption = /* @__PURE__ */ (0, import_react14.forwardRef)(
1124
+ var SystemOption = /* @__PURE__ */ (0, import_react.forwardRef)(
1095
1125
  function SystemOption2({
1096
1126
  value,
1097
1127
  asChild = false,
@@ -1114,10 +1144,10 @@ var SystemOption = /* @__PURE__ */ (0, import_react14.forwardRef)(
1114
1144
  "<Recipe.SystemOption> must be rendered inside <Recipe.SystemToggle>."
1115
1145
  );
1116
1146
  const { labels } = useRecipeData("Recipe.SystemOption");
1117
- const id = (0, import_react14.useId)();
1147
+ const id = (0, import_react22.useId)();
1118
1148
  const disabled = groupDisabled || ownDisabled;
1119
1149
  const checked = value === system;
1120
- (0, import_react14.useEffect)(
1150
+ (0, import_react22.useEffect)(
1121
1151
  () => disabled ? void 0 : offer(id, value),
1122
1152
  [disabled, offer, id, value]
1123
1153
  );
@@ -1132,6 +1162,7 @@ var SystemOption = /* @__PURE__ */ (0, import_react14.forwardRef)(
1132
1162
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1133
1163
  Comp,
1134
1164
  {
1165
+ autoComplete: "off",
1135
1166
  ...props,
1136
1167
  type: type ?? "button",
1137
1168
  role: "radio",
@@ -1158,6 +1189,10 @@ function moveOnArrowKey(event) {
1158
1189
  if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
1159
1190
  return;
1160
1191
  }
1192
+ const group = event.currentTarget.closest("[data-recipe-system-toggle]");
1193
+ if (group === null) {
1194
+ return;
1195
+ }
1161
1196
  const { key } = event;
1162
1197
  let step;
1163
1198
  if (key === "ArrowDown") {
@@ -1165,17 +1200,16 @@ function moveOnArrowKey(event) {
1165
1200
  } else if (key === "ArrowUp") {
1166
1201
  step = -1;
1167
1202
  } else if (key === "ArrowRight") {
1168
- step = isRtl(event.currentTarget) ? -1 : 1;
1203
+ step = isRtl(group) ? -1 : 1;
1169
1204
  } else if (key === "ArrowLeft") {
1170
- step = isRtl(event.currentTarget) ? 1 : -1;
1205
+ step = isRtl(group) ? 1 : -1;
1171
1206
  } else {
1172
1207
  return;
1173
1208
  }
1174
- const group = event.currentTarget.closest("[data-recipe-system-toggle]");
1175
1209
  const options = Array.from(
1176
- group?.querySelectorAll(
1210
+ group.querySelectorAll(
1177
1211
  "[data-recipe-system-option]:not([data-disabled])"
1178
- ) ?? []
1212
+ )
1179
1213
  );
1180
1214
  const index = options.indexOf(event.currentTarget);
1181
1215
  if (index === -1) {
@@ -1187,7 +1221,11 @@ function moveOnArrowKey(event) {
1187
1221
  next?.click();
1188
1222
  }
1189
1223
  function isRtl(element) {
1190
- return element.closest("[dir]")?.getAttribute("dir")?.toLowerCase() === "rtl";
1224
+ return getComputedStyle(element).direction === "rtl";
1225
+ }
1226
+ if (process.env.NODE_ENV !== "production") {
1227
+ setDisplayName(SystemToggle, "Recipe.SystemToggle");
1228
+ setDisplayName(SystemOption, "Recipe.SystemOption");
1191
1229
  }
1192
1230
  // Annotate the CommonJS export names for ESM import in node:
1193
1231
  0 && (module.exports = {