mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-06-22 04:11:55 +10:00
6d8d8f6e55
* chore(ai): remove local AI store now that providers live server-side
The Zustand-based useAIStore has been replaced by the server-side
aiProviders oRPC router (encrypted credentials persisted in DB).
Delete the dead store + tests, drop the ./store export, and remove
zustand/immer deps which are no longer referenced anywhere in
packages/ai/src/.
* feat(agent): archive/delete actions and read-only state for agent threads
- Backend: mark archived threads as read-only in threads.get and reject
messages.send with CONFLICT when the thread is archived.
- Frontend: render archived threads in the sidebar with muted styling and
an Archived badge; add a per-thread dropdown menu in the chat header
with Archive (non-destructive) and Delete (with confirmation); show a
read-only banner above the message list that disambiguates archived
vs. missing-resource causes; suppress the Retry and Stop buttons in
read-only mode.
- Tests: new packages/api/src/services/agent.test.ts covering the
archived-thread isReadOnly flag and the archived-thread send refusal.
* fix(agent): abort run on archive and verify ownership before deleting thread
- threads.archive: before flipping status, abort any in-flight run controller
and clear the active-run state on the thread; cleanup failures are logged
but do not block the status update.
- threads.delete: assert thread ownership via getThread before destructive
work so an authenticated user cannot wipe another user's attachment rows
by passing a foreign threadId.
Adds focused tests for both behaviors.
* feat(agent): display patch diffs and surface revert conflicts
Render apply_resume_patch tool messages with a status-aware card (applied/
reverted/conflicted), expandable operation list, and a Revert button that
correctly handles RESUME_VERSION_CONFLICT responses. Adds unit tests for
the inverse-patch builder and the agentService.actions.revert flow.
* chore(agent): remove out-of-scope attachment tests accidentally added in Task 6
The Task 6 commit (73ef1acca) accidentally re-introduced three attachment-
related tests that belong to a separate task:
- `buildAttachmentModelParts > converts text, image, supported binary, and
unsupported attachments into model parts`
- `agentService.messages.send > persists the user message with file UI parts
and links selected attachments to it` (was failing — the `ToolLoopAgent`
mock is not callable as a constructor)
- `agentService.messages.send > rejects attachments that are missing, foreign,
or already linked before persisting a message`
These were likely re-added during a stash recovery and were not requested
for Task 6, whose scope was limited to the `agentService.actions.revert`
flow. Remove them along with the helpers/fixtures (`buildAttachment`,
`buildActiveThread`, `selectWhereResult`, `selectOrderByResult`) that they
were the only consumers of. `selectLimitResult` is preserved because it is
used by the revert tests.
* chore(agent): configure runtime dependencies
* feat(db): add agent workspace schema
* feat(api): add agent backend services
* feat(web): add agent workspace UI
* chore(agent): remove legacy builder assistant
* test(agent): make agent stream mocks constructible
* chore(web): remove unused resume replacement hook
* feat(api): add unsafe AI base URL flag
* chore(dev): expose local services in compose
* fix(web): normalize resume preview gaps
* feat(api): improve agent tool handling
* feat(web): polish agent workspace UI
* chore: update dependencies
* fix(api,web): address PR review feedback for agent workspace
Security/correctness:
- Restrict AI provider URLs to http/https even in unsafe mode
- Stop exposing Redis on host network by default
- Make .env.local optional and drop app profile in compose.dev.yml
- Store agent attachments with private ACL on S3
- Reset provider test status when provider/model/baseURL changes
- Decouple non-agent AI endpoints from REDIS_URL requirement
- Fix JSON Patch add inverse for existing object members
- Wrap resume patch + agent action insert in db transaction
- Validate partialMessage at runtime and rate-limit attachment uploads
- Add unique index on agent_messages (thread_id, sequence)
UX/bugs:
- Mark agent thread route as ssr: false and guard SSE chunk parsing
- Show config-specific banner only on known configuration error
- Gate AI provider checks behind loading state in resume import
- Fix relative-time formatter blank gap between 45-59 seconds
- Clarify thread delete confirmation message
Polish:
- Raise ENCRYPTION_SECRET minimum to 32 characters
- Bucket AI rate limits by resumeId/threadId/messageId
- Trim form values before submitting AI provider config
- Use single key identifier and nullish-coalesce baseURL display
* fix: address ai agent review feedback
* fix: preserve mobile agent chat state
* docs: add ai agent workspace guides
* feat: introduce design system for Reactive Resume
348 lines
21 KiB
Markdown
348 lines
21 KiB
Markdown
---
|
||
version: alpha
|
||
name: Reactive Resume
|
||
description: A monochrome, content-first design system for a free and open-source resume builder. Dark-by-default with light mode support.
|
||
colors:
|
||
primary: "#343434"
|
||
primary-foreground: "#FBFBFB"
|
||
secondary: "#F7F7F7"
|
||
secondary-foreground: "#343434"
|
||
background: "#FFFFFF"
|
||
foreground: "#252525"
|
||
muted: "#F7F7F7"
|
||
muted-foreground: "#8E8E8E"
|
||
card: "#FFFFFF"
|
||
card-foreground: "#252525"
|
||
border: "#EBEBEB"
|
||
input: "#EBEBEB"
|
||
ring: "#B5B5B5"
|
||
destructive: "#DC2626"
|
||
on-destructive: "#FFFFFF"
|
||
typography:
|
||
heading:
|
||
fontFamily: IBM Plex Sans Variable
|
||
fontSize: 1rem
|
||
fontWeight: 500
|
||
body:
|
||
fontFamily: IBM Plex Sans Variable
|
||
fontSize: 0.875rem
|
||
fontWeight: 400
|
||
body-sm:
|
||
fontFamily: IBM Plex Sans Variable
|
||
fontSize: 0.75rem
|
||
fontWeight: 400
|
||
label:
|
||
fontFamily: IBM Plex Sans Variable
|
||
fontSize: 0.8rem
|
||
fontWeight: 500
|
||
hero-heading:
|
||
fontFamily: IBM Plex Sans Variable
|
||
fontSize: 3.75rem
|
||
fontWeight: 700
|
||
letterSpacing: -0.025em
|
||
rounded:
|
||
sm: 0.18rem
|
||
md: 0.24rem
|
||
lg: 0.3rem
|
||
xl: 0.42rem
|
||
2xl: 0.54rem
|
||
3xl: 0.66rem
|
||
4xl: 0.78rem
|
||
spacing:
|
||
xs: 4px
|
||
sm: 8px
|
||
md: 16px
|
||
lg: 24px
|
||
xl: 32px
|
||
2xl: 48px
|
||
components:
|
||
button-default:
|
||
backgroundColor: "{colors.primary}"
|
||
textColor: "{colors.primary-foreground}"
|
||
rounded: "{rounded.lg}"
|
||
padding: 10px
|
||
height: 36px
|
||
button-outline:
|
||
backgroundColor: "{colors.background}"
|
||
textColor: "{colors.foreground}"
|
||
rounded: "{rounded.lg}"
|
||
padding: 10px
|
||
height: 36px
|
||
button-secondary:
|
||
backgroundColor: "{colors.secondary}"
|
||
textColor: "{colors.secondary-foreground}"
|
||
rounded: "{rounded.lg}"
|
||
padding: 10px
|
||
height: 36px
|
||
button-ghost:
|
||
backgroundColor: "{colors.background}"
|
||
textColor: "{colors.foreground}"
|
||
rounded: "{rounded.lg}"
|
||
padding: 10px
|
||
height: 36px
|
||
button-destructive:
|
||
backgroundColor: "{colors.destructive}"
|
||
textColor: "{colors.on-destructive}"
|
||
rounded: "{rounded.lg}"
|
||
padding: 10px
|
||
height: 36px
|
||
card:
|
||
backgroundColor: "{colors.card}"
|
||
textColor: "{colors.card-foreground}"
|
||
rounded: "{rounded.lg}"
|
||
padding: 16px
|
||
input:
|
||
backgroundColor: "{colors.background}"
|
||
textColor: "{colors.foreground}"
|
||
rounded: "{rounded.lg}"
|
||
height: 36px
|
||
padding: 10px
|
||
input-focus:
|
||
backgroundColor: "{colors.background}"
|
||
textColor: "{colors.foreground}"
|
||
rounded: "{rounded.lg}"
|
||
height: 36px
|
||
padding: 10px
|
||
badge:
|
||
backgroundColor: "{colors.primary}"
|
||
textColor: "{colors.primary-foreground}"
|
||
rounded: "{rounded.md}"
|
||
padding: 4px
|
||
popover:
|
||
backgroundColor: "{colors.card}"
|
||
textColor: "{colors.card-foreground}"
|
||
rounded: "{rounded.xl}"
|
||
padding: 4px
|
||
sidebar:
|
||
backgroundColor: "{colors.muted}"
|
||
textColor: "{colors.foreground}"
|
||
padding: 8px
|
||
sidebar-item:
|
||
backgroundColor: "{colors.muted}"
|
||
textColor: "{colors.muted-foreground}"
|
||
rounded: "{rounded.lg}"
|
||
padding: 8px
|
||
sidebar-item-active:
|
||
backgroundColor: "{colors.primary}"
|
||
textColor: "{colors.primary-foreground}"
|
||
rounded: "{rounded.lg}"
|
||
padding: 8px
|
||
tooltip:
|
||
backgroundColor: "{colors.primary}"
|
||
textColor: "{colors.primary-foreground}"
|
||
rounded: "{rounded.md}"
|
||
padding: 6px
|
||
separator:
|
||
backgroundColor: "{colors.border}"
|
||
height: 1px
|
||
dialog:
|
||
backgroundColor: "{colors.card}"
|
||
textColor: "{colors.card-foreground}"
|
||
rounded: "{rounded.xl}"
|
||
padding: 24px
|
||
input-invalid:
|
||
backgroundColor: "{colors.background}"
|
||
textColor: "{colors.destructive}"
|
||
rounded: "{rounded.lg}"
|
||
height: 36px
|
||
padding: 10px
|
||
---
|
||
|
||
## Overview
|
||
|
||
Reactive Resume is a monochrome, content-first design system built for a resume builder used by tens of thousands of people worldwide. The visual identity prioritizes readability and unobtrusiveness — the user's resume content is always the hero, never the chrome around it.
|
||
|
||
The system defaults to dark mode with a warm near-black backdrop that makes the resume preview "float" as the visual anchor. Light mode is supported as a full alternative. The authenticated app shell (dashboard, builder, settings) uses an entirely achromatic grayscale palette — the sole chromatic exception is destructive red for dangerous actions. The landing page introduces subtle chromatic accents: blue-tinted spotlight gradients on the hero, a multicolor text-mask animation on hover, and social auth provider brand colors (Google blue, LinkedIn blue) on the login page.
|
||
|
||
The overall aesthetic is a professional tool UI: clean grid lines, subtle borders, generous whitespace, and typography that steps back to let the content shine. Think "VS Code meets Figma" — a productivity workspace, not a marketing site.
|
||
|
||
One deliberate counterpoint to the serious UI: all resume templates are named after Pokemon (Azurill, Bronzor, Chikorita, Ditgar, Gengar, Pikachu, etc.). This is an intentional brand choice — playful naming for templates injects personality into an otherwise utilitarian interface, making templates feel collectible and memorable rather than generic ("Template 1", "Modern", "Classic").
|
||
|
||
## Colors
|
||
|
||
The palette is rooted in achromatic OKLch values (chroma = 0), producing a pure grayscale scale without warm or cool casts. Colors are defined as CSS custom properties using `oklch()` and consumed through Tailwind CSS 4 theme tokens. Always prefer CSS variables (e.g., `var(--primary)`) or Tailwind tokens (e.g., `bg-primary`) over raw color values. The hex values in this document's YAML front matter are agent-friendly approximations of the canonical OKLch definitions in `packages/ui/src/styles/globals.css` — use hex only where OKLch is unavailable.
|
||
|
||
- **Primary (#343434 light / #EBEBEB dark):** Used for high-emphasis interactive surfaces — default buttons, selected states, and text selection. In dark mode this inverts to near-white so buttons remain prominent.
|
||
- **Foreground (#252525 light / #FBFBFB dark):** Body text and headings. High contrast against the background in both themes.
|
||
- **Background (#FFFFFF light / #252525 dark):** The canvas. Pure white in light mode, warm near-black in dark mode.
|
||
- **Card (#FFFFFF light / #343434 dark):** Elevated surface for cards, panels, and the builder sidebar. In dark mode, one step lighter than the background to create subtle depth.
|
||
- **Muted (#F7F7F7 light / #454545 dark):** De-emphasized backgrounds for secondary UI regions, hover states, and inactive tabs.
|
||
- **Muted Foreground (#8E8E8E light / #B5B5B5 dark):** Captions, helper text, timestamps, and metadata. Deliberately low-contrast against the background to recede visually.
|
||
- **Border (#EBEBEB light / white at 10% opacity dark):** Thin separator lines. In dark mode, uses transparent white rather than a solid gray to blend naturally with any underlying surface color.
|
||
- **Input (#EBEBEB light / white at 15% opacity dark):** Form field borders, slightly more prominent than general borders to make input areas discoverable.
|
||
- **Destructive (#DC2626 light / #EF4444 dark):** The only chromatic color in the palette. Reserved exclusively for delete actions, error states, and danger-zone operations. Used at 10% opacity as a background tint with full saturation for text, creating a soft but unmistakable warning.
|
||
- **Ring (#B5B5B5 light / #8E8E8E dark):** Focus ring indicator at 50% opacity, surrounding focused interactive elements.
|
||
- **Sidebar Primary (dark only, #6366F1):** An indigo value inherited from the shadcn/ui defaults. Not actively used in the current UI — sidebar active states use the standard grayscale primary token instead. Retained in the CSS custom properties for potential future customization.
|
||
|
||
Resume templates have their own independent color system — users pick primary, text, and background colors per resume through a color picker in the builder's Design panel. These template colors are completely separate from the app shell palette.
|
||
|
||
## Typography
|
||
|
||
The entire application uses a single typeface: **IBM Plex Sans Variable**. This is a humanist sans-serif with an extensive weight range (100–900) and excellent readability at small sizes, both on screen and in PDFs.
|
||
|
||
- **Hero heading (responsive: 2.25rem mobile / 3rem tablet / 3.75rem desktop, weight 700, tracking-tight):** Landing page headline only. Large, bold, and commanding. Scales across three breakpoints.
|
||
- **Section heading (1rem / 16px, weight 500):** Used for section titles in the builder sidebar, settings panels, and dashboard cards. Medium weight provides hierarchy without shouting.
|
||
- **Body (0.875rem / 14px, weight 400):** The workhorse. All form labels, descriptions, card content, and general UI text.
|
||
- **Small body (0.75rem / 12px, weight 400):** Captions, helper text, timestamps, and metadata.
|
||
- **Label (0.8rem / ~13px, weight 500):** Button text, badge labels, and form field labels. Slightly heavier than body to denote interactivity.
|
||
|
||
The resume content itself uses a separate font system — users choose from 1,000+ Google Fonts for their resume headings and body text, with category-aware fallback stacks including CJK support (Noto Sans SC, PingFang SC, Hiragino Sans GB for sans-serif; Noto Serif SC, Songti SC for serif). Standard PDF fonts (Helvetica, Courier, Times-Roman) are available as offline fallbacks.
|
||
|
||
Font rendering uses `antialiased` (grayscale AA) and `proportional-nums` across the board for clean rendering and properly spaced numerals in dates and phone numbers.
|
||
|
||
## Layout
|
||
|
||
### Builder (Three-Panel Workspace)
|
||
|
||
The core builder uses a resizable three-panel layout powered by `react-resizable-panels`:
|
||
|
||
- **Left sidebar (default 22%):** Resume section forms — personal info, experience, education, skills, and custom sections. Scrollable with collapsible section groups.
|
||
- **Center artboard (default 56%):** Live resume preview rendered via PDF.js canvas. Supports zoom, pan, and pinch gestures via `react-zoom-pan-pinch`. The preview maintains A4 aspect ratio (210:297) with a subtle shadow to simulate a physical page.
|
||
- **Right sidebar (default 22%):** Design controls — template picker, font selection, color picker, layout manager (page assignments, section ordering via drag-and-drop).
|
||
|
||
Panel sizes persist in cookies. On mobile (< 768px), sidebars collapse to 0% width and become toggleable overlays (max 95% width when open). The desktop minimum collapsed width is 48px (icon rail).
|
||
|
||
### Dashboard
|
||
|
||
Standard sidebar navigation layout using the `Sidebar` component system. The sidebar contains: logo, resume list link, agent link, settings subnavigation (profile, preferences, authentication, API keys, integrations, danger zone), and a footer with user avatar. Content area shows a responsive grid of resume cards.
|
||
|
||
### Landing Page
|
||
|
||
Full-width single-column marketing layout:
|
||
1. **Floating builder preview** — A non-interactive screenshot of the builder as a hero visual, creating an immediate "this is what you get" impression.
|
||
2. **Hero** — Centered headline, subheadline, and two CTAs (primary "Get Started" with arrow, ghost "Learn More" with icon).
|
||
3. **Features grid** — 4-column responsive grid with icon + title + description cards, separated by thin border lines.
|
||
4. **Template carousel** — Horizontally scrolling row of template preview thumbnails with Pokemon-themed names.
|
||
5. **Testimonials** — Tiled user quotes in a masonry-style grid.
|
||
6. **Support / FAQ / Footer** — Accordion FAQ, community section, and a 4-column footer with logo, resource links, community links, and license info.
|
||
|
||
### Responsive Breakpoints
|
||
|
||
Mobile detection uses a 768px threshold via `MediaQueryList`. The layout is optimized for workspace productivity on larger screens, with responsive mobile support that adapts the multi-panel builder into a streamlined single-panel experience. Both desktop and mobile are supported experiences — the builder's three-panel layout leverages desktop space, while mobile surfaces the same editing capabilities through collapsible overlays.
|
||
|
||
### Page Aspect Ratio
|
||
|
||
A custom Tailwind token `--aspect-page: 210 / 297` enforces A4 paper proportions wherever resume pages are rendered (builder preview, public view, PDF export).
|
||
|
||
## Animation
|
||
|
||
Animations use the Motion library (formerly Framer Motion) and follow a consistent choreography pattern:
|
||
|
||
**Entrance animations** use a fade-up reveal: elements start at `opacity: 0, y: 20-100` and animate to `opacity: 1, y: 0`. The hero section uses a larger y-offset (100px) for dramatic effect; subsequent sections use 20px for subtlety.
|
||
|
||
**Timing principles:**
|
||
- **Base duration:** 0.35s–0.6s for standard section reveals, 0.45s for hero elements, up to 1.1s for the hero video entrance.
|
||
- **Stagger pattern:** Sequential delays within a group, typically 0.1s–0.15s apart (hero: 0.55s, 0.7s, 0.82s, 0.95s). For grids, use `index * 0.03`–`0.1` for per-item stagger.
|
||
- **Easing:** `easeOut` for entrances (elements decelerate into position). `easeInOut` for looping/ambient animations.
|
||
- **Performance:** Apply `will-change-[transform,opacity]` on animated elements and `will-change-transform` on continuously animated elements.
|
||
|
||
**Hover/interaction animations** are quick (0.2s) and subtle — small scale bumps (`scale: 1.01`), slight y-offsets (`y: -2`), and `active:translate-y-px` for button press.
|
||
|
||
**Ambient animations** loop infinitely with `easeInOut` — the scroll indicator bounces gently (`y: [0, 5, 0]` over 1.5s).
|
||
|
||
**Reduced motion:** All CSS transitions and animations collapse to `0.01ms` duration and single iteration when `prefers-reduced-motion: reduce` is active. Motion library animations should also respect this preference.
|
||
|
||
## Elevation & Depth
|
||
|
||
Elevation is handled through background color layering rather than drop shadows:
|
||
|
||
- **Level 0 — Background:** The base canvas (`--background`).
|
||
- **Level 1 — Card:** One step lighter in dark mode (`--card`), used for sidebars, panels, and cards.
|
||
- **Level 2 — Popover:** Same as card, but appears above the content layer in popovers, dropdowns, and command palette.
|
||
- **Level 3 — Overlay:** Backdrop blur (`backdrop-blur-xs` at 0.5px or `backdrop-blur-2xl` at 40px) with `backdrop-saturate-150` for modal overlays, creating a frosted-glass effect over the workspace.
|
||
|
||
The resume preview page uses a subtle drop shadow to simulate a physical sheet of paper floating above the dark artboard — one of the few places actual shadows appear.
|
||
|
||
## Shapes
|
||
|
||
Border radius follows a multiplicative scale from a single `--radius` base of `0.3rem`:
|
||
|
||
| Token | Value | Usage |
|
||
|:------|:------|:------|
|
||
| `sm` | 0.18rem (≈3px) | Small badges, inline chips |
|
||
| `md` | 0.24rem (≈4px) | XS/SM buttons, compact elements |
|
||
| `lg` | 0.3rem (≈5px) | Default buttons, cards, inputs |
|
||
| `xl` | 0.42rem (≈7px) | Larger cards, modal corners |
|
||
| `2xl` | 0.54rem (≈9px) | Dialog containers |
|
||
| `3xl` | 0.66rem (≈11px) | Large panels |
|
||
| `4xl` | 0.78rem (≈12px) | Full-page modals |
|
||
|
||
The radius scale is deliberately tight — the largest value (0.78rem) is still quite subtle. This avoids the "rounded everything" aesthetic and keeps the UI feeling precise and tool-like. Interactive elements consistently use `rounded-lg` as the default.
|
||
|
||
## Components
|
||
|
||
### Buttons
|
||
|
||
Six variants, all sharing `rounded-lg` corners, `font-medium`, `text-sm`, and a 1px `translate-y` on active press (except when the button opens a popup):
|
||
|
||
- **Default:** Solid primary background. The highest-emphasis action on any screen.
|
||
- **Outline:** Transparent with a border. For secondary actions that need clear boundaries.
|
||
- **Secondary:** Muted background. For paired actions alongside a primary button.
|
||
- **Ghost:** No background or border. For toolbar actions and inline controls where chrome would be noise.
|
||
- **Destructive:** Red at 10% opacity background with red text. Visually alarming without being garish.
|
||
- **Link:** Underline-on-hover text. For inline navigation within prose.
|
||
|
||
Size scale: `xs` (28px), `sm` (32px), `default` (36px), `lg` (40px), plus `icon` variants at each size for square icon-only buttons.
|
||
|
||
### Cards
|
||
|
||
White/dark surface with foreground text. Composed of `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`, and `CardAction` slots. Default vertical padding is `py-4` (compact: `py-3`).
|
||
|
||
### Forms
|
||
|
||
Built on TanStack Form with Zod validation. Composed of `FormItem`, `FormLabel`, `FormControl`, `FormMessage`, and `FormDescription`. Validation errors only appear after field touch. Invalid fields get a red destructive border with a ring.
|
||
|
||
### Dialogs
|
||
|
||
Centralized dialog manager with 40+ dialog types, all rendered via pattern matching (`ts-pattern`). Dialogs support before-close validation, form blocking for unsaved changes, and confirmation prompts. Used for all CRUD operations on resume sections, settings changes, and import/export flows.
|
||
|
||
### Command Palette
|
||
|
||
Triggered by `Cmd+K` / `Ctrl+K`. Built on `cmdk` with fuzzy search via `Fuse.js`. Multi-page navigation (resumes, settings, preferences) with back navigation via Backspace. Screen-reader accessible with `sr-only` headings.
|
||
|
||
### Toast Notifications
|
||
|
||
Powered by Sonner, positioned bottom-right with rich colors. Used for auto-save feedback, form submission status, error reporting, and donation prompts. Loading toasts are used during async operations (PDF generation, resume creation) with dismiss-on-complete.
|
||
|
||
### Drag and Drop
|
||
|
||
Powered by `@dnd-kit` with `PointerSensor` and `KeyboardSensor`. Used in chip inputs (skill tags, URL lists) and page layout management (section ordering across resume pages). Smooth animations via Motion library.
|
||
|
||
## Internationalization
|
||
|
||
The app supports 40+ locales including RTL languages (Arabic, Hebrew, Persian, Urdu, Uyghur, Yiddish). i18n is not an afterthought — it shapes layout decisions:
|
||
|
||
**Direction:** The `<html>` element receives `dir="rtl"` or `dir="ltr"` based on the active locale, detected via `isRTL()` which checks the language prefix against a known RTL set. All layout mirroring flows from this single attribute.
|
||
|
||
**Logical properties:** Use CSS logical properties (`ps-`, `pe-`, `ms-`, `me-`, `inline-start`, `inline-end`, `inset-s-`, `inset-e-`) instead of physical (`pl-`, `pr-`, `ml-`, `mr-`, `left`, `right`). Button components already use `has-data-[icon=inline-start]:ps-2` and `has-data-[icon=inline-end]:pe-2` patterns. This ensures correct spacing in both LTR and RTL layouts without separate stylesheets.
|
||
|
||
**Variable-length text:** Translations can be 30–50% longer than English (German, Finnish) or significantly shorter (CJK). UI elements should accommodate variable text length — avoid fixed widths on buttons and labels. Use `whitespace-nowrap` only where truncation is acceptable, and prefer `min-w-0` with `truncate` over fixed-width containers.
|
||
|
||
**Icons:** Directional icons (arrows, chevrons, progress indicators) should mirror in RTL contexts. Phosphor Icons provides mirrored variants for directional icons. Non-directional icons (settings gear, checkmark, delete) do not mirror.
|
||
|
||
**Strings:** All user-facing strings use Lingui macros (`t`, `msg`, `<Trans>`) — never hardcode English text in components. Translation files are `.po` format under `/locale/`.
|
||
|
||
## Do's and Don'ts
|
||
|
||
### Do
|
||
|
||
- **Use the grayscale palette for all app chrome.** The absence of color is the brand. The resume content is the only thing that should be colorful.
|
||
- **Default to dark mode.** The dark workspace makes resume previews pop and reduces eye strain during extended editing sessions.
|
||
- **Use `text-sm` (14px) as the base text size.** The UI is information-dense — form fields, section labels, metadata — and needs to be scannable without feeling cramped.
|
||
- **Keep border radius tight.** Use `rounded-lg` (0.3rem) as the default. The tool should feel precise, not playful.
|
||
- **Respect reduced motion preferences.** All animations collapse to 0.01ms when `prefers-reduced-motion: reduce` is active.
|
||
- **Use Phosphor Icons consistently.** Regular weight, `size-4` (16px) default. Icons should be functional labels, not decorative.
|
||
- **Maintain the three-panel builder proportions.** The center artboard should always dominate. Sidebars are support panels, not equal peers.
|
||
- **Use transparent-white borders in dark mode.** `oklch(1 0 0 / 10%)` blends naturally with any surface rather than introducing a distinct gray band.
|
||
|
||
### Don't
|
||
|
||
- **Don't introduce accent colors into the app shell.** No blues, greens, or purples for primary actions. The only chromatic color is destructive red. The inherited indigo sidebar-primary token exists in CSS custom properties but is not actively used.
|
||
- **Don't use drop shadows for elevation.** Rely on background color layering and border separation. The one exception is the resume page preview shadow.
|
||
- **Don't make the UI compete with the resume content.** If a new feature draws more visual attention than the resume preview, it needs to be toned down.
|
||
- **Don't use large border radii.** Nothing above `rounded-xl` on standard components. Large pills and full-round shapes conflict with the precision-tool aesthetic.
|
||
- **Don't hardcode colors outside the token system.** All colors flow through CSS custom properties so that dark/light mode switching works automatically.
|
||
- **Don't use multiple typefaces in the app shell.** IBM Plex Sans Variable is the only UI font. Resume templates have their own font system, but the chrome stays single-family.
|
||
- **Don't skip the `data-slot` attribute on components.** It's used for styling hooks and accessibility selectors throughout the component library.
|
||
- **Don't forget RTL.** The app supports 40+ locales including Arabic, Hebrew, Persian, and Urdu. Use logical properties (`ps`, `pe`, `ms`, `me`) instead of physical (`pl`, `pr`, `ml`, `mr`).
|