Skip to main content

Component gallery

Every frame on this page is a live form, not a screenshot. It is produced by RequestType::form — the same function behind every form the factory serves — from a manifest you can read, and it is regenerated and diffed in CI on every change to the renderer.

That matters more than it sounds. A gallery of hand-written HTML is a lie the first time the renderer changes, and a lie about what a form looks like is the most convincing kind, because nobody diffs a screenshot. So none of this is drawn: cargo run --example gallery walks the field kinds, loops over the built-in themes, and writes what you see below.

These forms submit nowhere

They are static pages. Clicking Submit demonstrates the HTML5 constraint layer — the browser refuses to navigate while a required field is empty, and announces why to a screen reader — and nothing else. Everything else works: type in the fields, tick the boxes, watch the conditional ones appear.

Use the palette buttons to switch themes. The frames follow this site's own light/dark setting, so what you see is what a reader with your preferences would get.

Every field kind

Ten kinds, one of each, plus the two acknowledgments that follow them. The help text under each label says what that kind enforces — which is a real answer, not a description, because validation is one function run at both ends: the browser's check is a convenience and the server runs the same rules again.

Two things worth looking at rather than reading about. The file field is not an input here: it renders as a note saying the upload comes after verification, because uploads happen once an address is proven and the public form has no file input at all. And every label is set in the theme's mono stack — a field label is a name for a value, which is closer to code than to prose, and that one choice is most of why a generated form reads as designed.

The manifest · its JSON Schema

When a submission is rejected

The same form, re-served with a deliberately bad submission on it. The messages below came out of the validator, not out of a designer's placeholder text — the gallery builds a wrong submission, runs validate_at(…, Phase::Submit) on it, and renders whatever comes back.

The summary at the top and a message beside each field, deliberately: the summary is what a screen reader announces on load, and the per-field message is what a sighted reader needs while fixing it.

Note what is not flagged. reference_code holds nh-301, which does not match the field's pattern — and there is no error on it, because a pattern is enforced by the browser and never by the server. The server enforces the cap and the type. If you are writing a second client, that distinction is the kind of thing to get from this page rather than from a surprise.

Note also supporting_document: a urlencoded string aimed at a file field is somebody probing, so it is refused rather than ignored.

Conditional fields

Change the format, tick the travel box, put a large number in attendance. Each of those reveals a field, and each rule is a show_when in the manifest that the server re-evaluates on submit — a client-side check is a convenience and never a control.

The form starts nearly empty, which is correct rather than broken: none of the conditions hold yet. Turn JavaScript off and every conditional field is simply shown — the safe direction, since a visible optional field is a question you can ignore where a hidden required one is a form that cannot be submitted and does not say why.

The rule that surprises people: a field is required only when it is visible. The browser did not show it, so the server cannot insist on it.

The manifest

A form in steps

Multi-step is server-side — one page per step, a POST between them — so it works with JavaScript off and survives a closed tab.

The other two: what you're asking and travel — the last of which only exists when the format is not remote. A whole step, skipped. A field on a hidden step is not required however it is declared, because a record carrying a field the browser never showed is a record that differs from what was submitted.

The manifest

The upload page

The one place a file input exists. A submitter reaches it after clicking the link in their verification mail, and by then every visibility question has already been answered by values that are no longer editable.

Submitting with nothing chosen is how an optional attachment is declined — absence is the decline, so there is no separate skip button to fall out of sync with the validator. The accepted kinds and the cap are stated under the input, and both are enforced against sniffed magic rather than a claimed media type or a filename extension.

Booking a time

kind = "booking" changes the flow rather than adding a field. The page shows a week of slots, a submission names one, a soft hold starts, and the verification click converts the hold into the booking. One manifest, two readers: [availability] is home's — the slot pipeline runs it — and [policy] is the box's.

Everything here works with JavaScript off: the slots are radio inputs, week paging is links, and the times are server-rendered in the host's zone and labelled as such. booking.js is enhancement only — it re-renders the times in your zone (which is why the label above probably does not say New York) and adds the duration filter. Nothing a submission depends on lives in it.

