Developer DocsScreens & UI

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.

RoleScreensSees
SalespersonQuotes list, Quote editor, PDF previewPrices, the margin badge, the two “Pricing detail” prices
OwnerSettingsEverything 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

ColumnSourceNotes
#quote.numbere.g. #1001
Customercustomer.name (+ company on a second line)Shows if the name is still blank
Signthe quoted text + describeConfig(...)e.g. "CAFE" over 4 × 18" front-lit channel letters — …
Totalquote.pricing.totalThe only money on this screen — whole dollars
Datequote.createdAtFormatted issue date
ActionsDuplicate 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):

CardSubtitle
Channel lettersIlluminated — front-lit or halo
Dimensional lettersNon-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.tsxSelectField, 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.

SharedWhat 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 materialshown 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 totalmoneyWhole(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 by p.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.fabPerInch and 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 from DEFAULT_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 letter table 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:

  1. Header — the shop logo (or an initials placeholder) and shop identity from Shop info.
  2. Quote metaQUOTE #1001, the issued date, and a valid-until date (createdAt + quoteValidityDays).
  3. Prepared for — the filled customer fields (blank ones are omitted).
  4. Sign summary — the quoted text and the describeConfig(...) prose line.
  5. 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.
  6. 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).