mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-14 23:07:01 +10:00
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:
@@ -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.
|
||||
@@ -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)`);
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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>
|
||||
```
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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 can’t be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction variant="destructive">Delete resume</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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";
|
||||
@@ -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/
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user