The slots are real. They come out of the availability engine run against the starter's own [availability] — Tuesday and Thursday afternoons, Wednesday mornings, 30 and 60 minutes, a 10-minute buffer, 24 hours' notice, three meetings a day — with two commitments already on the calendar. That is why Tuesday is missing its 7pm and why some days are shorter than their window: a hand-drawn week would show a layout no policy produces.

The box never computes availability. Home pushes slots as data and the box can only narrow them — by holds and bookings it knows about, and by the past. A public server that could compute your free time would need your calendar.

The manifest · a week later

One page here is ahead of the server

booking_page accepts values and errors and renders them, but both server callers pass the defaults, so a failed POST currently gets the week back with a one-line notice instead. The rejected view is what that path is growing into. Rendering it early is how two gaps became visible: its summary lists raw field names where an ordinary form's lists labels, and no slot comes back selected — so a visitor who mistypes their address would lose the time they picked.

Asking a group about times

A times poll is the same weekly frame over seeded candidates, each a tri-state answer. Seeded, never a blank 7×24 grid: what a participant is shown is only what the organizer could actually offer.

Two rows look identical on purpose. An unanswered candidate renders as "no" — when2meet's rule, where you paint the times you can do and silence is unavailability. It is worth knowing that the default is a real answer rather than an absence, because it means a participant who opens the page and closes it has said something.

The yes-counts beside each time are server-rendered. A times poll is names, capabilities and three words — never an address and never prose — which is what lets the page be shown to strangers at all.

The survey

Times are one question kind out of six. A general poll is a list of typed questions, and the two frames below are the same specimen survey at the two moments that matter: before your vote, and after the poll closed.

Two of six people have answered, Priya's own two answers are already on the form, and the page says what it owes her: this poll is show = "after_vote", so "Results appear after you answer." It also states the anonymity it can actually deliver — "your name is not shown with results… (Written answers still read like their author.)" — which is the sentence a course-eval tool usually leaves out.

Grab the ranking and drag it. The row has a grip, and pointer capture keeps the stroke alive when you leave the row; touch-action: none sits on the grip alone, so the page still scrolls under a thumb everywhere else. Then tab into the same list: / buttons are the keyboard's path, and the grip is aria-hidden, so the drag adds a way to answer rather than becoming the only one. The VAS behaves the same way — a slider laid over a number field, holding back its thumb until you touch it.

Underneath both is a form that works with the script blocked: ranking degrades to a <select> per option (, 1, 2, 3) and the VAS to the number field itself. That is the version the server accepts either way; the widgets are the layer on top.

This frame is wired for touching, not for saving

The specimen renders in a third state the real page never uses — script on, network off. A served page autosaves as you answer and polls its results; a golden file that did either would answer "Couldn't save — use the button below" the moment you dragged a row, and poll a 404 every ten seconds. So the form carries data-demo="1" and everything that reaches the server is switched off. Everything you can feel is the real code.

Four things here are the design rather than the styling:

  • Nothing is preselected but Priya's own two answers. The VAS field is empty, and an untouched one submits nothing — because a slider parked at 50 that nobody moved would enter the data as a real opinion. Absent is absent, all the way into the widget.
  • "Ranking only some is fine" is on the page, and it is true: a partial ranking is a legal ballot rather than an error, and the tally counts it as far as it goes.
  • The likert item labels every point, which is what makes it a Likert item rather than a numbered row. The alternative shape, anchors at the ends only, is the other legal spelling and never both at once.
  • The promise line is rendered from the policy, not written by hand. Who sees the results and whose name rides them are decided before the vote and fixed at creation, so the sentence on the page is the same object the server enforces.

The closed page is what the link in someone's mail is still good for a month later. It carries the organizer's resolution at the top — "Replication it is" — because a poll whose links answer "so what happened?" is the difference between a decision and a form.

The numbers below it are not invented. The gallery runs five specimen ballots through the same pure tally functions the box and factory-publish run, so the ranking question shows real instant-runoff rounds"Round 1: Replication study 2 · Reanalysis 2 · Novel study proposal 1 — Novel study proposal eliminated", all the way to a winner — and the likert question leads with the distribution and the median, which is the honest summary for ordinal data. Scroll to the text question and both readings of prose are there at once: the word cloud that would go on a projector (coding 3 · walkthroughs 2) and, below it, the sentences themselves.

