app/components/procedure-steps.tsx
208 lines and 24 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| Flag | 9 to 15 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| StepItem | 17 to 132 | 13 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| show | 40 to 40 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:56> | 56 to 56 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:59> | 59 to 63 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:64> | 64 to 68 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:69> | 69 to 73 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:74> | 74 to 78 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:79> | 79 to 84 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:86> | 86 to 96 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:99> | 99 to 110 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:115> | 115 to 115 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:119> | 119 to 127 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:120> | 120 to 120 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| commandGroups | 137 to 145 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| Command | 147 to 163 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| platformName | 165 to 165 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| Platforms | 171 to 191 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:176> | 176 to 180 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:183> | 183 to 188 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| Bullets | 194 to 206 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:200> | 200 to 202 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| Steps | 208 to 218 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:213> | 213 to 215 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
Imports
It imports app/components/phage-tool.tsx, app/components/procedure-materials.tsx, app/kb/procedures/parse.mjs, app/kb/procedures/recipe-views.mjs, app/kb/procedures/render.mjs. Imported by app/components/procedure.tsx.
Source
import { StepCalculators } from "~/components/phage-tool";
import { fillQuantities, type ProcedureRecord } from "~/kb/procedures/render.mjs";
import { PLATFORMS } from "~/kb/procedures/parse.mjs";
import { gramsPerCup, ingredientNames, showQuantity, showTemperatures, stepId, stepIngredients, type UnitSystem } from "~/kb/procedures/recipe-views.mjs";
import { type Step, html } from "~/components/procedure-materials";
/* The procedure page's method (procedure.tsx): its steps, their flags and the commands they run. */
function Flag({ kind, label, children }: { kind: string; label: string; children: React.ReactNode }) {
return (
<div className={`procedure-flag procedure-flag-${kind}`} role="note">
<strong className="procedure-flag-label">{label}</strong> {children}
</div>
);
}
export function StepItem({
step,
factor,
troubleshooting,
scope = "",
brief = false,
units = null,
densities = null,
names,
}: {
step: Step;
factor: number;
troubleshooting: ProcedureRecord["troubleshooting"];
/** Keys this step's calculators apart from the same tool under another list's step 1. */
scope?: string;
brief?: boolean;
/** A recipe's units as the reader asked for them (recipe-views.mjs). */
units?: UnitSystem | null;
/** A recipe's grams per cup by ingredient; null for a protocol or a how-to, which list no ingredients under a step. */
densities?: Map<string, number> | null;
/** A recipe's words for each ingredient, as its ingredient list gives them. */
names?: Map<string, string>;
}) {
const show = (q: Parameters<NonNullable<Parameters<typeof fillQuantities>[2]>>[0]) => showQuantity(q, factor, units, densities?.get(q.name?.toLowerCase() ?? "") ?? null);
const text = showTemperatures(fillQuantities(step, factor, densities ? show : undefined), units);
// A recipe step lists what goes into it (R2), with the amounts the reader asked for.
const ingredients = densities && !brief ? stepIngredients(step, factor, units, densities, names) : [];
return (
<li
value={step.number}
id={scope ? stepId(scope, step.number) : undefined}
className="procedure-step"
data-step={step.number}
data-timers={step.timers.length > 0 ? JSON.stringify(step.timers) : undefined}
>
<p className="procedure-step-text" data-run-swap="step" dangerouslySetInnerHTML={html(text)} />
{ingredients.length > 0 ? (
<p className="procedure-step-ingredients">
<span className="procedure-step-ingredients-label">Ingredients: </span>
{ingredients.map((i) => (i.amount ? `${i.amount} ${i.name}` : i.name)).join(", ")}
</p>
) : null}
{step.spin.map((s) => (
<Flag key={s} kind="spin" label="Spin:">
{s}
</Flag>
))}
{step.critical.map((c, i) => (
<Flag key={`c${i}`} kind="critical" label="Critical step.">
<span dangerouslySetInnerHTML={html(c)} />
</Flag>
))}
{step.pause.map((p, i) => (
<Flag key={`p${i}`} kind="pause" label="Pause point.">
<span dangerouslySetInnerHTML={html(p)} />
</Flag>
))}
{step.expect.map((e, i) => (
<Flag key={`e${i}`} kind="expect" label="Expected:">
<span dangerouslySetInnerHTML={html(e)} />
</Flag>
))}
{commandGroups(step.commands).map((group, g) =>
group.length === 1 && !group[0]!.platform ? (
<Command key={`cmd${g}`} cmd={group[0]!} />
) : (
<Platforms key={`cmd${g}`} group={group} id={`${scope ? stepId(scope, step.number) : `step-${step.number}`}-${g}`} brief={brief} />
),
)}
{step.photos.map((p) => (
<img
key={p.src}
className="procedure-photo"
src={p.src}
alt={p.alt}
loading="lazy"
decoding="async"
{...(p.width && p.height ? { width: p.width, height: p.height } : {})}
/>
))}
{brief
? null
: (step.challenges ?? []).map((c, i) => (
<div key={`ch${i}`} className="procedure-challenge" role="note">
<strong className="procedure-flag-label">Challenge.</strong>
<div dangerouslySetInnerHTML={html(c.html)} />
{c.solutionHtml ? (
<details className="procedure-solution">
<summary>Solution</summary>
<div dangerouslySetInnerHTML={html(c.solutionHtml)} />
</details>
) : null}
</div>
))}
{/* A row compiled before calculators existed has no `calculators` until the next sync rewrites it. */}
{brief || !step.calculators?.length ? null : <StepCalculators calculators={step.calculators} scope={`${scope}s${step.number}`} />}
{brief
? null
: step.why.map((w, i) => <div key={`w${i}`} className="procedure-why" dangerouslySetInnerHTML={html(w)} />)}
{step.troubleshooting.length > 0 ? (
<p className="procedure-step-trouble">
Troubleshooting:{" "}
{step.troubleshooting.map((id, i) => {
const row = troubleshooting.find((r) => r.id === id);
return (
<span key={id}>
{i > 0 ? ", " : ""}
<a href={`#trouble-${id}`}>{(row?.problem ?? id).replace(/<[^>]+>/g, "")}</a>
</span>
);
})}
</p>
) : null}
</li>
);
}
type Cmd = Step["commands"][number];
/** A step's commands in runs: each run of blocks that name a platform is one set of tabs; any other block stands alone. */
function commandGroups(commands: Cmd[]) {
const groups: Cmd[][] = [];
for (const cmd of commands) {
const last = groups[groups.length - 1];
if (cmd.platform && last?.[0]?.platform) last.push(cmd);
else groups.push([cmd]);
}
return groups;
}
function Command({ cmd }: { cmd: Cmd }) {
return (
<div className="procedure-command">
<pre data-lang={cmd.lang || "text"}>
<code>{cmd.code}</code>
</pre>
{cmd.output !== null ? (
<>
<p className="procedure-output-label">Expected output</p>
<pre className="procedure-output">
<code>{cmd.output}</code>
</pre>
</>
) : null}
</div>
);
}
const platformName = (p: string | null) => PLATFORMS[p as keyof typeof PLATFORMS] ?? p ?? "";
/**
* One command for each platform (S4). With no script every block shows, under its name, and the tabs are links to them;
* app/enhance/platforms.ts turns the links into tabs and remembers the reader's platform. The sheet lists them all.
*/
function Platforms({ group, id, brief }: { group: Cmd[]; id: string; brief: boolean }) {
return (
<div className="procedure-platforms" data-platforms="">
{brief ? null : (
<p className="procedure-platform-tabs">
{group.map((cmd) => (
<a key={cmd.platform} href={`#${id}-${cmd.platform}`} data-platform={cmd.platform ?? ""}>
{platformName(cmd.platform)}
</a>
))}
</p>
)}
{group.map((cmd) => (
<div key={cmd.platform} id={`${id}-${cmd.platform}`} className="procedure-platform" data-platform={cmd.platform ?? ""}>
<p className="procedure-platform-label">{platformName(cmd.platform)}</p>
<Command cmd={cmd} />
</div>
))}
</div>
);
}
/** A list of short markdown lines under a heading: a tutorial's questions, objectives or key points (S2). */
export function Bullets({ id, title, items }: { id: string; title: string; items: string[] }) {
if (items.length === 0) return null;
return (
<>
<h3 id={id}>{title}</h3>
<ul>
{items.map((x, i) => (
<li key={i} dangerouslySetInnerHTML={html(x)} />
))}
</ul>
</>
);
}
export function Steps({ steps, factor, record, scope, units = null }: { steps: Step[]; factor: number; record: ProcedureRecord; scope: string; units?: UnitSystem | null }) {
const densities = record.profile === "recipe" ? gramsPerCup(record) : null;
const names = record.profile === "recipe" ? ingredientNames(record) : undefined;
return (
<ol className="procedure-steps" start={steps[0]?.number ?? 1}>
{steps.map((step) => (
<StepItem key={step.number} step={step} factor={factor} troubleshooting={record.troubleshooting} scope={scope} units={units} densities={densities} names={names} />
))}
</ol>
);
}