* fix: use language-specific Noto fonts for CJK PDF fallback
* feat: extend fallback to Arabic/Hebrew/Thai
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix: use non-empty placeholder for redacted resume name
* fix: update stale test title to match new placeholder behavior
---------
Co-authored-by: Albano <alumno26.fazzito.albano@ipm.edu.ar>
Some providers (notably Anthropic via proxies) wrap JSON output in
markdown code fences (```json ... ```), causing Output.object to
throw NoObjectGeneratedError / JSONParseError.
Replace Output.object with manual JSON boundary extraction that works
regardless of fencing. Also propagate the original AISDKError as cause
in throwAiProviderGatewayError for better diagnostics.
* feat(editor): add multicolor highlight with auto-contrast text
Enable the Tiptap Highlight extension in multicolor mode, replacing the
single-color yellow toggle with a full color picker (16 presets + custom).
When the chosen highlight color is perceptually dark, text inside the mark
automatically renders white for readability.
Changes span the full pipeline:
- Editor: ColorPicker UI, extended renderHTML for contrast detection
- PDF: normalizeMarkElements preserves data-color as inline style
- DOCX: mergeStyle reads actual background-color from <mark>
- Utils: new isDarkColor() luminance helper
Backward-compatible: legacy <mark> without data-color still renders yellow.
Resolves#3109
* fix: handle multicolor highlight edge cases
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* feat: add section heading icons to PDF templates
Add customizable Phosphor icons before section titles in PDF output.
Users can toggle visibility globally via a new "Hide section heading icons"
switch (independent of item-level icons) and customize individual section
icons through the builder sidebar icon picker.
- Add `icon` field to `baseSectionSchema` and `summarySchema`
- Add `hideSectionIcons` to `pageSchema` (defaults to true for backward compat)
- Implement `SectionHeadingIcon` component with heading font-size scaling
- Support "none" sentinel for per-section icon hiding
- Fallback to sensible defaults (briefcase, graduation-cap, etc.) for legacy data
- Add icon picker to builder sidebar sections and custom section dialogs
Closes#2632
* test: add unit tests for section heading icons
- Add tests for getResumeSectionIcon() covering built-in sections,
summary, custom sections, "none" sentinel, and default fallbacks
- Add schema tests for baseSectionSchema icon field, summarySchema icon,
and pageSchema hideSectionIcons default behavior
* refactor: minor updates to icon display
* Update apps/web/locales/es-ES.po
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* fix(pdf): apply custom style fontSize to icon and level indicator sizes
Map fontSize from Icon and Level Indicator custom style slots to Phosphor
icon size and level indicator dimensions, since react-pdf icons ignore
fontSize in favor of the size prop.
* fix: separate global icon and scoped level indicator font sizes
Icon slot fontSize now drives all resume icons plus level display
decorations. Level indicator fontSize overrides only within level display.
Shared sizing logic lives in schema; design sidebar preview uses global rules.
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Remove attachments and soft-delete the thread before storage cleanup so
partial failures do not leave inconsistent DB state. Log storage errors
without failing the request after the thread is marked deleted.
Co-authored-by: Cursor <cursoragent@cursor.com>
Re-export isRTL from @reactive-resume/utils/locale in the web locale
helper and consolidate RTL detection tests in the utils package.
Co-authored-by: Cursor <cursoragent@cursor.com>
Introduce createRtlStyleHelpers and a single rtl flag on RenderProvider,
migrate every template page to mirrored layout styles, and rename
alignRight to alignEnd. Fix plain rich text rendering via PdfText
paragraph renderers and map legacy Times New Roman to Times-Roman.
Co-authored-by: Cursor <cursoragent@cursor.com>
The CJK fallback (Noto Sans SC / Noto Serif SC) was only registered at
weight 400. When react-pdf rendered CJK characters with font-weight 700
(e.g. <strong> from a rich-text section, or templates' bold style), it
walked the font-family stack [primary, cjkFallback], failed on the
primary (no CJK glyphs), then fell back to the only registered fallback
variant (400) — and react-pdf does not synthesize bold. The bold style
was silently dropped for CJK runs in both the live preview and the
exported PDF, while still working for Latin runs.
Register the CJK fallback at the same weight range as the primary font
(lowest + highest, both styles). When body and heading share the same
fallback (the common case where both are sans or both are serif), merge
their weight ranges so each weight is registered exactly once.
webfontlist.json already ships all weights for the default CJK
fallbacks, so no font-list changes are required.
Closes#3079
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix(pdf): align textkit line-box and font metrics to browser behaviour
CJK characters in resumes with a tightened typography line-height
(< ~1.4) had their descenders clipped by the next line. Latin glyphs
in the same resume rendered fine. Fixes the visual regression vs the
v5.0.x Puppeteer-based renderer reported in issue #2986 and follow-ups.
The clipping is caused by two independent gaps in @react-pdf/textkit
relative to standard CSS line-box rules:
1. `height(run)` short-circuits to the user-supplied lineHeight and
ignores the run's intrinsic ascent + descent. CSS line-boxes are
spec'd as `max(line-height, content-area)` — when CJK glyphs are
present the content-area is taller than a tightened lineHeight, so
the box must grow. textkit didn't, so the baseline (computed from
the real, larger CJK ascent) sat below the box and the descender
bled into the next line.
2. `ascent / descent / lineGap` are read directly from fontkit's hhea
defaults. For Source Han Sans/Serif (the CJK fallbacks registered
in #3013) hhea is intentionally inflated for legacy Windows GDI
compatibility (1.45 em vs 1.0 em), so even a fixed line-box would
have been excessively tall. Browsers (and the v5.0.x Puppeteer
renderer) read OS/2 sTypoAscender/Descender/LineGap instead, which
are the values the type designers intend for modern shaping.
Both are upstream behaviours of `@react-pdf/textkit`, but waiting for
an upstream release would leave existing users with broken CJK output.
The fix is shipped as a pnpm patch (~30 LOC):
- `resolveTypoMetrics(font)`: prefer OS/2 typo metrics, fall back to
hhea when an OS/2 table is absent (e.g. the StandardFont stand-ins
for Helvetica/Courier/Times). Used by ascent/descent/lineGap so all
height-related calculations stay consistent.
- `height(run)`: `Math.max(lineHeight || 0, intrinsic)` instead of
the original short-circuit, matching CSS line-box rules.
The patch is self-contained: existing Latin-only resumes are
unaffected (IBM Plex Serif's typo metrics equal hhea; Roboto's typo
is slightly smaller, but only changes the rendered line-box for users
who set lineHeight below ~1.17, which already used to clip ascenders
under v5.1.x and now lays out as it would in a browser).
Tooling notes:
- `Dockerfile.dev` copies `patches/` before `pnpm install` so the
dev image build no longer fails on `--frozen-lockfile`. The
production `Dockerfile` already gets it for free via
`turbo prune --docker` (the patch reference in package.json marks
the directory as part of the pruned slice).
- The patch will become a no-op once an equivalent fix lands upstream
in @react-pdf/textkit; the entry can then be removed from
`pnpm.patchedDependencies` and the file deleted.
* fix(deps): regenerate lockfile and move patchedDependencies for pnpm 11
The previous commit's lockfile was authored by pnpm 8 (lockfileVersion 6.0)
and kept patchedDependencies under package.json#pnpm. The repository now
declares packageManager: pnpm@11.1.2, which:
- writes lockfileVersion 9.0 and rejects v6 with ERR_PNPM_LOCKFILE_BREAKING_CHANGE
on --frozen-lockfile (CI failure observed in autofix.ci);
- reads pnpm settings from pnpm-workspace.yaml, silently ignoring the
package.json#pnpm field — so the textkit patch was no longer applied.
Regenerate pnpm-lock.yaml with pnpm 11.1.2 and move patchedDependencies
to pnpm-workspace.yaml so the patch is applied and CI passes.
* chore: update dependencies
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* chore(ai): remove local AI store now that providers live server-side
The Zustand-based useAIStore has been replaced by the server-side
aiProviders oRPC router (encrypted credentials persisted in DB).
Delete the dead store + tests, drop the ./store export, and remove
zustand/immer deps which are no longer referenced anywhere in
packages/ai/src/.
* feat(agent): archive/delete actions and read-only state for agent threads
- Backend: mark archived threads as read-only in threads.get and reject
messages.send with CONFLICT when the thread is archived.
- Frontend: render archived threads in the sidebar with muted styling and
an Archived badge; add a per-thread dropdown menu in the chat header
with Archive (non-destructive) and Delete (with confirmation); show a
read-only banner above the message list that disambiguates archived
vs. missing-resource causes; suppress the Retry and Stop buttons in
read-only mode.
- Tests: new packages/api/src/services/agent.test.ts covering the
archived-thread isReadOnly flag and the archived-thread send refusal.
* fix(agent): abort run on archive and verify ownership before deleting thread
- threads.archive: before flipping status, abort any in-flight run controller
and clear the active-run state on the thread; cleanup failures are logged
but do not block the status update.
- threads.delete: assert thread ownership via getThread before destructive
work so an authenticated user cannot wipe another user's attachment rows
by passing a foreign threadId.
Adds focused tests for both behaviors.
* feat(agent): display patch diffs and surface revert conflicts
Render apply_resume_patch tool messages with a status-aware card (applied/
reverted/conflicted), expandable operation list, and a Revert button that
correctly handles RESUME_VERSION_CONFLICT responses. Adds unit tests for
the inverse-patch builder and the agentService.actions.revert flow.
* chore(agent): remove out-of-scope attachment tests accidentally added in Task 6
The Task 6 commit (73ef1acca) accidentally re-introduced three attachment-
related tests that belong to a separate task:
- `buildAttachmentModelParts > converts text, image, supported binary, and
unsupported attachments into model parts`
- `agentService.messages.send > persists the user message with file UI parts
and links selected attachments to it` (was failing — the `ToolLoopAgent`
mock is not callable as a constructor)
- `agentService.messages.send > rejects attachments that are missing, foreign,
or already linked before persisting a message`
These were likely re-added during a stash recovery and were not requested
for Task 6, whose scope was limited to the `agentService.actions.revert`
flow. Remove them along with the helpers/fixtures (`buildAttachment`,
`buildActiveThread`, `selectWhereResult`, `selectOrderByResult`) that they
were the only consumers of. `selectLimitResult` is preserved because it is
used by the revert tests.
* chore(agent): configure runtime dependencies
* feat(db): add agent workspace schema
* feat(api): add agent backend services
* feat(web): add agent workspace UI
* chore(agent): remove legacy builder assistant
* test(agent): make agent stream mocks constructible
* chore(web): remove unused resume replacement hook
* feat(api): add unsafe AI base URL flag
* chore(dev): expose local services in compose
* fix(web): normalize resume preview gaps
* feat(api): improve agent tool handling
* feat(web): polish agent workspace UI
* chore: update dependencies
* fix(api,web): address PR review feedback for agent workspace
Security/correctness:
- Restrict AI provider URLs to http/https even in unsafe mode
- Stop exposing Redis on host network by default
- Make .env.local optional and drop app profile in compose.dev.yml
- Store agent attachments with private ACL on S3
- Reset provider test status when provider/model/baseURL changes
- Decouple non-agent AI endpoints from REDIS_URL requirement
- Fix JSON Patch add inverse for existing object members
- Wrap resume patch + agent action insert in db transaction
- Validate partialMessage at runtime and rate-limit attachment uploads
- Add unique index on agent_messages (thread_id, sequence)
UX/bugs:
- Mark agent thread route as ssr: false and guard SSE chunk parsing
- Show config-specific banner only on known configuration error
- Gate AI provider checks behind loading state in resume import
- Fix relative-time formatter blank gap between 45-59 seconds
- Clarify thread delete confirmation message
Polish:
- Raise ENCRYPTION_SECRET minimum to 32 characters
- Bucket AI rate limits by resumeId/threadId/messageId
- Trim form values before submitting AI provider config
- Use single key identifier and nullish-coalesce baseURL display
* fix: address ai agent review feedback
* fix: preserve mobile agent chat state
* docs: add ai agent workspace guides
* feat: introduce design system for Reactive Resume
* fix(fonts): restore legacy local font names via metric-compatible aliases
Closes#2989.
In v5.0.x the Puppeteer renderer resolved fonts like 'Times New Roman'
or 'Arial' through the browser's font stack. The v5.1 migration to
@react-pdf/renderer requires every font to be Font.register()-ed; the
legacy local-font names were not carried over, so resumes upgraded
from v5.0.x had their typography silently replaced with IBM Plex Serif,
changing line breaks, page counts and overall layout.
This adds a render-time alias layer mapping the old names to
metric-compatible web fonts already shipped in the webfont list:
Times New Roman → Tinos
Cambria → Tinos
Arial → Arimo
Garamond → EB Garamond
Calibri → Source Sans 3
- packages/fonts:
- new `legacyFontAliases` map and `resolveLegacyFontAlias` helper.
- `getFont` falls back to the alias map when the direct lookup misses,
so any caller that asked 'is this a known family?' now answers
truthfully for the legacy names.
- `getFontDisplayName` is intentionally unchanged: the typography
sidebar keeps showing the user's original choice ('Times New Roman'),
while the renderer transparently swaps in the alias target.
- packages/pdf/use-register-fonts:
- `resolvePdfFontFamily` returns the alias target when one applies,
so `Font.register` runs against the right web font and templates
receive a family name they can actually render.
Backwards compatible: families that were never aliased (Roboto, IBM
Plex Serif, the standard PDF fonts, ...) take exactly the same code
path as before. The CJK glyph fallback added in #2986 / PR #3013
continues to apply on top of the resolved primary family.
* fix(fonts): use Carlito (not Source Sans 3) as Calibri alias
Per maintainer review feedback: Carlito is metric-compatible with
Calibri, while Source Sans 3 only matches visually. Switching gives
upgraded resumes the same line widths, line breaks and page counts
they had under v5.0.x.
- packages/fonts/webfontlist.json: add Carlito (Google Fonts, weights
400/700 + italics) so it's a registerable target.
- packages/scripts/fonts/generate.ts: add a getMetricCompatibleFonts
helper and merge it into the output, mirroring how Computer Modern
fonts are appended. This way regenerating the list (`pnpm generate`)
re-emits Carlito automatically and dedupes if it ever enters the
Google Fonts popularity slice.
- packages/fonts/src/index.ts: alias `Calibri → Carlito`.
- packages/fonts/src/index.test.ts: update alias test cases.
* fix(web): use native pdf viewer for public resumes
* fix(web): render public resumes with pdf.js
* chore: revert vite hook paths
* chore(web): address pdf viewer review
All Heading elements apply overflow:hidden via safeTextStyle in primitives.tsx.
At 1.5× heading font size with lineHeight 1.2, the line box is too tight to
fully render descenders (g, p, y, etc.) in the resume header name field,
causing them to appear visually cut off.
Raising headerNameLineHeight from 1.2 to 1.3 adds enough vertical room for
descenders across all 13 templates that share this constant.
Fixes#3042
* test(web): add tests for zustand stores and pure helpers
Cover stores and pure helpers across the builder/dashboard/command-palette
surfaces that previously had 0% coverage:
- command-palette store (open/close, page stack, search clearing, goBack)
- builder assistant-store
- builder sidebar store + parseBuilderLayoutCookie / mapPanelLayoutToBuilderLayout
- builder section store (collapse, toggle, toggleAll)
- builder preview page-layout toggle
- dashboard resume-thumbnail render-size math + cache key
- MCP tool name + annotations invariants
* test(web): cover MCP helpers and template metadata
Add tests for previously 0%-coverage MCP and dialog helpers:
- buildMcpServerCard: server-info, tool catalog vs MCP_TOOL_NAME, prompts,
resource templates, configuration schema, auth schemes
- registerPrompts (build/improve/review): registration, args schema, resource
context with interpolated resume id, read-only / no-fabrication directives
- registerResources (resume://{id}, resume://_meta/schema): handler reads via
oRPC client, error on missing id, schema returns valid JSON
- templates metadata: ids match display names, valid sidebar positions,
unique image URLs, every entry has tags + description
* test(web): cover sidebar section helpers and layout screens
Add tests for previously near-0%-coverage modules:
- libs/resume/section: getSectionTitle / getSectionIcon return distinct,
exhaustive results for every sidebar section + cover-letter; icon props
forwarding; left/right sidebar collections do not overlap.
- layout/loading-screen: spinner + text render.
- layout/error-screen: error message surfaces, Refresh button triggers reset.
- layout/breakpoint-indicator: default + each corner positioning, all
breakpoint labels rendered, print-hidden class applied.
* test(web): cover preview canvas math and font-weight defaults
Add tests for pure helpers that previously had no direct coverage:
- typography/getNextWeights: prefers 400 + 600 when both are available,
returns null for unknown families, never produces duplicates, bounded
to two weights from the 100..900 set.
- preview.shared/normalizeResumePreviewProps: documented defaults +
pass-through.
- preview.shared/getScaledPreviewPageSize: scaling identity at 1, and
fractional scaling.
- preview.shared/getPreviewCanvasScale: respects 4x desired scale for
small pages, honors high devicePixelRatio, clamps to the 16M-pixel
canvas budget for large pages.
* test(utils): cover DOCX section renderers and html-to-paragraphs
@reactive-resume/utils/resume/docx was previously at ~2.84% statement
coverage despite being load-bearing for the resume DOCX export.
- section-renderers: empty-string / hidden-section / hidden-item branches
for renderSummary, renderBuiltInSection, and renderCustomSection;
cover-letter and summary custom-section dispatch; unknown-type fallback;
setRenderConfig idempotency.
- html-to-docx: whitespace-only short-circuit, multiple top-level blocks,
h1..h6 paragraph mapping, inline style and link rendering, custom
font/size/color/linkColor config, ignored script/comment nodes.
* test: cover DOCX builder smoke paths and reactive-resume JSON importer
- utils/resume/docx/builder: buildDocument runs end-to-end against the
default and sample resume data, both page formats, full-width and
sidebar layouts, and gracefully degrades with unparseable color or
empty font family inputs.
- import/reactive-resume-json: ReactiveResumeJSONImporter validates
malformed JSON, recovers missing built-in sections by appending them
to page 1 without reordering, and preserves layouts that already
contain every built-in section.
* test(import): cover JSONResumeImporter parse/convert
JSONResumeImporter (450 lines) was previously at 0% coverage. Add tests
for the public surface:
- Invalid JSON / invalid-shape errors are surfaced.
- basics, summary, picture, education, projects, skills, profiles all
map to the corresponding ResumeData sections.
- Empty work/education entries (missing key field) are filtered out.
- Highlights become HTML list items in the description field.
- Skill level parsing flows through utils/level.parseLevel.
- formatLocation joins city, region, countryCode with commas.
* test(web): cover query client serializer and home-page animations
- libs/query/client: getQueryClient returns a fresh QueryClient,
queryKeyHashFn produces stable JSON envelopes for matching keys
(and distinct strings for different keys), dehydrate/hydrate
round-trip Date values via the oRPC serializer.
- components/animation/spotlight: overlay container is
pointer-events-none, both beam groups render, custom
width/height/translateY/gradient props flow into inline styles.
- components/animation/comet-card: children mount inside the
perspective wrapper, custom className is merged with the 3D
baseline classes, glare overlay renders, mouse move/leave
handlers do not throw.
* test(web): cover Copyright footer
Verify the footer's MIT license link, Amruth Pillai attribution,
external-tab targets, embedded app version (via __APP_VERSION__ stub),
and custom className merging — previously at 0% coverage.
* test(api): cover flags, auth providers, and resume-access cookies
Unlock @reactive-resume/api by mocking @reactive-resume/env/server and
@tanstack/react-start/server. Previously the only services tested were
the standalone AI test and resume-access-policy.
- services/flags: flagsService.getFlags reads disableSignups/disableEmailAuth
from env (no stale cache).
- services/auth: providers.list always exposes credential + passkey, and
only adds Google/GitHub/LinkedIn/custom when both id and secret are set;
custom provider uses OAUTH_PROVIDER_NAME with a 'Custom OAuth' fallback.
- helpers/resume-access: hasResumeAccess validates against signed cookies
with constant-time comparison; grantResumeAccess writes a 10-minute
httpOnly cookie with the secure flag matching APP_URL's https-ness.
* test: cover statistics service and email transport via env mocks
- api/services/statistics: github star count succeeds, retries on
non-OK, falls back to last-known on fetch error / non-positive /
non-numeric responses; user and resume counts roll up DB count.
- email/src/transport: returns silently with no text/html, logs when
SMTP is not configured, dispatches via nodemailer with the env
config when fully wired, renders react elements to html + text
bodies, swallows transport errors instead of crashing.
* test(api): cover resume-events publish + subscribe
- publishResumeUpdated issues pg_notify with channel and serialized
event payload.
- subscribeResumeUpdated yields events whose resumeId+userId match
the subscription, filters out other resumes/users, ignores
malformed JSON and notifications on other channels, calls
LISTEN/UNLISTEN and releases the client, and terminates
immediately if the abort signal fires before iteration starts.
* test(api): cover oRPC auth resolution
resolveUserFromRequestHeaders is the single point where every oRPC
procedure picks up the authenticated user. Test the priority chain:
- x-api-key wins when present and valid
- on invalid api key, falls back to session via auth.api.getSession
- Bearer JWT in Authorization header is verified via verifyOAuthToken
- invalid Bearer falls back to session
- Authorization scheme other than Bearer is ignored entirely
- thrown errors from token verification are logged and swallowed
(caller still tries session)
- returns null when no auth method succeeds
* test(api): cover storage helpers
inferContentType, isImageFile, processImageForUpload were 0%
coverage despite being on the picture upload path.
- inferContentType maps known image and pdf extensions, is
case-insensitive, ignores path depth, and falls back to
application/octet-stream for unknown.
- isImageFile allows only the upload allowlist (gif/png/jpeg/webp)
and rejects image/svg+xml, application/pdf, and empty strings.
- processImageForUpload short-circuits to the original bytes when
FLAG_DISABLE_IMAGE_PROCESSING is true, otherwise pipes through
sharp and returns image/jpeg.
* test(import): broaden v4 importer section-mapping coverage
The existing v4 importer test focused on a single bug (description-only
custom items) and the skill/language level scaling. This new test
exercises the bulk of the v4 → v5 transformation path:
- basics, picture (with border), summary, customFields
- every section's filter-by-required-field invariant (awards needs
title, certifications needs name, education needs institution,
experience needs company, volunteer needs organization, etc.)
- experience / education / awards / certifications / references field
renames between schemas
- language and skill level scaling (v4 0..10 → v5 0..5)
Brings reactive-resume-v4-json from ~66% statement coverage to a
materially higher figure (the bulk of the 410-line transformer body).
* test: cover buildDocx entry and AI configuration store
- utils/resume/docx/index: buildDocx returns a non-empty Blob for both
default and populated resumes (previously 0% coverage despite being
the public DOCX entry point).
- ai/store: useAIStore preserves verification status across no-op
updates, but resets testStatus + enabled whenever provider, model,
apiKey, or baseURL changes; canEnable is gated to testStatus=success;
setEnabled(true) is refused unless verified; reset clears every
field. Brings @reactive-resume/ai from ~72% to materially higher
coverage.
* test(db): cover resume schema definitions
packages/db was previously at 0% coverage. Smoke-test the public
resume / resume_statistics / resume_analysis tables:
- getTableName matches the SQL identifier used by migrations
- expected columns are present on each table
- defaultResumeData wiring on the data column resolves to a valid
shape
These are structural assertions that catch accidental renames /
removals without needing a live database connection.
* test(db): cover auth schema tables and relations export
- src/schema/auth: table-driven test for each of the 12 auth tables
asserting SQL name and presence of the key columns (user/session/
account/verification/two_factor/passkey/apikey/jwks/oauth_*).
- src/relations: smoke test confirming the relations export is defined.
Brings @reactive-resume/db from 0% to materially higher coverage.
* test(auth): cover getSession isomorphic helper
@reactive-resume/auth was previously at 0% coverage. functions.ts
is the server entry point that other packages call. Mock the auth
config + tanstack/react-start to verify:
- getSession forwards getRequestHeaders() to auth.api.getSession
- returns null when better-auth returns null
* test(web): cover BuilderSidebarEdge
Small presentational component on the builder layout — assert children
mount, left/right positioning class branches, and the sm:flex
mobile-hide behavior.
* test(web): cover section-title-locale resolver cache and hook
The section-title-locale module wraps createSectionTitleResolver with
a per-locale async cache and a React hook for consumers in the
builder. Cover:
- createSectionTitleResolverForLocale returns a usable resolver
- repeated calls for the same locale share a cached promise
- unknown locales fall back through resolveLocale
- useSectionTitleResolver returns null while loading and when no
locale is passed
- the hook resolves to a function once the async loader settles
* test(web): cover BaseCommandGroup page-stack gating
BaseCommandGroup conditionally renders based on the top of the
command-palette page stack. Tests cover:
- root group renders when no sub-page is active
- root group hides when a sub-page is on top
- sub-page group renders only when its page matches
- mismatched sub-page leaves the group hidden
* test(web): cover ThemeProvider context
- useTheme outside ThemeProvider throws the documented error
- useTheme inside ThemeProvider returns the theme + setTheme +
toggleTheme helpers
* test(web): cover ConfirmDialogProvider + useConfirm hook
- useConfirm outside provider throws the documented error
- confirm returns a pending promise
- promise resolves false when the Cancel button is clicked
- promise resolves true when the Confirm button is clicked
- works with custom confirmText label
apps/web has its own copy of this hook distinct from
packages/ui (mirrors the existing UI-package tests).
* test(web): cover PromptDialogProvider + usePrompt hook
- usePrompt outside provider throws the documented error
- returns a function when wrapped
- Cancel click resolves the promise to null
- Confirm click resolves to the current input value
- defaultValue option seeds the initial input value
* test(web): cover DashboardHeader
Small presentational header used across dashboard routes — title h1,
icon rendering, className merge, mobile sidebar trigger present and
hidden on md+.
* test(web): cover Create/Import resume cards
Both cards on the resumes dashboard wire a click handler to open
the appropriate dialog via the dialog store:
- CreateResumeCard opens resume.create
- ImportResumeCard opens resume.import
Also asserts the i18n copy strings (icons aside, the cards are
otherwise structural).
* test(web): cover command-palette language sub-page
LanguageCommandPage is a BaseCommandGroup gated on page='language'.
Tests assert:
- it is hidden when 'language' is not the top of the page stack
- when active, it renders a CommandItem per localeMap entry
- documented locale codes (en-US, de-DE, ja-JP) appear
* test(web): cover command-palette theme + preferences sub-pages
- ThemeCommandPage: hidden when 'theme' is not on top, renders Light
and Dark options when active
- PreferencesCommandGroup: root group renders both Change theme to...
and Change language to... items; clicking each pushes the
corresponding page onto the command-palette stack
* test(ai): cover executePatchResume tool
- patchResumeInputSchema rejects empty operations and unknown op
values; accepts valid replace/add/remove
- executePatchResume returns the applied operations on success
- executePatchResume throws when an operation targets an invalid path
(passes through the underlying applyResumePatches validation)
- multi-op patches against top-level fields succeed end-to-end
* test(ai): cover sanitize edge branches
Hit the previously-uncovered branches in sanitize.ts:
- numeric 1 coerces to true
- '1' / '0' string shorthand coerces to true/false
- missing item.hidden gets salvaged to false
- empty input causes a non-Zod throw (caught + rethrown with generic message)
* test(ai): cover patch-proposal preview + normalize edge cases
- remove operations surface before-value with after=undefined
- buildResumePatchProposalPreview labels metadata/page paths sanely
- normalizeResumePatchProposals stamps every proposal with baseUpdatedAt
- normalizeResumePatchProposals preserves input order
* test(web): cover getLocaleOptions helper
Locale combobox surface — verify the option list mirrors localeMap
shape, uses locale codes as values, populates label + keywords with
the translated display name, and produces unique values.
* test(web): cover LevelTypeCombobox option mapping
LevelTypeCombobox maps levelDesignSchema.shape.type.options through
the internal getLevelTypeName labeler. Assert all 7 level types are
exposed and that each produces a non-empty label.
* test(web): cover ThemeToggleButton fallback paths
- aria-label flips between 'Switch to light theme' and 'Switch to
dark theme' based on current theme
- clicking when document.startViewTransition is unavailable
short-circuits to toggleTheme directly
- prefers-reduced-motion forces the direct toggle path even when
the view-transition API is available
* test(web): cover NotFoundScreen
Mock the TanStack Router Link so the screen renders standalone, then
assert: documented error heading, routeId is surfaced verbatim, and
the Go Back link points to '..' (parent route).
* test(web): cover InformationSectionBuilder
Stub SectionBase so the donation/info section renders standalone.
Assert: donation prompt copy, OpenCollective CTA link, all 5
external resource links present, and external links target _blank
with rel=noopener.
* test(web): cover NotesSectionBuilder
Mock SectionBase, RichInput, and the resume-draft hooks so the
notes section renders in isolation. Assert: privacy hint copy
renders, RichInput is seeded with metadata.notes, and onChange
proxies through updateResumeData with a draft recipe that mutates
metadata.notes.
* test(web): cover TemplateSectionBuilder
Stub SectionBase and useCurrentResume so the right-sidebar template
section renders standalone. Asserts: current template name in the
heading, template tags rendered as badges, preview image points to
the catalog asset, and clicking the preview opens the
resume.template.gallery dialog.
* test(web): cover ColorPicker preset selection and trigger override
Mock the heavy @uiw/react-color-colorful dependency. Test:
- the trigger swatch reflects the controlled value
- clicking a preset color invokes onChange with an rgba() string
- a custom trigger replaces the default swatch when provided
* test(web): cover ExportSectionBuilder
Mock the heavy export pipelines (buildDocx, createResumePdfBlob,
downloadWithAnchor) and the resume-draft hook to test:
- JSON button packages resume.data as application/json and triggers
download with the {name}.json filename
- DOCX button awaits buildDocx and downloads .docx
- PDF button awaits createResumePdfBlob and downloads .pdf
* test(web): cover ProfilesSectionBuilder
Stub the resume-draft hooks, SectionBase, SectionItem, and
SectionAddItemButton so the profiles section renders standalone:
- one SectionItem per profile with network as title and username as
subtitle
- 'Add a new profile' affordance present
- when items.length > 0, the wrapper uses a solid border (not dashed)
* test(web): cover SkillsSectionBuilder
Mirror the profiles test for the skills section — verifies one
SectionItem per skill (name → title, proficiency → subtitle) and
the Add a new skill affordance.
* test(web): bulk-cover 7 left-sidebar section builders
Single test file covers awards, certifications, interests, languages,
publications, references, and volunteer builders. For each:
- one SectionItem rendered with the documented field → title/subtitle
mapping
- awards: title → awarder
- certifications: title → 'issuer • date'
- interests: name → (no subtitle)
- languages: language → fluency
- publications: title → publisher
- references: name → (no subtitle)
- volunteer: organization → location
- the 'Add a new {kind}' affordance with the matching copy
Mocks SectionBase, SectionItem, SectionAddItemButton, and the
resume-draft hooks so each builder renders standalone.
* test(web): cover ProjectsSectionBuilder buildSubtitle
The projects section is the only left-sidebar builder with a
composite subtitle. Tests three branches of its inline
buildSubtitle helper:
- period + website.label → joined with ' • '
- period only → just the period
- empty period + whitespace-only website.label → returns undefined
* test(web): cover Education + Experience section builders
- Education: school → title, degree → subtitle, add-new affordance
- Experience: position → subtitle when set; falls back to '1 role' /
'N roles' (lingui plural) when position empty and roles[] present;
add-new affordance
* test(web): cover CountUp animated number renderer
- default aria attributes (aria-live=polite, aria-atomic=true)
- initial textContent seeds to 'from' (up) or 'to' (down) value
- separator option formats with grouping
- decimal places are preserved when from/to are fractional
- aria-hidden=true strips aria-live + aria-atomic
- custom className is applied to the rendered span
* test(web): cover TextMaskEffect SVG renderer
- supplied text renders in every visible <text> layer
- aria-hidden + aria-label forwarded onto the root svg
- mouse enter/move/leave handlers don't throw
- custom className merges into the svg's class attribute
* test(web): cover URLInput prefix handling
- displayed input strips the https:// prefix so users only edit the
meaningful portion
- editing re-adds the prefix on the way back through onChange
- pre-prefixed input is preserved
- cleared input emits an empty url (no prefix forced)
- hideLabelButton=true removes the popover trigger; default keeps it
* test(web): cover GithubStarsButton
Mocks useQuery + the CountUp animation so the button renders
standalone. Asserts:
- anchor points at the project repo with rel=noopener + target=_blank
- aria-label is the no-count copy when star count is undefined
- CountUp renders only once the count loads
- aria-label includes the localized count once data arrives
* test(web): cover ui/Combobox trigger label rendering
The shared Combobox wraps base-ui's combobox primitives. Smoke-test
the trigger label resolution:
- placeholder shows when nothing is selected
- selected option's label renders in the trigger
- multi-select default values render all labels
- empty options array renders the placeholder without crashing
* test(web): cover IconPicker trigger rendering
Stub react-window's Grid so happy-dom can render the picker without
layout-measurement deps. Verify:
- trigger renders an <i class='ph-{value}'> for the current value
- changing value updates the trigger icon class
- the picker emits a trigger button
* test(web): cover ChipInput add/dedupe/description behaviors
- existing chips render as Badges
- Enter adds the typed value to the chip list
- comma also commits the typed value
- duplicate input is dropped (onChange not called with a longer list)
- empty / whitespace-only input is dropped
- hideDescription removes the keyboard hint <kbd>; default keeps it
* test(web): cover StatisticsSectionBuilder
Mock useQuery + useParams + section-base so the right-sidebar
statistics section renders standalone:
- returns null content while the query is loading
- shows the private-resume hint when isPublic=false
- shows views/downloads counters and labels when isPublic=true
- includes 'Last viewed' timestamp copy when lastViewedAt is set
* test(web): cover TemplateGalleryDialog selection flow
- title + intro copy render
- one tile per template (>= 14)
- currently-selected template tile carries the ring-highlight
- clicking a different tile triggers updateResumeData with a recipe
that sets metadata.template to the chosen template id
* test(web): cover Prefooter home-page section
- community tagline heading renders
- community-thanks paragraph renders
- the decorative TextMaskEffect renders an svg
* test(web): cover home-page Footer
- Resources and Community headings render
- documented resource links (Documentation, Sponsorships, Source
Code, Changelog) all appear in the rendered output
- documented community links (Report an issue, Translations,
Subreddit, Discord) all appear
- social anchors point at GitHub, LinkedIn, and X (Twitter)
- Copyright sub-component surfaces the app version via __APP_VERSION__
* test(web): cover home-page Header navigation
Mock TanStack Router Link + child components so the header renders
standalone. Verify:
- homepage anchor (/ link) carries the documented aria-label
- dashboard anchor points to /dashboard
- ThemeToggleButton and GithubStarsButton both mount
- the <nav> landmark is labeled 'Main navigation'
* chore: fix linter warnings
- Bump @tanstack/react-form version to 1.32.0 in package.json.
- Refactor rich-input component to simplify highlight configuration.
- Improve rich text HTML normalization to handle <mark> elements and apply styles correctly in PDF output.
- Update global CSS for WYSIWYG to adjust paragraph and list margins.
Closes#2986.
Since v5.1.0 the renderer was migrated from Puppeteer to
@react-pdf/renderer. The new pipeline only registers the user-selected
typography family (e.g. Roboto, IBM Plex Serif), which contains no CJK
glyphs, so any Chinese / Japanese / Korean characters in the resume
fall back to .notdef and render as garbled boxes in both the in-app
preview and the exported PDF.
@react-pdf/renderer's textkit layer already supports per-codepoint
font substitution when a Text node is styled with `fontFamily` as a
string array — but only if every family in the stack has been
registered via Font.register. This change wires that up:
- packages/fonts: new `getPdfCjkFallbackFontFamily(family)` returns
Noto Sans SC / Noto Serif SC depending on whether the primary font
is sans-serif or serif, and `null` when no fallback is needed
(standard PDF font, or primary already is the fallback). Source Han
Sans/Serif SC covers all CJK-Unified ideographs, so a single font
transparently handles Simplified/Traditional Chinese, Japanese
kanji and Korean hanja.
- packages/pdf/hooks/use-register-fonts: after registering the
primary body/heading fonts as before, additionally register the
resolved CJK fallback (regular weight only — substitution is
per-codepoint, not per-weight, so one face is enough). The
function's return type is widened to a new `PdfTypography` whose
`body.fontFamily` and `heading.fontFamily` become
`[primary, cjkFallback]` two-element stacks.
- packages/pdf/document: cast the widened typography back through the
schema-typed `ResumeData` so the wider runtime value reaches
templates without changing the public `Typography` schema. All 15
templates already consume `metadata.typography.body.fontFamily`
directly, and `StyleSheet.fontFamily` accepts both string and
string[], so no template edits are required.
Latin-only resumes are unaffected:
- `getPdfCjkFallbackFontFamily` returns `null` for standard PDF fonts
and existing CJK selections, so the extra Font.register call is
skipped.
- When no fallback applies, `registerFonts` returns the original
typography reference unchanged (zero allocation).
- Even when the fallback is registered, textkit only consults it for
codepoints the primary font cannot render, so Latin glyphs still
come from the user-selected font with identical metrics.
The redactResumeForViewer helper intentionally blanks the dashboard
title for non-owner viewers (the title can leak owner-only context like
"Senior Eng @ Foo - final draft"), but the getBySlug output schema
inherits name.min(1) from resumeSchema. Zod rejects the redacted
payload, oRPC throws Output validation failed, and every public resume
URL returns HTTP 500.
Relax the constraint to z.string() on the getBySlug output only -
ownership-gated procedures (getById, update, patch, list, ...) keep
min(1).
Fixes#3011
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix: resolve local data directory to /app/data in production Docker
In the official Docker image, cwd is /app/apps/web (set via WORKDIR), but
the data volume is mounted at /app/data. Without pnpm-workspace.yaml present
in the runtime image, findWorkspaceRoot() returns null, so getLocalDataDirectory()
fell back to <cwd>/data = /app/apps/web/data, which the node user has no
permission to create. This caused the storage healthcheck to fail with
EACCES.
Add a production fallback: when cwd ends in apps/web, resolve the data
directory to two levels up (matching /app/data in the official image).
Re-resolves #2990.
https://claude.ai/code/session_015pSTtukxf7mFTty2Y6PHZf
* fix: replace apps/web heuristic with LOCAL_STORAGE_PATH env var
The previous fix special-cased a cwd ending in apps/web to land on /app/data,
but the heuristic could false-positive on any path with that suffix and was
fragile to Dockerfile changes. pnpm-workspace.yaml is never copied into the
runtime image, so the workspace-root walk was also dead code in production.
Replace the heuristic with an explicit LOCAL_STORAGE_PATH env var:
- Set LOCAL_STORAGE_PATH=/app/data in the Dockerfile (single source of truth).
- Add LOCAL_STORAGE_PATH to the env schema; storage and statistics services
pass it through to getLocalDataDirectory.
- getLocalDataDirectory now uses the override when set, else workspace root
(dev), else cwd/data.
- New Nitro plugin validates the resolved local data directory at startup
and refuses to boot with a clear error if it isn't writable, surfacing
permission issues immediately instead of at first upload/healthcheck.
- Document the new variable in .env.example and the Docker self-hosting docs.
https://claude.ai/code/session_015pSTtukxf7mFTty2Y6PHZf
* fix: address review feedback on storage path handling
- apps/web/plugins/2.storage.ts: use the default-import style for
node:fs/promises (matches the rest of the repo, sidesteps any
named-export concerns for fs.constants).
- packages/env/src/server.ts: reject relative LOCAL_STORAGE_PATH values
via a zod refinement. Relative paths would be resolved against cwd,
which differs between dev and Docker — exactly the same surprise the
original bug had. Failing fast at config validation time gives a
clear error before the server boots.
https://claude.ai/code/session_015pSTtukxf7mFTty2Y6PHZf
* fix: update data volume configuration in Docker Compose and enhance Nitro plugin
* fix: remove "Can I customize the templates?" FAQ entry from multiple language files
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
* feat: add text color support to the rich text editor
* improve design of text color picker
* Update translations for color picker features in multiple languages
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Add and update tests for new security utilities and tightened UI behavior to prevent regressions in validation and error handling paths.
Made-with: Cursor
Normalize frontend error rendering and tighten input/path handling across auth, builder, dashboard, and shared components for more resilient UX behavior.
Made-with: Cursor
Add stricter URL and redirect validation, endpoint rate limiting, safer defaults for printer and compose config, and CSP protections across server and API surfaces.
Made-with: Cursor
When an experience entry has multiple roles (Role Progression),
the period/date for each role was left-aligned in the grid layout,
causing it to appear off-center instead of right-aligned under the
location field.
Added `text-end` class to the role period element to match the
alignment behavior of the single-role header period. Uses `text-end`
instead of `text-right` for proper RTL language support.
The 400italic and 700italic variants for Computer Modern Sans pointed
to incorrect filenames. `cmunsl.woff` does not exist in the upstream
bitmaks/cm-web-fonts repository (returns 404 from jsDelivr CDN).
This causes PDF export to fail with "Waiting failed: 5000ms exceeded"
because Puppeteer's waitForFonts stalls on the 404, preventing the
page from signalling data-wf-loaded="true" within the timeout.
Correct mapping verified against the upstream @font-face declarations
in bitmaks/cm-web-fonts font/Sans/cmun-sans.css:
- 400 normal: cmunss.woff (unchanged)
- 400 italic: cmunsi.woff (was cmunsl.woff, which does not exist)
- 700 normal: cmunsx.woff (unchanged)
- 700 italic: cmunso.woff (was cmunsi.woff, which is the 400 italic)
Co-authored-by: Claude <noreply@anthropic.com>
* feat(mcp): add OAuth 2.1 authentication for claude.ai MCP connector
Enable OAuth 2.1 (RFC 8414 + RFC 7591) for the MCP endpoint using
better-auth's MCP plugin. This allows claude.ai and other MCP clients
to authenticate via Dynamic Client Registration and Authorization Code
flow with PKCE, using the existing login page.
- Add `mcp()` plugin to better-auth config with login page redirect
- Add `.well-known/oauth-authorization-server` discovery endpoint
- Add `.well-known/oauth-protected-resource` metadata endpoint
- Update MCP handler to accept Bearer tokens via `getMcpSession`
- Retain `x-api-key` fallback for backward compatibility
- Return proper HTTP 401 + WWW-Authenticate header for unauthed requests
- Add `oauthApplication`, `oauthAccessToken`, `oauthConsent` tables
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(mcp): use typed AuthError and suppress noisy verifyApiKey throws
- Replace string-matching error detection with instanceof AuthError
- Wrap verifyApiKey in try-catch to avoid logging malformed key errors
- Move console.error below auth check so 401s don't pollute logs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(mcp): add database migration for OAuth tables
Creates oauth_application, oauth_access_token, and oauth_consent tables
required for MCP OAuth 2.1 Dynamic Client Registration flow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(mcp): resolve OAuth Bearer token auth for oRPC tool calls
The oRPC context only checked session cookies and API keys, causing
MCP tool calls from OAuth clients (claude.ai) to fail with Unauthorized
even though the MCP endpoint itself authenticated successfully.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(mcp): look up user by userId from OAuth access token
getMcpSession returns OAuthAccessToken (with userId), not a session
object with a user property. Must query the user table by userId.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(mcp): migrate from deprecated mcp() plugin to @better-auth/oauth-provider
The better-auth MCP plugin is marked for deprecation in favor of the
OAuth Provider plugin. This refactors the entire OAuth 2.1 flow to use
@better-auth/oauth-provider with JWT-based token verification, replacing
the opaque token lookup via getMcpSession().
Key changes:
- Replace mcp() with jwt() + oauthProvider() in auth config
- Replace getMcpSession() with verifyAccessToken() (JWT/JWKS)
- Replace oauthApplication table with oauthClient (RFC 7591 compliant)
- Add oauthRefreshToken table and jwks table for JWT signing keys
- Extract shared authBaseUrl and verifyOAuthToken helper
- Hoist McpServer to module scope (avoid per-request reconstruction)
- Update .well-known discovery endpoints for OAuth Provider
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(mcp): resolve OAuth 2.1 flow for claude.ai MCP connector
Multiple fixes required to make the full MCP OAuth flow work with
claude.ai's implementation:
- Add RFC 8414 discovery route at /.well-known/oauth-authorization-server/api/auth
(claude.ai appends the issuer path per spec)
- Add /auth/oauth server route to handle login/consent flow
(generates auth codes directly, bypassing h3 cookie issues)
- Default token_endpoint_auth_method to "none" via onRequest plugin hook
(claude.ai omits this field, causing confidential client rejection)
- Strip prompt=consent from authorize requests via onRequest hook
(better-auth checks prompt before skipConsent, causing redirect loops)
- Add validAudiences for MCP resource URL
(JWT aud claim contains the MCP URL, not the base URL)
- Disable CSRF check for cross-origin OAuth flows
- Log token endpoint errors for debugging
- Set skipConsent on OAuth clients via /auth/oauth route
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(mcp): harden OAuth security and enforce lock on delete
- Scope CSRF bypass to OAuth2 paths only instead of disabling globally
- Validate redirect_uri against registered client URIs (prevents code interception)
- Use pathname matching instead of fragile url.includes() for route guards
- Replace biased modulo code generation with crypto.randomBytes
- Enforce resume lock check on delete (previously silently ignored)
- Remove debug console.error logging of OAuth token response bodies
- Use Response.json() consistently for MCP 401 response
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update dependencies, refine ignore patterns, and enhance documentation
- Updated various dependencies in package.json and pnpm-lock.yaml for improved stability and features.
- Adjusted ignore patterns in knip.json to include specific component directories.
- Enhanced documentation for the MCP server, clarifying authentication methods and configuration options.
- Made minor adjustments to VSCode settings for better code organization.
* fix(mcp): resolve OAuth client registration and stale token handling
Claude.ai sends token_endpoint_auth_method: "client_secret_post" without
a client_secret during Dynamic Client Registration, causing Better Auth to
reject it as an unauthenticated confidential client. Force to "none" for
unauthenticated registrations.
Also catch JWKS verification errors (e.g. key rotation after redeployment)
so stale Bearer tokens return 401 instead of 200 with an error body,
allowing clients to re-initiate the OAuth flow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* reiterate on tests
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* feat: add job listings feature with JSearch API integration, resume tailoring, and per-user rate limiting
* feat(jobs): add search filters UI, filter helper functions with tests, and job_search_quota DB migration
* feat(jobs): add pagination with 30 results per page and prev/next navigation
* refactor(job-detail): Adjust sheet width and scroll area height
* feat(ai): Add resume tailoring feature and prompt
* refactor(ai): Revise tailoring prompts and schema for full skill rewrite
* feat(ai): Add reference tailoring and output sanitization
* feat(testing): Add Vitest testing framework
* fix: address PR review - atomic rate limiting, calendar-month quota, skill sync warning, gitignore routeTree.gen.ts
* feat(jobs): Add location filter to job listings
* feat(job-listings): Add DOCX document generation
* feat(job-listings): Enable search by location and on Enter key
* feat(job-listings): Split location filter into city, state, and country
* feat(jobs): Implement job search adapter and JSearch
* Update 'locale/' directory
* feat(resume): Simplify filename generation and add tests
* fix(JSearch): reduce JSearch API usage to 1 request per search to prevent quota exhaustion
* fix(JSearch): Displayed quota amounts on Job Search functionality and settings fixed to pull from RapidAPI/JSearch response
* fix(internal rate limit): Removed internal rate limit and .env.example addition, cloud based implementation handles.
* style(job-filters): Adjust layout of switch filters
* fix(typecheck): Fixed typecheck issues introduced to sync with origin
* feat(jobs): Enhance tailor dialog with apply link and tags
* feat(locale files): updated locale files with the latest build
* feat(jobs): Add job search provider and integrate testing functionality
- Introduced `createJobSearchProvider` function to instantiate a JSearchProvider.
- Enhanced job search provider with methods for searching jobs, retrieving job details, and testing connection.
- Updated `vite.config.ts` to include new testing configurations and plugins.
- Added new dependencies in `package.json` for testing and document generation.
- Removed obsolete `vitest.config.ts` file.
- Improved job search provider tests for better coverage and reliability.
* refactor: Update job search routes and remove obsolete test configurations
- Removed the test configuration from `vite.config.ts`.
- Updated localization files to reflect changes in job search routes, renaming references from `jobs` to `job-search` across multiple languages.
- Adjusted autofix workflow to run formatting without the `--fix` flag for better control over code style adjustments.
* chore: Update dependencies and improve animation performance
- Added `jsdom` as a new dependency in `package.json`.
- Updated `vite-plus` and `vitest` to the latest versions for better compatibility.
- Enhanced animation components with `willChange` styles to optimize rendering performance.
- Adjusted various UI components to improve responsiveness and visual effects.
- Removed obsolete job details functionality from the job search provider and related tests.
* chore(locales): Update localization files for job search improvements
- Modified job search related strings to remove references to "this month" for a more concise format.
- Updated file references in localization entries to reflect changes in the job search component structure.
- Added new strings for API usage, quota remaining, and job fetching error messages across multiple languages.
- Removed obsolete "Monthly Usage" string from localization files.
* chore(dependencies): Update @typescript/native-preview to version 7.0.0-dev.20260319.1
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* Migrate from Biome to Oxlint/Oxfmt
* pin version of autofix
* set version of autofix
* pin version of autofix
* [autofix.ci] apply automated fixes
* better comments, test formatter
* [autofix.ci] apply automated fixes
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* feat(experience): add role progression support for career advancement
* fix(experience): remove indent and border from role progression items
* refactor(experience): enhance role progression UI and functionality
- Updated the ExperienceItem component to improve role display and styling.
- Refactored the CreateExperienceDialog and UpdateExperienceDialog to support role reordering using drag-and-drop.
- Added role handling in JSON and ReactiveResume importers.
- Enhanced experience schema to include roles, ensuring better data structure for career progression.
- Improved overall user experience with clearer role management in the experience section.
---------
Co-authored-by: root <root@reactive-resume-dev.one.one.one.one>
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
The editor_content CSS class used --page-body-font-weight-bold without
a fallback value. This variable is only injected in the resume preview
context (via use-css-variables.tsx), so in the editor UI the variable
was undefined, causing <strong> tags to render with no visible weight
change.
Adding a 'bold' fallback ensures bold text is correctly displayed in
the editor regardless of whether the CSS variable is set.
Fixes#2730
- Removed `@modelcontextprotocol/sdk` from ignoreDependencies in knip.json.
- Updated versions for several dependencies in package.json and pnpm-lock.yaml:
- `@ai-sdk/google` to 3.0.23
- `@ai-sdk/react` to 3.0.80
- `@aws-sdk/client-s3` to 3.986.0
- `ai` to 6.0.78
- Added new Afrikaans localization file and updated multiple existing localization files to remove passkey-related entries.
# This flag disables new signups, both on the web app and the server.
FLAG_DISABLE_SIGNUPS="false"
# This flag disables email/password login. Disables email verification, forgot password, and reset password flows. Users can still sign up via social auth (Google/GitHub/Custom OAuth), unless FLAG_DISABLE_SIGNUPS is also set to true.
# This flag disables email/password login. Disables email verification, forgot password, and reset password flows.
# Users can still sign up via social auth (Google/GitHub/Custom OAuth), unless FLAG_DISABLE_SIGNUPS is also set to true.
FLAG_DISABLE_EMAIL_AUTH="false"
# This flag disables the image processing.
# This is useful if you are using a machine with limited resources, like a Raspberry Pi.
FLAG_DISABLE_IMAGE_PROCESSING="false"
# This flag disables API rate limiting for authentication endpoints.
# Rate limiting is enabled by default in production to prevent abuse.
FLAG_DISABLE_API_RATE_LIMIT="false"
# This flag shows sponsor placements on the public landing page.
FLAG_SHOW_SPONSORS="false"
# Allows dynamic OAuth client registration to use any parseable redirect URI,
# including custom schemes, private hosts, and non-loopback http:// URLs.
# WARNING: Enabling this on a public or multi-tenant deployment can enable phishing
# or token exfiltration. Only enable this on a trusted, self-hosted instance.
FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI="false"
# Allows AI providers to be configured with any base URL, including http:// and
# private/loopback addresses (e.g. http://localhost:11434 for a local Ollama instance).
# WARNING: Enabling this on a multi-tenant deployment is a Server-Side Request Forgery (SSRF)
# risk. Only enable this on a trusted, single-tenant self-hosted instance.
FLAG_ALLOW_UNSAFE_AI_BASE_URL="false"
# --- Others ---
# Google Cloud API Key (optional)
# This is not used within Reactive Resume, but in src/scripts/fonts/generate.ts to generate a list of fonts served by Google Fonts.
# Note: Make sure "Google Fonts Developer API" is unrestricted.
# For font-list generation tooling.
# Requires "Google Fonts Developer API" to be enabled.
description:A detailed description of the feature you would like to see in Reactive Resume. Please provide as much detail as possible as it will help me implement the feature faster.
Reactive Resume is a pnpm monorepo (Turborepo) with two deployable apps: `apps/web` (TanStack Start / React 19 / Vite) and `apps/server` (Hono / Node.js). The production Docker image runs a single Node.js process on port 3000, with `apps/server` mounting the API/auth/MCP/static routes and serving the built web app.
Internal packages are source-consumed through `package.json` export maps that point at `src` files. Do not assume package-local `dist` output exists unless a package explicitly adds it.
### Prerequisites
- **Node.js 24** (matches Dockerfile `ARG NODE_VERSION=24`). Use `nvm install 24 && nvm use 24` if needed.
- **Docker** is required to run PostgreSQL. Start it with `sudo dockerd &` if the daemon isn't running.
- **pnpm 11.1.2** is managed via corepack (`corepack enable`).
### Codebase map
-`apps/web` owns TanStack Start routes, Vite config, PWA setup, oRPC browser client wiring, web features, and the resume builder UI.
-`apps/server` owns the production Hono app, route composition, auth/RPC/MCP/OpenAPI handlers, static uploads, schema JSON, web-dist fallback serving, and startup checks.
-`packages/api` contains oRPC routers, DTOs, rate limiting, and feature-owned API modules under `packages/api/src/features/*`. The router export at `@reactive-resume/api/routers` aggregates those feature routers for `/api/rpc`.
-`packages/auth` contains Better Auth config, auth helper functions, and exported auth types. The server auth adapter in `apps/server/src/http/auth.ts` delegates to `auth.handler`.
-`packages/db` contains the Drizzle client and schema. Migration files live at the repo root in `migrations/`.
-`packages/env` defines server environment validation and auto-loads the root `.env` for app/server code.
-`packages/schema` contains Zod schemas and typed resume/page/template models.
-`packages/pdf` contains the React PDF document, font registration, shared template primitives, template implementations, and browser/server PDF generation adapters. PDF.js viewer UI stays in `apps/web`.
-`packages/resume` contains pure resume-domain behavior such as JSON Patch helpers and social-network icon mapping.
-`packages/ui` contains shared Base UI/shadcn-style components and hooks.
-`packages/fonts`, `packages/email`, `packages/import`, `packages/ai`, `packages/utils`, and `packages/config` provide focused support surfaces. Prefer their existing exports over adding cross-package shortcuts.
- Development-only scripts live in `tooling/`, not under `packages/`, so packages only contain code bundled by the app/runtime.
### Web app conventions
- Routes are file-based under `apps/web/src/routes`. Do not hand-edit `apps/web/src/routeTree.gen.ts`; it is generated by TanStack Router tooling.
- Server-owned HTTP behavior lives in `apps/server/src/{http,rpc,mcp,openapi,static,startup}`. Keep API/RPC/auth/MCP/static route wiring in `apps/server`, not in web routes.
-`apps/web/src/router.tsx` initializes router context with `queryClient`, `orpc`, `theme`, `locale`, `session`, and `flags`. Reuse route context where possible instead of refetching these concerns ad hoc.
- The builder shell lives under `apps/web/src/routes/builder/$resumeId`. The nested preview route is client-only (`ssr: false`), while the public resume route `apps/web/src/routes/$username/$slug.tsx` uses `ssr: "data-only"`.
- Browser-only resume preview code lives under `apps/web/src/features/resume/preview`, and public resume PDF viewer code lives under `apps/web/src/features/resume/public`. Keep PDF.js/canvas/browser APIs out of SSR paths and out of `packages/pdf`.
- The isomorphic oRPC client is in `apps/web/src/libs/orpc/client.ts`; server calls use an in-process router client and browser calls use `/api/rpc` with credentials included.
- For React components with explicit props, prefer a named TypeScript props type over inline object annotations in the function signature, especially once the props include more than one field or generics. For example:
- Workspace dependencies must go through package names and package export maps. Do not import another workspace's `src` tree through repository paths, `@reactive-resume/*/src/*`, or TypeScript path aliases.
-`turbo boundaries` is the executable package-boundary check. Workspace-level `turbo.json` files declare coarse tags:
-`app:web` for the TanStack Start app.
-`app:server` and `runtime:server` for the Node/Hono process.
-`runtime:server` for server-only packages such as API/auth/db/env/email/MCP.
-`runtime:browser` for browser-only shared UI.
-`runtime:universal` for environment-neutral domain packages.
-`role:domain`, `role:infra`, `role:adapter`, `role:api`, `role:rendering`, and `role:tooling` for package intent.
- Browser/server runtime-specific code should live behind explicit export subpaths such as `@reactive-resume/pdf/browser`, `@reactive-resume/pdf/server`, or `@reactive-resume/env/server`. Keep root exports environment-neutral unless the package is intentionally server-only.
- Wildcard exports are allowed only for leaf libraries whose public surface is intentionally file-like, currently `@reactive-resume/ui/components/*`, `@reactive-resume/ui/hooks/*`, and schema resume model files. Prefer explicit exports for packages that own runtime behavior.
- Add new API procedures and business logic inside the owning `packages/api/src/features/*` module. Keep route wiring, DTO usage, helpers, and services colocated by feature/capability, then expose only intentional public surfaces through `packages/api/package.json`. Prefer `protectedProcedure` from `packages/api/src/context.ts` for authenticated procedures.
- Add database columns/tables in `packages/db/src/schema/*`, then generate root-level migrations with `dotenvx run -f .env.local -- pnpm db:generate`.
- Add or change resume data shape in `packages/schema/src/resume/*` first, then update API DTOs, importers, PDF rendering, and web forms that consume that shape.
- Add or rename templates in all relevant places: `packages/schema/src/templates.ts`, `packages/pdf/src/templates/index.ts`, template source under `packages/pdf/src/templates/<name>/`, and static previews under `apps/web/public/templates/{jpg,pdf}`.
- Resume JSON Patch behavior belongs in `@reactive-resume/resume/patch`; do not put resume-domain helpers in `@reactive-resume/utils`.
- DOCX export behavior belongs in `@reactive-resume/docx`; do not put DOCX builders in `@reactive-resume/utils`.
- Shared PDF section filtering lives in `packages/pdf/src/templates/shared/filtering.ts`. Keep template-specific visual exceptions in the owning template directory unless multiple templates need the same behavior.
-`packages/pdf/src/hooks/use-register-fonts.ts` owns React PDF font registration, standard PDF font handling, CJK fallback stacks, and global hyphenation behavior.
- PDF generation helpers live behind `@reactive-resume/pdf/browser` and `@reactive-resume/pdf/server`; locale-specific section-title resolution stays in the caller.
- MCP implementation belongs in `@reactive-resume/mcp`; app packages must not import MCP implementation from another app's source tree.
-`packages/utils` has narrowly exported helpers. If another package needs a utility, add an explicit export path instead of importing private files.
Placement decision tree:
1. If the change is a web route, route loader, or user-facing web workflow, start in `apps/web/src/routes` or `apps/web/src/features`.
2. If the change is a server HTTP route/adapter, startup check, static handler, MCP transport, or OpenAPI/well-known handler, start in `apps/server/src`.
3. If it is authenticated API behavior, put the contract and implementation in the owning `packages/api/src/features/*` module.
4. If it is pure resume data behavior with no DB, HTTP, DOM, or PDF renderer dependency, put it in `packages/resume`.
5. If it renders resume PDFs, put shared React PDF/template code in `packages/pdf`; put PDF.js viewer/canvas UI in `apps/web/src/features/resume`.
6. If it creates DOCX exports, put it in `packages/docx`.
7. If it exposes MCP tools/prompts/resources, put it in `packages/mcp`.
8. If it is a generic UI primitive or hook, put it in `packages/ui`; if it is workflow-specific UI, keep it in the owning web feature.
9. If it is a narrow cross-cutting helper, add an explicit `packages/utils` export only after checking that no domain package is a better owner.
### Database
PostgreSQL runs via Docker Compose:
```
sudo docker compose -f compose.dev.yml up -d postgres
```
The dev default connection string is `postgresql://postgres:postgres@localhost:5432/postgres`.
**Important**: `drizzle-kit` (used by `pnpm db:migrate`) reads `DATABASE_URL` from `process.env` directly — it does **not** auto-load the `.env` file. Run migration commands through `dotenvx`, for example `dotenvx run -f .env.local -- pnpm db:migrate`, so `DATABASE_URL` is present in the process environment.
The production server runs migrations during startup before serving traffic. Manual `pnpm db:migrate` is mainly for first setup, migration debugging, or applying migrations without starting the app.
### Environment
Copy `.env.example` to `.env`. The three required variables are:
S3/SeaweedFS is optional. If `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, and `S3_BUCKET` are all set, the app uses S3-compatible storage. The checked-in `.env.example` sets SeaweedFS defaults, so either start the `seaweedfs` compose service too or comment out those S3 vars to use local filesystem storage under `<workspace>/data`. `LOCAL_STORAGE_PATH` must be absolute when set.
When running dev servers or migration commands, prefix the command with `dotenvx run -f .env.local --`. For example: `dotenvx run -f .env.local -- pnpm dev`. Tests, typechecks, linters, boundary checks, and `pnpm build` do not need this prefix by default. If one of those commands fails because a specific environment variable is required, rerun it with the `dotenvx run -f .env.local --` prefix.
### Common commands
| Task | Command |
|------|---------|
| Install deps | `pnpm install` |
| Start Postgres only | `sudo docker compose -f compose.dev.yml up -d postgres` |
| Run migrations | `dotenvx run -f .env.local -- pnpm db:migrate` |
| Dev server | `dotenvx run -f .env.local -- pnpm dev` (starts on port 3000) |
| Web dev server only | `dotenvx run -f .env.local -- pnpm dev:web` |
| Lint/format | `pnpm check` (Biome) |
| Boundary check | `pnpm exec turbo boundaries` |
| Tests | `pnpm test` (Vitest) |
| Build | `pnpm build` |
| Typecheck | `pnpm typecheck` |
For focused validation, prefer package filters before repo-wide commands, for example:
```
pnpm --filter web typecheck
pnpm --filter @reactive-resume/pdf test
pnpm --filter @reactive-resume/api test
pnpm exec turbo boundaries
```
Vitest test paths are package-relative when running through `pnpm --filter <package> test -- <path>`.
### Gotchas
- The server startup path auto-runs migrations before serving traffic, so `pnpm db:migrate` is mainly needed for first-time setup, migration debugging, or applying migrations without starting the app.
- Email sending requires SMTP config; without it, emails are logged to console. This is fine for dev — the app still functions, but email verification links appear in server logs.
- The `lefthook.yml` pre-commit hook runs `biome check` on staged files. Run `pnpm check` before committing to avoid hook failures.
-`pnpm check` is write-capable (`biome check --write --unsafe .`). Call that out when using it, and use narrower Biome commands if you need a non-mutating inspection.
- Biome uses tabs, double quotes, line width 120, organized import groups, and sorted Tailwind classes for `clsx`, `cva`, and `cn`.
- Most packages use `tsgo --noEmit` for typechecking and `vitest run --passWithNoTests` for tests.
- There may be unrelated local edits in the worktree. Inspect `git status --short` first and avoid reverting files you did not touch.
- **New env vars require a `turbo.json` entry.** Turborepo 2.x runs in strict env mode by default — it filters out env vars that are not listed in `globalEnv` (or task-level `env`/`passThroughEnv`). Any new environment variable added to `packages/env/src/server.ts` must also be added to the `globalEnv` array in `turbo.json`, or the variable will be `undefined` inside child processes at runtime even if it is correctly set in the OS/container environment.
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Reactive Resume is a free, open-source resume builder built with TanStack Start (React 19 + Vite), using ORPC for type-safe RPC APIs, Drizzle ORM with PostgreSQL, and Better Auth for authentication.
## Development Commands
```bash
# Start development server (runs on port 3000)
pnpm dev
# Build for production
pnpm build
# Start production server
pnpm start
# Linting (uses Biome)
pnpm lint
# Type checking
pnpm typecheck
# Database operations
pnpm db:generate # Generate migration files
pnpm db:migrate # Run migrations
pnpm db:push # Push schema changes directly
pnpm db:studio # Open Drizzle Studio
# Extract i18n strings for translation
pnpm lingui:extract
# Find unused exports
pnpm knip
```
## Local Development Setup
1. Copy `.env.example` to `.env` and configure environment variables
2. Start required services: `docker compose -f compose.dev.yml up -d`
- PostgreSQL (port 5432)
- Browserless/Chromium for PDF generation (port 4000)
Templates are React components in `src/components/resume/templates/`. Each template (azurill, bronzor, chikorita, etc.) renders the resume data with different visual styles. Templates use shared components from `src/components/resume/shared/`.
description:A monochrome, content-first design system for a free and open-source resume builder. Dark-by-default with light mode support.
colors:
primary:"#343434"
primary-foreground:"#FBFBFB"
secondary:"#F7F7F7"
secondary-foreground:"#343434"
background:"#FFFFFF"
foreground:"#252525"
muted:"#F7F7F7"
muted-foreground:"#8E8E8E"
card:"#FFFFFF"
card-foreground:"#252525"
border:"#EBEBEB"
input:"#EBEBEB"
ring:"#B5B5B5"
destructive:"#DC2626"
on-destructive:"#FFFFFF"
typography:
heading:
fontFamily:IBM Plex Sans Variable
fontSize:1rem
fontWeight:500
body:
fontFamily:IBM Plex Sans Variable
fontSize:0.875rem
fontWeight:400
body-sm:
fontFamily:IBM Plex Sans Variable
fontSize:0.75rem
fontWeight:400
label:
fontFamily:IBM Plex Sans Variable
fontSize:0.8rem
fontWeight:500
hero-heading:
fontFamily:IBM Plex Sans Variable
fontSize:3.75rem
fontWeight:700
letterSpacing:-0.025em
rounded:
sm:0.18rem
md:0.24rem
lg:0.3rem
xl:0.42rem
2xl:0.54rem
3xl:0.66rem
4xl:0.78rem
spacing:
xs:4px
sm:8px
md:16px
lg:24px
xl:32px
2xl:48px
components:
button-default:
backgroundColor:"{colors.primary}"
textColor:"{colors.primary-foreground}"
rounded:"{rounded.lg}"
padding:10px
height:36px
button-outline:
backgroundColor:"{colors.background}"
textColor:"{colors.foreground}"
rounded:"{rounded.lg}"
padding:10px
height:36px
button-secondary:
backgroundColor:"{colors.secondary}"
textColor:"{colors.secondary-foreground}"
rounded:"{rounded.lg}"
padding:10px
height:36px
button-ghost:
backgroundColor:"{colors.background}"
textColor:"{colors.foreground}"
rounded:"{rounded.lg}"
padding:10px
height:36px
button-destructive:
backgroundColor:"{colors.destructive}"
textColor:"{colors.on-destructive}"
rounded:"{rounded.lg}"
padding:10px
height:36px
card:
backgroundColor:"{colors.card}"
textColor:"{colors.card-foreground}"
rounded:"{rounded.lg}"
padding:16px
input:
backgroundColor:"{colors.background}"
textColor:"{colors.foreground}"
rounded:"{rounded.lg}"
height:36px
padding:10px
input-focus:
backgroundColor:"{colors.background}"
textColor:"{colors.foreground}"
rounded:"{rounded.lg}"
height:36px
padding:10px
badge:
backgroundColor:"{colors.primary}"
textColor:"{colors.primary-foreground}"
rounded:"{rounded.md}"
padding:4px
popover:
backgroundColor:"{colors.card}"
textColor:"{colors.card-foreground}"
rounded:"{rounded.xl}"
padding:4px
sidebar:
backgroundColor:"{colors.muted}"
textColor:"{colors.foreground}"
padding:8px
sidebar-item:
backgroundColor:"{colors.muted}"
textColor:"{colors.muted-foreground}"
rounded:"{rounded.lg}"
padding:8px
sidebar-item-active:
backgroundColor:"{colors.primary}"
textColor:"{colors.primary-foreground}"
rounded:"{rounded.lg}"
padding:8px
tooltip:
backgroundColor:"{colors.primary}"
textColor:"{colors.primary-foreground}"
rounded:"{rounded.md}"
padding:6px
separator:
backgroundColor:"{colors.border}"
height:1px
dialog:
backgroundColor:"{colors.card}"
textColor:"{colors.card-foreground}"
rounded:"{rounded.xl}"
padding:24px
input-invalid:
backgroundColor:"{colors.background}"
textColor:"{colors.destructive}"
rounded:"{rounded.lg}"
height:36px
padding:10px
---
## Overview
Reactive Resume is a monochrome, content-first design system built for a resume builder used by tens of thousands of people worldwide. The visual identity prioritizes readability and unobtrusiveness — the user's resume content is always the hero, never the chrome around it.
The system defaults to dark mode with a warm near-black backdrop that makes the resume preview "float" as the visual anchor. Light mode is supported as a full alternative. The authenticated app shell (dashboard, builder, settings) uses an entirely achromatic grayscale palette — the sole chromatic exception is destructive red for dangerous actions. The landing page introduces subtle chromatic accents: blue-tinted spotlight gradients on the hero, a multicolor text-mask animation on hover, and social auth provider brand colors (Google blue, LinkedIn blue) on the login page.
The overall aesthetic is a professional tool UI: clean grid lines, subtle borders, generous whitespace, and typography that steps back to let the content shine. Think "VS Code meets Figma" — a productivity workspace, not a marketing site.
One deliberate counterpoint to the serious UI: all resume templates are named after Pokemon (Azurill, Bronzor, Chikorita, Ditgar, Gengar, Pikachu, etc.). This is an intentional brand choice — playful naming for templates injects personality into an otherwise utilitarian interface, making templates feel collectible and memorable rather than generic ("Template 1", "Modern", "Classic").
## Colors
The palette is rooted in achromatic OKLch values (chroma = 0), producing a pure grayscale scale without warm or cool casts. Colors are defined as CSS custom properties using `oklch()` and consumed through Tailwind CSS 4 theme tokens. Always prefer CSS variables (e.g., `var(--primary)`) or Tailwind tokens (e.g., `bg-primary`) over raw color values. The hex values in this document's YAML front matter are agent-friendly approximations of the canonical OKLch definitions in `packages/ui/src/styles/globals.css` — use hex only where OKLch is unavailable.
- **Primary (#343434 light / #EBEBEB dark):** Used for high-emphasis interactive surfaces — default buttons, selected states, and text selection. In dark mode this inverts to near-white so buttons remain prominent.
- **Foreground (#252525 light / #FBFBFB dark):** Body text and headings. High contrast against the background in both themes.
- **Background (#FFFFFF light / #252525 dark):** The canvas. Pure white in light mode, warm near-black in dark mode.
- **Card (#FFFFFF light / #343434 dark):** Elevated surface for cards, panels, and the builder sidebar. In dark mode, one step lighter than the background to create subtle depth.
- **Muted (#F7F7F7 light / #454545 dark):** De-emphasized backgrounds for secondary UI regions, hover states, and inactive tabs.
- **Muted Foreground (#8E8E8E light / #B5B5B5 dark):** Captions, helper text, timestamps, and metadata. Deliberately low-contrast against the background to recede visually.
- **Border (#EBEBEB light / white at 10% opacity dark):** Thin separator lines. In dark mode, uses transparent white rather than a solid gray to blend naturally with any underlying surface color.
- **Input (#EBEBEB light / white at 15% opacity dark):** Form field borders, slightly more prominent than general borders to make input areas discoverable.
- **Destructive (#DC2626 light / #EF4444 dark):** The only chromatic color in the palette. Reserved exclusively for delete actions, error states, and danger-zone operations. Used at 10% opacity as a background tint with full saturation for text, creating a soft but unmistakable warning.
- **Ring (#B5B5B5 light / #8E8E8E dark):** Focus ring indicator at 50% opacity, surrounding focused interactive elements.
- **Sidebar Primary (dark only, #6366F1):** An indigo value inherited from the shadcn/ui defaults. Not actively used in the current UI — sidebar active states use the standard grayscale primary token instead. Retained in the CSS custom properties for potential future customization.
Resume templates have their own independent color system — users pick primary, text, and background colors per resume through a color picker in the builder's Design panel. These template colors are completely separate from the app shell palette.
## Typography
The entire application uses a single typeface: **IBM Plex Sans Variable**. This is a humanist sans-serif with an extensive weight range (100–900) and excellent readability at small sizes, both on screen and in PDFs.
- **Hero heading (responsive: 2.25rem mobile / 3rem tablet / 3.75rem desktop, weight 700, tracking-tight):** Landing page headline only. Large, bold, and commanding. Scales across three breakpoints.
- **Section heading (1rem / 16px, weight 500):** Used for section titles in the builder sidebar, settings panels, and dashboard cards. Medium weight provides hierarchy without shouting.
- **Body (0.875rem / 14px, weight 400):** The workhorse. All form labels, descriptions, card content, and general UI text.
- **Small body (0.75rem / 12px, weight 400):** Captions, helper text, timestamps, and metadata.
- **Label (0.8rem / ~13px, weight 500):** Button text, badge labels, and form field labels. Slightly heavier than body to denote interactivity.
The resume content itself uses a separate font system — users choose from 1,000+ Google Fonts for their resume headings and body text, with category-aware fallback stacks including CJK support (Noto Sans SC, PingFang SC, Hiragino Sans GB for sans-serif; Noto Serif SC, Songti SC for serif). Standard PDF fonts (Helvetica, Courier, Times-Roman) are available as offline fallbacks.
Font rendering uses `antialiased` (grayscale AA) and `proportional-nums` across the board for clean rendering and properly spaced numerals in dates and phone numbers.
## Layout
### Builder (Three-Panel Workspace)
The core builder uses a resizable three-panel layout powered by `react-resizable-panels`:
- **Left sidebar (default 22%):** Resume section forms — personal info, experience, education, skills, and custom sections. Scrollable with collapsible section groups.
- **Center artboard (default 56%):** Live resume preview rendered via PDF.js canvas. Supports zoom, pan, and pinch gestures via `react-zoom-pan-pinch`. The preview maintains A4 aspect ratio (210:297) with a subtle shadow to simulate a physical page.
- **Right sidebar (default 22%):** Design controls — template picker, font selection, color picker, layout manager (page assignments, section ordering via drag-and-drop).
Panel sizes persist in cookies. On mobile (< 768px), sidebars collapse to 0% width and become toggleable overlays (max 95% width when open). The desktop minimum collapsed width is 48px (icon rail).
### Dashboard
Standard sidebar navigation layout using the `Sidebar` component system. The sidebar contains: logo, resume list link, agent link, settings subnavigation (profile, preferences, authentication, API keys, integrations, danger zone), and a footer with user avatar. Content area shows a responsive grid of resume cards.
### Landing Page
Full-width single-column marketing layout:
1.**Floating builder preview** — A non-interactive screenshot of the builder as a hero visual, creating an immediate "this is what you get" impression.
2.**Hero** — Centered headline, subheadline, and two CTAs (primary "Get Started" with arrow, ghost "Learn More" with icon).
3.**Features grid** — 4-column responsive grid with icon + title + description cards, separated by thin border lines.
4.**Template carousel** — Horizontally scrolling row of template preview thumbnails with Pokemon-themed names.
5.**Testimonials** — Tiled user quotes in a masonry-style grid.
6.**Support / FAQ / Footer** — Accordion FAQ, community section, and a 4-column footer with logo, resource links, community links, and license info.
### Responsive Breakpoints
Mobile detection uses a 768px threshold via `MediaQueryList`. The layout is optimized for workspace productivity on larger screens, with responsive mobile support that adapts the multi-panel builder into a streamlined single-panel experience. Both desktop and mobile are supported experiences — the builder's three-panel layout leverages desktop space, while mobile surfaces the same editing capabilities through collapsible overlays.
### Page Aspect Ratio
A custom Tailwind token `--aspect-page: 210 / 297` enforces A4 paper proportions wherever resume pages are rendered (builder preview, public view, PDF export).
## Animation
Animations use the Motion library (formerly Framer Motion) and follow a consistent choreography pattern:
**Entrance animations** use a fade-up reveal: elements start at `opacity: 0, y: 20-100` and animate to `opacity: 1, y: 0`. The hero section uses a larger y-offset (100px) for dramatic effect; subsequent sections use 20px for subtlety.
**Timing principles:**
- **Base duration:** 0.35s–0.6s for standard section reveals, 0.45s for hero elements, up to 1.1s for the hero video entrance.
- **Stagger pattern:** Sequential delays within a group, typically 0.1s–0.15s apart (hero: 0.55s, 0.7s, 0.82s, 0.95s). For grids, use `index * 0.03`–`0.1` for per-item stagger.
- **Easing:** `easeOut` for entrances (elements decelerate into position). `easeInOut` for looping/ambient animations.
- **Performance:** Apply `will-change-[transform,opacity]` on animated elements and `will-change-transform` on continuously animated elements.
**Hover/interaction animations** are quick (0.2s) and subtle — small scale bumps (`scale: 1.01`), slight y-offsets (`y: -2`), and `active:translate-y-px` for button press.
**Ambient animations** loop infinitely with `easeInOut` — the scroll indicator bounces gently (`y: [0, 5, 0]` over 1.5s).
**Reduced motion:** All CSS transitions and animations collapse to `0.01ms` duration and single iteration when `prefers-reduced-motion: reduce` is active. Motion library animations should also respect this preference.
## Elevation & Depth
Elevation is handled through background color layering rather than drop shadows:
- **Level 0 — Background:** The base canvas (`--background`).
- **Level 1 — Card:** One step lighter in dark mode (`--card`), used for sidebars, panels, and cards.
- **Level 2 — Popover:** Same as card, but appears above the content layer in popovers, dropdowns, and command palette.
- **Level 3 — Overlay:** Backdrop blur (`backdrop-blur-xs` at 0.5px or `backdrop-blur-2xl` at 40px) with `backdrop-saturate-150` for modal overlays, creating a frosted-glass effect over the workspace.
The resume preview page uses a subtle drop shadow to simulate a physical sheet of paper floating above the dark artboard — one of the few places actual shadows appear.
## Shapes
Border radius follows a multiplicative scale from a single `--radius` base of `0.3rem`:
The radius scale is deliberately tight — the largest value (0.78rem) is still quite subtle. This avoids the "rounded everything" aesthetic and keeps the UI feeling precise and tool-like. Interactive elements consistently use `rounded-lg` as the default.
## Components
### Buttons
Six variants, all sharing `rounded-lg` corners, `font-medium`, `text-sm`, and a 1px `translate-y` on active press (except when the button opens a popup):
- **Default:** Solid primary background. The highest-emphasis action on any screen.
- **Outline:** Transparent with a border. For secondary actions that need clear boundaries.
- **Secondary:** Muted background. For paired actions alongside a primary button.
- **Ghost:** No background or border. For toolbar actions and inline controls where chrome would be noise.
- **Destructive:** Red at 10% opacity background with red text. Visually alarming without being garish.
- **Link:** Underline-on-hover text. For inline navigation within prose.
Size scale: `xs` (28px), `sm` (32px), `default` (36px), `lg` (40px), plus `icon` variants at each size for square icon-only buttons.
### Cards
White/dark surface with foreground text. Composed of `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`, and `CardAction` slots. Default vertical padding is `py-4` (compact: `py-3`).
### Forms
Built on TanStack Form with Zod validation. Composed of `FormItem`, `FormLabel`, `FormControl`, `FormMessage`, and `FormDescription`. Validation errors only appear after field touch. Invalid fields get a red destructive border with a ring.
### Dialogs
Centralized dialog manager with 40+ dialog types, all rendered via pattern matching (`ts-pattern`). Dialogs support before-close validation, form blocking for unsaved changes, and confirmation prompts. Used for all CRUD operations on resume sections, settings changes, and import/export flows.
### Command Palette
Triggered by `Cmd+K` / `Ctrl+K`. Built on `cmdk` with fuzzy search via `Fuse.js`. Multi-page navigation (resumes, settings, preferences) with back navigation via Backspace. Screen-reader accessible with `sr-only` headings.
### Toast Notifications
Powered by Sonner, positioned bottom-right with rich colors. Used for auto-save feedback, form submission status, error reporting, and donation prompts. Loading toasts are used during async operations (PDF generation, resume creation) with dismiss-on-complete.
### Drag and Drop
Powered by `@dnd-kit` with `PointerSensor` and `KeyboardSensor`. Used in chip inputs (skill tags, URL lists) and page layout management (section ordering across resume pages). Smooth animations via Motion library.
## Internationalization
The app supports 40+ locales including RTL languages (Arabic, Hebrew, Persian, Urdu, Uyghur, Yiddish). i18n is not an afterthought — it shapes layout decisions:
**Direction:** The `<html>` element receives `dir="rtl"` or `dir="ltr"` based on the active locale, detected via `isRTL()` which checks the language prefix against a known RTL set. All layout mirroring flows from this single attribute.
**Logical properties:** Use CSS logical properties (`ps-`, `pe-`, `ms-`, `me-`, `inline-start`, `inline-end`, `inset-s-`, `inset-e-`) instead of physical (`pl-`, `pr-`, `ml-`, `mr-`, `left`, `right`). Button components already use `has-data-[icon=inline-start]:ps-2` and `has-data-[icon=inline-end]:pe-2` patterns. This ensures correct spacing in both LTR and RTL layouts without separate stylesheets.
**Variable-length text:** Translations can be 30–50% longer than English (German, Finnish) or significantly shorter (CJK). UI elements should accommodate variable text length — avoid fixed widths on buttons and labels. Use `whitespace-nowrap` only where truncation is acceptable, and prefer `min-w-0` with `truncate` over fixed-width containers.
**Icons:** Directional icons (arrows, chevrons, progress indicators) should mirror in RTL contexts. Phosphor Icons provides mirrored variants for directional icons. Non-directional icons (settings gear, checkmark, delete) do not mirror.
**Strings:** All user-facing strings use Lingui macros (`t`, `msg`, `<Trans>`) — never hardcode English text in components. Translation files are `.po` format under `/locale/`.
## Do's and Don'ts
### Do
- **Use the grayscale palette for all app chrome.** The absence of color is the brand. The resume content is the only thing that should be colorful.
- **Default to dark mode.** The dark workspace makes resume previews pop and reduces eye strain during extended editing sessions.
- **Use `text-sm` (14px) as the base text size.** The UI is information-dense — form fields, section labels, metadata — and needs to be scannable without feeling cramped.
- **Keep border radius tight.** Use `rounded-lg` (0.3rem) as the default. The tool should feel precise, not playful.
- **Respect reduced motion preferences.** All animations collapse to 0.01ms when `prefers-reduced-motion: reduce` is active.
- **Use Phosphor Icons consistently.** Regular weight, `size-4` (16px) default. Icons should be functional labels, not decorative.
- **Maintain the three-panel builder proportions.** The center artboard should always dominate. Sidebars are support panels, not equal peers.
- **Use transparent-white borders in dark mode.** `oklch(1 0 0 / 10%)` blends naturally with any surface rather than introducing a distinct gray band.
### Don't
- **Don't introduce accent colors into the app shell.** No blues, greens, or purples for primary actions. The only chromatic color is destructive red. The inherited indigo sidebar-primary token exists in CSS custom properties but is not actively used.
- **Don't use drop shadows for elevation.** Rely on background color layering and border separation. The one exception is the resume page preview shadow.
- **Don't make the UI compete with the resume content.** If a new feature draws more visual attention than the resume preview, it needs to be toned down.
- **Don't use large border radii.** Nothing above `rounded-xl` on standard components. Large pills and full-round shapes conflict with the precision-tool aesthetic.
- **Don't hardcode colors outside the token system.** All colors flow through CSS custom properties so that dark/light mode switching works automatically.
- **Don't use multiple typefaces in the app shell.** IBM Plex Sans Variable is the only UI font. Resume templates have their own font system, but the chrome stays single-family.
- **Don't skip the `data-slot` attribute on components.** It's used for styling hooks and accessibility selectors throughout the component library.
- **Don't forget RTL.** The app supports 40+ locales including Arabic, Hebrew, Persian, and Urdu. Use logical properties (`ps`, `pe`, `ms`, `me`) instead of physical (`pl`, `pr`, `ml`, `mr`).
@@ -31,12 +31,26 @@ Reactive Resume makes building resumes straightforward. Pick a template, fill in
Built with privacy as a core principle, Reactive Resume gives you complete ownership of your data. The codebase is fully open-source under the MIT license, with no tracking, no ads, and no hidden costs.
## Sponsors
Reactive Resume stays free, open-source, and independent because companies choose to support the work behind it. Thank you to every sponsor who helps fund hosting, maintenance, and continued development for the community.
[Atlas Cloud](https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=reactive-resume) supports Reactive Resume as a project sponsor. Atlas Cloud provides a unified AI platform for developers, with access to hundreds of models for chat, image generation, video generation, media processing, and GPU cloud workloads through one API key, one endpoint, and one billing account.
If your company would like to sponsor Reactive Resume, email [hello@amruthpillai.com](mailto:hello@amruthpillai.com).
## Features
**Resume Building**
- Real-time preview as you type
- Multiple export formats (PDF, JSON)
- Multiple export formats (PDF, JSON, DOCX)
- Drag-and-drop section ordering
- Custom sections for any content type
- Rich text editor with formatting support
@@ -69,63 +83,71 @@ Built with privacy as a core principle, Reactive Resume gives you complete owner
| [Getting Started](https://docs.rxresu.me/getting-started) | First-time setup and basic usage |
| [Self-Hosting](https://docs.rxresu.me/self-hosting/docker) | Deploy on your own server |
| [Development Setup](https://docs.rxresu.me/contributing/development) | Local development environment |
| [Project Architecture](https://docs.rxresu.me/contributing/architecture) | Codebase structure and patterns |
| [Exporting Your Resume](https://docs.rxresu.me/guides/exporting-your-resume)| PDF and JSON export options |
## Self-Hosting
Reactive Resume can be self-hosted using Docker. The stack includes:
- **PostgreSQL** — Database for storing user data and resumes
- **Printer** — Headless Chromium service for PDF and screenshot generation
- **SeaweedFS** (optional) — S3-compatible storage for file uploads
> **From v5.1.0 onwards** — PDF generation now runs entirely client-side via `@react-pdf/renderer`. New deployments no longer require Browserless, Chromium, or any external print service as a dependency. The `PRINTER_*` and `BROWSERLESS_*` environment variables are no longer read and can be removed from your `.env`.
Pull the latest image from Docker Hub or GitHub Container Registry:
"Reactive Resume is a free and open-source resume builder. Use this MCP server to interact with your resume using an LLM of your choice.",
icons:[
{
src:"https://rxresu.me/icon/light.svg",
mimeType:"image/svg+xml",
theme:"light",
},
{
src:"https://rxresu.me/icon/dark.svg",
mimeType:"image/svg+xml",
theme:"dark",
},
],
},
{
instructions:[
"You are connected to Reactive Resume over MCP.",
"Authenticate with OAuth (recommended) or an API key (`x-api-key`).",
`Discover resume IDs with \`${MCP_TOOL_NAME.listResumes}\` (not \`resources/list\`).`,
`List distinct tags with \`${MCP_TOOL_NAME.listResumeTags}\`.`,
`Read schema at \`resume://_meta/schema\`; read resume JSON via \`resume://{id}\` or \`${MCP_TOOL_NAME.getResume}\`.`,
`Apply body edits with JSON Patch through \`${MCP_TOOL_NAME.patchResume}\`.`,
`Change name, slug, tags, or public visibility with \`${MCP_TOOL_NAME.updateResume}\` (returns canonical share URL; anonymous access only when \`isPublic\` is true; passwords are managed in the web app only).`,
`Create short-lived authenticated PDF download URLs with \`${MCP_TOOL_NAME.downloadResumePdf}\`.`,
`Import full ResumeData JSON with \`${MCP_TOOL_NAME.importResume}\`; read saved AI analysis with \`${MCP_TOOL_NAME.getResumeAnalysis}\`.`,
<metaname="description"content="Reactive Resume is a free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.">
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.