User Guide
SignQuote has four screens and two implicit roles. A salesperson lives in the
quote screens; the owner lives in Settings. There is no login — the navigation tabs are
the only role switch. The top bar (src/App.tsx) shows just two tabs, Quotes and
Settings, so anyone can move between roles, but the app is built so the two worlds never
leak into each other.
The hard rule that draws the line: no cost, hourly rate, labor-hour, or full margin number ever appears in the quote screens or on the PDF (tested as AC-10). The only money the salesperson sees is customer-facing prices, plus a margin badge and two reference prices. The complete cost breakdown lives on exactly one card on Settings. See Architecture for how the role boundary is enforced in code.
| Role | Screens | Sees |
|---|---|---|
| Salesperson | Quotes list, Quote editor, PDF preview | Prices, the margin badge, the two “Pricing detail” prices |
| Owner | Settings | Everything above plus costs, rates, hours, the full cost-plus breakdown |
Screen 1 — Quotes list
The salesperson’s landing screen (src/screens/QuotesList.tsx). It is a single table of recent
quotes, sorted ascending by quote number. Click any row to open it in the editor.
Columns
| Column | Source | Notes |
|---|---|---|
| # | quote.number | e.g. #1001 |
| Customer | customer.name (+ company on a second line) | Shows — if the name is still blank |
| Sign | the quoted text + describeConfig(...) | e.g. "CAFE" over 4 × 18" front-lit channel letters — … |
| Total | quote.pricing.total | The only money on this screen — whole dollars |
| Date | quote.createdAt | Formatted issue date |
| Actions | — | Duplicate and Delete buttons |
Row actions and buttons
- New quote (top-right, and again in the empty state) opens the editor in new-quote mode.
- Duplicate clones a quote. The button stops the row click so it does not also open the quote.
- Delete prompts a
window.confirm("Delete quote #…?")before removing the quote.
No costs or margins appear here. The only number on the list is the customer-facing
Total (moneyWhole(q.pricing.total)). No badge, no breakdown — consistent with the role
boundary above.
Never empty. The app seeds two example quotes — #1001 “CAFE” (channel letters, customer Maria Torres / Cafe Luna) and #1002 “LAW OFFICE” (dimensional letters, customer David Chen / Chen & Associates LLP) — so a demo always opens onto data (AC-9). If every quote is deleted, the list shows an empty state (“No quotes yet”) with a centered New quote button. The seeds (and all settings) can be restored from Settings (see below).
Screen 2 — Quote editor
The core screen (src/screens/QuoteEditor.tsx). It has two modes.
New-quote mode — the forced first choice
Opening New quote (quoteId === null) shows NewQuotePicker: a centered prompt, “What kind
of sign is this?”, over the two large category cards (src/screens/editor/CategoryCards.tsx):
| Card | Subtitle |
|---|---|
| Channel letters | Illuminated — front-lit or halo |
| Dimensional letters | Non-illuminated — cast or flat-cut |
Picking one calls createQuote(category), which seeds that category’s defaults
(defaultConfigFor in src/data/defaults.ts) and drops you into the editor. There is no blank
editor — the category choice always comes first.
Editor mode — layout
The editor is a responsive grid: a two-column form on the left (lg:col-span-2) and a
sticky live price panel on the right. The form is built from large-target primitives in
src/screens/editor/controls.tsx — SelectField, StepperField (−/+ with a typeable number),
ToggleField, TextField, and a Disclosure for collapsible sections. At the top sit the same
two category cards, now compact; clicking the inactive one converts the quote (see below).
Every form change reprices instantly: each handler calls updateQuoteConfig, which recomputes the
pricing snapshot in context, and the price panel re-renders.
The form sections
The Sign section holds the shared fields plus a category-specific branch.
Shared — What does the sign say? (signTextLabel, max 40 chars), How many letters?
(letterCount, 1–100), How tall are the letters? (inches) (letterHeight).
The height stepper caps differ by category: 6–60 in for channel letters, 6–24 in for
dimensional. For dimensional letters the field commits raw typed values (the engine clamps to
its 6–24 in grid), and shows a note — Price grid covers 6–24 in, or, when the engine clamped,
Outside the grid — clamped to 6–24 in for pricing.
Channel-letter branch (ChannelFields): How is it lit? (front-lit / halo-reverse),
Return depth (3/4/5/6 in, with the note “5 in is the most common coil width”), Face
material — shown only for front-lit, since halo letters have no acrylic face — LED system
(from the owner’s seeded LED products), and Mounting (flush/stud or raceway).
Dimensional branch (DimensionalFields): Material (the rows of the owner’s price grid,
with a thickness + base-finish note), Finish, and Mounting (flush stud / spacer-standoff /
rail). Spacer/standoff carries the note “Priced same as flush (no sourced uplift)”. There is
no illumination field — dimensional letters are implicitly non-illuminated.
Below the sections, a small derived strip shows Total upright inches = letterCount × letterHeight, the input the pricing engine works from.
Category conversion
Clicking the inactive category card calls convertCategory, which rebuilds the config from the
other category’s defaults but preserves the sign text (signTextLabel) and letter count
(letterCount) the salesperson already entered. Everything else resets to that category’s
sensible defaults.
The live price panel
The sticky right rail (src/screens/editor/PricePanel.tsx) is the salesperson’s payoff:
- Big total —
moneyWhole(p.total), the headline price. - Margin badge (channel letters only) — a pill reading e.g.
Margin 74.7%(the seeded CAFE quote), colored green / amber / red byp.badge. A red badge adds a warning line: “Below margin floor — this price approaches material + labor cost.” This percentage is one of the deliberate exceptions to the role boundary (decision D3). - Line items — the customer-facing price lines (
p.lineItems), label and amount only. - Derived LED/power chips (channel letters only) — modules per letter, total modules, total watts, and power-supply count. (Install hours stay owner-only — they never appear here.)
- Pricing detail — a collapsed disclosure showing exactly two prices (decision D3):
Quoted (per-inch) =
p.fabPerInchand Reference (cost-plus ×markup) =p.fabCostPlus. No costs, rates, or hours — just these two price numbers, for the salesperson to sanity-check the quote. - PDF layout toggle — a segmented Itemized / Total only control that sets
quote.pdfMode. - Download PDF — the primary one-click action; disabled until a customer name is entered
(it shows “Enter a customer name first”). It generates the file directly via
downloadQuotePdf. - Preview PDF — secondary; also name-gated; opens the preview modal (Screen 4).
The margin badge and the two “Pricing detail” prices are the only numbers in the quote screens that touch margin or the cost-plus path — and even these are prices, never costs. Everything beyond them (material costs, rates, labor hours) lives on Settings. See the Pricing Engine for what these two numbers mean.
The 2-minute path (AC-3)
The whole product is built so a salesperson can go from nothing to a downloaded PDF in under eight interactions (AC-3). For a front-lit channel-letter quote:
New quote → Channel letters
On the list, click New quote, then the Channel letters card. (2 clicks.) The editor opens fully pre-filled — front-lit, 4 letters, 18 in, SloanLED Prism 12 LEDs.
Set the letter count
Use the How many letters? stepper to match the job. (1 interaction.)
Set the letter height
Use the How tall are the letters? stepper. (1 interaction.)
Type the customer name
Enter a name in the Customer name field — this unlocks the PDF buttons. (1 interaction.)
Download the PDF
Click Download PDF. The branded quote-{number}.pdf downloads. (1 click.)
That is six interactions, with the price re-computing live the entire time. Leaning on the defaults — most fields arrive already sensible — is what keeps the count low.
Screen 3 — Settings (owner)
The owner’s screen (src/screens/SettingsScreen.tsx). Changes save automatically and apply to new
quote calculations immediately; saved quotes refresh when reopened. Every field shows its seeded
default and a source note beneath it, which lends the demo credibility. Full per-field details
live in the Settings Reference; the data shapes are in the
Data Model.
The screen is a stack of cards, in spec section order:
-
Grouped constant cards (
ConstantGroupCard) — the Table B numeric constants, grouped by section (per-inch rates, materials, labor, install & fees, …). Each field has a “Default: … — ‹hint›” line and each card has a per-section Reset that restores just that group’s keys fromDEFAULT_SETTINGS. -
LEDs & power (
LedProductsCard) — an editable table of the three seeded LED products (cost $/module, watts/module, modules/ft) plus the power constants. Seeded rows include Generic 12V module, SloanLED Prism 12 (the default), and SloanLED high output. -
Dimensional letter price grid + multipliers (
DimGridCard) — the editable$ per lettertable at the 6/12/18/24 in anchors (quotes interpolate linearly between them; heights clamp to 6–24 in), plus the finish/mount/dealer multipliers. The card carries the taxonomy’s own warning tag.⚠️The grid is reseller list pricing — dealer-net is lower. This is shown as an amber tag on the card so an owner knows to adjust the numbers down to their actual dealer cost before relying on the dimensional prices.
-
Shop info (
ShopInfoCard) — the identity fields that feed the PDF header (name, address, phone, email), plus logo upload (PNG/JPG, read to a data URL and shown on the PDF header), quote-validity days, and terms text. Seeded shop: Summit Signs & Lighting, Austin TX. -
Demo data (
DemoDataCard) — a Reset demo data button. After a confirm, it restores the two seeded quotes (#1001 “CAFE”, #1002 “LAW OFFICE”) and returns every setting to its default; quotes you created are deleted. Use it between demo runs (AC-9). -
Pricing preview — full cost breakdown (
PricingPreviewCard) — pick any quote and see a worked, line-by-line decomposition: per-letter and per-job materials, labor hours and rates, the cost subtotal (the floor), the markup, the cost-plus reference vs the quoted per-inch price, the margin badge, and install. For dimensional letters it shows the grid lookup and interpolation instead.
The Pricing preview is the only place in the entire app where costs, hourly rates, and labor
hours render (AC-10). It is computed live against the current constants via
computePricing — not the quote’s stored snapshot — so edits made in the cards above reflect
here instantly. It is never shown to the salesperson and never reaches the PDF.
Screen 4 — PDF preview modal
Opening Preview PDF from the price panel mounts PdfPreviewModal
(src/pdf/PdfPreviewModal.tsx). It renders the branded quote inline in a PDFViewer and has a
Download PDF button. Close it with the Close button, the backdrop, or the Escape key.
Both the inline preview and the downloaded file are driven by the same QuotePdfDocument
component (src/pdf/QuotePDF.tsx), generated entirely in the browser, so what you preview is
exactly what downloads. The file is named quote-{number}.pdf (e.g. quote-1001.pdf).
What’s on the PDF — one letter-size page:
- Header — the shop logo (or an initials placeholder) and shop identity from Shop info.
- Quote meta —
QUOTE #1001, the issued date, and a valid-until date (createdAt+quoteValidityDays). - Prepared for — the filled customer fields (blank ones are omitted).
- Sign summary — the quoted text and the
describeConfig(...)prose line. - Pricing — honors
quote.pdfMode: Itemized lists each price line then the total; Total only shows just the total. Either way, only prices and the total appear. - Footer — the shop terms, a signature/date line, and the note “This document is a quote, not an invoice.”
The PDF never renders costs, margins, hourly rates, labor/install hours, the cost-plus
reference price, or the margin badge (AC-10). Only pricing.lineItems and
pricing.total reach the document. For what those line items contain and how they are derived,
see the Pricing Engine.
See Getting Started to run the app, the Pricing Engine for the math behind every number, and Assumptions & Decisions for the rationale behind choices like D3 (the two reference prices in the editor).