Auth pages, dialogs, the command palette, the user menu, Applications, the rich-text toolbar, the stylesheet editor and the error screens use the Material Symbols set instead of Phosphor; the icon subset gains the 35 glyphs they need. Phosphor stays for brand logos, the landing page and the icons printed on resumes. Also removes the form field and test mock that only wrapped the old rich input.
Every class that used the old names (muted, primary, card, border, input and the rest) now uses the Desk & Paper token it resolved to, with text colours on their text tokens (accent-text, danger-text, ink-3). The compatibility block in the theme is gone.
Write becomes the Basics card, the outline of sections in print order and
Add section. Entries open in place (one at a time, the editor selection),
save as you type and show drafts as "Draft · not printed". Dates use a
structured field with a Present switch and the review note for text that
wasn't read exactly; descriptions use a restricted rich-text toolbar shown
on focus. Sections and entries reorder by drag or Alt+Up/Down, rows carry
the eye, count and every section option, and deleting shows Undo.
Clicking the page opens the entry and scrolls it into view; focusing a field
outlines its block. Phones push an open entry full screen. While a field
has focus the preview waits for a pause in typing.
The left section sidebar, the entry dialogs and the hidden-sections list are
removed.
Replace the achromatic shadcn palette with the Desk & Paper tokens (warm
neutrals, one moss accent, danger, warn and info), exposed to Tailwind
under their spec names; the previous names resolve to the new tokens
until every screen is rebuilt. Hard-coded palette classes become semantic
tokens and stage colors follow the spec.
Fonts move to Newsreader, Hanken Grotesk and JetBrains Mono. The theme
gains a System option that follows the operating system live, applied
before first paint, and Base UI now receives the locale's direction.
Primitives follow the component spec: button variants primary,
secondary, ghost, danger and link with a loading state, accent focus
rings on inputs, SwitchRow, semantic badges, segmented and underline
tabs, restyled menus, dialogs, sheets, tooltips and command bar, and a
single bottom-center toast with an Undo action. New: IconButton,
SegmentedControl, RadioGroup and NativeSelect.
Audit every animation in the app and shared UI primitives against a
frequency-first motion bar: keyboard and high-frequency actions no longer
animate, remaining motion uses interruptible CSS transitions with shared
easing tokens, and redundant animation code is removed.
UI primitives (@reactive-resume/ui)
- Dialog, alert dialog, popover and tooltip move from tw-animate keyframes
to Base UI data-starting/ending-style transitions; menus, popovers and
tooltips skip motion when opened from the keyboard (data-instant).
- Dialog gains an `instant` prop; the command palette uses it.
- Accordion animates its real panel height; caret rotates instead of
swapping icons.
- Menu backdrop blur moves onto the popup so it no longer snaps in after
the fade; context menus and comboboxes fade only.
- Sidebar collapse uses the strong ease-out curve and snaps on Cmd+B.
- Toast, sheet, checkbox, tabs, toggle, inputs and message scroller get
tokenised easing, correct transition properties and press feedback.
- Tabs no longer squeeze a trigger narrower than its label.
- Spinners keep spinning under prefers-reduced-motion.
Web app
- Remove the default route view transition and page-entrance slides.
- Add EASE_OUT_STRONG for Motion; replace built-in "easeOut" everywhere.
- Switch LazyMotion to domMax so layout and Reorder animations run.
- Builder: consolidate 12 section list files into one ItemsSection,
opacity-only list rows with popLayout, instant Cmd+0, faster dock zoom,
crossfade that no longer dips, transform-based progress bars.
- Dashboard: keep previous results while sorting/filtering, no empty-state
flash, uncontrolled sidebar (no network round trip on collapse), calmer
resume card tilt, no stacked hover wrappers.
- Settings: drop entrance/stagger wrappers and ActionButton.
- Agent: CSS marquee paused on hover; thread switches keep the layout.
- Homepage: fix invalid transition declarations, CSS spotlight drift,
scroll-hiding header without a JS spring, tokenised curves.
- Theme switches change every color at once.
- Remove SSR-only useIsClient guards from the SPA.
Docs: rewrite the DESIGN.md animation section around the new tokens.
* feat(editor): support whole-paragraph indentation
* fix(docx): retain indentation in quotes and RTL documents
* fix(exports): bound paragraph insets in narrow PDF columns
* fix(pdf): type bounded paragraph rendering consistently
* test(editor): use explicit list conversion commands
* fix(docx): preserve quote inset on list items
* fix(components/form): resolve FormControl label target regressions (#3369)
- Expose FormControlContext and wrap FormControl children in Base UI's
LabelableProvider so the generated control id reaches the actual
labelable element.
- Update InputGroup/InputGroupInput to consume the context and place
the id on the real input instead of the fieldset.
- Update Slider to discard the wrapper id and use the context via
LabelableProvider so the thumb input receives the id and
aria-labelledby.
- Update ChipInput to consume the context, set id and aria-labelledby
on the inner input, and only fall back to aria-label when not inside
a FormItem.
- Restructure the sidebar layout so a single FormControl labels the
numeric input and the visible FormLabel is referenced by id for the
sibling Slider, removing the duplicate-id defect.
- Add a dev-time warning when the generated id lands on a non-labelable
or missing element.
- Extend form.test.tsx with regression coverage.
* test(form): add regression coverage for chip-input and dual-control layout
* fix(ui): surface FormControl error state as aria-invalid on the Slider control
- Problem: FormControl injects aria-invalid={hasError} onto its rendered
element, but Slider stripped it without re-applying it anywhere, so the
error state never reached the DOM (flagged by Codacy/Greptile/CodeRabbit).
- Fix: bridge aria-invalid onto Base UI's native range input via the Thumb's
public inputRef prop; Base UI v1.7 has no prop path for it (its validation
props only apply through Base UI Field context). id stays stripped since
LabelableProvider already delivers it to the input.
- Verification: new regression test in form.test.tsx fails on the pre-fix
head (aria-invalid null) and passes post-fix; packages/ui 363/363 tests
green; tsc --noEmit on packages/ui clean.
* fix(ui): let a caller-supplied data-slot override the Slider default
- Problem: the FormControl label-target fix moved data-slot="slider" after
{...props} on SliderPrimitive.Root, so a caller's data-slot was silently
overwritten with the default — a prop-ordering regression against both the
prior file and the repo-wide convention (FormItem, FormLabel, InputGroup all
place data-slot before the spread).
- Fix: restore data-slot="slider" before {...props} so caller values win.
- Verification: packages/ui — vitest src/components/slider.test.tsx
src/components/form.test.tsx = 30/30 passing; new regression test
("lets a caller-supplied data-slot override the default") fails on the
pre-fix head (data-slot="slider" wins) and passes with the fix; tsc
--noEmit clean.
* fix(ui): preserve standalone Slider and InputGroup identity props
- Problem: the FormControl prop strip dropped a standalone caller's id on
Slider and id/aria-describedby/aria-invalid on InputGroup, so standalone
compositions rendered no element carrying those attributes (regression
vs main, flagged by maintainer review on this PR).
- Fix: strip the FormControl-generated props only when a FormControl
ancestor is present (useFormControl context); preserve explicit caller
props for standalone usage in both components.
- Verification: new standalone + FormControl-wrapped tests fail on the
prior head and pass after the fix; packages/ui 367/367, apps/web
595/595, tsgo --noEmit clean.
* fix(ui): remove internal label provider dependency
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix(stylesheet): keep color picker state aligned with source
* fix(stylesheet): serialize picker edits as hex with alpha
* fix: preserve contextual colors in stylesheet editor
Rewrites the landing page, in-app microcopy, and public docs, then fixes what the rewrite exposed: stale template counts, a broken quickstart anchor, out-of-sync FAQ structured data, dead error-hint branches in the MCP tools, and wrong-sense translations across all 53 locales. Adds GLOSSARY.md so translators get the right sense of the ambiguous UI terms.
Swaps sonner's toast.success/error/loading/dismiss for the new toast.add({ type,
description }) and toast.close across dialogs, auth pages, the builder, the
dashboard and the applications views. Behaviour is unchanged.
The icon picker grid starts with an empty string entry that renders the
"no icon" (prohibit) symbol, but the onClick guard "if (icon)" treated
the empty string as falsy and ignored the click. Change the guard to
check for a defined string value so the empty/no-icon option can be
selected.
Closes#3252Closes#3261
Co-authored-by: Devin <devin@example.com>
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* feat(applications): job application tracker with AI copilot
Add an Applications module at /dashboard/applications: pipeline board
(dnd-kit), table view with bulk actions, Insights (fit tiles, funnel,
sources, shareable funnel-flow SVG), campaigns, tags, CSV import, and
Add/Edit/Detail slide-overs. Each application links a live Reactive
Resume.
AI "Application Copilot" (applications.ai.*): job-posting autofill,
resume↔job match score (fit ring), resume tailoring, and cover-letter /
follow-up drafting — via the user's configured provider.
Board cards + table rows get context menus (edit / move / archive /
delete). Charts are CSS/SVG (no new chart dep); adds a UI Checkbox.
Also includes local TanStack devtools setup and toolchain bumps.
Claude-Session: https://claude.ai/code/session_01TEeRHnEayw2MFCShFRyL5f
* feat(applications): close follow-up gaps + squash migrations
Finish the deferred/open items on the applications tracker:
- Cover-letter upload re-enabled. Fix the storage blocker by deriving the
key extension from content type (buildFileKey/EXTENSION_BY_CONTENT_TYPE)
instead of hardcoding .jpeg, so PDFs serve correctly and non-JPEG image
avatars keep working under FLAG_DISABLE_IMAGE_PROCESSING. Add
coverLetterUrl/coverLetterName columns + Documents-section upload/remove.
- Contacts editor in the detail sheet (add/edit/remove, keyed per app).
- Board caps rendered cards per column (COLUMN_PAGE_SIZE=50 + "Show more").
- Extract new Lingui messages across locales.
- Guard coverLetterUrl to http(s)/relative at the API boundary.
Squash the five branch-only application-table migrations (create -> +tags
-> +cover-letter -> drop -> re-add) into a single clean CREATE TABLE via
drizzle-kit generate.
Claude-Session: https://claude.ai/code/session_01TEeRHnEayw2MFCShFRyL5f
* chore: update dependencies
* fix(web): address React Doctor findings — compiler, purity, query, component structure
prefer-module-scope-pure-function: hoist buildSubtitle, getDecimalPlaces,
handleLocaleChange, onLocaleChange, stop, listContent/groupedListContent to
module scope so they aren't rebuilt on every render.
react-compiler-todo (??=): rewrite draft.metadata.styleRules ??= [] to the
non-assignment form to unblock auto-memoization.
set-state-in-effect: derive updatedAtLabel at render time instead of syncing
it through useState + useEffect.
query-destructure-result: destructure useQuery results at call site in
resume-analysis and resume-thumbnail to follow TanStack Query v5 convention.
only-export-components: extract non-component exports to sibling .ts files so
Fast Refresh can preserve component state:
- getNextWeights → typography/get-next-weights.ts
- detectJsonImportType + ImportType → dialogs/resume/import.utils.ts
- getLocaleOptions → features/locale/locale-options.tsx
- preview helpers + DEFAULT_PDF_PAGE_SIZE → preview.shared.utils.ts
- resolveHighlightToolbarState + defaultHighlightColor → rich-input.utils.ts
- computeDelta + getSparklinePoints → statistics.utils.ts
no-multi-comp: split multi-component files into focused companions:
- ResumePane + ToolbarButton → routes/agent/-components/resume-pane.tsx
- DesktopBuilderShell → builder/$resumeId/-components/desktop-builder-shell.tsx
- MobileBuilderShell + helpers → builder/$resumeId/-components/mobile-builder-shell.tsx
- setBuilderLayout/getBuilderLayout moved to -store/sidebar.ts
fix(tests): add Resume type import to section-builder mocks and cast partial
mock data as unknown as Resume to satisfy stricter type checking; fix
noExplicitAny Biome errors in the same mocks.
* feat(applications): improve performance
* chore: fix knip issues
* perf(builder): halve per-keystroke render cost
Section-form fields called `form.handleSubmit()` on every keystroke, which
re-validated the whole form and toggled submit state — firing the render
cascade twice per character (~6809 renders/keystroke, FPS dropping to 9).
Persist via a form-level `listeners.onChange` instead and drop the per-field
`handleSubmit()` (basics, custom-fields, design). Narrow header/dock resume
subscriptions to metadata slices so they no longer re-render on content edits.
Cuts renders 6809 -> 3403 per keystroke (50%), 0 frame drops. Save, preview,
and design controls verified working; 449/449 web tests pass.
* perf(home): eliminate hero CLS from unreserved video box
The hero <section> is `flex items-center` (shrink-to-fit), so the video
wrapper's width depended on the video's intrinsic size, which only resolves
after the media loads. aspect-ratio couldn't reserve height without a definite
width, so the video grew from ~190px to ~563px after first paint and shoved the
centered hero text down ~373px (CLS ~0.095).
Give the wrapper a definite width (w-full + mx-auto on the CometCard) and set an
explicit aspect ratio + width/height on the video so its box is reserved before
load. CLS 0.095 -> 0; hero stays visually centered at max-w-4xl.
* docs: add application tracker guides
* chore(db): squash application migrations
* fix(email): import React in auth template for server-side rendering compatibility
* chore(release): v5.2.1
* Refactor resume rendering and builder workflows
* fix: address application tracker review findings
Code-review findings:
- donation-toast: restore path/secure/sameSite cookie attributes the
inlined useCookie dropped (security/scope regression).
- utils/color: restore @uiw fallback so percentage-notation rgb() still
converts (custom style-rule colors are arbitrary strings); add tests
pinning the one real difference vs the black fallback.
- knip: drop stale npm-check-updates ignoreDependencies entry.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
The 107-line useCookie hook had exactly one consumer (donation-toast) that
only read + set-with-expiry. Inline the two Cookies.* calls directly and
delete the hook + its 128-line test. Drop js-cookie and @types/js-cookie
from packages/ui/package.json (apps/web retains its own js-cookie dep).
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
* 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>
* 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>
* 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
* 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.