Compare commits

..
Author SHA1 Message Date
Amruth Pillai c288675b16 Release v5.2.9 (#3382)
* feat(ats): add ATS checker and replace resume analysis

Adds a public, browser-only ATS checker at /ats-checker and an ATS Check
section in the builder's right sidebar. PDFs are parsed locally: text
extraction, reading order, contact and date recovery, section detection,
and file-level readability are scored deterministically, with evidence
cited per finding and skipped checks reported rather than counted as
passes.

Removes the AI-scored resume analysis it supersedes: the resume_analysis
table (dropped via migration), the get_resume_analysis MCP tool, and
POST /ai/analyze-resume. The replacement, POST /ai/ats-review, reviews
extracted resume text and returns qualitative feedback with no score.

Also bumps the version to 5.2.9 and adds the changelog entry.

* chore(deps): bump workspace dependencies

* fix(ats-checker): keep negation inside each 'what this does not do' bullet

The three bullets were bare fragments whose negation came from the
section heading, which translators never see. A dozen locales rendered
them as affirmative assertions or imperatives, so the page claimed the
checker enforces a one-page rule and predicts rejection -- the opposite
of the source, and directly contradicted by the sentence beside it.

Each bullet now carries its own negation, so the polarity cannot be
lost in translation. Re-extracted and refilled across all 53 target
locales.
2026-08-27 03:37:01 +02:00
Santhi PrakashandAmruth Pillai e065a10824 fix(pdf): resolve bold text weight from the family's bold face (#3335)
* fix(pdf): resolve bold text weight from the family's bold face

Bold text (<strong>, rich-text bold, template bold styles) previously
rendered at the last stored body weight, which is ambiguous: families
are commonly stored as ["400","600"] (the typography picker's default
pairing), so bold rendered at SemiBold — nearly indistinguishable from
Regular for faces like Open Sans (#3310).

Add resolveBoldFontWeight() to the fonts package: keep a deliberate
stored bold-class choice (>= 700), else prefer the family's true Bold
face ("700"), else the heaviest >= 600 face; return null so callers
keep their existing fallback when the family has no bold-class face.

Wire it through use-register-fonts, the shared base-template-styles
builder, base-styles and the Scizor template. Default body IBM Plex
Serif ["400","500"] now renders bold at 700 (base-reset-fidelity
expectation updated accordingly).

Fixes #3310

* fix(pdf): register bold fallback faces for CJK glyph substitution

When resolveBoldFontWeight maps stored weights like ["400","600"] to the
family's 700 face, register that weight on each PDF fallback font too so
glyph-level substitution keeps bold glyphs instead of snapping to 600.

Also reorder @reactive-resume/fonts imports per Biome convention.

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-08-27 03:16:46 +02:00
Santhi Prakashgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>Amruth Pillaiautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
b47f805321 fix(pdf): render emoji via a Noto Emoji script fallback (#3351)
* fix(pdf): render emoji via a Noto Emoji script fallback

Emoji in resume content (flags, globe, pictographs) rendered as mojibake
in the preview and PDF export because the per-codepoint fallback chain
registered no emoji-capable font: every font in the stack lacked the
glyphs, so layout fell through to single-byte standard-font encoding —
each UTF-16 code unit truncated to its low byte (#3321).

Follows the #2986/#3190 script-fallback pattern: detect emoji content
(regional indicators unioned with Extended_Pictographic), map it to the
monochrome Noto Emoji web font (TrueType glyf outlines, PDF-embeddable),
and register it in the fallback stack for both serif and sans stacks.
Out-of-range weight requests alias to the nearest served weight (300-700)
so registration never falls back to the preview subset.

* fix(pdf): detect keycap emoji via the combining enclosing keycap

Greptile review on #3351: keycap sequences like 1\uFE0F\u20E3 carry no
regional indicator and no Extended_Pictographic codepoint, so they
bypassed the emoji detector and rendered garbled — the exact class of
bug #3321 fixes. Union U+20E3 into the detector; every valid keycap
sequence contains it.

* Update packages/utils/src/locale.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-27 03:15:34 +02:00
Santhi Prakash 2761bd6715 fix(fonts): register Vazirmatn in webfont catalog for JSON imports (#3331)
* fix(fonts): register Vazirmatn in webfont catalog for JSON imports

Imported resumes can set typography.fontFamily to Vazirmatn, but the
popularity-sorted Google Fonts slice omits it so PDF registration fell
back to IBM Plex Serif and Persian/Arabic glyphs stacked or tofu (#3098).

Add Vazirmatn as a locale-coverage manual entry (same pattern as Carlito)
and cover catalog resolution with unit tests.

* test(pdf): keep Vazirmatn as primary family for fa-IR registration

Prove JSON-imported Vazirmatn is handed to Font.register instead of
being rewritten to IBM Plex Serif (#3098).

* fix(fonts): address CodeRabbit review on Vazirmatn catalog

Assert getWebFontSource resolves files["400"] for Vazirmatn instead of
only matching the preview fallback, and split the font-generation log
line to satisfy the 120-column Biome limit.
2026-08-27 03:15:31 +02:00
Santhi Prakash a416d01112 fix(pdf): constrain bullet list content width within horizontal margin (#3367)
Change richListItemContent from flex: initial to flex: 1 with minWidth: 0.
This makes the content area fill remaining space after the marker and
columnGap, so text wraps within the user-set margin instead of overflowing.

Fixes #3336
2026-08-27 03:15:28 +02:00
github-actions[bot]andCrowdin Bot 7fac6f29c0 Sync Translations from Crowdin (#3381)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-08-26 16:25:56 +02:00
Amruth Pillai d3dddf229b Update .gitignore and AGENTS.md 2026-08-26 14:23:42 +00:00
Amruth Pillai 3c195dc3f8 Release v5.2.8 (#3375)
Upgrades to Better Auth 1.7, expands Custom Styles coverage of item headers, and adds a human-approval step to the AI agent's resume edits.

Breaking for self-hosters using a custom OAuth provider: the callback path changes from /api/auth/oauth2/callback/custom to /api/auth/callback/custom, and installs using OAUTH_DISCOVERY_URL need one additional UPDATE after upgrading. Both are documented in docs/self-hosting/sso.mdx.

- Better Auth 1.7, with the account issuer migration and the jwks alg/crv columns the 1.7 jwt plugin requires
- Agent edits gated behind an approval step, with crash-safe runs and context pruning
- item-header now covers every section header row on every template; adds the item-header-row part
- Fixes provider unlinking, auth error messages, and version conflicts on freshly created resumes
- New /auth/error page, translated across all 53 target locales
- DeepSeek Harness plugin moved into packages/dsh-plugin
- Dependency bumps across the workspace
2026-08-24 21:44:16 +02:00
github-actions[bot]andCrowdin Bot 3221afda9d Sync Translations from Crowdin (#3365)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-08-20 10:38:31 +02:00
Amruth Pillai 8ce899a04b feat(agent): omit resume documents from the copied conversation json 2026-08-20 09:40:57 +02:00
Amruth Pillai 39f36b4ac5 fix(resume): guard patch versions in the transaction, not in sql
Postgres defaultNow() stores microseconds while JS Dates are millisecond-truncated, so the SQL equality guard matched zero rows on freshly created resumes and every guarded agent patch failed with a permanent version conflict. The SELECT ... FOR UPDATE lock plus the in-transaction ms-precision check already provide the guarantee; drop the SQL predicate. Verified A/B against a live database.
2026-08-20 09:28:31 +02:00
Amruth Pillai 39590eaff6 fix(auth): allow unlinking providers after the session ages past a day (#3364)
Better Auth guards `/unlink-account` with `freshSessionMiddleware`, which
rejects any session whose `createdAt` is older than `freshAge` (one day by
default). Sessions here last a week and there is no re-authentication flow to
refresh that timestamp, so disconnecting a provider failed with
`SESSION_NOT_FRESH` for every user who signed in more than a day ago.

Disable the freshness gate, and teach `getReadableErrorMessage` to read plain
error objects: Better Auth client errors are `{ code, message, status }`
objects rather than `Error` instances, so every auth toast was collapsing to
its generic fallback instead of showing the real reason.
2026-08-20 08:20:18 +02:00
Amruth Pillai c8081ac2fe feat(agent): adopt AI SDK v7 — crash safety, context pruning, HITL approvals (#3362)
* docs(adr): propose agent AI SDK v7 adoption plan

* fix(ai): bind analyzeResume through aiService in service test

The test destructured analyzeResume as a named export that does not exist; main was red.

* test(agent): keep pure ai helpers real via spread-actual mock factory

* feat(agent): add run guards, patch version guard, run wall-clock timeout

* feat(agent): validate UI messages at the send boundary

* feat(agent): crash-safe draft-row persistence and server-side cancellation

* feat(agent): reap stale run claims at boot, on send, and on thread open

* feat(agent): fresh-document patch output and tiered context pruning

* feat(ai): shared agent tool contracts and message metadata schema

* feat(agent): add per-thread review-patches setting with update endpoint

* feat(agent): gate resume patches behind hmac-signed tool approval

* feat(agent): merge question answers and approval decisions before run claim

* feat(agent): approval ui with composed auto-send and fixture-driven tests

* feat(agent): usage metadata, tool activity cards, smoother streaming

* feat(agent): tool-call repair, input examples, structured step logging

* chore(i18n): translate new agent workspace strings across all locales

* fix(agent): gate stale-run draft cancellation on winning the claim clear

Snapshot streaming drafts before the conditional clear and skip the flip entirely when another reaper or a replacement run already cleared the claim. Also address review nits in eleven locale catalogs.

* fix(agent): flip reaped drafts only when their snapshotted state is unchanged

* fix(agent): address review findings across run lifecycle, context budget, and approval flow

- bind patches to the revision the model read via signed baseUpdatedAt
- claim the run before consuming a continuation; recorded-but-unexecuted approvals retry as pending continuations
- keep run ownership on stop() until cancellation persists; preserve the claim for the reaper when final persistence fails
- estimate tokens without serializing binary attachments (tokenx) and enforce the budget by dropping oldest whole turns
- mark crash-recovered patch results as snapshot boundaries; strip /data prefixes at execution time
- retry failed continuations without regenerate; mount a single AgentChat; disable response controls on read-only threads; freeze review toggle during runs (client+server)
- accumulate usage across continuations and match the SDK's nested usage shape; label-form token strings; reorderable source label; accessible note field; state-neutral web-search label

* chore(i18n): translate revised agent strings across all locales

* fix(agent): harden baseUpdatedAt validation and address review follow-ups

- bundle tokenx in the server runtime dependencies (e2e boot failure)
- strict ISO schema for baseUpdatedAt plus loud executor rejection of unparseable values
- it-IT source label consistency (Fonte)
- prove penultimate-turn retention in the context pruning test

* chore(deps): exempt tokenx from knip for the externalized server bundle
2026-08-20 08:06:53 +02:00
autofix-ci[bot] dbbab6fd76 [autofix.ci] apply automated fixes 2026-08-19 03:49:17 +00:00
Emanuele Tonello 8acde4c1ac fix(ai): provide current date to resume analysis (#3353) 2026-08-19 05:48:31 +02:00
Amruth Pillai 4d53a6d1de fix(stylesheet): apply item-header to every header row on every template (#3357)
`SectionItemHeader` only rendered its own box when a template opted into
`mainItemHeaderBorder` (only Ditgar did). Everywhere else it walked the
header children and attached the resolved `item-header` style to the first
descendant that happened to be a literal `View` or `InlineItemHeader`.

Sections whose header starts with anything else — certifications, awards,
projects, publications, references — matched nothing, so the style was
silently dropped; stacked headers such as experience matched only their
first row, so a second row went unstyled.

The header now always renders its own `Div`, so `item-header` covers the
whole header row of every section on every template. `Div` rather than
`View` keeps the base row gap the rows used to inherit from the item box,
and Ditgar keeps its tight header via `rowGap: 0` on its own
`sectionItemHeader` slot, so rendered output is unchanged apart from the
newly styled rows. `mainItemHeaderBorder` is now dead and removed.

Fixes #3349
2026-08-19 02:24:38 +02:00
331 changed files with 105059 additions and 9259 deletions
-55
View File
@@ -1,55 +0,0 @@
# design-sync notes — @reactive-resume/ui
Syncs to Claude Design project **Reactive Resume** (`3c0f6556-050a-41e5-9886-c3f1ea950517`).
## Repo shape / build
- `@reactive-resume/ui` is **source-consumed** (pnpm workspace, no `dist`, exports point at `src/components/*.tsx`). Runs in the converter's **synth-entry mode** (no `--entry`).
- `buildCmd` = `node .design-sync/build-css.mjs`. That one script does three things, all required before every converter run:
1. Creates the workspace **self-symlink** `packages/ui/node_modules/@reactive-resume/ui -> ../../../ui` (pnpm doesn't self-install it; the converter resolves the DS as `node_modules/<pkg>` and esbuild needs it for `@reactive-resume/ui/components/*` self-imports).
2. Emits real **`.d.ts`** to `packages/ui/dist/types` via `tsc -p packages/ui/tsconfig.emit.json`. Without this, synth-entry mode gives weak `{[key]: unknown}` prop contracts; with it the converter's `findTypesRoot` picks up `dist/types` and every component gets real props (variant/size unions, inherited Base UI props).
3. Compiles Tailwind v4 `globals.css` → self-contained `packages/ui/.ds-compiled.css` (`cfg.cssEntry`): inlines the IBM Plex Sans latin variable woff2 as a data-URI and strips all other `@font-face` (extra scripts + the Phosphor icon web font, which previews don't use — components render Phosphor as inline React SVGs). This is why previews are fully styled with tokens + brand font and there are zero dangling font URLs.
- CSS entry scans `.design-sync/tw-entry.css` which `@import`s globals.css and adds `@source "./previews/*.tsx"` so utility classes used in authored previews are compiled. **Preview layout wrappers use inline styles** anyway (so subagents needn't recompile the shared CSS); only component-level utility classes need the recompile.
## Card scope
- The package exports **202 symbols** (39 primary components + 163 compound sub-parts). User chose **~40 primary cards**: `cfg.componentSrcMap` nulls the 163 sub-parts. All 202 stay importable from `window.RRUI` (the bundle exports everything regardless of the card list), so previews compose sub-parts (`RRUI.DialogContent`, etc.) freely.
- Multi-primary files represented by one card: `combobox.tsx`→ComboboxRoot, `form.tsx`→FormItem, `resizable.tsx`→ResizableGroup, `toast.tsx`→Toaster.
## Preview authoring conventions (calibrated on Button / Alert / Dialog)
- Import naturally: `import { Button } from "@reactive-resume/ui/components/button"` — converter rule 2 redirects any exported-component module to `window.RRUI`, and sub-parts resolve too.
- Icons: `@phosphor-icons/react` with the `*Icon` suffix (e.g. `PlusIcon`, `TrashIcon`, `WarningIcon`). Bundles into the preview.
- Base UI compose pattern: `render={<Button variant="outline" />}` on `*.Trigger` / `*.Close` etc.
- Layout wrappers: inline `style={{ display:"flex", gap, padding }}` — not Tailwind (keeps fan-out from needing CSS recompiles).
- **Overlays** (Dialog, and expect the same for AlertDialog/Sheet/Popover/HoverCard/DropdownMenu/ContextMenu/Tooltip/Command-dialog): render open via `defaultOpen`, and set `cfg.overrides.<Name> = {cardMode:"single", primaryStory:"<export>", viewport:"WxH"}`. Use viewport width ≥ 640 so `sm:` breakpoint styles (e.g. horizontal dialog footer) engage — Dialog uses `760x440`.
- Realistic resume-app content (resumes, sections, publish/export/share), never foo/bar.
## Component composition notes (from the authoring wave)
- **Real `.d.ts` contracts require the barrel** (see build step 2 + `publishConfig.types`). Base UI prop names differ from Radix/native: Switch `defaultChecked`+`size`; Toggle `defaultPressed`+`variant`+`size`; Slider `defaultValue` array (`[n]` single / `[a,b]` range). Use uncontrolled `default*` props in previews to avoid controlled-without-onChange warnings.
- **BrandIcon renders the app's own logo/icon** (`variant="logo"|"icon"`), NOT a social/brand-slug icon. It `<img src>`s `/logo/*.svg` + `/icon/*.svg`, which the preview server (serving `ds-bundle/`) 404s. The BrandIcon preview inlines the real `apps/web/public/{logo,icon}/light.svg` as base64 `src` overrides (component spreads `{...props}` after its own `src`, so the override wins).
- **Overlays** handled by the orchestrator with `cfg.overrides` (cardMode single + primaryStory Open + viewport): Dialog, AlertDialog, Sheet, Popover, Tooltip, HoverCard, DropdownMenu, ContextMenu, ComboboxRoot. Command renders **inline** (cmdk, no overlay); Sidebar uses `collapsible="none"` to render inline (default offcanvas is fixed-positioned); Toaster fires a `duration:Infinity` toast on mount.
- **Providers composed in-preview** (no cfg.provider): Tooltip→TooltipProvider, Sidebar→SidebarProvider, MessageScroller→MessageScrollerProvider (+ explicit container height — Root is `size-full min-h-0` and collapses otherwise), FormItem carries its own context.
- **Accordion** opens statically via `defaultValue={[...itemValues]}` (the `--accordion-panel-height` warn is a non-issue — panels measure fine). **Tabs** via `defaultValue`. **ScrollArea/ResizableGroup/InputGroup** need an inline container height/width. **Separator** vertical needs an explicit height.
- Chat/attachment components (Attachment, Bubble, Message, MessageScroller, Marker) are all used only in `apps/web/src/routes/agent/-components/agent-chat.tsx` — the canonical composition source.
## Build/verify gotchas (learned the hard way)
- **A full `package-build` takes ~3-4 minutes** — not a hang. `@phosphor-icons/react` is a giant barrel, so each icon-importing preview costs ~10-20s of esbuild parse, and 30+ authored previews compile serially. Always run it in a real background task (not a 120s-capped foreground shell) and wait for completion.
- **Do NOT add a barrel `index.d.ts` + `publishConfig.types`** to get rich props for inline-param-typed components: it makes ts-morph resolve all 200+ inline Base UI param types and hangs the build for many minutes. Tried and reverted. Result: components with a named `<Name>Props` source type (Button) get real props; the rest get honest `{[key]: unknown}`.
- **Base UI menu Labels must be inside a Group**: `DropdownMenuLabel`/`ContextMenuLabel` throw `MenuGroupContext is missing` unless wrapped in `DropdownMenuGroup`/`ContextMenuGroup`. Same likely for other `*Label`/`*GroupLabel` menu parts.
- **`[RENDER_THIN]` (height 0px) is benign for fixed-position overlays** (Dialog, AlertDialog, Sheet): the content is `position:fixed` so it measures 0 in normal flow, but `rootEmpty:false` and the screenshot is correct. Confirmed via review sheets — not a failure.
- **`[GRID_OVERFLOW]` wide** → `cfg.overrides.<Name> = {cardMode:"column"}` applied to: Accordion, Attachment, Bubble, FormItem, InputGroup, Marker, Message, ResizableGroup, Tabs, Textarea. Toaster (portal escape) → `{cardMode:"single", primaryStory:"Notification"}`.
## Known render warns (triaged, not failures)
- `[TOKENS_MISSING]`: `--active-tab-{top,left,height,width}` (Base UI tab indicator sets these at runtime), `--accordion-panel-height` (Base UI accordion runtime), `--tw` (Tailwind internal), plus app-level `--resume-preview-page-gap` / `--page-primary-color` (defined by apps/web, not this package). All expected absent from the shipped stylesheet — components set them at runtime. Do not chase.
- `--font-heading` is referenced (DialogTitle `font-heading`) but not defined in the UI package tokens (app-level). Falls back to `--font-body` (IBM Plex). Cosmetic only.
- Unauthored primitives render near-empty floor cards (`[RENDER_BLANK]` for empty Button/Input/etc.) — resolved once authored.
## Re-sync risks
- `packages/ui/dist/types`, `packages/ui/.ds-compiled.css`, `packages/ui/.ds-tw-raw.css`, and the self-symlink are all gitignored build artifacts regenerated by `buildCmd` — always run `node .design-sync/build-css.mjs` before the converter/driver.
- The inlined IBM Plex font path in `build-css.mjs` is pinned to `@fontsource-variable/ibm-plex-sans/files/ibm-plex-sans-latin-wght-normal.woff2`; if that dep moves, the font inline breaks (previews fall back to system sans).
- `tsconfig.emit.json` is committed; if the package adds a real build later, prefer pointing the converter at that dist and drop the emit step.
-55
View File
@@ -1,55 +0,0 @@
#!/usr/bin/env node
// design-sync CSS build: compile the UI package's Tailwind v4 globals.css to
// static CSS, then make it self-contained for preview rendering by inlining the
// IBM Plex Sans (latin) variable webfont as a data-URI and dropping the other
// @font-face rules (extra scripts + the Phosphor icon font, which previews
// don't use — components render Phosphor as inline React SVGs).
//
// Output: packages/ui/.ds-compiled.css (cfg.cssEntry, bounded to the package)
import { execFileSync } from "node:child_process";
import { existsSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
const repo = resolve(here, "..");
// pnpm doesn't self-install the workspace package into its own node_modules,
// but the design-sync converter resolves the DS as node_modules/<pkg>. Create
// the self-symlink so PKG_DIR resolves and esbuild finds @reactive-resume/ui/*
// self-imports. Mirrors the sibling symlinks pnpm already writes (utils, config).
const selfLink = resolve(repo, "packages/ui/node_modules/@reactive-resume/ui");
if (!existsSync(selfLink)) symlinkSync("../../../ui", selfLink);
// Emit real .d.ts declarations (the package is source-consumed with no build).
// The converter's findTypesRoot picks up dist/types, giving components real
// prop contracts (variant/size unions, inherited Base UI props) instead of the
// weak `{[key]: unknown}` synth-entry fallback.
execFileSync(resolve(repo, "node_modules/.bin/tsc"), ["-p", "tsconfig.emit.json"], {
cwd: resolve(repo, "packages/ui"),
stdio: "inherit",
});
// NOTE: a barrel index.d.ts + publishConfig.types was tried to give the prop
// extractor an entry for components with inline param types — but resolving all
// 200+ inline Base UI param types through ts-morph's checker hangs the build
// (many minutes). Reverted. Components with a named <Name>Props source type
// (e.g. Button) still extract real props from dist/types; the rest fall back to
// the honest `{[key]: unknown}` contract, with usage carried by the preview +
// .prompt.md. See .design-sync/NOTES.md "Re-sync risks".
const cli = resolve(repo, ".ds-sync/node_modules/.bin/tailwindcss");
const entry = resolve(here, "tw-entry.css");
const tmp = resolve(repo, "packages/ui/.ds-tw-raw.css");
const out = resolve(repo, "packages/ui/.ds-compiled.css");
const font = resolve(
repo,
"packages/ui/node_modules/@fontsource-variable/ibm-plex-sans/files/ibm-plex-sans-latin-wght-normal.woff2",
);
execFileSync(cli, ["-i", entry, "-o", tmp], { stdio: "inherit" });
let css = readFileSync(tmp, "utf8");
css = css.replace(/@font-face\s*\{[^}]*\}/g, ""); // drop all shipped @font-face
const b64 = readFileSync(font).toString("base64");
const face = `@font-face{font-family:"IBM Plex Sans Variable";font-style:normal;font-weight:100 700;font-display:swap;src:url(data:font/woff2;base64,${b64}) format("woff2-variations")}\n`;
writeFileSync(out, face + css);
console.error(` build-css: wrote ${out} (${(Buffer.byteLength(face + css) / 1024).toFixed(0)} KB, font inlined)`);
-256
View File
@@ -1,256 +0,0 @@
{
"projectId": "3c0f6556-050a-41e5-9886-c3f1ea950517",
"pkg": "@reactive-resume/ui",
"globalName": "RRUI",
"shape": "package",
"buildCmd": "node .design-sync/build-css.mjs",
"tsconfig": "tsconfig.json",
"cssEntry": ".ds-compiled.css",
"componentSrcMap": {
"AccordionContent": null,
"AccordionItem": null,
"AccordionTrigger": null,
"AlertAction": null,
"AlertDescription": null,
"AlertDialogAction": null,
"AlertDialogCancel": null,
"AlertDialogContent": null,
"AlertDialogDescription": null,
"AlertDialogFooter": null,
"AlertDialogHeader": null,
"AlertDialogMedia": null,
"AlertDialogOverlay": null,
"AlertDialogPortal": null,
"AlertDialogTitle": null,
"AlertDialogTrigger": null,
"AlertTitle": null,
"AttachmentAction": null,
"AttachmentActions": null,
"AttachmentContent": null,
"AttachmentDescription": null,
"AttachmentGroup": null,
"AttachmentMedia": null,
"AttachmentTitle": null,
"AttachmentTrigger": null,
"AvatarBadge": null,
"AvatarFallback": null,
"AvatarGroup": null,
"AvatarGroupCount": null,
"AvatarImage": null,
"BubbleContent": null,
"BubbleGroup": null,
"BubbleReactions": null,
"ButtonGroupSeparator": null,
"ButtonGroupText": null,
"ComboboxChip": null,
"ComboboxChips": null,
"ComboboxChipsInput": null,
"ComboboxClear": null,
"ComboboxCollection": null,
"ComboboxContent": null,
"ComboboxEmpty": null,
"ComboboxGroup": null,
"ComboboxInput": null,
"ComboboxItem": null,
"ComboboxLabel": null,
"ComboboxList": null,
"ComboboxSeparator": null,
"ComboboxTrigger": null,
"ComboboxValue": null,
"CommandDialog": null,
"CommandEmpty": null,
"CommandGroup": null,
"CommandInput": null,
"CommandItem": null,
"CommandList": null,
"CommandSeparator": null,
"CommandShortcut": null,
"ContextMenuCheckboxItem": null,
"ContextMenuContent": null,
"ContextMenuGroup": null,
"ContextMenuItem": null,
"ContextMenuLabel": null,
"ContextMenuPortal": null,
"ContextMenuRadioGroup": null,
"ContextMenuRadioItem": null,
"ContextMenuSeparator": null,
"ContextMenuShortcut": null,
"ContextMenuSub": null,
"ContextMenuSubContent": null,
"ContextMenuSubTrigger": null,
"ContextMenuTrigger": null,
"DialogClose": null,
"DialogContent": null,
"DialogDescription": null,
"DialogFooter": null,
"DialogHeader": null,
"DialogOverlay": null,
"DialogPortal": null,
"DialogTitle": null,
"DialogTrigger": null,
"DropdownMenuCheckboxItem": null,
"DropdownMenuContent": null,
"DropdownMenuGroup": null,
"DropdownMenuItem": null,
"DropdownMenuLabel": null,
"DropdownMenuPortal": null,
"DropdownMenuRadioGroup": null,
"DropdownMenuRadioItem": null,
"DropdownMenuSeparator": null,
"DropdownMenuShortcut": null,
"DropdownMenuSub": null,
"DropdownMenuSubContent": null,
"DropdownMenuSubTrigger": null,
"DropdownMenuTrigger": null,
"FormControl": null,
"FormDescription": null,
"FormLabel": null,
"FormMessage": null,
"HoverCardContent": null,
"HoverCardTrigger": null,
"InputGroupAddon": null,
"InputGroupButton": null,
"InputGroupInput": null,
"InputGroupText": null,
"InputGroupTextarea": null,
"KbdGroup": null,
"MarkerContent": null,
"MarkerIcon": null,
"MessageAvatar": null,
"MessageContent": null,
"MessageFooter": null,
"MessageGroup": null,
"MessageHeader": null,
"MessageScrollerButton": null,
"MessageScrollerContent": null,
"MessageScrollerItem": null,
"MessageScrollerProvider": null,
"MessageScrollerViewport": null,
"PopoverContent": null,
"PopoverDescription": null,
"PopoverHeader": null,
"PopoverTitle": null,
"PopoverTrigger": null,
"ResizablePanel": null,
"ResizableSeparator": null,
"ScrollBar": null,
"SheetClose": null,
"SheetContent": null,
"SheetDescription": null,
"SheetFooter": null,
"SheetHeader": null,
"SheetTitle": null,
"SheetTrigger": null,
"SidebarContent": null,
"SidebarFooter": null,
"SidebarGroup": null,
"SidebarGroupAction": null,
"SidebarGroupContent": null,
"SidebarGroupLabel": null,
"SidebarHeader": null,
"SidebarInput": null,
"SidebarInset": null,
"SidebarMenu": null,
"SidebarMenuAction": null,
"SidebarMenuBadge": null,
"SidebarMenuButton": null,
"SidebarMenuItem": null,
"SidebarMenuSkeleton": null,
"SidebarMenuSub": null,
"SidebarMenuSubButton": null,
"SidebarMenuSubItem": null,
"SidebarProvider": null,
"SidebarRail": null,
"SidebarSeparator": null,
"SidebarTrigger": null,
"TabsContent": null,
"TabsIndicator": null,
"TabsList": null,
"TabsTrigger": null,
"TooltipContent": null,
"TooltipProvider": null,
"TooltipTrigger": null
},
"overrides": {
"Dialog": {
"cardMode": "single",
"primaryStory": "Open",
"viewport": "760x440"
},
"AlertDialog": {
"cardMode": "single",
"primaryStory": "Open",
"viewport": "640x460"
},
"Sheet": {
"cardMode": "single",
"primaryStory": "Open",
"viewport": "760x480"
},
"Popover": {
"cardMode": "single",
"primaryStory": "Open",
"viewport": "420x340"
},
"Tooltip": {
"cardMode": "single",
"primaryStory": "Open",
"viewport": "360x260"
},
"HoverCard": {
"cardMode": "single",
"primaryStory": "Open",
"viewport": "440x320"
},
"DropdownMenu": {
"cardMode": "single",
"primaryStory": "Open",
"viewport": "440x360"
},
"ContextMenu": {
"cardMode": "single",
"primaryStory": "Open",
"viewport": "440x340"
},
"ComboboxRoot": {
"cardMode": "single",
"primaryStory": "Open",
"viewport": "420x360"
},
"Accordion": {
"cardMode": "column"
},
"Attachment": {
"cardMode": "column"
},
"Bubble": {
"cardMode": "column"
},
"FormItem": {
"cardMode": "column"
},
"InputGroup": {
"cardMode": "column"
},
"Marker": {
"cardMode": "column"
},
"Message": {
"cardMode": "column"
},
"ResizableGroup": {
"cardMode": "column"
},
"Tabs": {
"cardMode": "column"
},
"Textarea": {
"cardMode": "column"
},
"Toaster": {
"cardMode": "single",
"primaryStory": "Notification"
}
},
"readmeHeader": ".design-sync/conventions.md"
}
-60
View File
@@ -1,60 +0,0 @@
# Reactive Resume UI — how to build with it
This is `@reactive-resume/ui`: a shadcn-style React component library built on **Base UI**
primitives and **Tailwind CSS v4**. Every component is real upstream code, bundled to the
`window.RRUI` global; the 39 cards are the primary components, but all their compound
sub-parts (e.g. `DialogContent`, `AccordionItem`, `SidebarMenuButton`) are also on `RRUI`.
## Setup & wrapping
- **No global provider is required.** All design tokens live on `:root` in `styles.css` (loaded
for you), so components are styled out of the box. For dark mode, add `class="dark"` to a
wrapping element — the same tokens flip to their dark values.
- **A few components need their own provider — wrap only where you use them:**
- `Tooltip*` → wrap in `RRUI.TooltipProvider`.
- `Sidebar*` → wrap in `RRUI.SidebarProvider`.
- `MessageScroller*` → wrap in `RRUI.MessageScrollerProvider` and give it a bounded height.
- Form fields → `RRUI.FormItem` provides the field context for `FormLabel`/`FormControl`/`FormMessage`.
- **Compose compound components** from their parts, e.g. `Dialog` = `DialogTrigger` + `DialogContent`
(+ `DialogHeader`/`DialogTitle`/`DialogDescription`/`DialogFooter`). Overlay parts (Dialog, Sheet,
Popover, DropdownMenu, ContextMenu, Tooltip, HoverCard) render into a portal. Menu labels must sit
inside a `*Group` (`DropdownMenuGroup`, `ContextMenuGroup`).
- Icons come from `@phosphor-icons/react` (the `*Icon` suffix, e.g. `PlusIcon`).
## Styling idiom — Tailwind utilities on semantic tokens
Components style themselves; for **your own** layout and surfaces, use Tailwind utility classes
bound to the design system's **semantic color tokens** (never raw hex — these adapt to light/dark):
| Purpose | Utilities |
|---|---|
| Surfaces | `bg-background`, `bg-card`, `bg-popover`, `bg-muted`, `bg-sidebar` |
| Brand / actions | `bg-primary` + `text-primary-foreground`, `bg-secondary` + `text-secondary-foreground` |
| Accents / hover | `bg-accent` + `text-accent-foreground`, `hover:bg-muted` |
| Danger | `bg-destructive`, `text-destructive` |
| Text | `text-foreground` (primary), `text-muted-foreground` (secondary) |
| Borders / focus | `border`, `border-input`, `ring-ring`, `outline-ring` |
| Radius | `rounded-md`, `rounded-lg` (driven by `--radius`) |
Each token is also a CSS variable (`var(--primary)`, `var(--muted-foreground)`, `var(--border)`,
`var(--radius)`, `--font-body` = IBM Plex Sans) if you need it in inline styles.
## Where the truth lives
- **Styling:** `styles.css` and its `@import` closure (`_ds_bundle.css` = component styles; the
token definitions on `:root`/`.dark`). Read these before inventing a class or color.
- **Per component:** `components/<group>/<Name>/<Name>.prompt.md` (usage) and `<Name>.d.ts` (props —
variant/size unions where a named type exists; some fall back to a permissive shape).
## Idiomatic snippet
```jsx
// A confirm action, styled with the DS's own tokens for the surrounding layout.
<div className="flex flex-col gap-3 rounded-lg border bg-card p-4">
<p className="text-sm text-muted-foreground">Publish this resume to your public profile?</p>
<div className="flex justify-end gap-2">
<RRUI.Button variant="outline">Cancel</RRUI.Button>
<RRUI.Button>Publish</RRUI.Button>
</div>
</div>
```
-52
View File
@@ -1,52 +0,0 @@
import type * as React from "react";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@reactive-resume/ui/components/accordion";
const wrap: React.CSSProperties = { width: 420, padding: 16 };
// Open by default so the panel content is visible in the card (Base UI accordion
// is uncontrolled via defaultValue, matching item `value` props).
export const Sections = () => (
<div style={wrap}>
<Accordion defaultValue={["experience"]}>
<AccordionItem value="experience">
<AccordionTrigger>Work Experience</AccordionTrigger>
<AccordionContent>
<p>Senior Product Designer · Framer — 2021 to Present</p>
<p>Led the redesign of the onboarding flow, lifting activation by 24% across web and mobile.</p>
</AccordionContent>
</AccordionItem>
<AccordionItem value="education">
<AccordionTrigger>Education</AccordionTrigger>
<AccordionContent>
<p>B.Des in Interaction Design · Rhode Island School of Design</p>
</AccordionContent>
</AccordionItem>
<AccordionItem value="skills">
<AccordionTrigger>Skills</AccordionTrigger>
<AccordionContent>
<p>Figma, prototyping, design systems, user research, and front-end handoff.</p>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
export const MultipleOpen = () => (
<div style={wrap}>
<Accordion multiple defaultValue={["summary", "certifications"]}>
<AccordionItem value="summary">
<AccordionTrigger>Professional Summary</AccordionTrigger>
<AccordionContent>
<p>Full-stack engineer with eight years shipping resilient TypeScript services and design systems.</p>
</AccordionContent>
</AccordionItem>
<AccordionItem value="certifications">
<AccordionTrigger>Certifications</AccordionTrigger>
<AccordionContent>
<p>AWS Solutions Architect · Professional</p>
<p>Certified Kubernetes Administrator</p>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
-38
View File
@@ -1,38 +0,0 @@
import type * as React from "react";
import { InfoIcon, WarningIcon } from "@phosphor-icons/react";
import { Alert, AlertAction, AlertDescription, AlertTitle } from "@reactive-resume/ui/components/alert";
import { Button } from "@reactive-resume/ui/components/button";
const wrap: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 16, padding: 16, maxWidth: 540 };
export const Default = () => (
<div style={wrap}>
<Alert>
<InfoIcon />
<AlertTitle>Resume saved</AlertTitle>
<AlertDescription>Your changes were saved automatically and synced to your account.</AlertDescription>
</Alert>
</div>
);
export const Destructive = () => (
<div style={wrap}>
<Alert variant="destructive">
<WarningIcon />
<AlertTitle>Export failed</AlertTitle>
<AlertDescription>We couldn't generate your PDF. Check your connection and try again.</AlertDescription>
</Alert>
</div>
);
export const WithAction = () => (
<div style={wrap}>
<Alert>
<AlertTitle>Unsaved changes</AlertTitle>
<AlertDescription>You have edits that haven't been published to your public resume yet.</AlertDescription>
<AlertAction>
<Button size="sm">Publish</Button>
</AlertAction>
</Alert>
</div>
);
-34
View File
@@ -1,34 +0,0 @@
import { WarningIcon } from "@phosphor-icons/react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
} from "@reactive-resume/ui/components/alert-dialog";
// Overlay — rendered open (defaultOpen). cfg.overrides.AlertDialog pins
// cardMode: single + viewport (content is fixed-positioned, centred).
export const Open = () => (
<AlertDialog defaultOpen>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogMedia>
<WarningIcon />
</AlertDialogMedia>
<AlertDialogTitle>Delete this resume?</AlertDialogTitle>
<AlertDialogDescription>
“Software Engineer” and its entire version history will be permanently removed. This action can’t be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="destructive">Delete resume</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
-84
View File
@@ -1,84 +0,0 @@
import type * as React from "react";
import { DownloadSimpleIcon, FileDocIcon, FilePdfIcon, TrashIcon, WarningIcon } from "@phosphor-icons/react";
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentGroup,
AttachmentMedia,
AttachmentTitle,
} from "@reactive-resume/ui/components/attachment";
const wrap: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 12, padding: 16, width: 360 };
export const WithActions = () => (
<div style={wrap}>
<Attachment>
<AttachmentMedia>
<FilePdfIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>Ansel_Bradford_Resume.pdf</AttachmentTitle>
<AttachmentDescription>248 KB · PDF</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Download">
<DownloadSimpleIcon />
</AttachmentAction>
<AttachmentAction aria-label="Remove">
<TrashIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
</div>
);
export const States = () => (
<div style={wrap}>
<Attachment size="sm" state="uploading">
<AttachmentMedia>
<FileDocIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>cover-letter.docx</AttachmentTitle>
<AttachmentDescription>Uploading…</AttachmentDescription>
</AttachmentContent>
</Attachment>
<Attachment size="sm" state="error">
<AttachmentMedia>
<WarningIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>portfolio-2024.zip</AttachmentTitle>
<AttachmentDescription>Upload failed · file too large</AttachmentDescription>
</AttachmentContent>
</Attachment>
</div>
);
export const Group = () => (
<div style={{ padding: 16, width: 360 }}>
<AttachmentGroup>
<Attachment orientation="vertical" size="sm">
<AttachmentMedia>
<FilePdfIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>Resume.pdf</AttachmentTitle>
<AttachmentDescription>248 KB</AttachmentDescription>
</AttachmentContent>
</Attachment>
<Attachment orientation="vertical" size="sm">
<AttachmentMedia>
<FileDocIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>cover-letter.docx</AttachmentTitle>
<AttachmentDescription>19 KB</AttachmentDescription>
</AttachmentContent>
</Attachment>
</AttachmentGroup>
</div>
);
-71
View File
@@ -1,71 +0,0 @@
import type * as React from "react";
import { CheckIcon } from "@phosphor-icons/react";
import {
Avatar,
AvatarBadge,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
} from "@reactive-resume/ui/components/avatar";
const row: React.CSSProperties = { display: "flex", alignItems: "center", gap: 16, padding: 20 };
export const Fallback = () => (
<div style={row}>
<Avatar>
<AvatarFallback>AP</AvatarFallback>
</Avatar>
<Avatar>
<AvatarFallback>JD</AvatarFallback>
</Avatar>
<Avatar>
<AvatarFallback>MK</AvatarFallback>
</Avatar>
</div>
);
export const WithStatus = () => (
<div style={row}>
<Avatar>
<AvatarFallback>AP</AvatarFallback>
<AvatarBadge>
<CheckIcon weight="bold" />
</AvatarBadge>
</Avatar>
<Avatar size="lg">
<AvatarFallback>SR</AvatarFallback>
<AvatarBadge />
</Avatar>
</div>
);
export const Sizes = () => (
<div style={row}>
<Avatar size="sm">
<AvatarFallback>AP</AvatarFallback>
</Avatar>
<Avatar size="default">
<AvatarFallback>AP</AvatarFallback>
</Avatar>
<Avatar size="lg">
<AvatarFallback>AP</AvatarFallback>
</Avatar>
</div>
);
export const Group = () => (
<div style={row}>
<AvatarGroup>
<Avatar>
<AvatarFallback>AP</AvatarFallback>
</Avatar>
<Avatar>
<AvatarFallback>JD</AvatarFallback>
</Avatar>
<Avatar>
<AvatarFallback>MK</AvatarFallback>
</Avatar>
<AvatarGroupCount>+5</AvatarGroupCount>
</AvatarGroup>
</div>
);
-40
View File
@@ -1,40 +0,0 @@
import type * as React from "react";
import { CheckCircleIcon, PencilSimpleIcon, SparkleIcon } from "@phosphor-icons/react";
import { Badge } from "@reactive-resume/ui/components/badge";
const row: React.CSSProperties = { display: "flex", flexWrap: "wrap", alignItems: "center", gap: 10, padding: 20 };
export const Variants = () => (
<div style={row}>
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="destructive">Destructive</Badge>
<Badge variant="outline">Outline</Badge>
</div>
);
export const StatusLabels = () => (
<div style={row}>
<Badge variant="secondary">
<CheckCircleIcon weight="fill" data-icon="inline-start" />
Published
</Badge>
<Badge variant="outline">
<PencilSimpleIcon data-icon="inline-start" />
Draft
</Badge>
<Badge>
<SparkleIcon weight="fill" data-icon="inline-start" />
Pro
</Badge>
<Badge variant="destructive">Expired</Badge>
</div>
);
export const Counts = () => (
<div style={row}>
<Badge>12</Badge>
<Badge variant="secondary">New</Badge>
<Badge variant="outline">v5.2</Badge>
</div>
);
File diff suppressed because one or more lines are too long
-48
View File
@@ -1,48 +0,0 @@
import type * as React from "react";
import { Bubble, BubbleContent, BubbleGroup, BubbleReactions } from "@reactive-resume/ui/components/bubble";
const wrap: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 8, padding: 16, width: 420 };
export const Conversation = () => (
<div style={wrap}>
<BubbleGroup>
<Bubble align="end">
<BubbleContent>Can you make my summary sound more senior without exaggerating?</BubbleContent>
</Bubble>
<Bubble variant="muted" align="start">
<BubbleContent>
I tightened it to lead with scope and outcomes. Want me to mirror that tone in your experience bullets too?
</BubbleContent>
</Bubble>
<Bubble align="end">
<BubbleContent>Yes, keep it concise.</BubbleContent>
</Bubble>
</BubbleGroup>
</div>
);
export const Variants = () => (
<div style={wrap}>
<Bubble variant="default" align="end">
<BubbleContent>Applied 3 edits to your resume.</BubbleContent>
</Bubble>
<Bubble variant="tinted" align="start">
<BubbleContent>I emphasized measurable launch outcomes in your last role.</BubbleContent>
</Bubble>
<Bubble variant="outline" align="start">
<BubbleContent>Draft saved — publish when you're ready.</BubbleContent>
</Bubble>
<Bubble variant="destructive" align="start">
<BubbleContent>Couldn't reach the AI provider. Retry?</BubbleContent>
</Bubble>
</div>
);
export const WithReactions = () => (
<div style={{ padding: 24, width: 420 }}>
<Bubble variant="secondary" align="start">
<BubbleContent>Rewrote your headline to target a Senior Product Manager role.</BubbleContent>
<BubbleReactions>👍 2</BubbleReactions>
</Bubble>
</div>
);
-51
View File
@@ -1,51 +0,0 @@
import type * as React from "react";
import { ArrowRightIcon, PlusIcon, TrashIcon } from "@phosphor-icons/react";
import { Button } from "@reactive-resume/ui/components/button";
const row: React.CSSProperties = { display: "flex", flexWrap: "wrap", alignItems: "center", gap: 12, padding: 16 };
export const Variants = () => (
<div style={row}>
<Button>Save changes</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Delete</Button>
<Button variant="link">Learn more</Button>
</div>
);
export const Sizes = () => (
<div style={row}>
<Button size="xs">Extra small</Button>
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
</div>
);
export const WithIcons = () => (
<div style={row}>
<Button>
<PlusIcon /> Add section
</Button>
<Button variant="outline">
Continue <ArrowRightIcon />
</Button>
<Button variant="destructive">
<TrashIcon /> Remove
</Button>
<Button size="icon" variant="outline" aria-label="Add section">
<PlusIcon />
</Button>
</div>
);
export const Disabled = () => (
<div style={row}>
<Button disabled>Saving…</Button>
<Button variant="outline" disabled>
Disabled
</Button>
</div>
);
-67
View File
@@ -1,67 +0,0 @@
import type * as React from "react";
import {
AlignCenterHorizontalIcon,
AlignLeftIcon,
AlignRightIcon,
ArrowClockwiseIcon,
ArrowCounterClockwiseIcon,
TextBIcon,
TextItalicIcon,
TextUnderlineIcon,
} from "@phosphor-icons/react";
import { Button } from "@reactive-resume/ui/components/button";
import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText } from "@reactive-resume/ui/components/button-group";
const wrap: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 16, padding: 16 };
export const Formatting = () => (
<div style={wrap}>
<ButtonGroup>
<Button variant="outline" size="icon" aria-label="Bold">
<TextBIcon />
</Button>
<Button variant="outline" size="icon" aria-label="Italic">
<TextItalicIcon />
</Button>
<Button variant="outline" size="icon" aria-label="Underline">
<TextUnderlineIcon />
</Button>
<ButtonGroupSeparator />
<Button variant="outline" size="icon" aria-label="Align left">
<AlignLeftIcon />
</Button>
<Button variant="outline" size="icon" aria-label="Align center">
<AlignCenterHorizontalIcon />
</Button>
<Button variant="outline" size="icon" aria-label="Align right">
<AlignRightIcon />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">
<ArrowCounterClockwiseIcon /> Undo
</Button>
<Button variant="outline">
<ArrowClockwiseIcon /> Redo
</Button>
</ButtonGroup>
</div>
);
export const WithText = () => (
<div style={wrap}>
<ButtonGroup>
<ButtonGroupText>Zoom</ButtonGroupText>
<Button variant="outline">50%</Button>
<Button variant="outline">100%</Button>
<Button variant="outline">150%</Button>
</ButtonGroup>
<ButtonGroup orientation="vertical">
<Button variant="outline">Export PDF</Button>
<Button variant="outline">Export DOCX</Button>
<Button variant="outline">Copy link</Button>
</ButtonGroup>
</div>
);
-30
View File
@@ -1,30 +0,0 @@
import {
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxRoot,
} from "@reactive-resume/ui/components/combobox";
const skills = ["TypeScript", "React", "Node.js", "GraphQL", "PostgreSQL", "Kubernetes"];
// Base UI Combobox — items passed to Root, rendered open (defaultOpen).
// cfg.overrides.ComboboxRoot pins cardMode: single + viewport with room below.
export const Open = () => (
<div style={{ width: 320, padding: 16, paddingBottom: 200 }}>
<ComboboxRoot items={skills} defaultOpen>
<ComboboxInput placeholder="Add a skill…" />
<ComboboxContent>
<ComboboxEmpty>No skills found.</ComboboxEmpty>
<ComboboxList>
{(item: string) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</ComboboxRoot>
</div>
);
-42
View File
@@ -1,42 +0,0 @@
import { DownloadSimpleIcon, GearIcon, PlusIcon, UserIcon } from "@phosphor-icons/react";
import {
Command,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "@reactive-resume/ui/components/command";
// Command renders inline (cmdk) — a searchable command palette. No overlay.
export const Palette = () => (
<div style={{ width: 400, padding: 16 }}>
<div style={{ border: "1px solid var(--border)", borderRadius: 10, overflow: "hidden" }}>
<Command>
<CommandInput placeholder="Type a command or search…" />
<CommandList>
<CommandGroup heading="Actions">
<CommandItem>
<PlusIcon /> New resume
<CommandShortcut>⌘N</CommandShortcut>
</CommandItem>
<CommandItem>
<DownloadSimpleIcon /> Export as PDF
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Account">
<CommandItem>
<UserIcon /> Profile
</CommandItem>
<CommandItem>
<GearIcon /> Settings
<CommandShortcut>⌘,</CommandShortcut>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</div>
</div>
);
-48
View File
@@ -1,48 +0,0 @@
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuTrigger,
} from "@reactive-resume/ui/components/context-menu";
// Right-click menu — rendered open (defaultOpen) so the card shows the menu.
// cfg.overrides.ContextMenu pins cardMode: single + viewport.
export const Open = () => (
<div style={{ display: "flex", justifyContent: "center", padding: 24, paddingBottom: 160 }}>
<ContextMenu defaultOpen>
<ContextMenuTrigger>
<div
style={{
display: "grid",
placeItems: "center",
width: 240,
height: 96,
border: "1px dashed var(--border)",
borderRadius: 8,
color: "var(--muted-foreground)",
fontSize: 13,
}}
>
Right-click a resume card
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuGroup>
<ContextMenuLabel>Software Engineer</ContextMenuLabel>
<ContextMenuItem>Open</ContextMenuItem>
<ContextMenuItem>
Rename
<ContextMenuShortcut>F2</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>Duplicate</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuItem variant="destructive">Delete</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</div>
);
-30
View File
@@ -1,30 +0,0 @@
import { Button } from "@reactive-resume/ui/components/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@reactive-resume/ui/components/dialog";
// Overlay component — rendered open (defaultOpen) so the card shows the real
// dialog surface. cfg.overrides.Dialog pins cardMode: single + a viewport for
// the portal (content is fixed-positioned at the viewport centre).
export const Open = () => (
<Dialog defaultOpen>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete resume</DialogTitle>
<DialogDescription>
This permanently deletes “Software Engineer” along with its version history. This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose render={<Button variant="outline" />}>Cancel</DialogClose>
<DialogClose render={<Button variant="destructive" />}>Delete resume</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
-41
View File
@@ -1,41 +0,0 @@
import { CopyIcon, DownloadSimpleIcon, PencilIcon, TrashIcon } from "@phosphor-icons/react";
import { Button } from "@reactive-resume/ui/components/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@reactive-resume/ui/components/dropdown-menu";
// Anchored menu — rendered open (defaultOpen), positioned below its trigger.
// cfg.overrides.DropdownMenu pins cardMode: single + viewport with room below.
export const Open = () => (
<div style={{ display: "flex", justifyContent: "center", paddingTop: 16, paddingBottom: 220 }}>
<DropdownMenu defaultOpen>
<DropdownMenuTrigger render={<Button variant="outline" />}>Resume actions</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuGroup>
<DropdownMenuLabel>Software Engineer</DropdownMenuLabel>
<DropdownMenuItem>
<PencilIcon /> Rename
</DropdownMenuItem>
<DropdownMenuItem>
<CopyIcon /> Duplicate
<DropdownMenuShortcut>⌘D</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<DownloadSimpleIcon /> Export PDF
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">
<TrashIcon /> Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
-27
View File
@@ -1,27 +0,0 @@
import type * as React from "react";
import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
import { Input } from "@reactive-resume/ui/components/input";
// FormItem carries its own field context (id + error state) — FormLabel /
// FormControl / FormDescription / FormMessage compose under it standalone.
const wrap: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 16, padding: 16, width: 340 };
export const Default = () => (
<div style={wrap}>
<FormItem>
<FormLabel>Headline</FormLabel>
<FormControl render={<Input placeholder="Senior Software Engineer" />} />
<FormDescription>Shown under your name at the top of the resume.</FormDescription>
</FormItem>
</div>
);
export const WithError = () => (
<div style={wrap}>
<FormItem hasError>
<FormLabel>Email</FormLabel>
<FormControl render={<Input defaultValue="jane@" />} />
<FormMessage errors={["Enter a valid email address."]} />
</FormItem>
</div>
);
-19
View File
@@ -1,19 +0,0 @@
import { Button } from "@reactive-resume/ui/components/button";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@reactive-resume/ui/components/hover-card";
// Anchored preview-card overlay — rendered open (defaultOpen).
// cfg.overrides.HoverCard pins cardMode: single + viewport with room below the trigger.
export const Open = () => (
<div style={{ display: "flex", justifyContent: "center", paddingTop: 24, paddingBottom: 180 }}>
<HoverCard defaultOpen>
<HoverCardTrigger render={<Button variant="link" />}>@jane-doe</HoverCardTrigger>
<HoverCardContent>
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
<span style={{ fontWeight: 600 }}>Jane Doe</span>
<span style={{ color: "var(--muted-foreground)" }}>Senior Software Engineer · San Francisco</span>
<span style={{ color: "var(--muted-foreground)", fontSize: 12 }}>3 published resumes · joined 2023</span>
</div>
</HoverCardContent>
</HoverCard>
</div>
);
-40
View File
@@ -1,40 +0,0 @@
import type * as React from "react";
import { Input } from "@reactive-resume/ui/components/input";
import { Label } from "@reactive-resume/ui/components/label";
const field: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 6, padding: 16, width: 320 };
export const Default = () => (
<div style={field}>
<Label htmlFor="full-name">Full name</Label>
<Input id="full-name" defaultValue="Ada Lovelace" />
</div>
);
export const Placeholder = () => (
<div style={field}>
<Label htmlFor="headline">Headline</Label>
<Input id="headline" placeholder="e.g. Senior Software Engineer" />
</div>
);
export const Email = () => (
<div style={field}>
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" defaultValue="ada@analyticalengine.dev" />
</div>
);
export const Invalid = () => (
<div style={field}>
<Label htmlFor="website">Website</Label>
<Input id="website" aria-invalid defaultValue="not-a-valid-url" />
</div>
);
export const Disabled = () => (
<div style={field}>
<Label htmlFor="username">Username</Label>
<Input id="username" disabled defaultValue="ada.lovelace" />
</div>
);
-55
View File
@@ -1,55 +0,0 @@
import type * as React from "react";
import { CopyIcon, GlobeIcon, MagnifyingGlassIcon } from "@phosphor-icons/react";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@reactive-resume/ui/components/input-group";
const wrap: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 16, padding: 16, width: 380 };
export const Addons = () => (
<div style={wrap}>
<InputGroup>
<InputGroupAddon>
<MagnifyingGlassIcon />
</InputGroupAddon>
<InputGroupInput placeholder="Search resumes" defaultValue="Product Designer" />
</InputGroup>
<InputGroup>
<InputGroupAddon>
<GlobeIcon />
<InputGroupText>rxresu.me/u/</InputGroupText>
</InputGroupAddon>
<InputGroupInput defaultValue="jordan-rivera" />
</InputGroup>
<InputGroup>
<InputGroupInput readOnly defaultValue="rxr_live_9f3c8a21bd47e50a" />
<InputGroupAddon align="inline-end">
<InputGroupButton size="icon-sm" aria-label="Copy API key">
<CopyIcon />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
);
export const WithTextarea = () => (
<div style={wrap}>
<InputGroup>
<InputGroupTextarea
rows={3}
defaultValue="Senior product designer focused on design systems, accessibility, and shipping polished interfaces."
/>
<InputGroupAddon align="block-end">
<InputGroupText>240 characters left</InputGroupText>
<InputGroupButton style={{ marginLeft: "auto" }}>Generate with AI</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
);
-68
View File
@@ -1,68 +0,0 @@
import type * as React from "react";
import { Kbd, KbdGroup } from "@reactive-resume/ui/components/kbd";
const row: React.CSSProperties = { display: "flex", flexWrap: "wrap", alignItems: "center", gap: 12, padding: 20 };
const listRow: React.CSSProperties = {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 24,
fontSize: 13,
color: "var(--foreground)",
};
const col: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 10, padding: 20, minWidth: 260 };
export const Keys = () => (
<div style={row}>
<Kbd>⌘</Kbd>
<Kbd>⇧</Kbd>
<Kbd>⌥</Kbd>
<Kbd>Esc</Kbd>
<Kbd>Enter</Kbd>
<Kbd>Tab</Kbd>
</div>
);
export const Combinations = () => (
<div style={row}>
<KbdGroup>
<Kbd>⌘</Kbd>
<Kbd>K</Kbd>
</KbdGroup>
<KbdGroup>
<Kbd>⌘</Kbd>
<Kbd>S</Kbd>
</KbdGroup>
<KbdGroup>
<Kbd>⌘</Kbd>
<Kbd>⇧</Kbd>
<Kbd>P</Kbd>
</KbdGroup>
</div>
);
export const ShortcutList = () => (
<div style={col}>
<div style={listRow}>
<span>Command palette</span>
<KbdGroup>
<Kbd>⌘</Kbd>
<Kbd>K</Kbd>
</KbdGroup>
</div>
<div style={listRow}>
<span>Save resume</span>
<KbdGroup>
<Kbd>⌘</Kbd>
<Kbd>S</Kbd>
</KbdGroup>
</div>
<div style={listRow}>
<span>Undo</span>
<KbdGroup>
<Kbd>⌘</Kbd>
<Kbd>Z</Kbd>
</KbdGroup>
</div>
</div>
);
-39
View File
@@ -1,39 +0,0 @@
import type * as React from "react";
import { Input } from "@reactive-resume/ui/components/input";
import { Label } from "@reactive-resume/ui/components/label";
import { Switch } from "@reactive-resume/ui/components/switch";
const field: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 6, padding: 16, width: 320 };
const row: React.CSSProperties = { display: "flex", alignItems: "center", gap: 10, padding: 16, width: 320 };
export const WithInput = () => (
<div style={field}>
<Label htmlFor="company">Company</Label>
<Input id="company" defaultValue="Analytical Engine Co." />
</div>
);
export const Required = () => (
<div style={field}>
<Label htmlFor="job-title">
Job title <span style={{ color: "var(--destructive)" }}>*</span>
</Label>
<Input id="job-title" placeholder="e.g. Lead Engineer" />
</div>
);
export const WithSwitch = () => (
<div style={row}>
<Switch id="public-resume" defaultChecked />
<Label htmlFor="public-resume">Public resume</Label>
</div>
);
export const Disabled = () => (
<div style={field}>
<Label htmlFor="locked-field" data-disabled="true" style={{ opacity: 0.5 }}>
Locked field
</Label>
<Input id="locked-field" disabled defaultValue="Read only" />
</div>
);
-39
View File
@@ -1,39 +0,0 @@
import type * as React from "react";
import { CheckCircleIcon, SparkleIcon, WarningCircleIcon } from "@phosphor-icons/react";
import { Marker, MarkerContent, MarkerIcon } from "@reactive-resume/ui/components/marker";
const wrap: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 12, padding: 16, width: 360 };
export const Statuses = () => (
<div style={wrap}>
<Marker style={{ width: "fit-content", borderRadius: 8, padding: "12px 16px", background: "var(--muted)" }}>
<MarkerIcon>
<SparkleIcon />
</MarkerIcon>
<MarkerContent>Tailoring your resume…</MarkerContent>
</Marker>
<Marker style={{ width: "fit-content" }}>
<MarkerIcon>
<CheckCircleIcon />
</MarkerIcon>
<MarkerContent>Applied 4 edits to your resume</MarkerContent>
</Marker>
<Marker style={{ width: "fit-content" }}>
<MarkerIcon>
<WarningCircleIcon />
</MarkerIcon>
<MarkerContent>Couldn't reach the AI provider</MarkerContent>
</Marker>
</div>
);
export const Dividers = () => (
<div style={wrap}>
<Marker variant="separator">
<MarkerContent>Today</MarkerContent>
</Marker>
<Marker variant="border">
<MarkerContent>Conversation history</MarkerContent>
</Marker>
</div>
);
-65
View File
@@ -1,65 +0,0 @@
import type * as React from "react";
import { SparkleIcon, UserIcon } from "@phosphor-icons/react";
import { Bubble, BubbleContent } from "@reactive-resume/ui/components/bubble";
import {
Message,
MessageAvatar,
MessageContent,
MessageFooter,
MessageGroup,
MessageHeader,
} from "@reactive-resume/ui/components/message";
const avatar: React.CSSProperties = { display: "flex", alignItems: "center", justifyContent: "center", padding: 8 };
export const Conversation = () => (
<div style={{ display: "flex", padding: 16, width: 460 }}>
<MessageGroup style={{ width: "100%" }}>
<Message align="end">
<MessageAvatar>
<span style={avatar}>
<UserIcon />
</span>
</MessageAvatar>
<MessageContent>
<Bubble align="end">
<BubbleContent>Tailor my resume for a product manager role.</BubbleContent>
</Bubble>
</MessageContent>
</Message>
<Message align="start">
<MessageAvatar>
<span style={avatar}>
<SparkleIcon />
</span>
</MessageAvatar>
<MessageContent>
<Bubble variant="muted" align="start">
<BubbleContent>
Done — I emphasized roadmap ownership and stakeholder communication in your summary.
</BubbleContent>
</Bubble>
</MessageContent>
</Message>
</MessageGroup>
</div>
);
export const WithMeta = () => (
<div style={{ display: "flex", padding: 16, width: 460 }}>
<Message align="start">
<MessageAvatar>
<span style={avatar}>
<SparkleIcon />
</span>
</MessageAvatar>
<MessageContent>
<MessageHeader>Reactive AI</MessageHeader>
<Bubble variant="tinted" align="start">
<BubbleContent>I found 4 weak bullets and rewrote them with stronger verbs and metrics.</BubbleContent>
</Bubble>
<MessageFooter>Just now · applied 4 edits</MessageFooter>
</MessageContent>
</Message>
</div>
);
-57
View File
@@ -1,57 +0,0 @@
import type * as React from "react";
import { SparkleIcon, UserIcon } from "@phosphor-icons/react";
import { Bubble, BubbleContent } from "@reactive-resume/ui/components/bubble";
import { Message, MessageAvatar, MessageContent } from "@reactive-resume/ui/components/message";
import {
MessageScroller,
MessageScrollerButton,
MessageScrollerContent,
MessageScrollerItem,
MessageScrollerProvider,
MessageScrollerViewport,
} from "@reactive-resume/ui/components/message-scroller";
const avatar: React.CSSProperties = { display: "flex", alignItems: "center", justifyContent: "center", padding: 8 };
const turns = [
{ role: "user", text: "Can you review my resume for a senior engineering role?" },
{ role: "assistant", text: "Sure — I'll focus on scope, impact, and leadership signals. Reading it now." },
{ role: "user", text: "Great, keep the tone concise." },
{
role: "assistant",
text: "I rewrote your summary and tightened three experience bullets with measurable outcomes.",
},
{ role: "user", text: "Perfect, publish the draft." },
{ role: "assistant", text: "Draft saved and published to your public resume. Anything else you'd like to refine?" },
];
export const Thread = () => (
<div style={{ height: 340, width: 460, padding: 12 }}>
<MessageScrollerProvider autoScroll defaultScrollPosition="end">
<MessageScroller>
<MessageScrollerViewport>
<MessageScrollerContent style={{ padding: 12 }}>
{turns.map((turn, index) => (
<MessageScrollerItem key={turn.text} messageId={`turn-${index}`}>
<Message align={turn.role === "user" ? "end" : "start"}>
<MessageAvatar>
<span style={avatar}>{turn.role === "user" ? <UserIcon /> : <SparkleIcon />}</span>
</MessageAvatar>
<MessageContent>
<Bubble
variant={turn.role === "user" ? "default" : "muted"}
align={turn.role === "user" ? "end" : "start"}
>
<BubbleContent>{turn.text}</BubbleContent>
</Bubble>
</MessageContent>
</Message>
</MessageScrollerItem>
))}
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>
</div>
);
-33
View File
@@ -1,33 +0,0 @@
import { Button } from "@reactive-resume/ui/components/button";
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from "@reactive-resume/ui/components/popover";
// Anchored overlay — rendered open (defaultOpen), positioned below its trigger.
// cfg.overrides.Popover pins cardMode: single + viewport with room for the popup.
export const Open = () => (
<div style={{ display: "flex", justifyContent: "center", paddingTop: 24, paddingBottom: 220 }}>
<Popover defaultOpen>
<PopoverTrigger render={<Button variant="outline" />}>Share resume</PopoverTrigger>
<PopoverContent>
<PopoverHeader>
<PopoverTitle>Public link</PopoverTitle>
<PopoverDescription>Anyone with this link can view your published resume.</PopoverDescription>
</PopoverHeader>
<div style={{ display: "flex", gap: 8 }}>
<Button size="sm" variant="secondary">
Copy link
</Button>
<Button size="sm" variant="ghost">
Open
</Button>
</div>
</PopoverContent>
</Popover>
</div>
);
-57
View File
@@ -1,57 +0,0 @@
import type * as React from "react";
import { ResizableGroup, ResizablePanel, ResizableSeparator } from "@reactive-resume/ui/components/resizable";
const panelStyle: React.CSSProperties = { height: "100%", padding: 16, fontSize: 14, lineHeight: 1.6 };
const label: React.CSSProperties = {
fontSize: 11,
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.05em",
color: "var(--muted-foreground)",
marginBottom: 8,
};
export const BuilderLayout = () => (
<div style={{ height: 240, width: 460, border: "1px solid var(--border)", borderRadius: 8, overflow: "hidden" }}>
<ResizableGroup orientation="horizontal">
<ResizablePanel defaultSize={40}>
<div style={panelStyle}>
<div style={label}>Editor</div>
<div>Basics</div>
<div>Work Experience</div>
<div>Education</div>
<div>Skills</div>
</div>
</ResizablePanel>
<ResizableSeparator withHandle />
<ResizablePanel defaultSize={60}>
<div style={{ ...panelStyle, background: "var(--muted)" }}>
<div style={label}>Live Preview</div>
<div style={{ fontWeight: 600, fontSize: 16 }}>Jordan Rivera</div>
<div style={{ color: "var(--muted-foreground)" }}>Senior Product Designer</div>
</div>
</ResizablePanel>
</ResizableGroup>
</div>
);
export const VerticalSplit = () => (
<div style={{ height: 240, width: 300, border: "1px solid var(--border)", borderRadius: 8, overflow: "hidden" }}>
<ResizableGroup orientation="vertical">
<ResizablePanel defaultSize={50}>
<div style={panelStyle}>
<div style={label}>Summary</div>
<div>Eight years building design systems and shipping delightful product experiences.</div>
</div>
</ResizablePanel>
<ResizableSeparator withHandle />
<ResizablePanel defaultSize={50}>
<div style={{ ...panelStyle, background: "var(--muted)" }}>
<div style={label}>Contact</div>
<div>jordan.rivera@email.com</div>
<div>San Francisco, CA</div>
</div>
</ResizablePanel>
</ResizableGroup>
</div>
);
-39
View File
@@ -1,39 +0,0 @@
import type * as React from "react";
import { ScrollArea } from "@reactive-resume/ui/components/scroll-area";
const templates = [
{ name: "Azurill", tag: "Minimal" },
{ name: "Bronzor", tag: "Classic" },
{ name: "Chikorita", tag: "Modern" },
{ name: "Ditto", tag: "Compact" },
{ name: "Gengar", tag: "Bold" },
{ name: "Glalie", tag: "Elegant" },
{ name: "Kakuna", tag: "Timeless" },
{ name: "Leafish", tag: "Creative" },
{ name: "Nosepass", tag: "Formal" },
{ name: "Onyx", tag: "Technical" },
{ name: "Pikachu", tag: "Friendly" },
{ name: "Rhyhorn", tag: "Corporate" },
];
const rowStyle: React.CSSProperties = {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "10px 14px",
borderBottom: "1px solid var(--border)",
fontSize: 14,
};
export const TemplateList = () => (
<ScrollArea style={{ height: 240, width: 320, border: "1px solid var(--border)", borderRadius: 8 }}>
<div style={{ padding: 4 }}>
{templates.map((template) => (
<div key={template.name} style={rowStyle}>
<span style={{ fontWeight: 500 }}>{template.name}</span>
<span style={{ color: "var(--muted-foreground)", fontSize: 12 }}>{template.tag}</span>
</div>
))}
</div>
</ScrollArea>
);
-45
View File
@@ -1,45 +0,0 @@
import type * as React from "react";
import { Separator } from "@reactive-resume/ui/components/separator";
const block: React.CSSProperties = {
display: "flex",
flexDirection: "column",
gap: 12,
padding: 20,
maxWidth: 360,
fontSize: 13,
color: "var(--foreground)",
};
const inline: React.CSSProperties = {
display: "flex",
alignItems: "center",
gap: 12,
padding: 20,
fontSize: 13,
color: "var(--muted-foreground)",
};
export const Horizontal = () => (
<div style={block}>
<div>
<strong style={{ display: "block", fontSize: 14 }}>Amruth Pillai</strong>
<span style={{ color: "var(--muted-foreground)" }}>Senior Software Engineer</span>
</div>
<Separator />
<span style={{ color: "var(--muted-foreground)" }}>
Building resume tooling at Reactive Resume. Open-source enthusiast.
</span>
</div>
);
export const Vertical = () => (
<div style={inline}>
<span>Profile</span>
<Separator orientation="vertical" style={{ height: 16 }} />
<span>Experience</span>
<Separator orientation="vertical" style={{ height: 16 }} />
<span>Education</span>
<Separator orientation="vertical" style={{ height: 16 }} />
<span>Skills</span>
</div>
);
-38
View File
@@ -1,38 +0,0 @@
import { Button } from "@reactive-resume/ui/components/button";
import { Label } from "@reactive-resume/ui/components/label";
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} from "@reactive-resume/ui/components/sheet";
// Side drawer — rendered open (defaultOpen), anchored to the right edge.
// cfg.overrides.Sheet pins cardMode: single + viewport.
export const Open = () => (
<Sheet defaultOpen>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Resume settings</SheetTitle>
<SheetDescription>Control how “Software Engineer” appears when shared publicly.</SheetDescription>
</SheetHeader>
<div style={{ display: "flex", flexDirection: "column", gap: 14, padding: "0 16px" }}>
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
<Label>Public slug</Label>
<span style={{ fontSize: 13, color: "var(--muted-foreground)" }}>rxresume.me/jane-doe</span>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
<Label>Visibility</Label>
<span style={{ fontSize: 13, color: "var(--muted-foreground)" }}>Anyone with the link can view</span>
</div>
</div>
<SheetFooter>
<SheetClose render={<Button variant="outline" />}>Cancel</SheetClose>
<SheetClose render={<Button />}>Save changes</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
);
-61
View File
@@ -1,61 +0,0 @@
import { FileTextIcon, GearIcon, HouseIcon, PlusIcon } from "@phosphor-icons/react";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
} from "@reactive-resume/ui/components/sidebar";
// SidebarProvider supplies context + --sidebar-width. `collapsible="none"`
// renders the sidebar inline (the default offcanvas variant is fixed-positioned
// and would escape the card).
export const Navigation = () => (
<SidebarProvider>
<div
style={{ height: 400, display: "flex", border: "1px solid var(--border)", borderRadius: 10, overflow: "hidden" }}
>
<Sidebar collapsible="none">
<SidebarHeader>
<div style={{ padding: 8, fontWeight: 600, fontSize: 14 }}>Reactive Resume</div>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Workspace</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton isActive>
<HouseIcon /> Dashboard
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<FileTextIcon /> Resumes
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<PlusIcon /> New resume
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton>
<GearIcon /> Settings
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
</div>
</SidebarProvider>
);
-41
View File
@@ -1,41 +0,0 @@
import type * as React from "react";
import { Skeleton } from "@reactive-resume/ui/components/skeleton";
const pad: React.CSSProperties = { padding: 20 };
export const TextLines = () => (
<div style={{ ...pad, display: "flex", flexDirection: "column", gap: 10, width: 320 }}>
<Skeleton style={{ height: 12, width: "70%" }} />
<Skeleton style={{ height: 12, width: "100%" }} />
<Skeleton style={{ height: 12, width: "90%" }} />
<Skeleton style={{ height: 12, width: "40%" }} />
</div>
);
export const ProfileHeader = () => (
<div style={{ ...pad, display: "flex", alignItems: "center", gap: 14 }}>
<Skeleton style={{ height: 48, width: 48, borderRadius: "9999px" }} />
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<Skeleton style={{ height: 14, width: 160 }} />
<Skeleton style={{ height: 12, width: 100 }} />
</div>
</div>
);
export const ResumeCard = () => (
<div
style={{
...pad,
display: "flex",
flexDirection: "column",
gap: 12,
width: 220,
border: "1px solid var(--border)",
borderRadius: 12,
}}
>
<Skeleton style={{ height: 140, width: "100%" }} />
<Skeleton style={{ height: 14, width: "60%" }} />
<Skeleton style={{ height: 12, width: "40%" }} />
</div>
);
-33
View File
@@ -1,33 +0,0 @@
import type * as React from "react";
import { Label } from "@reactive-resume/ui/components/label";
import { Slider } from "@reactive-resume/ui/components/slider";
const field: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 10, padding: 16, width: 320 };
export const Single = () => (
<div style={field}>
<Label>Skill level</Label>
<Slider defaultValue={[4]} min={0} max={5} step={1} />
</div>
);
export const Range = () => (
<div style={field}>
<Label>Experience (years)</Label>
<Slider defaultValue={[2, 8]} min={0} max={15} step={1} />
</div>
);
export const FontScale = () => (
<div style={field}>
<Label>Font size</Label>
<Slider defaultValue={[62]} min={0} max={100} />
</div>
);
export const Disabled = () => (
<div style={field}>
<Label style={{ opacity: 0.5 }}>Line height (locked)</Label>
<Slider defaultValue={[50]} min={0} max={100} disabled />
</div>
);
-35
View File
@@ -1,35 +0,0 @@
import type * as React from "react";
import { Spinner } from "@reactive-resume/ui/components/spinner";
const row: React.CSSProperties = { display: "flex", alignItems: "center", gap: 20, padding: 24 };
export const Sizes = () => (
<div style={row}>
<Spinner style={{ width: 16, height: 16 }} />
<Spinner style={{ width: 24, height: 24 }} />
<Spinner style={{ width: 32, height: 32 }} />
</div>
);
export const Colors = () => (
<div style={row}>
<Spinner style={{ width: 28, height: 28, color: "var(--primary)" }} />
<Spinner style={{ width: 28, height: 28, color: "var(--muted-foreground)" }} />
</div>
);
export const LoadingRow = () => (
<div
style={{
display: "flex",
alignItems: "center",
gap: 10,
padding: 20,
fontSize: 13,
color: "var(--muted-foreground)",
}}
>
<Spinner style={{ width: 18, height: 18 }} />
<span>Generating your PDF…</span>
</div>
);
-40
View File
@@ -1,40 +0,0 @@
import type * as React from "react";
import { Label } from "@reactive-resume/ui/components/label";
import { Switch } from "@reactive-resume/ui/components/switch";
const row: React.CSSProperties = { display: "flex", alignItems: "center", gap: 10, padding: 16, width: 300 };
const stack: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 16, padding: 16, width: 300 };
export const On = () => (
<div style={row}>
<Switch id="sw-public" defaultChecked />
<Label htmlFor="sw-public">Public resume</Label>
</div>
);
export const Off = () => (
<div style={row}>
<Switch id="sw-template" />
<Label htmlFor="sw-template">Show icons in template</Label>
</div>
);
export const Small = () => (
<div style={row}>
<Switch id="sw-page-numbers" size="sm" defaultChecked />
<Label htmlFor="sw-page-numbers">Show page numbers</Label>
</div>
);
export const Disabled = () => (
<div style={stack}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<Switch id="sw-ai" disabled defaultChecked />
<Label htmlFor="sw-ai">AI suggestions</Label>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<Switch id="sw-index" disabled />
<Label htmlFor="sw-index">Index on search engines</Label>
</div>
</div>
);
-57
View File
@@ -1,57 +0,0 @@
import type * as React from "react";
import { BriefcaseIcon, GraduationCapIcon, SparkleIcon } from "@phosphor-icons/react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@reactive-resume/ui/components/tabs";
const wrap: React.CSSProperties = { width: 460, padding: 16 };
const panel: React.CSSProperties = { padding: "12px 4px", lineHeight: 1.6 };
export const ResumeSections = () => (
<div style={wrap}>
<Tabs defaultValue="experience">
<TabsList>
<TabsTrigger value="experience">
<BriefcaseIcon /> Experience
</TabsTrigger>
<TabsTrigger value="education">
<GraduationCapIcon /> Education
</TabsTrigger>
<TabsTrigger value="skills">
<SparkleIcon /> Skills
</TabsTrigger>
</TabsList>
<TabsContent value="experience" style={panel}>
<strong>Staff Engineer · Vercel</strong>
<div>Owned the edge runtime rollout serving 2B requests per day.</div>
</TabsContent>
<TabsContent value="education" style={panel}>
<strong>M.S. Computer Science · Carnegie Mellon</strong>
<div>Focus on distributed systems and human-computer interaction.</div>
</TabsContent>
<TabsContent value="skills" style={panel}>
<strong>Core stack</strong>
<div>TypeScript, React, Go, PostgreSQL, and Kubernetes.</div>
</TabsContent>
</Tabs>
</div>
);
export const LineVariant = () => (
<div style={wrap}>
<Tabs defaultValue="preview">
<TabsList variant="line">
<TabsTrigger value="preview">Preview</TabsTrigger>
<TabsTrigger value="share">Share</TabsTrigger>
<TabsTrigger value="export">Export</TabsTrigger>
</TabsList>
<TabsContent value="preview" style={panel}>
Your resume renders live as you edit each section.
</TabsContent>
<TabsContent value="share" style={panel}>
Publish a public link at reactive-resume.app/u/your-name.
</TabsContent>
<TabsContent value="export" style={panel}>
Download a print-ready PDF or DOCX in one click.
</TabsContent>
</Tabs>
</div>
);
-37
View File
@@ -1,37 +0,0 @@
import type * as React from "react";
import { Label } from "@reactive-resume/ui/components/label";
import { Textarea } from "@reactive-resume/ui/components/textarea";
const field: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 6, padding: 16, width: 360 };
export const Default = () => (
<div style={field}>
<Label htmlFor="summary">Summary</Label>
<Textarea id="summary" placeholder="Write a short professional summary…" rows={4} />
</div>
);
export const Filled = () => (
<div style={field}>
<Label htmlFor="about">About</Label>
<Textarea
id="about"
rows={4}
defaultValue="Mathematician and writer, known for early work on Charles Babbage's Analytical Engine and the first published algorithm intended for a machine."
/>
</div>
);
export const Invalid = () => (
<div style={field}>
<Label htmlFor="bio">Biography</Label>
<Textarea id="bio" aria-invalid rows={3} defaultValue="Too short." />
</div>
);
export const Disabled = () => (
<div style={field}>
<Label htmlFor="notes">Internal notes</Label>
<Textarea id="notes" disabled rows={3} defaultValue="Notes are locked while this resume is published." />
</div>
);
-20
View File
@@ -1,20 +0,0 @@
import { useEffect } from "react";
import { Toaster, toast } from "@reactive-resume/ui/components/toast";
// Toaster is the toast host. Fire a persistent toast on mount so the card
// shows a real notification instead of an empty portal.
export const Notification = () => {
useEffect(() => {
toast.add({
type: "success",
title: "Resume published",
description: "“Software Engineer” is now live at rxresume.me/jane-doe.",
timeout: 0,
});
}, []);
return (
<div style={{ minHeight: 140 }}>
<Toaster />
</div>
);
};
-66
View File
@@ -1,66 +0,0 @@
import type * as React from "react";
import {
TextAlignCenterIcon,
TextAlignLeftIcon,
TextAlignRightIcon,
TextBolderIcon,
TextItalicIcon,
TextUnderlineIcon,
} from "@phosphor-icons/react";
import { Toggle } from "@reactive-resume/ui/components/toggle";
const row: React.CSSProperties = { display: "flex", alignItems: "center", gap: 8, padding: 16 };
const group: React.CSSProperties = { display: "flex", alignItems: "center", gap: 2, padding: 16 };
export const States = () => (
<div style={row}>
<Toggle aria-label="Bold" defaultPressed>
<TextBolderIcon />
</Toggle>
<Toggle aria-label="Italic">
<TextItalicIcon />
</Toggle>
<Toggle aria-label="Underline" disabled>
<TextUnderlineIcon />
</Toggle>
</div>
);
export const Outline = () => (
<div style={row}>
<Toggle variant="outline" defaultPressed>
<TextBolderIcon /> Bold
</Toggle>
<Toggle variant="outline">
<TextItalicIcon /> Italic
</Toggle>
</div>
);
export const Sizes = () => (
<div style={row}>
<Toggle size="sm" aria-label="Bold small">
<TextBolderIcon />
</Toggle>
<Toggle size="default" aria-label="Bold default" defaultPressed>
<TextBolderIcon />
</Toggle>
<Toggle size="lg" aria-label="Bold large">
<TextBolderIcon />
</Toggle>
</div>
);
export const AlignmentGroup = () => (
<div style={group}>
<Toggle variant="outline" aria-label="Align left" defaultPressed>
<TextAlignLeftIcon />
</Toggle>
<Toggle variant="outline" aria-label="Align center">
<TextAlignCenterIcon />
</Toggle>
<Toggle variant="outline" aria-label="Align right">
<TextAlignRightIcon />
</Toggle>
</div>
);
-18
View File
@@ -1,18 +0,0 @@
import { InfoIcon } from "@phosphor-icons/react";
import { Button } from "@reactive-resume/ui/components/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@reactive-resume/ui/components/tooltip";
// Anchored overlay — needs TooltipProvider; rendered open (defaultOpen).
// cfg.overrides.Tooltip pins cardMode: single + viewport with room above the trigger.
export const Open = () => (
<TooltipProvider>
<div style={{ display: "flex", justifyContent: "center", paddingTop: 120, paddingBottom: 24 }}>
<Tooltip defaultOpen>
<TooltipTrigger render={<Button variant="outline" size="icon" aria-label="About visibility" />}>
<InfoIcon />
</TooltipTrigger>
<TooltipContent>Only you can see private resumes</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
);
-4
View File
@@ -1,4 +0,0 @@
/* design-sync Tailwind entry — compiles the UI package's globals.css to static CSS
for preview rendering, and also scans authored previews for used utilities. */
@import "../packages/ui/src/styles/globals.css";
@source "./previews/*.tsx";
+17
View File
@@ -0,0 +1,17 @@
<!-- caveman-begin -->
Respond terse like smart caveman. All technical substance stay. Only fluff die.
Rules:
- Drop: articles (a/an/the), filler (just/really/basically), pleasantries, hedging
- Fragments OK. Short synonyms. Technical terms exact. Code unchanged.
- Pattern: [thing] [action] [reason]. [next step].
- Not: "Sure! I'd be happy to help you with that."
- Yes: "Bug in auth middleware. Fix:"
Switch level: /caveman lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra
Stop: "stop caveman" or "normal mode"
Auto-Clarity: drop caveman for security warnings, irreversible actions, user confused. Resume after.
Boundaries: code/commits/PRs written normal.
<!-- caveman-end -->
+4 -11
View File
@@ -49,23 +49,16 @@ temp
.agents
.claude
.cursor
.codegraph
.opencode
.superpowers
.worktrees
.migration
graphify-out
# Local Storage Data
/data
/apps/web/data
# Git Hooks
.vite-hooks/
.ds-sync/
ds-bundle/
.design-sync/.cache/
.design-sync/learnings/
.design-sync/node_modules
packages/ui/.ds-compiled.css
packages/ui/.ds-tw-raw.css
packages/ui/dist/
i18n.cache
.vite-hooks
+43
View File
@@ -9,8 +9,51 @@ Before editing files for a substantial task:
- Multiple matches: prefer the most specific local skill for the package or concern you are changing; load additional skills only when the task spans multiple packages or concerns.
<!-- intent-skills:end -->
<!-- caveman-begin -->
Respond terse like smart caveman. All technical substance stay. Only fluff die.
Rules:
- Drop: articles (a/an/the), filler (just/really/basically), pleasantries, hedging
- Fragments OK. Short synonyms. Technical terms exact. Code unchanged.
- Pattern: [thing] [action] [reason]. [next step].
- Not: "Sure! I'd be happy to help you with that."
- Yes: "Bug in auth middleware. Fix:"
Switch level: /caveman lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra
Stop: "stop caveman" or "normal mode"
Auto-Clarity: drop caveman for security warnings, irreversible actions, user confused. Resume after.
Boundaries: code/commits/PRs written normal.
<!-- caveman-end -->
<!-- graphify-begin -->
## graphify
This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else.
Rules:
- For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
- Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it.
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
<!-- graphify-end -->
# AGENTS.md
## Agent skills
### Issue tracker
Issues and specs are tracked in GitHub Issues for `amruthpillai/reactive-resume`. See `docs/agents/issue-tracker.md`.
### Domain docs
This repository uses a multi-context domain-doc layout. See `docs/agents/domain.md`.
## Cursor Cloud specific instructions
### Overview
+31 -30
View File
@@ -18,25 +18,25 @@
"#react-pdf-renderer": "@react-pdf/renderer"
},
"dependencies": {
"@ai-sdk/anthropic": "^4.0.39",
"@ai-sdk/cerebras": "^3.0.31",
"@ai-sdk/cohere": "^4.0.27",
"@ai-sdk/deepseek": "^3.0.28",
"@ai-sdk/fireworks": "^3.0.33",
"@ai-sdk/google": "^4.0.44",
"@ai-sdk/groq": "^4.0.28",
"@ai-sdk/mistral": "^4.0.29",
"@ai-sdk/openai": "^4.0.42",
"@ai-sdk/openai-compatible": "^3.0.31",
"@ai-sdk/perplexity": "^4.0.29",
"@ai-sdk/togetherai": "^3.0.32",
"@ai-sdk/xai": "^4.0.40",
"@aws-sdk/client-s3": "^3.1111.0",
"@better-auth/api-key": "^1.6.29",
"@better-auth/drizzle-adapter": "^1.6.29",
"@better-auth/infra": "^0.3.7",
"@better-auth/oauth-provider": "^1.6.29",
"@better-auth/passkey": "^1.6.29",
"@ai-sdk/anthropic": "^4.0.44",
"@ai-sdk/cerebras": "^3.0.39",
"@ai-sdk/cohere": "^4.0.33",
"@ai-sdk/deepseek": "^3.0.35",
"@ai-sdk/fireworks": "^3.0.42",
"@ai-sdk/google": "^4.0.54",
"@ai-sdk/groq": "^4.0.33",
"@ai-sdk/mistral": "^4.0.35",
"@ai-sdk/openai": "^4.0.50",
"@ai-sdk/openai-compatible": "^3.0.39",
"@ai-sdk/perplexity": "^4.0.34",
"@ai-sdk/togetherai": "^3.0.40",
"@ai-sdk/xai": "^4.0.47",
"@aws-sdk/client-s3": "^3.1119.0",
"@better-auth/api-key": "^1.7.2",
"@better-auth/drizzle-adapter": "^1.7.2",
"@better-auth/infra": "^0.4.3",
"@better-auth/oauth-provider": "^1.7.2",
"@better-auth/passkey": "^1.7.2",
"@bramus/specificity": "^2.4.2",
"@hono/node-server": "^2.1.1",
"@modelcontextprotocol/sdk": "^1.30.0",
@@ -46,7 +46,7 @@
"@orpc/openapi": "^1.15.0",
"@orpc/server": "^1.15.0",
"@orpc/zod": "^1.15.0",
"@react-pdf/renderer": "^4.6.1",
"@react-pdf/renderer": "^4.8.1",
"@reactive-resume/api": "workspace:*",
"@reactive-resume/auth": "workspace:*",
"@reactive-resume/db": "workspace:*",
@@ -57,17 +57,17 @@
"@sindresorhus/slugify": "^3.0.0",
"@t3-oss/env-core": "^0.13.11",
"@uiw/color-convert": "^2.10.3",
"ai": "^7.0.66",
"ai": "^7.0.83",
"bcrypt": "^6.0.0",
"better-auth": "1.6.29",
"better-auth": "1.7.2",
"cjk-regex": "^3.4.0",
"css-tree": "^3.2.1",
"deepmerge-ts": "^8.0.1",
"deepmerge-ts": "^8.0.2",
"drizzle-orm": "1.0.0-rc.4",
"drizzle-zod": "1.0.0-beta.14-a36c63d",
"es-toolkit": "^1.51.0",
"fast-json-patch": "^3.1.1",
"hono": "^4.13.2",
"hono": "^4.13.5",
"jsonrepair": "^3.15.0",
"node-html-parser": "^9.0.1",
"nodemailer": "^9.0.5",
@@ -75,24 +75,25 @@
"pg": "^8.23.0",
"phosphor-icons-react-pdf": "^0.1.3",
"react": "^19.2.8",
"react-email": "^6.9.2",
"react-email": "^6.9.3",
"react-pdf-html": "^2.1.5",
"resumable-stream": "^2.2.12",
"sharp": "^0.35.3",
"sharp": "^0.35.4",
"tokenx": "^2.1.0",
"ts-pattern": "^5.9.0",
"unique-names-generator": "^4.7.1",
"uuid": "^14.0.1",
"uuid": "^14.0.2",
"zod": "^4.4.3"
},
"devDependencies": {
"@reactive-resume/config": "workspace:*",
"@types/node": "^26.2.0",
"@types/pg": "^8.23.0",
"@types/node": "^26.4.0",
"@types/pg": "^8.23.1",
"@types/react": "^19.2.18",
"@typescript/native-preview": "7.0.0-dev.20260707.2",
"tsdown": "^0.22.14",
"tsx": "^4.23.12",
"typescript": "^7.0.2",
"vitest": "^4.1.10"
"vitest": "^4.1.11"
}
}
+1 -1
View File
@@ -54,7 +54,7 @@ export function createMcpServer(request: Request) {
`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}\`; set target to \`cover-letter\` to export a visible cover letter separately.`,
`Import full ResumeData JSON with \`${MCP_TOOL_NAME.importResume}\`; read saved AI analysis with \`${MCP_TOOL_NAME.getResumeAnalysis}\`.`,
`Import full ResumeData JSON with \`${MCP_TOOL_NAME.importResume}\`.`,
].join(" "),
},
);
+11
View File
@@ -61,7 +61,18 @@ async function validateLocalStoragePath() {
}
}
async function reapStaleAgentRuns() {
try {
const { reapStaleAgentRunsAtBoot } = await import("@reactive-resume/api/features/agent/runs");
await reapStaleAgentRunsAtBoot();
} catch (error) {
// A reap failure must not block serving traffic; stuck runs also heal lazily on access.
console.error("Failed to reap stale agent runs at boot", { error });
}
}
export async function runStartupChecks() {
await runDatabaseMigrations();
await validateLocalStoragePath();
await reapStaleAgentRuns();
}
+1
View File
@@ -33,6 +33,7 @@ describe("SEO static endpoints", () => {
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("application/xml; charset=UTF-8");
expect(text).toContain("<loc>https://app.example.com/</loc>");
expect(text).toContain("<loc>https://app.example.com/ats-checker</loc>");
expect(text).not.toContain("docs.rxresu.me");
expect(text).not.toContain("/auth");
expect(text).not.toContain("/dashboard");
+3
View File
@@ -42,6 +42,9 @@ export function handleSitemap(options?: StaticSeoOptions) {
" <url>",
` <loc>${baseUrl}/</loc>`,
" </url>",
" <url>",
` <loc>${baseUrl}/ats-checker</loc>`,
" </url>",
"</urlset>",
"",
].join("\n");
+41
View File
@@ -89,6 +89,47 @@ describe("web app fallback classification", () => {
expect(await dashboardResponse.text()).not.toContain('rel="canonical"');
});
describe("the ATS checker page", () => {
const shell = `<html><head><title>Reactive Resume — A free and open-source resume builder</title><meta name="description" content="Marketing copy."></head><body></body></html>`;
it("serves an indexable shell rather than a 404", async () => {
vi.mocked(fs.readFile).mockResolvedValue(shell);
const response = await handleWebApp(new Request("https://example.com/ats-checker"));
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("text/html; charset=UTF-8");
expect(response.headers.get("X-Robots-Tag")).toBeNull();
});
it("replaces the shell metadata with the checker's own", async () => {
vi.mocked(fs.readFile).mockResolvedValue(shell);
const html = await (await handleWebApp(new Request("https://example.com/ats-checker"))).text();
expect(html).toContain("<title>ATS Checker - Reactive Resume</title>");
expect(html).toContain('<link rel="canonical" href="https://rxresu.me/ats-checker">');
expect(html).toContain('<meta property="og:url" content="https://rxresu.me/ats-checker">');
expect(html).toContain('<meta property="og:image" content="https://rxresu.me/opengraph/ats-checker.png">');
expect(html).toContain('id="ats-checker-structured-data"');
expect(html).not.toContain("Marketing copy.");
});
it("answers HEAD without a body", async () => {
const response = await handleWebApp(new Request("https://example.com/ats-checker", { method: "HEAD" }));
expect(response.status).toBe(200);
expect(await response.text()).toBe("");
});
it("does not treat the checker path as a public resume owner", async () => {
const response = await handleWebApp(new Request("https://example.com/ats-checker/anything"));
expect(response.status).toBe(404);
expect(mocks.getPublicResumeSocialMeta).not.toHaveBeenCalled();
});
});
describe("public resume social cards", () => {
const shell = `<html><head><title>Reactive Resume — A free and open-source resume builder</title><meta name="description" content="Marketing copy."></head><body></body></html>`;
+57 -1
View File
@@ -20,6 +20,13 @@ function resolveWebDistPath() {
const staticRoot = resolveWebDistPath();
const indexHtmlPath = `${staticRoot}/index.html`;
const noindexShellPrefixes = ["/auth", "/dashboard", "/builder", "/agent", "/templates"];
/**
* Marketing pages the SPA owns that search engines should index.
*
* Without an entry here the fallback below returns 404 for the path in production — the dev Vite
* server serves the shell for anything, so this failure only ever shows up once deployed.
*/
const indexableAppPaths = new Set(["/ats-checker"]);
const reservedPublicResumeSegments = new Set([
"api",
"mcp",
@@ -30,6 +37,7 @@ const reservedPublicResumeSegments = new Set([
"builder",
"agent",
"templates",
"ats-checker",
]);
function isAssetPath(pathname: string): boolean {
@@ -160,6 +168,43 @@ function createRootSeoMarkup(canonicalUrl: string) {
`;
}
const ATS_CHECKER_TITLE = "ATS Checker - Reactive Resume";
// Kept under ~120 characters so Google's mobile SERP snippet is not truncated at 3 lines.
const ATS_CHECKER_DESCRIPTION =
"Check whether software can read your resume PDF. Runs in your browser, so your file is never uploaded.";
function createAtsCheckerSeoMarkup(origin: string) {
const canonicalUrl = `${origin}/ats-checker`;
const imageUrl = `${origin}/opengraph/ats-checker.png`;
const structuredData = {
"@context": "https://schema.org",
"@type": "WebApplication",
name: "ATS Checker",
url: canonicalUrl,
description: ATS_CHECKER_DESCRIPTION,
applicationCategory: "BusinessApplication",
operatingSystem: "Web",
isAccessibleForFree: true,
offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
isPartOf: { "@type": "WebSite", name: "Reactive Resume", url: `${origin}/` },
};
return `
<link rel="canonical" href="${canonicalUrl}">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Reactive Resume">
<meta property="og:title" content="${ATS_CHECKER_TITLE}">
<meta property="og:description" content="${ATS_CHECKER_DESCRIPTION}">
<meta property="og:url" content="${canonicalUrl}">
<meta property="og:image" content="${imageUrl}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="${ATS_CHECKER_TITLE}">
<meta name="twitter:description" content="${ATS_CHECKER_DESCRIPTION}">
<meta name="twitter:image" content="${imageUrl}">
<script id="ats-checker-structured-data" type="application/ld+json">${JSON.stringify(structuredData)}</script>
`;
}
// Resume names, headlines, and summaries are user-authored, so they must never reach the served
// HTML unescaped.
const escapeAttribute = (value: string) =>
@@ -215,7 +260,9 @@ export const serveWebDistStatic = serveStatic({
});
function getFallbackResponseHeaders(pathname: string) {
if (pathname === "/") return { "Content-Type": "text/html; charset=UTF-8", ...BASE_SECURITY_HEADERS };
if (pathname === "/" || indexableAppPaths.has(pathname)) {
return { "Content-Type": "text/html; charset=UTF-8", ...BASE_SECURITY_HEADERS };
}
if (isNoindexShellPath(pathname) || isPublicResumePath(pathname)) {
return {
"Content-Type": "text/html; charset=UTF-8",
@@ -258,6 +305,15 @@ export async function handleWebApp(request: Request) {
return new Response(html.replace("</head>", `${createRootSeoMarkup(canonicalUrl)}</head>`), { headers });
}
if (pathname === "/ats-checker") {
const origin = new URL(env.APP_URL).origin;
const withTitle = html
.replace(/<title>[^<]*<\/title>/, `<title>${ATS_CHECKER_TITLE}</title>`)
.replace(/<meta\s+name="description"[^>]*>/, `<meta name="description" content="${ATS_CHECKER_DESCRIPTION}">`);
return new Response(withTitle.replace("</head>", `${createAtsCheckerSeoMarkup(origin)}</head>`), { headers });
}
if (isPublicResumePath(pathname)) {
const resumeSeo = await createPublicResumeSeoMarkup(pathname, new URL(env.APP_URL).origin);
if (resumeSeo) {
+1 -1
View File
@@ -1,6 +1,6 @@
import { fileURLToPath } from "node:url";
// @boundaries-ignore root shared Vitest config
import { createVitestProjectConfig } from "../../vitest.shared";
import { createVitestProjectConfig } from "../../vitest.shared.mts";
export default createVitestProjectConfig({
name: "server",
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1200 -71
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1198 -68
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1200 -71
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
File diff suppressed because it is too large Load Diff
+1199 -70
View File
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