You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This PR modernizes the seating tool's state management and integrates it with the suite's unified wedding data model.
Summary
Converts the tableaux store from JavaScript to TypeScript, refactors the action system to work with the shared Trousseau wedding model, and removes local persistence in favor of the suite's centralized sync system. This enables the seating tool to participate in the multi-user, multi-tool wedding planning workflow.
Key Changes
Store Architecture
Migrated actions.js → actions.ts with full type safety
Replaced local undo/redo middleware with the suite's centralized history system
Removed useStore.js local persistence; new useStore.ts reads from useTrousseauStore
Introduced plan.ts to bridge between Trousseau's event model and seating's internal types
Added patch.ts for forward-only command application (no inverse needed with centralized history)
Type System
Created types.ts with TypeScript definitions for Plan, Table, Guest, Family, etc.
Removed planSchema.js in favor of shared @/lib/model/types
All action creators now return typed commands with proper payload shapes
Data Flow
Removed CSV import modal and local guest management (ImportModal.jsx, csvParser.js)
Guest data now flows through shared readGuests() from @/lib/model/slices
Seating reads/writes to the wedding event via sliceBridge.ts (simplified from previous version)
Removed local auto-save hook; sync is now handled by the suite's centralized system
UI Updates
Converted key components to TypeScript (.tsx files)
Updated imports to use new store structure
Removed modal for local document management (ModalRoot.jsx → ModalRoot.tsx)
Guest panel now integrates with suite-wide guest management
New Pages & Features
Added /setup page for initial wedding configuration
Added /weddings page to list and select weddings
Added guest import UI in shell components
Added supplier and share link management pages
Integrated with accounts system for multi-user support
Testing
Added live.test.js for store integration tests
Updated existing tests to import from new TypeScript modules
Added tests for new API routes (share, suppliers, accounts)
Implementation Details
The action creators now work with a Plan object that mirrors the Trousseau event structure. Commands are simple forward patches—undo/redo is handled by the wedding's own history system, which keeps full state snapshots. This means:
No more inverse calculations in actions
Simpler, more predictable state mutations
Automatic multi-user conflict resolution via the suite's sync layer
Guests and tables stay in sync across all tools
The store still maintains ephemeral UI state (selection, pan/zoom) locally, but all document mutations go through the shared model.
With Seating open, import three guests from the Data panel: the panel says
"3 new" and the header says 103. Rename a table and the next save writes
Seating's own copy of the 100 back, and the couple's names revert with it.
Timeline does the same to a date moved in the panel. Reproduced in unit
tests and against a production build.
Each tool reads the wedding once, on mount, and writes its whole copy back
on every save. The generation guard already refused that copy after a
whole document was swapped; nothing covered one slice being written from
outside, which is what the Data panel does, over whichever tool is open.
Each tool's gate now declares the slices it copies, each tool tags its own
writes, and a write to a held slice from anywhere else starts a new
generation: the tool remounts onto the current document and its stale save
is refused. One rule in the store, so it covers every such writer.
A panel edit now resets the open tool's undo history, as a restore already
does. The e2e test fails with the rule disabled and passes with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
… tools
The maintainer's answers from the 2026-09-28 audit, recorded as decisions:
a planner role alongside the two partners, sides named after the partners,
a phone-first day-of binder, one importer, a live guest link on the
account, and every tool converging on the live document. Also the design
language new windows follow, how signing in on a device with its own
wedding stays safe, and the phases in order.
The roadmap gains subsystems J to O pointing at it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
A save that failed looked exactly like one that worked. The store set its
error "so it goes on screen", but both things that read it only did so for
a wedding that could not be read. Shown with a browser refusing writes: no
message anywhere. The two cases are now separate fields, and the Data button
carries the state: a dot while all is well, and the problem in its own words
("Not saved", "Needs you", "Offline") when something needs the couple. On the
button rather than beside it, because a separate pill pushed Timeline's
Present button out of the header at 1440px.
The Data panel's native <dialog> becomes the one Dialog, and Confirm replaces
the two window.confirm calls guarding account deletion and loading the
example wedding. The account deletion copy said the wedding always goes with
the account; it stays with a remaining partner, and now says so.
The tour was a positioned div saying aria-modal and doing none of it: focus
stayed on the page behind and its arrow keys listened on the whole window.
It is a native modal dialog now. Axe also runs on /seat, /invite, the open
Data panel and the open tour.
Also records in the plan that the flaky immediate-reload test fails on
untouched main too (2 in 80 runs), so it is not the S1 fix's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
The design language's floor: nothing a person reads is below 12px. The one
scale moves together through --text-xs, and the five labels that set their
own 10px or 11px — group counts in Seating, the table palette, Place cards'
feature chips and row tags — now read the token instead. The count inside
Seating's 16px warnings badge stays at 10px, and says why.
Checked by screenshotting all five tools and the front page at 1440px and
1024px before and after. One thing moved: the table palette's fixed 58px
items cut "Rectangle" and "Top Table" to an ellipsis at 12px, so the width
is now a minimum and each item fits its own label.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
The couple's names could be edited in three places — the Data panel,
Timeline's Day panel, and a click-to-rename on Seating's title — and the
date and venue in two. Each tool wrote its own copy back into `event`, so
whichever saved last won.
They are edited in the Data panel only now. Timeline shows them, read live
from the wedding, with a button that opens the panel; it keeps the curfew,
the clocks and the venue's coordinates, which are the schedule's own, and
those are all it writes into `event`. Seating shows the names as a plain
title and no longer writes `event` at all; its `meta` echo is overlaid from
the wedding and never preferred over it. The front page's "Add one in
Timeline" becomes a button that opens the panel.
The Data panel's open state moves from the header into a small store so
anything can open it.
Also moves the header restructure, and its overflow at 1024px, into the
plan's Overview step, where the header changes shape anyway.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
Seating had its own importer beside the Data panel's, with different rules:
it could replace the whole list, matched by email, and stored diets as keys,
where the other matched by name only and stored the file's words. A list
imported one way looked different to the tools than one imported the other.
Shown in a real browser: the example wedding's thirteen vegetarians were
invisible to Seating's Vegetarian filter, and "None" was listed as a diet.
Now there is one importer, opened from the Data panel and from Seating's own
buttons. It keeps the better rule from each: a guest is found by email, then
by a name nobody else on the list shares, never when two emails disagree.
Each row is a person. A name shared by several guests is held back and shown
rather than guessed at, so neither Sarah Smith is edited and re-importing
does not add them again. RSVP answers are shown for the couple to say what
each means. Nothing is written until the preview, which lists who is new,
updated, unchanged, and on the list but not in the file — removed only if
ticked, and then from their table, groups and seating rules as well. Nobody
is ever unseated. A CSV's Table column is no longer offered, since it was
never used.
A diet is a key in `dietary` and the guest's own words in `dietaryRaw`,
defined once in lib/model/dietary (moved from Seating). Documents stored the
old way are converted as they load. Place cards, the reports and search show
the guest's words.
The header guesser now reads "Dietary Restrictions", and no longer takes
Joy's "Party" (a household) as a side. Checked against fixtures shaped after
Joy's and Zola's documented exports — not real files; the hosts were not
reachable from here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
Sides, group-shot roles, filters, exports and the shot sheet now read the
partners' own names from event.partners. Older weddings convert as they
load: bride/groom sides become a/b, the old role keys map to the partner
roles, and a title naming two people fills in the partners. The Data panel
asks for "One of you" and "The other"; the couple's title follows them.
Also removes lib/seating/exports.ts and stats.ts, which had no callers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
A device now stores which account wedding its document belongs to and what
the two last agreed on (trousseau.cloud.link), and a start decides from it:
nothing on the device takes the account's, nothing on the account takes the
device's, the same wedding merges per slice, and two different weddings with
work in both are asked about. Whichever is not chosen is kept as a copy on
this device and can be put back from Data. "Has content" counts what someone
entered, since opening a tool stores its empty defaults.
Fixes, each reproduced first:
- S4: accepting an invite, or signing in on a device already in use,
silently replaced the device's wedding with the account's.
- S12: an edit made while the account answered "unavailable" was lost at
the next start; an edit made offline went up at version 0 and conflicted
over every slice. The baseline lived only in memory.
- S6: the example wedding replaced a wedding of only group shots unasked,
and never said it would go to the account too. It now keeps a copy.
- S5: /login dropped `next`, and the invite's sign-in linked to bare /login,
so an invitee landed on "Create your wedding" and could never join.
- S3: accepting an invite or starting a wedding now ends in a full load,
where sync starts.
A wedding is created at sign-in, except on the way to an invite. Signing out
asks whether to keep the wedding on this device or remove it. The one-write
offline queue is removed: a start that merges from the stored baseline
carries the same changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
A wedding now has up to two partners and one planner, and an account can be
one of the couple on one wedding and a planner on any number.
Database (20260928000001_roles.sql): membership is keyed by wedding and user
with a role. The partner-in-one-wedding and one-planner rules are partial
unique indexes, and the two-partner cap is counted under the wedding's lock.
Invites carry a role. remove_member lets anyone leave, and lets one of the
couple remove their planner, whose access ends with the next request.
wedding_people lists who has access, with addresses, to members only.
Proved against PGlite, including every earlier partner rule and applying
the migration twice.
Every document request now names its wedding (?wedding=), checked against
membership. Nothing is inferred from "the" wedding.
On the device:
- A start lists the account's weddings and opens the one opened here last,
else the one it synced with, else the couple's own, else the only one. A
planner with several clients and none opened here chooses from the new
switcher.
- Switching is a full load of /open/<id>, outside the app layout, so no
store or tool can write the old wedding over the new one. The open wedding
is put aside with its link and comes back exactly as it was. Edits not yet
sent come back with it and go up once it is open again.
- The account page shows who has access, invites as partner or planner, and
leaves (which also removes the device's copy). /weddings lists every
wedding and starts a client's; signing in through it starts none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
/setup walks a new wedding through, in the order a couple would: names,
date and venue; who is coming; a room to start from; then who else plans it.
The first three steps are a draft, written as one change (one undo step, one
push) when the room is set. The last step can leave the page to sign in, so
it comes after the commit.
- Guests go into the draft through the one importer, which now takes a
target: the wedding, or setup's draft. Pasted names run through the same
matching, so nobody is added twice.
- The starting room is made with Seating's own addTable, in rows spaced from
what Seating actually draws. At the example wedding's 160px, neighbouring
chairs collide.
- The event change (names, the title, the day resolved from them) is one
function now, used by setEvent and by setup.
- The front page leads with setup while the wedding has nothing in it.
Also fixes a bug caught by the existing browser tests: `show` took an
optional target, and Seating and the Data panel hand it straight to onClick,
so the click event became the target and the importer never opened. `show`
now takes nothing; setup uses showInto.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
The guest link rode on the old passphrase sync, so publishing one asked a
signed-in couple for a second, unrecoverable credential (S8). Once
published, it showed the room as it was on the day it was published.
Now (20260928000002_guest_link.sql):
- wedding_shares belongs to the account wedding. Any member publishes
without a passphrase; members can read the key, and the public read_share
returns ciphertext only.
- The token and key are fixed at the first publish. A republish under
another key changes nothing, so two devices publishing at once can't break
the links guests hold.
- The four passphrase-sync tables and put_slice are dropped.
Proved against PGlite, including applying the migration twice.
On the device, the link republishes itself a few seconds after what guests
see changes. It checks the link again first, since a partner's device may
already have republished. The fingerprint leaves out publishedAt, or every
check would count as a change. A dietary note, which guests never see,
publishes nothing.
lib/sync, its API route and the passphrase UI are deleted. What was still
used moved: rate limiting and `check` to lib/server, the share encryption
and snapshot to lib/share, portable assets and retention to lib/documents.
The retention sweep now covers account weddings only.
The key being stored with the wedding makes the link exactly as readable to
the server's operator as the wedding already is. The Privacy Policy now says
that, no longer describes the passphrase system (a test keeps it that way),
and is dated today, as are the Terms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
"Take a tour" now walks all 28 steps across every tool; "How this page
works" still runs one chapter. The example wedding gains what each chapter
points at: sides, replies, families and groups; 97 of 106 seated with
families together and three left to seat; a crew with jobs and two tasks;
26 group shots; and a card design Place cards made from the room. A test
holds the fixture to all of it, and the duplicate copy under fixtures/ is
gone: tests read the one the app serves.
Walking the fuller example in each tool found, and this fixes:
- A guest who declined was still counted to seat and feed: Place cards,
What is left, Seating's header, Overview, Unassigned chip and dietary
tally, and the front page's Seated figure. One isComing, used everywhere.
- Delegation called a job with no block an orphan, a clash that held the
print run. It is a task, shown as one.
- What is left said the card design could not show dietary needs it drew
as icons; it now counts an icon's source column, as Plaque does.
- The cards' Side column printed "a"/"b" since sides took partner names.
- Seating's "worth checking" note treated a "None" answer as no answer.
- Accessibility: the team name field had no label, seated guest cards were
at 40% opacity, and seats nested a button inside the table's button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
The front page leads with the one thing to do next — What is left's first
item, blocking before advisory — then says how far along each area is
(guests, seating, cards, the day, jobs, shots), with the rest of What is
left quietly below. It replaces a grid of the five tools that repeated the
header, and four bare counts. An area with nothing in it says so rather
than reporting success.
The wedding's name takes the wordmark's place in the header and opens the
wedding's pages, and for a planner the other weddings, in place of the
switcher. It is a Popover, the kit's first.
At 1024px Timeline's zoom, Fit day and Present, and Delegation's filter,
were scrolled out of sight inside the header. Below 1280px the tool tabs
are now icons, the signed-in email is an icon, and the guest count is
gone. A test at 1024px checks every tool's header controls are in view; it
fails on the previous header.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
A page at /guests, under the wedding's name: every guest as a table,
sortable by name, reply, side, food and table, filtered by any of those and
by text. A reply, side, what they eat and table change in the row, or for
everyone ticked; tags are added and taken off in bulk; guests come off the
list with a confirmation. Plus-ones are shown both ways round.
It keeps no copy of the list: it reads the wedding and writes it, so every
change is on the one undo stack and in every tool at once. Table moves run
Seating's own commands over the stored slices, so a guest and a table's
list move together and a seat-level table keeps its holes; food is read
from the guest's words as Seating's inspector reads it; removal is the
importer's. Only guests the filter shows are acted on.
The Overview's guest area and the tour point at it. The tour's rule on
chapter size becomes at most six steps and under thirty in all, which is
the length the four-to-six rule was protecting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
A page at /money, under the wedding's name, and the one place money is
changed: the budget, each supplier's cost and deposit, when the deposit was
paid, and when the balance falls due and was paid. That last date is new —
balancePaidOn — because nothing recorded that a balance had been paid. The
balance itself is the cost less the deposit, never stored twice.
The payments still to make are listed soonest first, each with "Paid
today". What is left says a balance due in the next 30 days is coming, and
holds an overdue one up as a problem; the over-budget line points here now.
The front page gains a Money area.
Delegation's crew panel keeps who the suppliers are, their email and when
they confirmed, and says where their costs went. Its budget setter goes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
A page at /checklist over the crew's jobs with no block: late, the next 30
days, later, undated, and done. A task is added, dated, renamed, ticked
off or removed in place, on the one undo stack. Jobs gain dueOn.
"Add the usual tasks" adds eighteen, dated back from the wedding's day and
matched by name so asking twice adds nothing. The example wedding has
them, seven done. A task past its date joins What is left as a nudge, and
the front page gains a Checklist area.
The templates showed four places reading a task with nobody named as a
gap: What is left (as blocking), Delegation's coverage notes, its header
count and Unassigned filter, and the front page's Delegation area. A task
with nobody on it is the couple's own, so all of them now count the jobs
on the day. Delegation folds the tasks away, there to hand one to somebody.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
Two partners changing different guests, tables, blocks or jobs no longer
conflict. The wedding is cut into parts (lib/documents/parts) — one per
record in those four, the rest of each such slice as one, a list's order as
one, and every other slice whole — and each part is merged against what the
two sides last agreed, which is now kept part by part. A list's order never
conflicts: a record added on the other side is woven in after the one it
followed. The published day follows whichever timeline and event won, or is
published again from a merged one, rather than conflicting on its own.
Sync & history is the kit's first slide-over, at ?panel=sync, and where the
Data button goes when something needs choosing. What changed on both sides
is laid out field by field — yours, theirs — to keep one. The account's
saved versions are listed with who saved each and, on asking, what changed
in words; one can be put back, keeping the wedding as it is here first.
Two read-only routes serve the history under the members' own row-level
security.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
Ctrl/⌘ K, or the header's search button, opens a combobox over every page,
guest, table, block of the day, job and task. Names that start with what
was typed come first; each result says a little more (a guest's table, a
block's time, who does a job) and opens where it lives, on the record: a
table selected in Seating, a block in Timeline, a job in Delegation, a
guest found on the Guests page, a task on the Checklist.
The tools read one ?select=<id> once they have loaded, and keep watching
for it, so the palette can pick another record in the tool already open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
A planner's list of weddings now says how each stands, soonest first: the
date and how long to go, the next thing to do (What is left's own, run over
the stored document, with how many more), what is still to pay the
suppliers, and when it was last saved. The listing carries this from the
server, which already read each document to name it; it takes the asker's
own date for what has fallen due.
The list is its own component, tested apart from the page that loads it.
Dates — today, days until, a date in words — move out of the money module
into lib/dates, which the Checklist, What is left and the palette also use.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
A card design, a running order, a room or a checklist can be kept from one
wedding and put into another, at /library (and in the wedding menu for a
planner). Each is built from a whitelist of what it is: a design without
its rows or per-row tweaks, a day without its date or suppliers' names and
numbers, a room with every chair empty, a checklist as days before the
day. Putting one in says first what it replaces, is one undoable change,
and republishes the day when it brings a running order.
It is kept on the account in its own table, library_items: readable,
addable and removable only by its owner, never edited in place, capped in
size — proved against PGlite. Deleting the account deletes it. The Privacy
Policy says what it holds (dated 2026-09-28).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
…mple
The command palette sent a guest to /guests?q=<their name>, so the name
travelled in the address — into history, server logs and anything that
counts page views. It now goes by id, through the same ?select= the tools
read, and the Guests page looks the name up on the device.
The example wedding named its suppliers twice over: the running order's
"Eleanor Vane Photography" was the crew's "Ivy Lens Photography", and so
on for all six. The crew now uses the running order's names and numbers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
/binder, apart from the planning app and sized for a thumb: Now — what is
on and what is next, against the venue's own clock from the wedding's UTC
offset — then the Day's running order, who to Ring (every number the
running order and the crew hold, once each, as tel: links), Find a guest's
table, and the Shots to tick off. It writes nothing to the wedding; ticks
stay on the phone.
A service worker scoped to /binder keeps the page and every file it loaded
(the page hands the list over once the worker is ready), and never the
API. The wedding is already in the phone's storage, so a reload with no
signal opens it. The offline test fails with the worker taken away.
The Binder joins the wedding's pages in the menu and the palette.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
From Delegation, under a supplier's Contact, a link to their own call
sheet: when to arrive, their named people, their jobs on the day and
before it, and the wedding's names, date and venue. No guests, and
nothing of any other supplier's.
Sealed as the guest link is, under a key in the fragment kept with the
wedding, and republished a few seconds after anything on it changes. The
public page has one button, Confirm, which records when against that
link; the couple's side carries it into the team's confirmedOn as the
day it happened, never backwards and never as an undo step. A sheet
changed since confirming says so on both sides. A supplier removed from
the wedding has their link taken down.
supplier_links is members-only; reading and confirming are functions
that take only a token; deleting a wedding cascades to its links, all
proved against PGlite. The Privacy Policy gains a section on what a
supplier's link contains; robots refuses /supplier/.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
Brigade kept a copy of the crew and the day in its own store, with its
own undo history, filled on mount and written back by a 400ms autosave.
Tests written first showed the cost: an edit was not in the wedding
until that autosave ran, and a change made elsewhere was not seen until
a remount.
Its store now holds only what is its own (the picked job, the filter, a
notice). The crew and the day are read from the shared document through
one view memoised on it, and every edit goes straight into the crew
slice with a label, on the wedding's one history, which the header's
undo drives. The history module, restore and autosave are removed, and
Delegation is out of HOLDS, so nothing remounts it when the wedding
changes around it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
Plaque kept the design in its own store with a snapshot history, filled
from the stationery slice on mount and written back by a 400ms
autosave. Tests written first showed an edit was not in the wedding
until that autosave, a change to the slice from elsewhere was not seen,
and (S19, reproduced against the old store) undoing any edit silently
dropped the card's row scope and every per-row tweak, because the
snapshots kept only elements and background.
The design is now the stationery slice, and Plaque's store only shows
it: design edits are written there first with a label, on the wedding's
one history, and a subscription makes the store follow the slice
synchronously whoever changed it. Fonts, images, printers, selection and
page stay the window's own. A drag writes every frame under one label,
kept as one undo step; measured at under a millisecond per write.
Removed: the snapshot history, the undo stack stored in the slice,
restore-on-mount, the autosave and its unload flushes, and the canvas's
gesture-start hook. Place cards are out of HOLDS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
Cadence kept the day in its own store with its own history, filled on
mount and written back by a 400ms autosave. Its store now keeps only
what is its own (the picked block, a drag in progress, zoom, a notice);
the day is read from the wedding through one view memoised on it, and
every edit writes the timeline, republishes the resolved day and echoes
the curfew and clock into the event as one labelled change on the
wedding's history. A drag is still previewed locally and written once.
An edit that changes nothing writes nothing.
The fonts panel wrote back the day as it was before an uploaded file
was read; adding and removing a font are now actions that read the day
when they write.
Removed: the history module, restore, the autosave and its flushes.
Timeline is out of HOLDS; Seating is the last tool keeping a copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
Tableaux kept the plan in its own store with a command-inverse history,
filled by hydrate on mount and written back by a 400ms autosave. Shown
first against that store: a Seating edit was not in the wedding, and a
guest changed elsewhere was not in Seating.
The plan is now the wedding's guests and seating, followed synchronously
as they change; every command is applied and written as one labelled
step on the wedding's history. Seating recognises its own write coming
back by the moment rather than by object identity (an undo can bring
back the objects it last wrote), so an edit redraws only what it
touched. Drag frames stay local until pointer-up, so undo returns to
where a drag began. Pan and zoom become the window's own and the room
opens framed. Restoring a snapshot is one undoable step.
With no tool keeping a copy, the guards against stale copies are
removed: HOLDS, noteRead/mayWrite, held/hold/release, the `by` write
option, `generation` and the remount keyed on it. WhenDocumentReady
only waits for the wedding to be read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
Seating's model gets types of its own, built on the suite's Guest, Table,
Space and Constraint where they are the same thing. Its utilities,
actions, patch and store are converted, bottom up, with extensionless
imports so the rest could move file by file.
One way to read a plan: plan.ts reads the wedding's guests and seating
into a normalised plan (and back into the two slices), used by Seating's
store and by setup. The Guests page runs Seating's own commands over the
slices as stored and writes back only what changed, so a room Seating
would tidy on its next edit is left as it is.
Commands say only how to go forward: their inverses, unread since undo
became the wedding's history, are removed. Seating's guest removal now
also drops seating rules naming the removed guests, as the Guests page's
separate copy of removal already did; that copy (lib/seating/
removeGuests) is deleted and the page and importer use Seating's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
Seating's components, hooks and App are now TypeScript, on the model and
store converted before. The types found:
- a guest's side cleared to null rather than the model's '' (read back
the same, but not the one way to say it);
- a table resize with nothing resized written as an empty undo step;
- confirm options no caller used (onCancel, cancelLabel, keepOpen).
Modals are a typed map from name to what each is opened with, and the
colour picker hands out null only where clearing is offered.
The canvas's drags commit through the commands everything else uses —
rotateTable, resizeTable, moveZone, resizeZone, reshapeZone, editRoom,
movePillar — rather than inline commands carrying inverses nothing reads.
They were not tested with a pointer, so now they are: moving, turning
and resizing a table, moving a room, putting down and moving a pillar,
drawing and moving a zone, each undone from the header.
That found S20: a zone, a pillar, a room shape or a calibration line
could not be started on the floor of a room, only off it. The canvas
gave every press on a room to the room before any tool ran, and the
room took it as a click or a pan. The tools now act wherever they are
pressed; a press on a room is the room's only for the select tool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
Every tool and page now edits the wedding and keeps no history of its
own, so all nine callers of ToolUndo passed it the same six props from
the same store. It reads the wedding's history itself now; the callers
render <ToolUndo /> and no longer subscribe to the history, which
re-rendered each of them on every edit anywhere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
lib/seating held an earlier TypeScript port of Seating — actions,
geometry, table types, snapping, warnings, groups, room actions — that
nothing in the app called. It goes, and what used it now uses Seating's
own:
- the guest's find-my-seat plan draws with Seating's geometry, which
gives the same footprint for all 154 table type, capacity and size
combinations (compared before the old one went);
- the round-trip test seats its guest through Seating's store;
- the load-time pass keeps the one piece it needed, reconciling a seat
the guest and the table disagree about, now with a test of its own.
Before the port's tests went, each claim was put to Seating's own code.
Chairs clearing a crowded edge, a top table facing the room, groups and
families recorded on both sides: all true, and now tested there. One
was not (S21): a pair of guests could be given a second rule, the same
again or its opposite, which only added a warning that could not be
cleared. addConstraint now refuses a second rule for a pair and a rule
about someone and themselves, and the rules window says why Add is off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
Every accepted save is announced from the database, by a trigger, on
the wedding's private Realtime channel: its version number and nothing
else, so no guest, name or plan travels over it. Row-level security on
Realtime's messages lets only the wedding's members follow the channel,
and lets them say they are there but not announce a save.
A window that hears a version it does not have pulls, and merges as any
pull does; its own saves come back already agreed on. Rejoining after a
dropped connection pulls too, since an announcement made meanwhile is
not heard again, and coming back to the tab still looks. The poll is
gone.
Presence is each member's email and the page they are on, shown in the
header as initials, and nothing when nobody else is there. The privacy
page says what travels and who sees it.
The migration tests' stand-in for Supabase now has Realtime's messages,
topic() and send(), and the assets test uses the shared stand-in rather
than its own copy, which the new migration broke.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
Modernizes the seating tool around the shared wedding store while adding centralized history, account collaboration, sharing, supplier, library, Binder, guest, checklist, and money workflows.
Changes:
Migrates Tableaux state and UI modules to TypeScript and shared wedding data.
Centralizes persistence, sync, history, and multi-wedding account behavior.
Adds guest/supplier sharing, library, Binder, and supporting tests.
npm audit --audit-level=high failed on fast-uri 3.0.0–3.1.6
(GHSA-qw65-cvwx-89v3, GHSA-58mr-gqgx-xq4g), reached only at build time
through @sentry/nextjs → webpack → schema-utils → ajv. npm audit fix
moves it to 3.1.8, a patch release inside ajv's own range; nothing else
in the lockfile changes. The build, typecheck and unit tests pass on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
…pt page
Two findings from review, each reproduced before the fix.
Signing out and removing the wedding from a device cleared IndexedDB but
left the Binder's ticked-off shots in localStorage, one key per wedding,
to come back if that wedding were opened there again. They go now; that
the device has seen the tour is the device's and stays. The key is named
once, in lib/binder, for the Binder and the removal both.
The Binder's service worker kept whatever a page load returned while
there was signal, so a 500 replaced the working page and was all a phone
could show once the signal went. It keeps only a page that worked, as it
already did for files. A browser test answers the worker's own request
with an error and checks the kept page still opens offline; it fails on
the old worker.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH
github-advanced-security is red for a reason outside this PR. It is GitHub's AI code-scanning agent ("Code scanning AI findings"), and on both runs so far (b0d54a6 and a93c7a2) it stopped before analysing anything, with:
CAPIError: 400 The requested model is not supported.
COPILOT_AGENT_MODEL: sweagent-capi:claude-opus-5[ReasoningEffort=medium]
The agent asks for a model its own backend refuses. That is set in GitHub's code security settings, not in the repository, so no change here can make it pass. It needs fixing on GitHub's side, or turning off under Settings → Code security, until GitHub fixes it. The deterministic scans pass on the same commits: CodeQL and Analyze Code.
The other failing check, security-audit (npm audit --audit-level=high), is fixed in a93c7a2. It failed on fast-uri ≤ 3.1.6, which comes in only at build time through @sentry/nextjs → webpack → ajv, and that commit moves it to 3.1.8. The same advisory fails main, whose lockfile is unchanged by this PR. CI passes on that commit.
Existing documents that only have coupleNames are parsed with blank partners here. Local IndexedDB loads are repaired by reconcileLoadedDocument, but StoreHydrator runs that before startCloudSync; when an old account document is then adopted with replaceDocument, reconciliation is not run again. Those weddings therefore show “Partner one/two” for sides and shot roles until the names are manually re-entered. Derive partners as part of migration, or reconcile every cloud document before it becomes current.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR modernizes the seating tool's state management and integrates it with the suite's unified wedding data model.
Summary
Converts the tableaux store from JavaScript to TypeScript, refactors the action system to work with the shared Trousseau wedding model, and removes local persistence in favor of the suite's centralized sync system. This enables the seating tool to participate in the multi-user, multi-tool wedding planning workflow.
Key Changes
Store Architecture
actions.js→actions.tswith full type safetyuseStore.jslocal persistence; newuseStore.tsreads fromuseTrousseauStoreplan.tsto bridge between Trousseau's event model and seating's internal typespatch.tsfor forward-only command application (no inverse needed with centralized history)Type System
types.tswith TypeScript definitions for Plan, Table, Guest, Family, etc.planSchema.jsin favor of shared@/lib/model/typesData Flow
ImportModal.jsx,csvParser.js)readGuests()from@/lib/model/slicessliceBridge.ts(simplified from previous version)UI Updates
.tsxfiles)ModalRoot.jsx→ModalRoot.tsx)New Pages & Features
/setuppage for initial wedding configuration/weddingspage to list and select weddingsTesting
live.test.jsfor store integration testsImplementation Details
The action creators now work with a
Planobject that mirrors the Trousseau event structure. Commands are simple forward patches—undo/redo is handled by the wedding's own history system, which keeps full state snapshots. This means:The store still maintains ephemeral UI state (selection, pan/zoom) locally, but all document mutations go through the shared model.
https://claude.ai/code/session_01KfDv5hZZ3uYeubXH64bFVH