Skip to content

Place cards: a stationery suite, room-sized boards and seat-by-seat names - #97

Merged
JFrusher merged 20 commits into
mainfrom
claude/place-card-maker-brainstorm-zyyfl3
Oct 5, 2026
Merged

JFrusher merged 20 commits into
mainfrom
claude/place-card-maker-brainstorm-zyyfl3

Conversation

@JFrusher

@JFrusher JFrusher commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Place cards grows from one card design into a wedding's whole stationery: table cards, room-sized seating boards and floor plans, all printed live from Seating and tied seat by seat to whoever sits there.

What it adds

A suite of pieces. Place cards, escort cards, a seating board, an A–Z finder, table cards and a floor plan sit side by side, each with its own design and paper. The stationery slice moves to v3 (pieces); v1 and v2 weddings are migrated, combined cards included.

Live from the room. Rows come from guests and Seating, never a CSV copy. Tables are read in one order everywhere: the top table first (by type or by what it is for), then 2 before 10.

Big paper. A page can be the card's own size (A1/A2 boards) for a print-shop PDF, or the same board tiled onto A4 at home with overlap and landing marks.

New elements. All of them resolve to plain text, icons and lines, so the PDF and on-screen renderers are unchanged.

  • Grid: a block per table for a seating board, or newspaper columns for a finder that flows onto more pages. A letter carried onto a new page is headed again.
  • Room: the floor plan to scale. Names sit at numbered chairs, all at one size. Free-seating tables list their guests inside, in columns.
  • QR.

Seat by seat.

  • {{At seat 3}} names seat 3 of the card's own table, so one design serves every table. {{Table 1, seat 3}} names that one chair in the room.
  • A chair picker in the inspector inserts either token with a click: one Tab stop, arrow keys between chairs, and an "Up close" view of one table.
  • One name format per design: Ada Byron, Ada, Ada B., or Byron, Ada.
  • A plan's names can be stamped into text boxes of their own, linked to their chairs. They either follow the plan (keeping any nudge) or stay where they are put.

Known as. A guest can have a name of their own for the stationery ("Granny Jo"), set on the Guests page. {{Known As}} is that name, otherwise the design's name format. The greeting starters use it.

Reprints.

  • After printing, Place cards names the cards a change has made wrong, and can reprint just those.
  • The check fingerprints only what each design prints.
  • Undo does not erase what was printed (lib/store/unhistoried.ts).

Other changes.

  • Seating's Print & PDF now opens the floor plan in Place cards.
  • The wedding pack's room section is that floor plan, shrunk onto A4.
  • Seating's old jsPDF exporter is removed, along with jspdf and svg2pdf.js.
  • An empty value in a template takes the punctuation that only separated it: "Thistlewood, Zainab", not "Thistlewood, Zainab —".

Specs, decisions and as-built notes: docs/superpowers/specs/2026-10-04-seating-stationery-design.md and docs/superpowers/specs/2026-10-05-seat-picker-design.md.

Review

The branch had three independent reviews (core pipeline, state, UI), then a final full review. Every finding was reproduced with a test before it was fixed. The bugs they caught include:

  • a guest dropped at a finder page break
  • undo erasing the print record
  • prints recorded on the wrong piece
  • an infinite loop in the flow layout
  • a floating-point miss that fitted no card on a FIT page
  • a library design deleting a wedding's other pieces

A whole-room plan is now drawn once per room rather than once per card: about 225 ms down to under 1 ms per card at 300 guests.

Testing

  • npm run typecheck: clean
  • npx vitest run: 2074 passed
  • npx playwright test against next build: 134 passed, including the accessibility suite. New end-to-end tests cover pieces, live renames, print-shop and tiles, boards, finder spill, floor plan and name format, table cards, reprint-just-these (and its reset), Seating's Print, the chair picker, stamping, Known As and the wedding pack.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U


Generated by Claude Code

claude added 20 commits October 4, 2026 21:13
…om-sized boards

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
The stationery slice held one design, so making table numbers meant
overwriting the place cards. It now holds an ordered list of pieces, each
a whole design of its own, with uploaded assets' names, icons and two
workspace preferences shared between them (slice version 3).

- apps/plaque/state/suite.ts: the stored shape, import-free, so readiness,
  the overview, content and the library read it without loading Place
  cards. A v1/v2 single design is read as the one piece it always was,
  under the id and name every wedding's first piece gets.
- persist: each piece is validated on its own; one that cannot be read is
  left out and named, rather than discarding the whole suite.
- The editor still works on a flat Design — the open piece plus what is
  shared — so no panel changed. Which piece is open is the window's, not
  the wedding's: undo never switches pieces.