Those sentences are here because this poll is identity = "named" — so the voters are disclosed, and each answer carries its author. Switch that one word to anonymous and three things change at once: names never render, the CSV export goes nameless, and under three respondents the breakdown is withheld entirely — the suppression floor, asked as the same question of the same number by every emitter.

The ranking here has no grip and the VAS no slider, which is not an oversight: a closed poll is read-only, and the script returns early the moment it finds no submit button. Nothing is draggable because nothing is answerable.

The projector

The same five ballots, read the way a wall has to read them. This is the page behind live polls on a slide — a browser window on the second display, an <iframe> in a web deck, or the PowerPoint add-in's frame all load exactly this.

Three differences from the participant's page, and all three are one decision — a projector is an audience, mid-poll:

  • Nobody's name is anywhere, even though the ballots carry them. "Who answered what" is a participant's disclosure, never the room's.
  • The ranking shows first preferences alone. No runoff rounds, because rounds computed on a partial electorate imply a winner that one more ballot can flip — and the wall is the worst place to publish a result that moves.
  • The text question projects as a cloud, then says so in words: "3 written answers — the full text stays on the presenter's screen, not the wall." coding and walkthroughs reach the wall because two different ballots each chose them; a word one person used cannot render, which is the guard that replaces a profanity list. The sentences live in polls status and the TUI's /polls instead.

The join URL is printed large across the top because this specimen is a link poll. A roster poll's screen shows the counts without one — its doors are personal, so there is no address to put on a wall.

This frame does not move, and the served one does: screen.js refreshes every two seconds, and a golden file can't. The page is built to answer identically without it, one reload behind, which is the same enhancement contract as the form above.

A sixth kind cannot ship unseen

The generator matches QuestionKind exhaustively and then asserts the specimen actually contains one question of each general kind. Adding a seventh kind stops the gallery compiling, and then fails it until a real question of that kind is on this page. A gallery missing a kind is worse than no gallery, because it reads as a complete list.

One gap this page does not close yet: the survey's manifest is not exported beside the other source/ files, so the spec above is quoted on the polls page rather than linked as bytes. That is the same condition — prose here, no artifact — this gallery exists to remove.

The shipped starters

Request types are data, not code: copy one, edit the fields, and the form, both validators, the schema and the tool declarations regenerate. These five ship with mecha-manifest, and they render through the path everything above does — a new starter appears in this gallery without anyone adding it here.

book · lab-application · letter · meeting · speaking

Why there are two palettes

A schema-driven form should look designed without anyone designing it, and certainly without an agent designing it — a model that writes CSS per request produces a different form every time, which is the opposite of a surface people learn to trust. So the layout is fixed and shared, and a theme is tokens, never rules: nine role-named custom properties, and no selectors.

paper exists to prove that. One built-in theme is a stylesheet with extra steps; a second, deliberately unlike the first, is what shows the structural sheet hardcodes nothing. Switch any frame above between the two — if a violet edge or a dark ground survived into paper, a colour would be written somewhere it should not be.

The palette buttons are built from the gallery's own index.json, so a theme added in mecha-factory appears here without anyone editing this page.

Where this comes from

git clone https://github.com/ljchang/mecha-factory
cd mecha-factory
cargo run --example gallery -- gallery
xdg-open gallery/index.html

The output is committed at gallery/, and CI regenerates it and refuses a diff. That is the same shape as a golden-file test, on purpose: a change to the renderer or to a palette arrives as a reviewable diff of the rendered HTML, which is the review a form generator deserves.

The gallery's clock is a literal, and it has to be. A committed golden file rendered against the real clock would differ from itself every day and the drift check would cry wolf until somebody deleted it. So the booking pages are rendered at a fixed instant — a Monday morning in the starter's own timezone, chosen so its 24-hour minimum notice lands inside the first rendered week. The chrono crate reads no clock here; the caller supplies now, which is what makes that possible at all.

Two guards keep it honest as the crate moves. The generator matches FieldKind exhaustively, so adding a variant stops it compiling and then fails until a real field of that kind exists — a gallery missing a kind is worse than no gallery, because it reads as a complete list. And themes come from the built-in array rather than a list written by hand, which is what makes the sentence above about a third palette true rather than aspirational.

The whole directory also passes the publish gate — the same check a bundle faces, which refuses any external reference. Every page you have scrolled past fetches nothing.