DESIGN
rechaj-retrofit-app — product design
Section titled “rechaj-retrofit-app — product design”Multi-persona retrofit station app. RSM, Team Lead, Retrofit Tech, Auto-mech, QA Technician — same app, role-aware dashboards. Built on the canonical .phone mobile shell + rsm-* scaffold.
Rationale
Section titled “Rationale”(Why this product looks the way it does. Audience, primary jobs-to-be-done, the visual decisions that diverge from the global system and why.)
Inherits from global DESIGN.md
Section titled “Inherits from global DESIGN.md”Start here when adding a new screen or extending a persona. All these patterns are inherited verbatim from the master DESIGN.md — read them first:
- Mobile App Anatomy —
rsm-*scaffold —.phoneshell,.rsm-page-head,.rsm-body,.rsm-nav,.rsm-section-head,.rsm-wo-summary,.rsm-q-row,.rsm-result-band,.rsm-stat,.rsm-profile-*,.rsm-sheet-*. - Status chips — canonical 3-color rule — Pending / In progress / Completed + red failure.
- Class-name prefix ownership — this app owns
rt-*(capture row, shared with kit-production),tl-*(legacy Team Lead timeline rows). All other classes are sharedrsm-*. - Activated-sheet Success Modal — full-screen confirmation for major transitions (Sent to QA, Escalated to RSM, Password reset).
- Inline error sheet — bottom-sheet for recoverable failures (Invalid chassis, Plate mismatch).
- Read-only Evidence Sheet + Edit-submission affordance — view-only audit of any captured evidence with optional edit-resubmission flow.
- Camera-only evidence — no infrastructure hints — every camera-tile flow in this app: chassis VIN, retrofit checkpoints, auto-mech steps, QA sub-checks, MEI inspection.
The remainder of this file covers retrofit-specific extensions: typography lock-ins, the 3-photo-slot retrofit-tech capture pattern, MEI v2 weighted scoring, status timeline ordering, list picker usage.
Cards — no strokes
Section titled “Cards — no strokes”Cards never carry a visible border or stroke — shadow + background only. Inherited from the master DESIGN.md and restated here so it isn’t lost when this product’s CSS gets extended.
- ❌
border: 1px solid var(--grey3)(or any colour) on any card, readonly tile, sheet, row, or grouping container. - ❌ Single-side accent stripes — no
border-left,border-top,border-right, orborder-bottomto colour-code state. Use the card’s background tint alone to communicate state (amber bg = warning, red bg = fail, etc.). - ✅
background: <neutral or tinted bg>+box-shadow: var(--sh-card)+border-radius: 14px(or the matching radius token).
Sibling spacing — labels hug their content, similar elements 16px, different 24px+
Section titled “Sibling spacing — labels hug their content, similar elements 16px, different 24px+”Inherited from the master spacing rule. Restated here because every retrofit screen has stacks of similar cards (MEI components, step cards, session-header sections, crew cards) and getting this wrong reads as visual drift instantly.
- 8–12px from a section head to its first card. The section head is a label for the content beneath — they should read as a unit. Apply via
.rsm-body .rsm-section-head + * { margin-top: -10px; }to pull the next sibling closer than the body’s 18px flex gap. - 16px between two or more cards/rows of the same kind in a stack (e.g. session-header cards; the 7 MEI component cards; the 5 retrofit step cards; the crew progress cards). Implement by wrapping the stack in a flex-column container with
gap: 16px— do not compose the parent body’s gap with a per-itemmargin-top. - 8px is reserved for tightly grouped same-kind items (a row of photo tiles, adjacent score chips, sub-step pills). Use sparingly.
- 24px+ between two elements of a different kind on the same screen that are not in a label-to-content relationship — hero card → section head, content stack → bottom CTA block. Pick higher values (32–48px) for stronger visual separation when a section ends and another begins.
Typography weights — bold sparingly
Section titled “Typography weights — bold sparingly”Inherits from the global DESIGN.md typography rules and locks them in for this product:
- 400 / 500 — body copy, captions, metadata, labels (e.g.
BUNDLE SCANNED,Forgot password?). - 600 (semibold) — titles, section heads, names, IDs (WO-2407-1188, plate numbers, customer names), inline numerics (capacity counts, date rows, step numbers, avatars), button labels.
- 700 (bold) — reserved for:
- True KPI display numbers (
.rsm-stat-num, wallet amounts) where the digit is the focal point of the card. - Splash / onboarding / welcome / success display titles (
.ob-title,.welcome-title,.activated-title,.ob-brand-name). - The simulated iOS status-bar clock (
.sb-time) — system chrome only.
- True KPI display numbers (
Never use 700 for: customer names, WO numbers, profile names, avatar initials, section titles, result band headlines, or any ordinary inline emphasis. If you reach for bold, ask whether 600 + colour contrast already carries the weight.
Status chips — canonical 3-color rule
Section titled “Status chips — canonical 3-color rule”Status chips (WO state, task state, evidence state) across every persona — RSM, Team Lead, Retrofit Technician, Auto-mech — use exactly three colors:
| State | Background | Text | Use for |
|---|---|---|---|
| Pending | #FEF3C7 |
#B45309 |
Awaiting kit, Awaiting evidence, Pending, Booked |
| In progress | #DBEAFE |
#1E40AF |
In progress, With QA, currently being worked on |
| Completed | var(--g-3) |
var(--g-dark) |
QA passed, MEI passed, Done, Completed |
Use a 4th red variant (#FEE2E2 / #B91C1C) only for failure outcomes (MEI failed, QA failed).
Do not use an “Active” status. “In progress” covers the same meaning — collapse any “Active” pill to “In progress” so the chip vocabulary stays tight.
Canonical class names (apply on .rsm-q-card-status, .rsm-wo-card-status, .rsm-wo-detail-status):
<span class="rsm-q-card-status rsm-q-card-status--pending">Awaiting kit</span><span class="rsm-q-card-status rsm-q-card-status--in-progress">In progress</span><span class="rsm-q-card-status rsm-q-card-status--completed">QA passed</span><span class="rsm-q-card-status rsm-q-card-status--fail">MEI failed</span>Legacy aliases (--active, --pass, --info) are kept temporarily for backward-compat but are remapped to the canonical colors and should be removed when touched.
Note: this rule does NOT apply to workflow-type chips (e.g.
Retrofit/Inspection/Auto-mech/MEI) which use the brand-green and inspection-blue tints to identify the kind of work, not its state. Those keep their existing palette per the Workflow type cell pattern documented above.
Screens
Section titled “Screens”See screens/ for canonical interactive HTML mockups.
Prototypes
Section titled “Prototypes”See prototypes/ for narrated end-to-end flows.
Retrofit Technician — 3-checkpoint × 3-photo-slot capture
Section titled “Retrofit Technician — 3-checkpoint × 3-photo-slot capture”The Retrofit Technician evidence flow follows the Retrofit Evidence Capture App Surface Specification (docs/attachments/TH-Retrofit Evidence Capture App Surface Specification for Team Lead and Technician-150526-055211.pdf) verbatim.
Three checkpoints, three labelled photo slots each:
| # | Approved title | Slot labels |
|---|---|---|
| 1 | Decommission + bay clearance | Empty bay wide shot · Fuel isolation close-up · Storage / extraction proof |
| 2 | Mechanical mount + alignment | Motor seated / mounted · Alignment view · Fastener / torque proof |
| 3 | Electrical integration + live check | Harness routing / wiring overview · VCU + DU connection close-up · Live DU proof |
Rules:
- Page header title = short form
Checkpoint N(so it doesn’t wrap past the back button). The approved title + helper description live in the body (#step-instr-name+#step-instr-desc). - 3 fixed slots, 1 photo each. Each slot is a
.rt-slotwith a labelled.rt-slot-labelunderneath the.rt-slot-tile. Captured tiles get a.rt-slot-clearX button. Tap a captured tile to preview; tap X to clear; re-tap to recapture. - Helper description never gets shortened or rewritten — it’s quoted from the spec, full sentences, and wrapped in a white card (no stroke;
background:var(--white); border-radius:14px; padding:14px 16px; box-shadow:var(--sh-card)). - Live N-of-3 count + gated save CTA — the Save / Mark checkpoint complete button is disabled until all 3 slots are captured. The “Checkpoint complete” pill (
.rt-checkpoint-complete) reveals only after all 3 slots are filled. - Per-photo timestamps are stored in
stepState[idx].slots[i].timeso the Team Lead can show the per-photo timestamp in the read-only proof view. - Tech finished is the page-level submit CTA — disabled until all 3 checkpoints are green (
"All 3 checkpoints must be green to finish."is the helper line). - Notes placeholder:
"Anything noteworthy about this checkpoint..."(singular checkpoint, not step).
The Team Lead’s s-crew-detail-retrofit mirrors the checkpoint titles + 3-photo proof structure in read-only form. Tap a thumb to open the photo with its slot label + timestamp.
Camera-only evidence — no infrastructure hints
Section titled “Camera-only evidence — no infrastructure hints”Capture screens (retrofit checkpoints, auto-mech steps, QA Technician sub-checks, Team Lead retrofit/auto-mech evidence) never surface platform invariants like “EXIF + SHA-256 stamped on every shot” / “in-app camera only” / “no gallery import” as helper copy. These are infrastructure, not user-actionable rules.
Keep behavioural copy that adjacent to the camera tile — "Mandatory before pass or defect", "1 photo from Production QA", "View only after submission." — but strip pure-infra fragments. If a hint mixes both (e.g. "Camera-only · EXIF + SHA-256 stamped on every shot · view only after submission."), keep only the behavioural part ("View only after submission.").
Audit grep: grep -rn 'EXIF\|SHA-256\|In-app camera only' flows/ must return zero hits.
MEI v2 — weighted 7-component scoring
Section titled “MEI v2 — weighted 7-component scoring”The Auto-mech inspection flow defines the MEI v2 weighted composite model. Three persona screens render it (s-completed-inspection-detail in auto-mech, s-workflow-detail-inspection-completed + s-workflow-detail-inspection in RSM, s-completed-inspection + s-inspection-active + inspection-result-sheet in Team Lead) — all three must use the same components, weights, format, and band logic.
Components (in canonical order, weights sum to 100):
const MEI_LABELS = { frame: 'Frame integrity', // 25% electrical: 'Electrical wiring', // 20% brake: 'Brake system', // 15% suspension: 'Suspension & axle', // 15% tire: 'Tire condition', // 10% age: 'Keke age & model', // 10% modifications: 'Modification history', // 5%};const MEI_ORDER = ['frame', 'electrical', 'brake', 'suspension', 'tire', 'age', 'modifications'];const MEI_WEIGHTS = { frame: 25, electrical: 20, brake: 15, suspension: 15, tire: 10, age: 10, modifications: 5 };
function meiComposite(scores) { return MEI_ORDER.reduce((a, k) => a + (MEI_WEIGHTS[k] / 100) * (typeof scores[k] === 'number' ? scores[k] : 0), 0);}Bands (composite on a 1–5 scale):
| Letter | Range | Outcome | Pill |
|---|---|---|---|
| A | ≥ 4.0 | Fast-track to retrofit queue | Proceed |
| B | ≥ 3.0 | Retrofit with minor remediation · AM parts hold activated | Proceed |
| C | ≥ 2.0 | Mandatory remediation before retrofit queue entry | Conditional |
| D | ≥ 1.5 | VP approval required before retrofit | Escalate |
| F | < 1.5 | Reject — rider receives deficiency report; return after repair | Reject |
Display format on every record screen:
<div class="rsm-result-band" data-result="pass"> <div class="rsm-result-band-text"> <span class="rsm-result-band-label">Final MEI composite</span> <span class="rsm-result-band-score">3.85 / 5 · Band B</span> </div> <span class="rsm-result-band-pill">Proceed</span></div><p class="rsm-result-band-outcome">Retrofit with minor remediation · AM parts hold activated.</p>Per-component score chips render as X.X / 5 (1 decimal) since subject scores are averages of sub-step scores. Use --low (amber) when score ≤ 3 with caveats, --fail (red) when ≤ 2.
Don’t:
- Mix the old 7 components (Engine condition / Frame & chassis / Brakes / Suspension / Steering / Electrical & wiring / Body & cosmetic) — those are legacy and must not reappear.
- Surface
X / 100percentage form (e.g.88 / 100 · Band B) — always theX.XX / 5 · Band Xform. - Use a generic
Pass/Failpill — always use the band-specific action pill (Proceed/Conditional/Escalate/Reject).
Read-only evidence sheet + edit-submission affordance
Section titled “Read-only evidence sheet + edit-submission affordance”Completed retrofit checkpoints, auto-mech steps, and QA sub-checks all open as view-only bottom sheets so the Team Lead / QA can audit without re-entering the capture path. The pattern matches the kit-production sheet-evidence + .ka-evi-* treatment: a result chip + read-only photo thumbs + meta + optional note. No EXIF/SHA-256 caption on the meta line — "View only after submission." is the canonical copy.
The retrofit Team Lead’s image-preview sheet (#step-image-sheet) carries the same audit role for checkpoint photos: open a thumb → see the slot label, capture time, and actor in a .rsm-image-meta block.
Activated-sheet success modal — full-screen, not bottom-sheet
Section titled “Activated-sheet success modal — full-screen, not bottom-sheet”Success confirmations that mark a major state transition use the canonical full-screen activated-sheet shell, not a bottom sheet. Examples in this app:
s-account-activated— account createds-password-reset— password reset successfuls-queue-assigned— retrofit sent to QAs-chassis-escalated— invalid chassis escalated to RSM
Shell:
<div class="screen" id="s-some-success"> <div class="activated-bg" aria-hidden="true"></div> <div class="sb sb--dark" style="position:absolute;top:0;left:0;right:0;z-index:3;"><!-- status bar --></div> <div class="activated-sheet"> <div class="activated-pill"></div> <div class="activated-ic"><!-- green check --></div> <h2 class="activated-title">Escalated to RSM</h2> <p class="activated-sub">The invalid-chassis WO has been routed to the RSM for review.</p> <button class="btn-primary btn-primary--lg" style="width:100%;" type="button" onclick="closeSuccess()">Done</button> </div> <div class="home-ind"></div></div>Register the screen ID in the SCREENS array so goTo() routes to it. The Done CTA should reset any local state (clear inputs, clear in-flight photo, etc.) before navigating back to the queue / home tab.
Don’t put a major-transition confirmation in a bottom sheet — bottom sheets are for picker/confirmation patterns where the user might cancel. Once a WO is escalated / a kit is sent to QA / a password is reset, that’s a completed action and deserves the full-screen treatment.
Inline error sheet — bottom sheet for recoverable failures
Section titled “Inline error sheet — bottom sheet for recoverable failures”For recoverable input errors (e.g. invalid chassis number, plate mismatch), use a bottom sheet with two CTAs — one primary path forward, one secondary “try again”:
- Title in red (
color: #B91C1C;) - Red status banner echoing the typed value + a short reason (
var(--red3)background, alert-triangle icon invar(--red9)) - Primary CTA: the forward path (“Escalate to RSM”, “Send for review”)
- Secondary CTA: the retry path (“Re-enter chassis number”)
gap: 10pxbetween CTAs;padding: 20px 0 24pxon the CTA stack to space it off the body
Primary forward CTA opens an activated-sheet success modal once committed. Secondary retry closes the sheet, clears the relevant input + state, and re-focuses.
List picker
Section titled “List picker”Use the canonical .lp-* list picker for any single-select. In this app: assign rider to workstation, pick a zone, pick a shift.
Pattern: rows with optional 36×36 icon → label + sub-label → radio (right). Selected row has green border + light-green tint background; the matching radio fills with a green dot. Disabled rows go to 45% opacity and lose pointer events.
Drop-in markup:
<div class="lp-group" role="radiogroup"> <button class="lp-item" type="button" role="radio" aria-checked="false" onclick="pickAssign(this)"> <div class="lp-content"> <div class="lp-label">Workstation 5</div> <div class="lp-sublabel">Lead: Ifeoluwa Olukoga · Idle</div> </div> <span class="lp-radio"><span class="lp-radio-dot"></span></span> </button> <button class="lp-item" type="button" role="radio" aria-checked="false" aria-disabled="true" disabled> <div class="lp-content"> <div class="lp-label">Workstation 1</div> <div class="lp-sublabel">Lead: Babajide Hassan · 1/1 active</div> </div> <span class="lp-radio"><span class="lp-radio-dot"></span></span> </button></div>Drop-in JS:
function pickAssign(btn) { const group = btn.closest('.lp-group'); if (!group) return; group.querySelectorAll('.lp-item').forEach(b => { const active = b === btn; b.classList.toggle('is-selected', active); b.setAttribute('aria-checked', active ? 'true' : 'false'); });}Don’ts:
- ❌ Use
box-shadowrings instead ofborder-colorfor selection state. - ❌ Skip the radio — it’s the canonical affordance.
- ❌ Mix iconed and non-iconed rows in the same group — labels won’t align.
Status timeline / activity log
Section titled “Status timeline / activity log”Use the canonical global Status timeline for any state-machine-style progress list. In this app: the WO lifecycle log on the Team Lead’s Active WO screen, the RSM Status timeline on the workflow detail screen, and any future per-WO history view.
Product-scoped class prefix: .rsm-wo-step-* (matches the generic .tl-step-* structure exactly — class name only; CSS, states, and markup are identical).
Lifecycle events the WO log surfaces (all others belong inside the relevant crew detail screen, never the lifecycle log):
- Assigned to workstation
- WO accepted at workstation
- Chassis validated
- Kit requested from Inventory
- Inventory delivered bundle
- Bundle scanned · timers started
- Retrofit started · Auto-mech started (concurrent — both timers begin at scan-in)
- Retrofit finished · Auto-mech finished
- Retrofit evidence captured · Auto-mech evidence captured
- Retrofit complete · sent to QA
Don’t log per-step crew captures (Engine removed, brake pads replaced, harness installed, etc.) in the WO timeline — those live inside the Retrofit / Auto-mech crew detail screens.
Alerts & toasts
Section titled “Alerts & toasts”The canonical pattern lives in the global DESIGN.md → Alert Toast. The full prototype-flavored snippet is duplicated here so you don’t need to round-trip during a build.
Pattern: tinted pill, leading white badge, message, close button. Auto-dismiss 4s. No icons, no emojis. Top-center anchored.
Do use this pattern for save confirmations (Save changes → “Saved · Today’s slots updated”), publish events, errors, warnings.
Don’t use a dark capsule with an icon circle, bottom-of-screen toasts, or omit the close button.
Drop-in markup (mobile prototype)
Section titled “Drop-in markup (mobile prototype)”Mount this once at the .phone level, after all screens. The variant class is set by showToast():
<div class="alert-toast alert-toast--success" id="toast" role="status" aria-live="polite"> <span class="alert-toast-badge">Saved</span> <span class="alert-toast-text">Today's slots updated</span> <button class="alert-toast-close" type="button" aria-label="Dismiss" onclick="hideToast()"> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"> <line x1="4" y1="4" x2="12" y2="12"/><line x1="12" y1="4" x2="4" y2="12"/> </svg> </button></div>Drop-in CSS (mobile prototype)
Section titled “Drop-in CSS (mobile prototype)”Note position: absolute (not fixed) — .phone has overflow: hidden, so absolute pins it to the device frame, not the page viewport. top: 56px clears the status bar.
.alert-toast { position: absolute; top: 56px; left: 50%; transform: translate(-50%, -16px); z-index: 80; display: inline-flex; align-items: center; gap: 8px; border-radius: 999px; padding: 4px 12px 4px 4px; white-space: nowrap; box-shadow: 0 4px 20px rgba(0,0,0,0.12); opacity: 0; pointer-events: none; transition: opacity 0.25s ease, transform 0.25s ease;}.alert-toast.is-visible { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }.alert-toast-badge { display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.85); border-radius: 12px; padding: 3px 10px; font-size: 13px; font-weight: 500; line-height: 20px; white-space: nowrap;}.alert-toast-text { font-size: 13px; font-weight: 500; line-height: 20px; white-space: nowrap; }.alert-toast-close { background: none; border: 0; cursor: pointer; padding: 0 2px; margin-left: 4px; display: flex; align-items: center; color: currentColor; opacity: 0.55;}.alert-toast-close:hover { opacity: 1; }.alert-toast-close svg { width: 12px; height: 12px; }
.alert-toast--success { background: #dcfbe2; color: #05853e; }.alert-toast--info { background: #e6f4fe; color: #0d74ce; }.alert-toast--warning { background: #fff7c2; color: #ab6400; }.alert-toast--error { background: #ffebe8; color: #e32627; }.alert-toast--gray { background: #f0f0f0; color: #646464; }Drop-in JS
Section titled “Drop-in JS”const TOAST_VARIANTS = ['success', 'info', 'warning', 'error', 'gray'];let _toastTimer = null;function showToast(message, badge, variant) { const t = document.getElementById('toast'); if (!t) return; variant = TOAST_VARIANTS.indexOf(variant) >= 0 ? variant : 'success'; TOAST_VARIANTS.forEach(v => t.classList.remove('alert-toast--' + v)); t.classList.add('alert-toast--' + variant); if (badge !== undefined) t.querySelector('.alert-toast-badge').textContent = badge; if (message) t.querySelector('.alert-toast-text').textContent = message; t.classList.add('is-visible'); clearTimeout(_toastTimer); _toastTimer = setTimeout(hideToast, 4000);}function hideToast() { const t = document.getElementById('toast'); if (!t) return; t.classList.remove('is-visible'); clearTimeout(_toastTimer);}Usage: showToast("Today's slots updated", 'Saved', 'success'). Variant defaults to success if omitted.