- PiecesBar: tabs, new, rename (double-click), duplicate, remove.
- Readiness: "cards from a file" now ignores a piece with no rows, which
  every new empty piece would otherwise trip; it and the dietary check
  look across every piece.
- Library: "cards" keeps and applies the whole suite, keeping each piece's
  row scope (a per-table board is broken without it). Designs kept before
  pieces still apply.
- The example wedding is stored in the new shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
"Use the room" copied the guest list into the design once; move someone
afterwards and their card kept the old table until it was pressed again.
Now nothing is copied: every piece's rows are read from the wedding as it
changes, and the stationery slice stores only the design and who shares a
card, by guest id.

- fromRoom: roomRows(doc), cached per document, keyed by guest id. New
  columns: {{Initial}}, {{Table Number}}, {{Table Size}}, {{Seat}}.
  withMerges puts combined cards in place, so a combined card follows its
  guests when one of them moves.
- lib/model/seats: the one seat lookup, shared with the guest link, read
  from the tables' own lists — the record lib/seating/normalise says wins.
  {{Seat}} is filled only where a table numbers its seats.
- The store re-reads when stationery, guests, seating or the event change,
  and only then, so a timeline edit costs Place cards nothing.
- Saves from before pieces: their stored rows go, and per-guest tweaks and
  combines are re-keyed from positions to guests by name. Any that match
  nobody are dropped and said so.
