Icon renders Material Symbols Rounded at weight 300 from a subset font
that holds only the glyphs listed in packages/ui/src/icons/names.ts.
pnpm icons:build checks every name against Google's codepoints and
regenerates the font; a test keeps the manifest and the list in sync.
Icons are aria-hidden and untranslated, and directional ones mirror in
right-to-left layouts.
* feat(applications): schedule interviews and view them on a calendar
Interviews (screening, technical, behavioral, onsite, other) are stored as
"interview" entries on an application's activity timeline, so an application
can have any number of them and they show in its timeline without a
migration. Each interview has a start date-time (timezone-aware), duration,
and optional location and notes.
- schema: interview timeline entry type, interviewDetailsSchema, INTERVIEW_KINDS
- api: addInterview / updateInterview procedures (delete via timeline entry);
generic timeline updates now only edit text on note entries
- web: Calendar view on the Applications page (month grid, type legend,
upcoming list grouped by day, schedule button with application picker,
per-day "+" and "+N more" popover), Interviews section and interview
dialog in the application detail panel, interview rows in the timeline
and CSV export
- mcp: add_application_interview / update_application_interview tools
- i18n: extract new strings into all locale catalogs (English fallback)
- docs: MCP tool table, scheduling guide section, resume-builder skill
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(applications): keep interview dates in local time and guard generic timeline edits
- Format the timeline date chip for interview entries in the viewer's local
timezone so it matches the interview's date-time label; stage and note
entries still render in UTC.
- Reject interview entries in the generic updateTimelineEntry path. A
day-granular date edit kept the UTC time of day and could move an interview
to the wrong local day; callers are pointed to updateInterview
(update_application_interview). The MCP tool description now says so, and
a service test covers the rejection.
- Associate each interview dialog label with its control via useId/htmlFor.
- Drop the duplicate onInput handler on the date-time input; onChange covers
controlled inputs.
- Give the calendar's per-day schedule button an accessible name that
includes the date, and update the extracted locale catalogs for the new
message.
* [autofix.ci] apply automated fixes
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Audit every animation in the app and shared UI primitives against a
frequency-first motion bar: keyboard and high-frequency actions no longer
animate, remaining motion uses interruptible CSS transitions with shared
easing tokens, and redundant animation code is removed.
UI primitives (@reactive-resume/ui)
- Dialog, alert dialog, popover and tooltip move from tw-animate keyframes
to Base UI data-starting/ending-style transitions; menus, popovers and
tooltips skip motion when opened from the keyboard (data-instant).
- Dialog gains an `instant` prop; the command palette uses it.
- Accordion animates its real panel height; caret rotates instead of
swapping icons.
- Menu backdrop blur moves onto the popup so it no longer snaps in after
the fade; context menus and comboboxes fade only.
- Sidebar collapse uses the strong ease-out curve and snaps on Cmd+B.
- Toast, sheet, checkbox, tabs, toggle, inputs and message scroller get
tokenised easing, correct transition properties and press feedback.
- Tabs no longer squeeze a trigger narrower than its label.
- Spinners keep spinning under prefers-reduced-motion.
Web app
- Remove the default route view transition and page-entrance slides.
- Add EASE_OUT_STRONG for Motion; replace built-in "easeOut" everywhere.
- Switch LazyMotion to domMax so layout and Reorder animations run.
- Builder: consolidate 12 section list files into one ItemsSection,
opacity-only list rows with popLayout, instant Cmd+0, faster dock zoom,
crossfade that no longer dips, transform-based progress bars.
- Dashboard: keep previous results while sorting/filtering, no empty-state
flash, uncontrolled sidebar (no network round trip on collapse), calmer
resume card tilt, no stacked hover wrappers.
- Settings: drop entrance/stagger wrappers and ActionButton.
- Agent: CSS marquee paused on hover; thread switches keep the layout.
- Homepage: fix invalid transition declarations, CSS spotlight drift,
scroll-hiding header without a JS spring, tokenised curves.
- Theme switches change every color at once.
- Remove SSR-only useIsClient guards from the SPA.
Docs: rewrite the DESIGN.md animation section around the new tokens.
- Fix Grid/Compact/List tab overlap on the resumes dashboard: the fixed
three-column grid forced cells narrower than their labels, so tab content
spilled into neighboring cells.
- Replace the "Resume styling" resume picker with a template picker in the
cover-letter create form and editor. The API accepts a `template` on create
and update, and refreshing style from a resume no longer overwrites it. The
resume control remains in the editor as "Sender details" since it is the
only source for the letter header.
- Remove the cover-letter library button from the builder sidebar and add an
"Import from library" option to the create-cover-letter dialog. Resume to
library copying stays in the library with its own resume picker.
- Remove the authored-pages/PDF-overflow note from the layout sidebar.
* feat(import): parse a PDF resume without an AI provider
Importing a PDF required a connected AI provider, so anyone without a
paid API key could only import the three JSON formats. Almost nobody
arrives with one of those files; they arrive with a PDF. The first thing
a new user tries to do was blocked behind bringing their own key.
Adds a deterministic parser that reads the text out of the PDF in the
browser and prefills the builder. It pulls the contact block, segments
the body on conventional headings, and maps entries to real items,
reusing the ATS period parser for dates so a date range is not mistaken
for a phone number.
Nothing is thrown away: header parts that do not map to a field go into
the description, and unrecognized headings become custom sections. The
imported sections are placed on the page so the result renders straight
away. Output is validated against the resume schema before it is
returned.
Text extraction groups items by baseline rather than trusting hasEOL,
and turns wide column gaps into a double space, which is what lets a
row split into company, position and location.
The AI path still runs when a provider is connected. Word import is
unchanged and still requires one.
Closes#3334
* fix(import): keep every section and entry the PDF actually contains
Review found three ways the parser lost or mangled content, all of them
reproducible.
A document whose first heading was not one of the known aliases never
started a section, because unknown-heading detection was gated on a
section already being open. Everything after it was swallowed as contact
header text. The header block is now bounded by where the contact
details stop, so a heading is recognized wherever it appears.
An entry spreading company, position and dates over three lines was
imported as two malformed items. A line that introduces an entry now
merges into the open entry instead of starting a second one.
An uppercase company such as ACME CORPORATION was read as a section
heading and fragmented the entry. A heading candidate followed by a date
line is now treated as an entry header, which is what it is.
Also escape single quotes, and construct the PDF worker inside the try
so the nested worker is terminated even if construction throws.
Title-case headings are deliberately still not treated as headings:
company and school names are title case too, and splitting on them would
fragment real entries. Such a section stays in the preceding one with its
text intact rather than risking loss.
* fix(import): look past a multi-line preamble before calling a line a heading
The previous guard only inspected the next line, so an uppercase company
followed by a separate role line and then the dates was still read as a
section heading. The experience or education entry was moved into a
custom section and lost.
Heading detection now scans a two-line window for the date that marks an
entry, and stops early at a bullet so a genuine heading whose section
opens with bullet points is still recognized.
The window can suppress a real heading whose first entry puts a bare date
two lines below it. That is the deliberate direction to fail in: a missed
heading leaves the text in the preceding section, while a misread entry
fragments structured content.
* fix(import): collect an entry preamble until its dates appear
An entry that spread company, role, location and dates over four lines
was imported as two broken items: the company with no dates, and the
location carrying the period.
The cause was in entry grouping rather than heading detection. Lines
before a date were only folded into the entry header when the date sat
on the very next line; anything earlier fell through to the description.
Preamble lines are now collected into the entry header until the dates
turn up, bounded by the same lookahead and stopping at a bullet, so an
undated section cannot swallow itself.
The heading lookahead widens to four lines to match, which is the
realistic maximum for company, role, location and dates.
* fix(import): harden local PDF resume parsing
* chore(import): document audited HTML construction
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* feat(applications): export applications as CSV
* fix(applications): strip export CSV formula guard on import and resort catalogs
Re-importing an exported CSV kept the apostrophe that csvCell prepends to
formula-triggering cells, so a note starting with "- " came back as "'- ".
mapCsvToApplications now drops a leading apostrophe when the remainder would
have been guarded, sharing the predicate with csvCell so both sides stay in
sync.
Also runs pnpm lingui:extract: the new msgids were hand-appended to en-US.po
and missing from the other 54 catalogs.
* fix(applications): preserve CSV import values
* fix(stylesheet): keep color picker state aligned with source
* fix(stylesheet): serialize picker edits as hex with alpha
* fix: preserve contextual colors in stylesheet editor
Rewrites the landing page, in-app microcopy, and public docs, then fixes what the rewrite exposed: stale template counts, a broken quickstart anchor, out-of-sync FAQ structured data, dead error-hint branches in the MCP tools, and wrong-sense translations across all 53 locales. Adds GLOSSARY.md so translators get the right sense of the ambiguous UI terms.
* feat(ats): add ATS checker and replace resume analysis
Adds a public, browser-only ATS checker at /ats-checker and an ATS Check
section in the builder's right sidebar. PDFs are parsed locally: text
extraction, reading order, contact and date recovery, section detection,
and file-level readability are scored deterministically, with evidence
cited per finding and skipped checks reported rather than counted as
passes.
Removes the AI-scored resume analysis it supersedes: the resume_analysis
table (dropped via migration), the get_resume_analysis MCP tool, and
POST /ai/analyze-resume. The replacement, POST /ai/ats-review, reviews
extracted resume text and returns qualitative feedback with no score.
Also bumps the version to 5.2.9 and adds the changelog entry.
* chore(deps): bump workspace dependencies
* fix(ats-checker): keep negation inside each 'what this does not do' bullet
The three bullets were bare fragments whose negation came from the
section heading, which translators never see. A dozen locales rendered
them as affirmative assertions or imperatives, so the page claimed the
checker enforces a one-page rule and predicts rejection -- the opposite
of the source, and directly contradicted by the sentence beside it.
Each bullet now carries its own negation, so the polarity cannot be
lost in translation. Re-extracted and refilled across all 53 target
locales.
Upgrades to Better Auth 1.7, expands Custom Styles coverage of item headers, and adds a human-approval step to the AI agent's resume edits.
Breaking for self-hosters using a custom OAuth provider: the callback path changes from /api/auth/oauth2/callback/custom to /api/auth/callback/custom, and installs using OAUTH_DISCOVERY_URL need one additional UPDATE after upgrading. Both are documented in docs/self-hosting/sso.mdx.
- Better Auth 1.7, with the account issuer migration and the jwks alg/crv columns the 1.7 jwt plugin requires
- Agent edits gated behind an approval step, with crash-safe runs and context pruning
- item-header now covers every section header row on every template; adds the item-header-row part
- Fixes provider unlinking, auth error messages, and version conflicts on freshly created resumes
- New /auth/error page, translated across all 53 target locales
- DeepSeek Harness plugin moved into packages/dsh-plugin
- Dependency bumps across the workspace
Fetching an arbitrary job URL server side meant owning SSRF defence, redirect
and size limits, and per-site scraping quirks. The autofill tool now takes only
pasted text, so the URL input, the fetch path and its MCP annotation are gone.
The sheet gates the call behind a tested AI provider and a minimum paste length
so a stray snippet does not spend an AI call.
The page now holds account-level actions rather than only destructive ones, so
it is reachable at /dashboard/settings/account and presented with a neutral
icon in the sidebar and command palette.
Swaps sonner's toast.success/error/loading/dismiss for the new toast.add({ type,
description }) and toast.close across dialogs, auth pages, the builder, the
dashboard and the applications views. Behaviour is unchanged.
The case passed cursor position 5 into "--resume-", which lands mid-token and
reads as a selector context, so it received the selector list. Every other case
in the file uses source.length.