With a section icon the heading renders as a row holding the icon and a
separate title text, and that text only took the heading's color from
Custom Styles. Font size, weight, style, letter spacing, line height,
alignment, decoration and text transform written for section-heading now
reach the title too, as they do without an icon.
Stored resumes, resume versions, letters and letter versions still in
the old editor's legacy mode, or carrying legacy style rules with no
stylesheet, are converted to Semantic CSS once, right after the SQL
migrations, and the result is recorded in a new data_migration table so
later starts skip it. Only metadata.stylesheet is rewritten (the rules
stay for rollback), a row edited meanwhile is retried on the next start,
and a failure leaves the data as it was without stopping the server.
The API no longer converts on every read and save. Imports of old
Reactive Resume JSON exports convert their legacy rules on the way in.
Now that the API hands out every resume with its legacy style rules
converted, the renderer runs one styling system: the stylesheet mode,
the per-slot legacy rule hooks in the template primitives and the
section style provider are gone, and the Custom Styles editor loses its
legacy draft banner and Activate step. The converter stays (the API
uses it), with its fixtures now checked by rendering the converted
stylesheets.
Resumes from before Semantic CSS carry legacy style rules that only the
old renderer understood. The API now converts them (with the existing,
parity-tested converter) whenever resume data is read or written, so
everything it hands out uses a Semantic CSS stylesheet and the database
catches up on the next save. The rules stay stored for rollback, and a
draft typed in the old editor but never activated is kept, commented
out, after the conversion. New resumes start with an empty stylesheet.
Custom styles no longer need @version 1; at the top: the language
version already lives beside the text, so new and converted stylesheets
leave it out, the editor hides it in older ones, and the compiler
ignores it. The validity status and inline diagnostics are gone too:
whatever applies shows in the preview and anything else is left out.
The drawer took a mouse press anywhere outside Drawer.Content as the
start of a swipe and captured the pointer, so switches and buttons in a
bottom sheet never received the click. The sheet's children now sit in
Drawer.Content; touch swipes still dismiss from anywhere.
The phone entry screen pushes in from the end edge and slides back out on
Back, reversing mid-push. The Design sheet keeps its full height and moves
by translate instead of animating height, and rises from the tab bar when
Design opens. The selection bar rises and fades in.
Sheet is now backed by Base UI Drawer: bottom sheets follow a downward
swipe, dismiss on a flick or a drag past half their height, and settle back
otherwise. Side and top sheets are unchanged.
On phones, Settings rows push the section in from the end and the back link
returns the list from the start, layered on the page view transition.
Views that replace each other (new-document steps, auth post-submit
screens, ATS checker states, consent, social sign-in, new API key) now
fade up 4px as they arrive. The ATS lenses both stay mounted so switching
back never re-reads the PDF, and the 1024-1279 assistant swap crossfades
in the panel's cell (instant on Cmd+J).
Things that used to push layout no longer move what the user is reading:
the bulk-selection bar floats at the bottom of the list, the rich text
toolbar sits under the text, and the letter draft bar takes the Stop
button's place under the draft. The drop-to-import frame, follow-up
nudge, job-match panel, public-link options, and rare save states fade in.
Adds a Swap primitive so copy buttons crossfade Copy -> Copied at a fixed
width, and POP_CLASS for status icons (progress checks, tool status,
proposal Applied, the public download icon).
Menus, popovers and combobox lists now slide in from their trigger on
whichever side they open (data-side aware, RTL-correct), and submenus
open in 120ms and close in 84ms. The combobox list exits at 0.7x like
every other popup, and tooltips scale from their trigger.
Toasts keep travelling in the swipe direction when dismissed, ease back
on a cancelled swipe, and a replaced toast drops out before the new one
has finished rising.
The tabs indicator uses the movement curve and now also draws the line
variant's underline, so it slides instead of teleporting. Hotkey mode
switches in the resume and letter builders land instantly. Radio dots
and checkbox ticks grow in instead of popping or only fading.
Add a Collapsible primitive to @reactive-resume/ui wrapping Base UI's
collapsible, styled like the accordion panel: height grows over 200ms and
folds over 140ms on ease-enter, reversing mid-animation.
Use it for entry cards, section rows, Basics, Check categories,
Design > Advanced, More options, AI provider rows and ATS checker findings,
so content no longer teleports while the chevron rotates. Chevrons at those
sites now share duration-standard ease-enter.
Opening an entry scrolls it back into view once the previous one has folded
away if it slid above the panel, and the page-to-panel reveal re-aims its
scroll after the panels settle. Advanced's open state moves up to the
Design panel so its nav pill can open it; the e2e fixture drives the new
trigger via aria-expanded.
Buttons (every variant except link, skipped while loading), chips, tiles,
nav items, rail and tab-bar icons now dip to 0.97 on press; document,
template, application and suggestion cards dip to 0.98. Full-width settings
and conversation rows darken to the press colour instead of shrinking.
Joined button groups cancel the scale so their seams stay closed, and the
switch thumb stretches toward its travel while held.
Document and template cards transitioned transform, but Tailwind v4 lifts
with the translate property, so their hover lift jumped; they now list
translate and scale. A new document card keeps its accent ring while
hovered, and letter template cards lift like resume template cards.
Set Tailwind's default transition duration and curve to --d1/--ease so
bare transition-* utilities run quick on the house curve. Mirror the
motion tokens in libs/motion.ts (EASE, EASE_MOVE, D1-D3, EXIT) and move
app UI off ease-out-strong and hand-typed 150/300ms durations; the
landing page keeps --ease-out-strong.
Give snapping hover and focus states a transition, slide progress fills
with translate instead of animating width, keep selection outlines'
width and style constant so they fade from transparent instead of
flashing dark, and drop the unused agent-prompt-marquee keyframes.
Forme gives an absolute box no height from its top and bottom offsets,
so the bar that react-pdf stretched down each section's items vanished.
The converter now draws such a bar as the left border of a box around
the other children, which is as tall as they are and is painted on
every page the section reaches.
RichInput, the full-toolbar editor only notes used, is gone with its tests; the editor extensions it defined move to rich-text-extensions.ts, which the rich text editor already used.
Resumes no longer hold cover-letter sections. A migration saves every
letter a resume carried as a letter linked to that resume's details and
design, hands a resume's only letter to its only letter-less application,
then removes the sections from resumes and their layouts. rollback.sql
puts them back.
Every resume write on the server moves any letter it still carries into
a saved letter in the same transaction, so stale tabs, older files, API
clients and restored versions keep working without duplicating letters.
The resume editor no longer adds or imports letters, the resume download
drops its Cover letter tab, and resume PDF downloads (API, signed links,
MCP) no longer take a cover-letter target. copy_embedded_cover_letter and
POST /cover-letters/from-resume are removed.
The expand steps of the redesign kept older app versions and API clients
working. This removes those compatibility paths:
- Entry date text (period/date) is written from the structured dates on
every save; an edit to the text alone is overwritten.
- application.archived is dropped; archived rows close first. CSV import
still reads the flag from older exports.
- resume_version.label is dropped; versions are named by kind and name.
- rejected is no longer accepted as a stage; remaining rows and history
close first. CSV import still maps it to closed.
BREAKING CHANGE: older app versions and API clients that read or write
archived, version labels, the rejected stage or date text alone no
longer work. migrations/20260929063245_contract_redesign_legacy_fields
has a rollback.sql that restores the dropped columns.
The file-level check places each finding's evidence on its page: the
rule's own box, or the line holding its snippet. Check keeps the report
with the resume it ran on and, while the resume is unchanged, outlines
each finding on the page with a pin that opens the full report.
Two-column templates take a sidebarSide from the layout (left or right).
Without one, each template keeps its own side and right-to-left pages
mirror it. The PDF templates reverse only their columns, header band
and sidebar background, and the DOCX export follows the same choice.
Design's Sidebar panel offers Left and Right.
On phones the formatting toolbar follows the visual viewport and sits on
the keyboard, 44px tall, with Improve and Done. Done closes the keyboard.
Larger screens keep the toolbar inside the field.
Work around more Forme 0.25 layout defects in the converter: row-reverse
laid out as row, min and max widths ignored along a row, text dropping
view children, empty text taking a line, overflowing optimal line
breaks, a text in a splitting row wrecking later pages, page breaks on
row items, four equal border sides, percentage corner radii, pictures
under their shadow, and imported table rows. Rotation is left out while
Forme misplaces rotated boxes; the stylesheet editor warns about it and
about percentage padding.
The page map rebuilds blocks Forme drops from its layout, the browser
renderer rejects documents whose fonts can't load so callers fall back
to the server PDF, and every render gets fresh font entries. Glalie's
sidebar is one band at the combined tint.
Tests are re-baselined where they encoded react-pdf metrics, and engine
limits (list marker keep-together, RTL line order, characters above
U+FFFF, percentage padding) are expected failures. The plan logs the
migration in section 13.
The web preview, downloads, template gallery, server export and public
PDF render through Forme. react-pdf, react-pdf-html, the react-pdf
hyphenation package, the Phosphor react-pdf icons and the four patches
are gone. Hyphenation now follows the page language for every language
Forme has patterns for.
Semantic CSS keeps its language; declarations Forme can't draw raise an
ENGINE_UNSUPPORTED warning in the editor. The page map rebuilds blocks
Forme leaves out of its layout when they break across pages, and a
render that misplaces a box is repeated with nested rows kept whole.
Tests move to a small shim with the react-pdf calls they were written
against; tests of react-pdf internals are dropped. Forme limits are
recorded as expected failures (RTL line order, characters above
U+FFFF).
Templates keep their react-pdf-style primitives. A small React renderer
records what they draw, and the result is converted to a Forme document:
styles, fonts (with ligatures disabled for text extraction), Phosphor
icons as SVG paths, pictures fitted by object-fit, and the page map from
source locations. Workarounds for Forme 0.25 layout defects live in the
converter: opaque translucent colours, border insets, row splitting,
column gaps across pages, absolute positioning and repeated backgrounds.
Consumers, the Semantic CSS adapter and the old react-pdf dependencies
follow in later commits.
On coarse pointers, buttons, switches, checkboxes and tabs get an invisible hit area of at least 44x44 centred on them, without changing how they look. A checked switch row's description steps up to ink-2, since ink-3 falls short of 4.5:1 on the accent tint. Adds contrastOnWhite to the colour utilities.
Auth pages, dialogs, the command palette, the user menu, Applications, the rich-text toolbar, the stylesheet editor and the error screens use the Material Symbols set instead of Phosphor; the icon subset gains the 35 glyphs they need. Phosphor stays for brand logos, the landing page and the icons printed on resumes. Also removes the form field and test mock that only wrapped the old rich input.
Every class that used the old names (muted, primary, card, border, input and the rest) now uses the Desk & Paper token it resolved to, with text colours on their text tokens (accent-text, danger-text, ink-3). The compatibility block in the theme is gone.
Attachment, bubble, empty, marker, message, message scroller, questionnaire, resizable and sidebar had no users after the assistant moved into the editor, and neither did react-resizable-panels or @shadcn/react.
The assistant opens beside the resume or letter it works on: a third column
at 1280px and wider, in place of the left panel from 1024px, a drawer below
that, and full screen on phones. The bar's button and Mod+J toggle it.
The panel sets up a provider in place, suggests what to ask, streams replies
with Stop and Continue, asks clarifying questions, and shows proposed edits as
change sets with page marks, the outline's "n proposed" pill and the page
caption. Context chips decide whether the next message shares the document and
the posting. Errors keep the message with Retry and Switch model, past
conversations are grouped by document with their outcomes, and the
conversation can be copied as a transcript.
Improve in the rich-text toolbar suggests a stronger verb, a result, a shorter
line or the user's own request for the line holding the caret, and replaces it
only on Replace.
The Agents pages, the builder's assistant sheet and the application copilot
panel are removed; /agent links redirect to the document with the assistant
open. Command palette Ask, Job match's missing terms, Applications' Prepare for
next step and Copy for a job all open the assistant on the right document.
Removes the unused react-resizable-panels and @shadcn/helpers dependencies.
Threads belong to a resume or a cover letter (agent_threads.cover_letter_id)
and count the edits they proposed and the user accepted. The propose_edits
tool rewrites or adds passages by id, resolved against the document as it is
now, and the edit statuses are stored with the message. read_letter joins
read_resume; apply_resume_patch, approvals, revert and archiving are removed.
Message context lets the user leave the document or the linked posting out of
a message; the posting now includes the application's notes. Redis is
optional: without it replies stream directly.
The proposal core (passages, additions, states) moves to
@reactive-resume/resume/proposals so the server and the web app share it.
Adds ai.improve, which suggests a rewrite of one line (stronger verb, a
result, shorter, or the user's own request) and says when it states
something new.
- The page: CSV import/export behind one icon, Add application, a dismissible
nudge for the application waiting longest without a reply (10+ days),
List · Board · Insights · Calendar, search across role, company, contacts
and tags, and Show closed.
- List (the default): grouped by stage in the order that needs you first,
with collapsible groups, the next step (warn when overdue), what was sent,
sorting from the headers and row checkboxes for bulk moves, tags, closing
and deleting. Phones get two-line rows and no board.
- Board: a column per stage; drops and Move to… show the same toast.
- Detail sheet (480 px, full screen on phones): the stage stepper with Move to
next, the next step (the next interview or follow-up) with Edit and Add to
calendar (.ics), what was sent (opening the version sent, read-only, in
History), Tailor a resume and Write a letter, editable salary and source,
contacts, tags, autosaved notes and the activity timeline. Close
application… takes a reason; Delete is in ⋯ and asks first.
- Add dialog: paste a link or posting; its role, company and requirements are
read into editable fields and the posting is saved with the application.
Add, or Add and tailor a resume.
- Insights: how far applications get (from their stage history), how many
heard back and how fast, and tailored against base resumes, above the
existing charts.
- CSV import shows how columns were matched before saving and lets you
download the rows it skips; export adds the closed reason.
- The builder opens History on a version from ?version=.
- Applications end in a `closed` stage with a reason (not selected, withdrew,
accepted another offer, no response). The migration moves `rejected` to
closed + not selected and archived applications to closed, rewrites
`rejected` in their stage history, and ships rollback.sql for older
versions. `archived` stays, deprecated; `rejected` is still accepted as
input and means closed.
- Once an application with a linked resume reaches Applied, the resume is
saved as a "sent" version named after the company, and the application keeps
its id and the resume's Check score then.
- New columns: closed_reason, cover_letter_id (backfilled where exactly one
letter was written for the application), sent_resume_version_id,
sent_check_score and requirements.
- applications.ai.parsePosting reads a pasted link or posting. Links are
fetched on the server: https only, public addresses checked at connect time,
three redirects, 2 MB and 10 s at most. A page's JobPosting data fills the
fields without AI; with a provider, the model reads role, company,
location, salary and requirements.
- MCP application tools take closedReason and coverLetterId.
- ai.atsReview takes optional passages (id, where, text). A suggestion that
rewrites one names it in passageId with the whole new passage, which the
editor offers as a proposal to accept or reject.
- The review prompt fills its placeholders in one pass, so resume text that
looks like a placeholder is sent as it is.
- resume.getById returns applicationId, the job application a resume was made
for, so Check's job match can read its posting.