- "Reprint just a few" picks cards, not guests, and narrows only the
  export (buildJob's `only`), so it works for combined cards and tables
  and cannot leave a short run behind in the design.
- There was no CSV import left (setCsv only ever took the room's rows), so
  "cards from an imported file" can no longer happen: that readiness check
  and the content summary's card count go.
- The PDF is named after the piece.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
- `FIT` paper, "the card's own size": the page is the card plus its
  margins, so a board prints one per page at full size with its bleed and
  crop marks — the PDF a print shop wants. Choosing it sets margins wide
  enough for the marks. Home-printer warnings no longer apply to it.
- A3 joins A4 and Letter.
- Board presets: A3, A2, A1, A0, 50 × 70cm, 18 × 24in, 24 × 36in, and an
  A5 table card. Presets may be up to A0 rather than up to home paper.
- Tiling (core/imposition/tile.ts): each full-size sheet is cut into
  A4/Letter/A3 tiles, turned whichever way needs fewest, with a 10mm margin
  and 10mm overlap, trim and landing lines, and each tile's name in the
  slug strip. A step after imposition: neither renderer changed. The
  export bar offers it beside the print-shop PDF.
- The "neighbouring bleeds overlap" warning only counts neighbours that
  exist: a lone board with no gap no longer trips it.
- Calibration and duplex test sheets print on the home paper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
- GridElement: a block per value of a column (a table), each a heading
  from the block's first row and a line per guest, in N columns of equal
  cells. One size for every block, shrunk together until the fullest fits
  (fitGrid), so no table reads smaller than its neighbour. Tables sort as
  people read them: Table 2 before Table 10. Guests with no table are left
  off and counted in a "left-out" warning.
- It resolves to ordinary text, as a list does, so neither renderer
  changed. Its pieces carry `sourceId`, so grabbing any block on the
  canvas moves the grid, and fit headroom is measured against it.
- Every place that switches on element kind now knows it: rebinding,
  unbound tokens, contrast, missing fonts (both its faces), font removal,
  the announcer and the inspector.
- Gallery entries may carry a sheet patch, so a board starter arrives on
  "the card's own size". Starters: "Seating board — every table, A1" and
  "Table card — who sits here, A5" (one per table).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
- The grid gains a `columns` layout: blocks flow down newspaper columns
  at the size asked for rather than shrinking into equal cells, and a
  `sortBy` column orders the lines in each block. Grouped by {{Initial}}
  and sorted by surname, that is a finder.
- core/template/grid.ts: gridBlocks (grouping, ordering and empty-line
  dropping, shared) and flowPlan, pure line-height arithmetic: a gap above
  each heading, never a heading alone at the foot of a column, a long
  block carried into the next column. A list that fits on one page is
  balanced across its columns.
- core/data/parts.ts: a list too long for one page becomes one artefact
  per page before imposition, cut by the same plan its layout uses, each
  keeping the artefact's identity so design tweaks apply to every page.
  artefactsOf is now the one way artefacts are built — App, inspector,
  data panel and buildJob — so the preview, counts and export all see the
  pages.
- Starter: "Finder — every guest A to Z, A2".
- The gallery corpus is checked with the room's derived columns (Name,
  Initial) and built through artefactsOf, as the app builds it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
- RoomElement draws the seating plan to scale in its box: walls as lines,
  tables and chairs as filled paths, table names and guests' names as
  text. It resolves into those primitives, so neither renderer changed;
  each piece carries sourceId, so the plan is grabbed as one element.
- Show the whole room, or just this card's table (found by {{Table}}),
  which is a table card's map. Seats can say a first name, a whole name,
  their number or nothing. Names hang off each chair away from the table,
  as wide as the gap to the next chair, fitted one by one, and stay
  upright however a table is turned.
- The geometry is Seating's own: layoutFloorPlan and getWallSegs are
  exported from tableaux/utils/floorPlanSvg (no behaviour change there),
  and state/roomScene turns the wedding into a RoomScene once per
  document. Core gets it through ResolveOptions.room, as it gets fonts
  and images; the preview, preflight and export all pass it.
- Starters: "Floor plan — the room to scale, A1"; the A5 table card now
  carries its table's map under its guests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
- QrElement encodes any template, {{Guest Link}} by default, as one
  vector path drawn through the icon pipeline, so it is sharp at any size
  and neither renderer changed. qrcode-generator (MIT, no dependencies)
  does the encoding, fed UTF-8 bytes — its own conversion keeps only the
  low byte of each character. Real renders decode with jsQR, accents and
  CJK included. With no guest link published, it prints nothing and says
  how to publish one.
- {{Guest Link}} is a room column, filled by the store from the published
  link and updated when it is published or taken down. {{Place}} reads
  "Table 4, seat 7", or "Table 4" where guests choose their seat.
- Each piece records what it last printed as (`printed`): per artefact,
  a fingerprint of what the design reads — the columns it binds
  (columnsUsed, now also behind unboundTokens) and the part of the room
  a plan draws. A whole-row fingerprint flagged 97 name cards in the
  browser when one table was renamed, because every table's number
  shifts; only the 8 at that table had changed.
- A notice says which cards have changed since printing, and which are no
  longer needed, and "Print just these" narrows the export to them. A
  reprint adds to the record; a full run or a tiling replaces it.
- Starter: "Escort card — name, table and seat".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
- Print in Seating now offers the floor plan, seating board, finder,
  place cards, escort cards and table cards, each a link into Place cards
  (?piece=<id>). openPiece opens that piece, or makes it once from the
  starter of the same id, so a board and its place cards cannot be printed
  two ways and disagree.
- Removed: cardTemplates.ts, exportCards and its card renderer, the
  exportFloorPlanPdf download wrapper, and the helpers only they used.
  buildFloorPlanPdf stays: the wedding PDF pack still draws its one-page
  plan with it.
- The piece bar is a group of buttons with aria-current, not a tab list:
  axe flagged a tab list holding the "+ New piece" button, and nothing
  there behaves as tab panels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
- template.chairName: how a design names a guest by their chair, as
  tokens of the sitter's own row — "{{First Name}}", "{{Last Name}},
  {{First Name}}", "{{First Name}} {{Initial}}." — set once, with
  suggestions and a live preview on real guests. It replaces the room
  element's fixed first-name/whole-name/number/none choice.
- {{At seat 3}} is seat 3 of the card's own table, so one design fills in
  every table's card; {{Table 1, seat 3}} is that one chair. Both say who
  sits there now, through the design's format; nothing for an empty chair
  or a seat a smaller table lacks. They are not columns: they never block
  an export as unbound, re-binding leaves them alone, and the reprint
  fingerprint includes who sits in them.
- Free-seating tables (Seating's default, and every example table) name
  their guests inside the table, as Seating draws them, never at a chair;
  chair tokens there say nothing and the warning says why. Names spread
  across as many columns as make them largest, so a long top table reads;
  the table's label moves above it.
- The room scene carries each sitter's whole room row, whether a table
  numbers its seats, and its interior (Seating's getTableInterior).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
- Every name at a chair on a plan is set at one size: the largest the
  tightest of them allows, so a longer name never reads smaller than the
  guest beside it. (Names inside free-seating tables already share one.)
- nameGap: how far a name sits out from its chair, in chair radii, set
  in the inspector beside the largest size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
- A text or list element's template field now has "Pick a chair": a map
  of this card's table, or of the whole room, drawn from the same room
  scene. Clicking a chair puts {{At seat n}} (this card's table — every
  table's card fills in its own) or {{Table x, seat n}} (that one chair)
  where the cursor is, with a space if it would run on, and the cursor
  after it.
- Chairs the text already names are ringed. Each chair is a focusable
  button named for its seat and its sitter, as the design names them.
  Free-seating tables are drawn faded, with the reason; the name format
  sits under the map, since chairs are read through it.
- A table-wise chair token on free-seating tables now warns once for the
  design rather than once per table card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
A floor plan's chair names can become one text box per chair, each
bound to its chair by token, to restyle one by one or set over artwork.
Boxes follow the plan (keeping any nudge) or stay where they are put; on
a table's own map they always follow. Stamping turns the plan's new
"Names at chairs" off so no name prints twice, removing the plan leaves
its boxes where they were, and chairs without a box are listed with a
one-click add.

Stamped boxes start at the one size the plan drew its names at
(chairNameSize, now shared with the plan's own drawing). Sized one by
one, short names filled their cells and ran together along long tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
- Top tables come first: one room order (top table by type or by what
  it is for, then 2 before 10) for Table Number and board blocks.
- Per-table pieces make no card for guests with no table; the data
  panel says how many are left out.
- The sheet preview and preflight show just the chosen few during
  "Print just these", as the PDF does.
- A font missing from a list element is caught like any other; one
  exhaustive list of the faces each element sets type in.
- On a table's own map, every seat any table has can be stamped, and
  the uncovered list reads "Seat 9" rather than one table's chairs.
- The wedding pack's room is the floor plan as Place cards draws it,
  shrunk onto A4. Seating's old exporter, jspdf and svg2pdf.js go.
- Names in columns inside a table keep a gutter: "Devendra" and
  "Anneliese" no longer meet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
A guest can be known as something other than what a design's name
format makes of them ("Granny Jo", "Dr Okafor"). Set under Known as on
the Guests page; plans, seat-by-seat tokens and the names inside
free-seating tables use it in place of the format, and designs can
print it as {{Known As}}. Changing it marks the cards that show it as
changed since printing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
Names and text
- An empty value takes the punctuation that only separated it ("Thistlewood,
  Zainab" not "…Zainab —"), and its brackets; a separator belonging to a
  value that is there is kept.
- {{Known As}} is the guest's own name, else the design's name format; the
  greeting starters use it, the table card's map and list agree. Cards and
  reprint checks read rows through one asKnown.
- A combined card names everyone when one of them has a name of their own.
- "Ada B." uses a Last Initial column: a one-name guest is not "Cher C.".
- Clearing the name format returns to the default, not to no names.

Pages and plans
- A letter carried onto a new finder page is headed again, so each page
  plans as the whole list did: no guest is dropped at a page break.
- Pages are cut by names as they print.
- Stamped boxes sit centred on their chair on every table's card.
- A whole-room plan is drawn once per room, not once per card (225ms to
  under 1ms a card at 300 guests).

Printing
- Undo and redo keep what was printed; a print is recorded on the piece it
  was made from; printing the chosen few resets to the whole run.
- The reprint check fingerprints what a plan draws, not every column of
  every sitter.
- The wedding pack prints a whole-list plan, never a per-guest card.
- Removing the open piece starts its neighbour afresh; a piece from a
  starter is one undo step; a link to a removed piece no longer crashes.

Chair picker
- Mode falls back to the room for cards with no table of their own; whole-
  list pieces have no "card's table".
- Picking keeps focus on the map: one tab stop, arrows chair to chair,
  a visible focus ring, larger hit targets, and a table can be brought
  up close.
- The pressed mode reads by fill, not bold; corners use a defined token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
- The table card keeps first names: on an A5 card's small map, full names
  shrank past reading. Its list follows the design's one format.
- Seat tokens at free-seating tables give one warning, not one per seat;
  "has one seat", not "has 1 seats".
- A grouped piece no longer repeats a per-card warning its count already
  covers.
- Review notes in the seat-picker spec.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
- A flowing grid too short for a heading and a line no longer loops
  forever: an empty column always takes one line. (Introduced when
  headings started repeating on new pages.)
- A page made to fit its card holds it exactly; adding then removing
  the margins lost a hair in floating point and fitted none.
- Putting a kept design into a wedding keeps the wedding's other pieces,
  and an older wedding's combined cards, by reading its stationery as
  Place cards does first.
- A copy of a printed piece has not been printed.
- A print is recorded against what it was built from, captured at the
  click; a piece removed meanwhile is nothing to record, not an error.
- On a combined card, the others are named as the design names guests.
- A board's sort column is rebound with its group column.
- A surname of spaces no longer hides a guest from the finder.
- A whole-room plan's reprint fingerprint is worked out once, not per card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeAntAq32mr3MdpB3Jci6U
@vercel

vercel Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
knotwork-suite Ready Ready Preview Oct 5, 2026 11:23am UTC

@JFrusher
JFrusher merged commit d68e88a into main Oct 5, 2026
9 of 10 checks passed
@JFrusher
JFrusher deleted the claude/place-card-maker-brainstorm-zyyfl3 branch October 5, 2026 11:28

This branch was successfully deployed

1 active deployment
Preview — 0c9fbf73 Deployed Oct 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants