Pattern gallery

Module settings: part type matching

The smallest V1 module: three customer fields and one admin field, still carrying the full checklist-and-hint anatomy.

What this solves

Part type matching is the smallest of the four V1 modules, three customer fields and one admin field, exactly the kind of surface a team is tempted to hand-roll as three inputs in a useState object rather than reach for a form engine. That temptation is where the discipline has to hold anyway: search_columnsstill needs the checklist-plus-sample-hint anatomy so a person isn't guessing which header holds a part name, and batchstill needs the tier gate so a customer never sees an internal call-size knob. A schema this short is not evidence the module doesn't need SchemaForm; it's evidence that even the smallest surface earns the same anatomy as the largest.
Use it when
  • A module needs the smallest possible settings surface: an output field name, which columns to search, and a taxonomy choicespeedway's part-type-matching job config today: a single hardcoded taxonomy radio inside the create-job modal, never its own settings page (NewJobForm.tsx:412-421)
  • A checklist's options are the file's own columns, with a data sample per option so a person isn't guessing which header holds the right valuesspeedway's Scrape module already does this shape for a different field, ScrapeFieldsPicker's fixed checkbox list (scrape-setup.tsx:120-217); walmart has no equivalent at setup time, going straight to a hierarchical picker at review time

Rendered

Fixture data; check both themes.
Viewer
Confidence threshold70%
0 of 5 chosen
patterns/module-part-type-matching/example.tsxCopy this into a fresh route and it renders as above.
"use client";
import { Button, Card, Chip, SchemaForm, type SchemaFormHandle, type XTier } from "@versable-git/ui";
import { useRef, useState } from "react";
import fixture from "../../../../../../docs/plan/65-schema-form-fixtures/part-type-matching.params.schema.json";
import { FILE_CONTEXT, resolveModuleOptions } from "../../components/schema-form/examples/module-stubs";
// Stub match rows for the aside: three sample titles with a faked matched
// part type and confidence. The schema itself has no confidence field; a
// live match step runs on the console's side once search_columns and
// vocabulary are set, this is a demo affordance so the threshold below
// visibly does something.
const SAMPLE_MATCHES = [
{ title: "Brake pad set", matched: "Brake pad", confidence: 92 },
{ title: "Rotor", matched: "Rotor", confidence: 78 },
{ title: "Cabin filter", matched: "Cabin filter", confidence: 54 },
];
function MatchPreview({ threshold }: { threshold: number }) {
return (
<Card title="Match preview" subtitle="3 sample rows, stub matches" noAnimate>
<div className="flex flex-col gap-2.5">
{SAMPLE_MATCHES.map((row) => {
const below = row.confidence < threshold;
return (
<div key={row.title} className="flex flex-col gap-0.5">
<div className="flex items-center justify-between gap-2">
<span className={below ? "text-base-content/65 text-sm" : "text-sm"}>{row.title}</span>
<div className="flex items-center gap-2">
<Chip size="sm" variant={below ? "ghost" : "soft"} color={below ? "neutral" : "primary"}>
{row.matched}
</Chip>
<span className={`text-xs tabular-nums ${below ? "text-base-content/65" : "text-base-content/70"}`}>{row.confidence}%</span>
</div>
</div>
{below && <span className="text-base-content/65 text-xs italic">below threshold</span>}
</div>
);
})}
</div>
</Card>
);
}
export function ModulePartTypeMatching() {
const ref = useRef<SchemaFormHandle>(null);
const [value, setValue] = useState<Record<string, unknown>>();
const [tier, setTier] = useState<XTier>("customer");
const [threshold, setThreshold] = useState(70);
const [reviewing, setReviewing] = useState(false);
const [result, setResult] = useState<string | null>(null);
return (
<div className="flex flex-col gap-4">
<div className="flex flex-wrap items-center gap-2">
<span className="text-base-content/65 text-xs">Viewer</span>
{(["customer", "admin"] as const).map((t) => (
<Button key={t} size="xs" variant={tier === t ? "solid" : "ghost"} color="primary" shade onClick={() => setTier(t)}>
{t.charAt(0).toUpperCase() + t.slice(1)}
</Button>
))}
<span className="text-base-content/65 ml-3 text-xs">Confidence threshold</span>
<input
type="range"
min={0}
max={100}
value={threshold}
onChange={(e) => setThreshold(Number(e.target.value))}
className="range range-primary range-sm w-32"
/>
<span className="text-base-content/70 text-xs tabular-nums">{threshold}%</span>
</div>
<SchemaForm
ref={ref}
schema={fixture}
value={value}
onChange={setValue}
mode={reviewing ? "review" : "edit"}
tier={tier}
context={FILE_CONTEXT}
resolveOptions={resolveModuleOptions}
aside={<MatchPreview threshold={threshold} />}
onEditSection={() => setReviewing(false)}
/>
<div className="flex items-center gap-2">
<Button
size="sm"
color="primary"
onClick={() => {
const out = ref.current?.submit();
setResult(out ? `Ready to run: ${Object.keys(out).length} settings` : "Fix the errors above");
}}
>
Validate
</Button>
<Button size="sm" variant="ghost" shade onClick={() => setReviewing((r) => !r)}>
{reviewing ? "Back to edit" : "Review"}
</Button>
{result && <span className="text-base-content/65 text-xs">{result}</span>}
</div>
</div>
);
}

Where it ships

  • speedway/app/components/NewJobForm.tsx:412-421normalizeUnacked: the vocab radio/checkbox for partType and normalize jobs, deliberately unchecked by default per an owner ruling requiring an explicit acknowledge; the closest real analogue to the fixture's vocabulary field, today a single taxonomy rather than a tenant/pcdb choice (moduleConfig.partType.vocabulary)
  • speedway/app/routes/workspaces/jobs/scrape-setup.tsx:120-217the Scrape module's own field checklist (ScrapeFieldsPicker), checkboxes over a fixed list plus a free-text add; the closest existing checklist-over-known-names shape to search_columns, on a different module
  • walmart-mvp/frontend/src/features/catalog/TaxonomyColumnPicker.tsx:43-403a Finder-style drill-down over about 385 taxonomy leaves, used to assign a part type at review time; spec 5.4 names the same shape as a V2 x-widget: tree candidate for the review editor, not this setup form
  • docs/plan/65-schema-form-spec.md §5.4the params shape this page renders: output_field, search_columns (checklist, x-options-from item.columns, x-hint-from item.sample(3)), vocabulary (segmented, tenant/pcdb), batch (admin, x-unit)

App-specific: The console supplies what this fixture stubs. resolveOptions here only answers the four paths module-stubs.ts fakes (models, taxonomies, attributes, product-types); it returns nothing for a real per-file column list, so search_columns and its sample hint work only because context carries item.columns and item.sample(3) directly. A live job would source both from the file the job actually holds, not a fixed array. The match preview beside the form (three sample rows, a faked matched part type, a confidence percent, a threshold slider) is entirely this page's invention: the schema in spec 5.4 carries no confidence field, and the real preview is the console's own match step, run against the job's real rows once search_columns and vocabulary are set, not a client-side stub reacting to a slider.

@versable-git/ui · composites proven in the apps