RikaidoDeveloper

@rikaido/react

The React components

Headless, composable, ready to serve. Every example below runs for real, on this page, with no key.

Install

npm
npm install @rikaido/react @rikaido/sdk

Compiled TypeScript, React 18 or later, nothing else. Every component is headless: it exposes data-rikaido="…" attributes and render props, and carries no styles of its own. @rikaido/react/atelier.css is one way to draw them — it is the one these examples wear — and yours is another.

the root
import { RikaidoProvider } from "@rikaido/react";
import "@rikaido/react/atelier.css";

<RikaidoProvider apiKey={import.meta.env.RIKAIDO_KEY} locale="en">
  {/* everything below */}
</RikaidoProvider>

The verdict

The harness returns a verdict; Verdict shows it as a re-read — eight rows in the validator's order, each clean, noted, failed, refused or not for this dish. Here, a peanut allergy meets a peanut-butter satay.

Verdict.Root · Rows · Locklive
Loading the example…

useVerdict(recipe, constraints) calls the API and caches the answer per request body. Verdict.Rows draws the eight rows; Verdict.Lock says the lock in a word: “Not served.”, “Served.”, “Served, with its notes.” The sentences come from the published spec, in English or French.

Verdict.Root{ verdict, locale?, attribution?, children }
The context. children can be a function ({ rows, safe, ok }) => … to draw everything yourself.
Verdict.Rows{ showSkipped?, children? }
The eight rows. children(row, i) to draw each row; showSkipped={false} hides the ones that did not run.
Verdict.Row{ row }
One row: the mark, the check's name, the findings.
Verdict.Issue{ issue }
One finding: the cook's sentence, the subject, the code.
Verdict.Lock{}
The lock, in a word.
useVerdict(recipe, constraints?)Async<Verdict>
{ status: 'idle' | 'loading' | 'ready' | 'error', data?, error? }.
verdictRows(verdict, locale)VerdictRow[]
The same thing, pure, for your own rendering.

With a render prop you draw all of it yourself, and the structure stays its own:

Verdict.Root with a render proplive
Loading the example…

Exposed attributes: data-rikaido="verdict" (data-safe, data-ok), verdict-rows, verdict-row (data-check, data-state), verdict-mark, verdict-check, verdict-findings, verdict-issue (data-code, data-severity: error, warn, safety), verdict-text, verdict-subject, verdict-code, verdict-lock.

The cast

Two portraits per ingredient: the kawaii drawing, a 64 × 64 SVG rendered inline, and the 3D toy, a WebP served by address and fetched by the component because it travels with your key.

IngredientPortraitlive
Loading the example…
IngredientPortrait{ name, form?, size?, alt?, fallback?, attribution?, children? }
form: kawaii (default) or toy. children({ portrait, toyUrl, status }) to draw it all yourself. A name the cast lacks shows fallback, or the initial.
useIngredientPortrait(name)Async<Portrait>
{ slug, kawaiiSvg, kawaiiUrl, toyUrl }.
useToyUrl(name)Async<string>
The toy as a blob: URL, revoked when the component leaves.

Exposed attributes: data-rikaido="portrait" (data-form, data-status), portrait-kawaii, portrait-toy, portrait-fallback.

The graph

What the graph knows about an ingredient: what it is made of, what replaces it. AllergenBadges takes the union over a whole list and says when an ingredient is unknown to it — an empty row because the graph never saw the food must never read as “allergen-free”.

AllergenBadges · Substitutionslive
Loading the example…
AllergenBadges{ names, locale?, attribution?, children? }
One name or a list. children({ allergens, unknown, status }) to draw it yourself.
Substitutions{ name, locale?, attribution?, children? }
The swaps the table holds, with the ratio and the role preserved. Empty for the long tail — never invented.
useIngredient(name, { neighbours? })Async<GraphIngredient>
{ canonical, known, facts, substitutions, art, neighbourhood? }.
allergenLabel(code, locale)string
“peanut”, « arachide » — the nine allergens in the cook's language.

Exposed attributes: data-rikaido="allergens" (data-status, data-unknown), allergen (data-allergen), allergen-none, allergen-unknown, substitutions, substitution (data-role), substitution-swap, substitution-ratio, substitution-why, substitution-caveat.

Reading a source

A URL, a markdown or a pasted text; the recipe it holds, in the shape the harness reads.

RecipeReaderlive
Loading the example…
RecipeReader{ onRead?, locale?, placeholder?, submitLabel?, children? }
One form with the right roles. children(state) to draw the state under the field.
useRecipeRead(){ state, read(source, locale?), reset }
The hook, without the form.

The small things

Pure, no network, no key — what every cooking app rewrites.

Scaling

“1 ½”, “2-3”, “3/4” read as numbers, multiplied, written back as nice fractions. What cannot be read — “a pinch” — stays as written.

useScaledRecipelive
Loading the example…
useScaledRecipe(recipe, servings, baseServings, locale?)Recipe
Memoised on its arguments.
scaleRecipe(recipe, factor, locale?)Recipe
The pure function. Every ingredient gains scaled (a number or null) and display.
parseQuantity(text) · formatQuantity(n, locale?)number | null · string
The two halves, separately.

Units

Metric and imperial, both ways: g ↔ oz and lb, ml ↔ tsp, tbsp and cups, °C ↔ °F. An unknown unit comes back untouched.

useUnitslive
Loading the example…

The timer

One reducer, one interval, and the two things a cook wants to know: how long is left, and did it ring. It does not drift: the remaining time is computed off the clock, not off the ticks.

useTimerlive
Loading the example…
useTimer(totalMs, { onDone?, tickMs? }){ clock, remainingMs, progress, running, done, start, pause, reset, add }
clock is “1:30” or “1:00:00”.
timerReducer · remainingOf · formatClockpure
For your own state, without the hook.

Steps, one at a time

Cook mode's pager: it holds the index, you draw the step. The keyboard is already wired on props.

useCookStepslive
Loading the example…
useCookSteps(steps, initial?){ step, index, count, isFirst, isLast, next, prev, go, props, tempC }
Spread props on the container: role="group", the “2 / 4” label, tabIndex, and onKeyDown for ←, →, Space, Enter, Home, End.

The mark

Every component that shows Rikaido's data — the verdict, a portrait, the graph's facts — carries “Powered by Rikaido”, a quiet link with the seal. That is the free tier's other half: the code is Apache-2.0, the mark stays. attribution="mark" keeps only the seal; attribution="none" is for an account with a card on file.

PoweredByRikaido
import { PoweredByRikaido, RikaidoSeal } from "@rikaido/react";

<PoweredByRikaido />                 // the seal and the words, in the provider's locale
<PoweredByRikaido variant="mark" />  // the seal alone, the words for screen readers
<RikaidoSeal size={24} />            // the seal, for your own link

Unstyled, or styled by you

Nothing here imposes a look. Every piece carries a data-rikaido attribute and, when it has a state, a data-* that names it. One stylesheet targeting those attributes dresses everything at once:

your stylesheet
[data-rikaido="verdict-row"][data-state="refused"] { color: crimson; }
[data-rikaido="allergen"] { border-radius: 999px; padding: 2px 10px; background: #fee; }
[data-rikaido="attribution"] { font-size: 12px; opacity: 0.7; }

@rikaido/react/atelier.css does exactly that, in the atelier's finish. Read it as a starting point.