frontend/DESIGN.md
Rama main · réplica de lectura del repositorio original.
---
name: "PrivaTools — Air and Play"
description: "Two distinct consumer experiences sharing one functional application."
updated: "2026-10-05"
experiences:
air:
light: "Morning Mist"
dark: "Graphite"
background-light: "#edf3f9"
background-dark: "#151619"
action-light: "#285cc7"
action-dark: "#91b5ff"
font: "Manrope"
play:
light: "Blush"
dark: "Charcoal"
background-light: "#fae7eb"
background-dark: "#181818"
action-light: "#a32749"
action-dark: "#ff9677"
body-font: "Outfit"
heading-font: "Bricolage Grotesque"
---
# PrivaTools design system
The user selected Air and Play and their four appearance palettes on 13 September 2026. This document describes the shipped implementation, live since v2.2.0; previous visual directions are superseded.
## Shared behavior
The same stable application owns routing, processing, files, account state and the local password vault. `ExperienceShell` changes navigation presentation without changing the mounted workspace ancestry. Never key the application or processor by experience or appearance. Processor keys use the tool slug only.
A tool page shows the tool first and its guide (`ToolGuide`) directly below: numbered "How to use" steps, every FAQ answer written out rather than folded away, and links to the guides that mention the tool. It is the same text the server renders for crawlers, loaded per tool.
One tool-page structure, the same in Air and Play (decided 2026-09-28, built 2026-10-02):
- **Header.** One row of wayfinding and page utilities (All tools, the category, "Free to use. No account needed." on wide screens, pin, How it works, and Find another tool with a search icon), then the H1: the tool's name and its one-line promise, the registry `description`, on two lines ("Compress PDF" / "Reduce the file size of your PDF"). The H1's text is "Name: promise"; the colon is visually hidden. The search title (`seoTitle`) is the tab and meta title only, never the heading and never a kicker: no eyebrow sits above the H1. The server's crawler HTML renders the same H1 (`_tool_page_body` in `backend/app/seo_meta.py`; both read `toolSeo` / the manifest). Registry copy is not edited for the layout.
- **Task order.** The intake (and what was chosen), then the options, then the action bar, then the result, which replaces them. The DOM and the focus order follow it. Play keeps its horizontal work shelf, now below the intake rather than above it.
- **Where the file goes** is readable text in the intake (`ToolWhere`, 13px, an icon per kind), in the words and the classification the header chip used to carry (`toolLocation`: the registry's `byok`, then `clientOnly`, else temporary server processing). It must stay exactly true for the tool, so a tool its flags cannot describe has its own words in `OWN_LOCATION`, taken from its engine cards, notes or guide: Background Remover, OCR PDF and Image OCR (a choice of engines), every tool that can use the visitor's AI key (what reaches the provider, what reaches PrivaTools, what stays on the device), the tools that upload a file as soon as it is chosen (`UPLOADS_WHEN_CHOSEN`: Organize Pages and Remove Watermark), Fill Form and Form Creator (their "Detect form fields" and "Detect fields" steps upload the PDF before the fill or the form is created; `upload-on-choice.test.tsx` also presses each step and holds it to one upload to its route), and the server tools whose input is not a file (URL to PDF, HTML to PDF, Generate Barcode, QR Code), whose sentence names what is sent. `src/test/upload-on-choice.test.tsx` chooses a file on every tool page and fails on one that sends anything without being listed and worded so. Tool UIs that do not say it themselves get the same line just above them. Once a tool UI has said it (a shared kit or its own line), the page line stays out for the rest of the page view (`data-own-where` on the workspace), so it never jumps in above an editor that replaced its intake. Two intakes side by side (Compare PDF, Add Attachment, and `PairedIntake`'s Alternate & Mix and Overlay) say it once, under the pair. A tool that takes no file says it in its own intake card (`StudioSource`: URL to PDF, HTML to PDF), and its sentence also names what the server fetches with what was sent: URL to PDF renders the page with the files it loads (stylesheets, fonts, images, attachments); HTML to PDF fetches an address's HTML alone, or the files pasted HTML loads. Name them as an open list ("such as its stylesheets, fonts and images"): WeasyPrint fetches whatever the HTML loads. Merge keeps its own, more specific disclosure.
- **One action bar** (`StudioActionBar`): the run action with the file count and the location label ("1 PDF · Temporary server processing"), nothing else; secondary actions such as "Reset to defaults" or "Clear selection" live with the options or the files. Beside the canvas it sits at the foot of the options column and sticks to the bottom of the screen within that column. In one column (Play at every size, Air below 900px, the media studio below 760px and in Play from 1001px) it sticks to the bottom of the screen while the tool is in view, once there is something to run: a strip along the bottom edge in Air, a floating dock 12px above it in Play (both clear of a bottom safe area). It docks only on screens taller than 420px: at 300% zoom and more, or on a phone held sideways, the header and a docked bar would leave the tool no room and could hide what has focus, so there it stays at the end of the tool. `useActionBarClearance` gives the root's bottom scroll padding and the toasts the room from its top edge to the bottom of the screen only while it sits low on the screen, docked or resting just above where toasts land: nothing while it sits mid-screen at the foot of the media studio's short settings column, or once the visitor has scrolled on to the guide. It arrives with a short ease-out rise (none under reduced motion), and toasts rise above it. The count names what the tool takes (`fileNoun`): "1 PDF" on a PDF-only tool, "2 images", otherwise "files".
- **After a choice** the intake shrinks to a quiet dashed row with an "Add files" text button (a plus, not a filled button), so the run action is the obvious next step. File-picker buttons use a folder icon and Find another tool a search icon: ↗ no longer marks a file picker or an in-page dialog.
- **Headings.** H1, then h2 for each part of the tool (the intake, the options, a result, the guide); file names in lists are labels, not headings, and a run's progress label is a status line. The media studio's stage heading appears only to say how a run ended.
- **The shared kit (step 2b, 2026-10-02).** A tool that makes files from files is built from `skins/experience/ToolStudio.tsx` and, on a `useMultiFileProcessor` queue (or a queue of the same shape), `ProcessorStudio.tsx`: `FileIntake` (with the location line), `ProcessorFiles` for the chosen files, the options in `StudioLayout`'s `options` (an h2 per group; fields in `.ts-setting` with a real `<label>`, choices as `.ts-choice` buttons with `aria-pressed`, yes/no as a `.ts-check` checkbox), the run button alone in `StudioActionBar`, `StudioProgress` while it runs, and `ProcessorResult` at the end, with `useDownloadOnce` for the download. New tools copy this rather than hand-drawing a drop zone, a queue or a result card. A tool whose run is one request (step 2c, 2026-10-04: Split PDF, Split by Text, Unlock PDF, Alternate & Mix, Overlay, HTML to PDF, URL to PDF) ends on `StudioResult` with one row: the result's file, or on a failure the input with its reason (`runFailure` and `runFailureDetail` in `studio-outcome.ts`), and `StudioActions`; it downloads its result once, in the run, and offers it again. A request that carries several files is one set, unlocked or refused whole ("None of these PDFs could be unlocked.", "2 PDFs, uploaded together" on the row). When the options may be what the server refused (a page range, a search phrase, a password), the failure also offers the way back to them with the input kept and focused ("Change the split", "Change the search text", "Try another password"). A single-file screen, whose chosen file's row replaces its intake, holds the intake's `useFileAcceptance` itself (`FileIntake`'s `acceptance`) and shows the `IntakeNotice` under that row, so the refusal of the rest of a mixed drop stays on the page. Two files in different roles take `PairedIntake`: each slot's heading is its role ("Base PDF (A)", "Overlay PDF (B)") with a line on what that role does, empty or chosen; a wrong file is refused beside its slot; choosing or removing a file keeps focus in that slot (its heading, or its chooser again) unless the visitor has moved on; where the order matters, a "Swap" text button under the pair exchanges the files and tells a screen reader what each slot then holds; on a phone the slots stack without their sheets. A tool that takes no file puts its own fields in `StudioSource`, the intake's card, headed by the field's label (`<h2><label>`). The media tools follow the same order in `MediaLayout`, all eight that kept `MediaRun` in their settings included (Add Subtitles, Favicon, Image Palette, Collage, Merge Images, Remove Image Watermark, Trim, View EXIF): their run button is the action bar now (`MediaRun`'s `bar`). Editors keep their own canvas (Sign, Edit, Annotate, Redact, Fill Form, Form Creator and the rest) and say where the file goes on their chosen-file row or header (`FileUploadZone`, Delete and Extract Pages, Edit PDF). Form Creator's detected fields are proposals, never fields: dashed on the page in their own colour (`PdfPageStage`'s `proposed` region kind) and listed apart from the fields the visitor placed, each accepted, edited first or rejected; only accepted fields are sent when the form is created. When detection answers, focus goes to the proposals' heading (the panel's heading if there are none); accepting or rejecting one moves it to the next proposal's name (to the panel's heading once none is left), both with `focusIfIdle`, and a status line tells a screen reader what was done and how many are left. The JSON / XML Formatter keeps its operation toolbar above its paired input and output panes, as code tools do; when those panes stack on a narrow screen, a run from its button moves focus to the output.
- **Not on the kit's failure result yet** (a follow-up): Compare PDF and Add Attachment take their two files through `FileIntake` and run from the action bar, but a failed run still shows an inline banner over the form instead of `StudioResult`'s failure, and their intakes predate `PairedIntake` (an eyebrow over each slot, the role gone once a file is chosen). On layouts of their own, by design: the editors, the code, text and archive tools in `LabWorkspace` (Create ZIP Archive among them), the AI workbenches, OCR PDF's engine panel (its failed and partial runs end on `StudioResult`), the reports and the generators.
- **One download policy (step 2b).** A finished run downloads its result by itself, once: one file, or one ZIP for several (`useDownloadOnce`; never twice for one run, and a retry's run downloads once too). The result then offers it again under one name, "Download again" or "Download ZIP again" (`downloadAgainLabel`), and says so ("The download has started."). Tools where the visitor reviews or edits before keeping the result keep an explicit download instead: the editors, whose save button is the download, and the tools that show their result to check first (Gemini Watermark Remover's corner, Background Remover's cut-out, the text of PDF to Text, OCR, Summarize, Translate and Transcribe, Subtitle Generator's captions (corrected while the video plays, then saved as SRT or VTT), Subtitle Translator's cues (each translation beside its original, then saved as SRT or VTT), Voice Noise Remover's cleaned sound (played beside the original, then saved with "Download WAV"), the generators' preview, Extract Archive's contents and the reports).
`useExperience()` stores Air/Play independently of light/dark/system. It synchronizes same-tab, cross-tab and operating-system changes, supports blocked storage, and coordinates the prepaint theme. Root attributes are `data-experience`, `data-appearance` and `data-theme`.
## Navigation
Both experiences use a sticky top navbar. Air is full-width with a quiet bottom rule; Play is a floating rounded bar. Desktop links expose All tools, Pipeline, Batch, AI Studio, Vault and Dev API, followed by My Stuff. The header actions are tool search (⌘K), the Air/Play switch, the light/dark toggle and an Account button that opens account settings (Sign in when signed out). On narrower screens the links move to a second header row that scrolls sideways; on phones All tools, AI Studio, Vault and Dev API come first. The footer holds the remaining destinations (About, Support, Guides, Compare, Account settings and the trust pages), the install/offline controls, the quick tour and keyboard shortcuts. There is no overflow menu, global sidebar or bottom navigation.
## Air
An open studio with content up to 1240px and 40px desktop gutters. Manrope handles headings and body text, with its standard ligatures off in text and form fields: they draw "--", "->" and "<-" as one dash or arrow, which misquotes code and comments in guide answers. Code is set without ligatures in both experiences. The `font` shorthand turns them back on, so a rule specific enough to outrank Air's sets its font with longhands. Morning Mist is cool and soft; Graphite is neutral with blue actions. Headers reach 58px, reducing to 38px on phones. Tool page headings are the tool's name at up to 48px (30px on phones) over a 17px muted promise line. Panels use 22px radii and fine borders; form fields and search boxes use the firmer field edge (`--pt-field-line`) described below. A compact category index sits beside the tool library. Tool settings sit beside the canvas; Pipeline uses an orderly chain; account and API pages pair focused controls with useful context. The policy reader has a sticky left contents column.
Air enters with a 9px upward fade over 420ms. Progress follows actual work; there are no fake completion timers.
## Play
A personal workshop with Bricolage Grotesque headings and Outfit body text. Blush uses raspberry actions; Charcoal uses warm coral actions. Colored papers carry paired foregrounds, including in dark mode: an ink (`--pt-pdf-ink` and its siblings) and a muted ink (`--pt-pdf-muted` …) for secondary copy such as category blurbs and recipe numbers, each at least 4.5:1 on its paper. Secondary copy on a paper is never dimmed with opacity, which put it at 2.9–4.4:1. The JSON / XML Formatter's output pane is the code paper in both themes, so its text takes the code inks and its syntax colours are darker variants, each at least 5:1 on the paper. The AI workbenches (Translate, Summarize, Transcribe, Image OCR) do the same: their intro sits on the image paper and their option panel on the code paper (Summarize's length panel on the media paper), so the headings, labels and links there take those inks, and the choices inside sit on a surface. Headers reach 66px; tool page names reach 60px (34px on phones) over an 18px promise line; panels use 24–30px radii, tactile shadows and pill actions.
Layouts change as well as colors: categories become a horizontal shelf and the library uses three columns; tool settings move to a horizontal work shelf below the intake, never above it, and the action bar floats under it as a dock; Batch uses job cards; the Vault form moves to the left; AI uses a horizontal studio navigation; API connection and example panels swap positions. Support puts its contact note beside the answer shelf; documents become a broad rounded reading sheet below chapter navigation. Advanced editors preserve their specialized controls inside the new workspace composition.
Play entrances use a small rotation, scale and overshoot over 550ms. Hover details lift or gently rotate paper objects. The same input nodes stay mounted through Air/Play changes.
## Components and accessibility
Self-host fonts. Keep semantic Tailwind tokens as HSL triplets in `tokens.css`; experience surfaces and color pairs live in `experience.css`. Secondary-page and workflow styling are isolated in their own stylesheets.
Use visible keyboard focus, proper labels, real disabled states and accessible progress. Respect reduced motion for all entrances, transitions, connectors and scroll navigation. Top navigation stays accessible on mobile; panels stack and long code/chapters scroll within their own containers. Labels wrap inside narrow provider controls. Native dialogs manage focus and dismissal.
Whisper (Transcribe Audio, Subtitle Generator) and OPUS-MT (Translate PDF, Subtitle Translator) run in a worker, not on the page's thread, so the page keeps drawing and Cancel answers at once. Summarize PDF and Smart Redact still run their models on the page's thread; a new on-device model should use a worker. Translate PDF and Subtitle Translator show the load as it is: the download from Hugging Face with a percent, only while files are downloading; then the model loading, a travelling bar with no percent, since building a model has no measure; then the work, counted in pieces or lines. Their Cancel ends the worker and clears a download it cut short, and a cancel is not reported as a failed run.
Keyboard and focus: the first Tab stop is the skip link, a solid action-colour tab styled in `index.html` itself (Tailwind does not scan that file). The root's `scroll-padding-top` follows the sticky header's real bottom edge (`--pt-header-clearance`, measured by `useStickyHeaderClearance`), so focused controls never sit under it at 200% zoom or on short screens. Its `scroll-padding-bottom` does the same for a tool's sticky action bar (`--pt-action-clearance`, measured by `useActionBarClearance` while the bar can stick). When a run finishes, focus moves to the result heading (`StudioResult`'s heading, the media studio's summary), and after "Choose a different file" to the intake, but only when nothing else holds focus (`focusIfIdle` in `skins/experience/focus-result.ts`): never out of an open dialog or a field the visitor moved to while the run finished. The ⌘K results count is announced once typing settles and cleared when the dialog closes, and its placeholder uses the muted ink.
Colour roles added in this pass, all in four palettes: `--warning` / `--warning-soft` (tokens.css) for "needs attention, not destructive": refused files, failed runs, read-this-first notes, at least 4.5:1 on cards, the page and its own tint. `--pt-field-line` (and `--input` for Tailwind fields) is the edge of text fields, selects and search boxes, at least 3:1 on the page, cards and panels, because a field's edge is often its only cue; panel and divider lines stay fine. A few outline-style controls already drew their edge with `--input` and so share the darker line: JSON / XML Formatter's segmented mode switch and outline buttons (`JsonXmlFormatterUI.css`) and `.cp-button-outline`. Their text names them, so the line is not their only cue; it is kept for one consistent control edge. Smart Redact's category chips use the paper pairs and semantic tones instead of dark-only Tailwind inks.
Failure grammar, shared by Air and Play and by every intake:
- A file an intake cannot take is never dropped silently. `FileIntake`, `MediaUpload`, `FileChooserButton` and the Pipeline's input (through `useFileAcceptance` and `IntakeNotice`) show a notice beside the intake that names the file and says what the tool takes (`lib/file-acceptance.ts`), and a single-file screen that holds its intake's acceptance itself (Split PDF, Split by Text) keeps that notice under the chosen file's row; drop zones that hand files straight to `useMultiFileProcessor.addFiles` get the same words in a toast, and so does the refused part of a mixed choice made from a result, which the result leaves as soon as the rest is accepted. `FileIntake`'s one real button is its visible call to action, and its accessible name starts with that text ("Choose files: …"); the native button's own activation opens the chooser, with no key handler of its own.
- The notice links another tool only when it does the same job for that format (Image Compressor for a PNG dropped on Compress PDF) or turns the file into what this tool takes (Image to PDF for Merge PDF); a converter handed what it makes says so ("It’s already a PDF."). Otherwise it stops at what the tool takes: a tool that merely opens the file is not advice. Job words come from slugs, never generic verbs ("convert", "extract", "remove", "format"), and adding and removing something (a watermark) are different jobs.
- `StudioResult` has three outcomes. Success keeps the check and "Ready for what’s next". Partial shows both parts: the receipt for what worked, a warning badge and the per-file reason for what didn't. Failure carries the warning mark, no eyebrow and no receipt, leads with "Choose a different file" (which opens the chooser directly), or, where what failed is not a file to choose again, with the way back to it, kept as it was (`StudioActions`' `back`: "Change the address", "Edit the HTML", and "Change the files" for two named files the server's reason does not tell apart), and offers "Try again" only for failures another attempt could fix: a dropped connection, a time limit, the rate limit or a server fault (`isTransientFailure` in `lib/toolRun.ts`). The line under the heading names only the recorded kind of failure (`retryLine` in `studio-outcome.ts`): "It ran out of time.", "Too many requests just now.", "The connection dropped.", "The server couldn’t finish it."; it never guesses a cause. Every tool screen built on `useMultiFileProcessor` ends on it, the ones moved in step 2b through `ProcessorResult`: the headings are "This PDF couldn’t be cropped.", "None of these PDFs could be cropped." and "1 of 2 PDFs cropped.", the failed file's reason sits on its row, and a result that worked but needs reading first (Remove Bates Numbers leaving a stamp in place) shows the partial badge instead of the success check.
- The media studio (`MediaBatchStudio`) follows the same grammar in its own layout: a warning triangle for every failure, "1 of 2 files ready." with a warning badge and "1 couldn’t be processed; the reason is shown with the file." for a partial run (the file list sits beside the summary on a wide screen and above it on a phone, so the line does not say "below"), and no "You can still process the original file." once processing it has failed.
- Merge leaves out a file the browser can't read as a PDF ("This doesn’t look like a valid PDF") or one locked with a password, keeps it listed with the reason and merges the rest.
- Batch and the Pipeline keep their own layouts but follow the same retry rule (step 2b): Batch offers "Try again" (per file, and "Try N again" for the queue) and counts in "Process N" only the failures another attempt could fix; a file the server refused keeps its reason, in full, on its row. The Pipeline offers "Try again from step N" only after a failure that can pass, with its recorded kind ("It ran out of time."); a refused step says why on the step and offers nothing. The shared `MultiFileQueue` offers its "Try again" the same way.
Status, file counts and percentages come from real state. No simulated success, invented uptime or certification claims. Local tools, temporary server processing and user-selected AI providers have different disclosures. A saved password is encrypted locally; a server operation may receive the password needed for that job.
## PWA
One compact install/offline entry opens the PWA controls. Native installation is offered when available; other browsers receive platform instructions. Updates wait for an explicit reload decision. Only static assets enter the app cache; private API/account/file responses are excluded. Production build hashes version the worker. The manifest includes real Air/Play desktop and mobile screenshots.
## Source and verification
- `src/skins/experience/ExperienceShell.tsx` and `ExperienceHome.tsx`
- `src/skins/experience/Studio.tsx`, `ToolWorkspace.tsx`, `ToolGuide.tsx`, `ToolStudio.tsx`, `ProcessorStudio.tsx`, `useDownloadOnce.ts`, `studio-outcome.ts`, `tool-location.ts`, `ToolWhere.tsx`, `useActionBarClearance.ts`, `ContentStudio.tsx`, `GuidesStudio.tsx`, `DocumentStudio.tsx`, `TrustCenter.tsx`
- `src/skins/experience/navigation.css`, `footer.css`, `studio.css`, `content.css`, `comparison-studio.css`, `documents.css`, `tool-workspace.css`, `tool-studio.css`, `workflows.css`, `secondary-pages.css`
- `src/lib/experience.ts`
- `src/components/pwa/`, `public/sw.js`, `public/manifest.json`
Keep browser checks, screenshots and backend-output evidence in local release records. Do not publish account-specific audit artifacts.