chore(design-sync): add Reactive Resume UI sync inputs (#3216)

* chore(design-sync): add Reactive Resume UI sync inputs

Sync inputs for the claude.ai/design "Reactive Resume" project — the
@reactive-resume/ui design system (39 primary components).

- .design-sync/config.json — converter config (synth-entry, 202->39 card
  prune, overlay/grid cardMode overrides, cssEntry, tsconfig, buildCmd)
- .design-sync/build-css.mjs + tw-entry.css — compile Tailwind v4 globals.css
  to a self-contained stylesheet (inlined IBM Plex font), emit real .d.ts,
  and create the workspace self-symlink the converter needs
- .design-sync/previews/*.tsx — 39 authored preview compositions
- .design-sync/conventions.md — design-agent usage header (readmeHeader)
- .design-sync/NOTES.md — re-sync notes, gotchas, and risks
- packages/ui/tsconfig.emit.json — declaration emit for real prop contracts

Build artifacts (dist/types, .ds-compiled.css, ds-bundle, .cache) are gitignored.

Claude-Session: https://claude.ai/code/session_01R8Aq8F1nTuvJwfut7g3DVE

* chore(knip): ignore .design-sync inputs and drop stale es-toolkit ignore

- Ignore .design-sync/** (design-sync tooling: build-css.mjs + authored
  previews are standalone, not part of the app import graph — knip --fix was
  deleting them and failing the autofix job).
- Remove es-toolkit from apps/server ignoreDependencies: it's now really used
  (apps/server/src/http/health.ts imports withTimeout), so the ignore is stale.

Claude-Session: https://claude.ai/code/session_01R8Aq8F1nTuvJwfut7g3DVE
This commit is contained in:
Amruth Pillai
2026-07-05 06:25:51 +02:00
committed by GitHub
parent 9f9268f380
commit a28e3baa61
47 changed files with 2166 additions and 2 deletions
+55
View File
@@ -0,0 +1,55 @@
# 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, `sonner.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
@@ -0,0 +1,55 @@
#!/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
@@ -0,0 +1,256 @@
{
"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
@@ -0,0 +1,60 @@
# 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>
```
+51
View File
@@ -0,0 +1,51 @@
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 openMultiple 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>
);
+37
View File
@@ -0,0 +1,37 @@
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
@@ -0,0 +1,34 @@
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 cant be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="destructive">Delete resume</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
+83
View File
@@ -0,0 +1,83 @@
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>
);
+70
View File
@@ -0,0 +1,70 @@
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>
);
+39
View File
@@ -0,0 +1,39 @@
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
+47
View File
@@ -0,0 +1,47 @@
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>
);
+50
View File
@@ -0,0 +1,50 @@
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>
);
+66
View File
@@ -0,0 +1,66 @@
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
@@ -0,0 +1,30 @@
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
@@ -0,0 +1,42 @@
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
@@ -0,0 +1,48 @@
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
@@ -0,0 +1,30 @@
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
@@ -0,0 +1,41 @@
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>
);
+26
View File
@@ -0,0 +1,26 @@
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
@@ -0,0 +1,19 @@
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>
);
+39
View File
@@ -0,0 +1,39 @@
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>
);
+54
View File
@@ -0,0 +1,54 @@
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>
);
+67
View File
@@ -0,0 +1,67 @@
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>
);
+38
View File
@@ -0,0 +1,38 @@
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>
);
+38
View File
@@ -0,0 +1,38 @@
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>
);
+64
View File
@@ -0,0 +1,64 @@
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>
);
+56
View File
@@ -0,0 +1,56 @@
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
@@ -0,0 +1,33 @@
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>
);
+56
View File
@@ -0,0 +1,56 @@
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>
);
+38
View File
@@ -0,0 +1,38 @@
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>
);
+44
View File
@@ -0,0 +1,44 @@
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
@@ -0,0 +1,38 @@
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
@@ -0,0 +1,61 @@
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>
);
+40
View File
@@ -0,0 +1,40 @@
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>
);
+32
View File
@@ -0,0 +1,32 @@
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>
);
+34
View File
@@ -0,0 +1,34 @@
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>
);
+39
View File
@@ -0,0 +1,39 @@
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>
);
+56
View File
@@ -0,0 +1,56 @@
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>
);
+36
View File
@@ -0,0 +1,36 @@
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>
);
+19
View File
@@ -0,0 +1,19 @@
import { useEffect } from "react";
import { toast } from "sonner";
import { Toaster } from "@reactive-resume/ui/components/sonner";
// 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.success("Resume published", {
description: "“Software Engineer” is now live at rxresume.me/jane-doe.",
duration: Number.POSITIVE_INFINITY,
});
}, []);
return (
<div style={{ minHeight: 140 }}>
<Toaster position="top-center" />
</div>
);
};
+65
View File
@@ -0,0 +1,65 @@
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
@@ -0,0 +1,18 @@
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
@@ -0,0 +1,4 @@
/* 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";
+8
View File
@@ -59,3 +59,11 @@ temp
# 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/
+1 -2
View File
@@ -3,7 +3,7 @@
"tags": ["-lintignore"],
"workspaces": {
".": {
"ignoreFiles": [".ncurc.cjs"]
"ignoreFiles": [".ncurc.cjs", ".design-sync/**"]
},
"apps/web": {
"entry": ["lingui.config.ts", "vite.config.ts", "vitest.config.ts"],
@@ -34,7 +34,6 @@
"cjk-regex",
"deepmerge-ts",
"drizzle-zod",
"es-toolkit",
"fast-json-patch",
"jsonrepair",
"node-html-parser",
+15
View File
@@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"declaration": true,
"emitDeclarationOnly": true,
"declarationMap": false,
"outDir": "dist/types",
"rootDir": "src",
"noUnusedLocals": false,
"noUnusedParameters": false
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "src/**/*.test.tsx", "src/**/*.test.ts"]
}