Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,60 @@
# CLAUDE.md — Powder Coating Logix redesign
You are implementing a visual redesign in this ASP.NET Core MVC codebase (`PowderCoating.Web`). A full design brief lives at `design_handoff_pcl_redesign/README.md`**read it first** before editing anything.
## Hard rules
1. **Do not introduce React, Vue, or SPA frameworks.** The app is Razor + Bootstrap 5 + jQuery. Stay in that stack.
2. **Do not copy the JSX files as-is.** They are design references. Translate layout, tokens, and behavior to `.cshtml` + CSS.
3. **Never edit files under `wwwroot/lib/`.** Those are vendored libraries.
4. **Preserve all existing data bindings, asp-controller/action attributes, permission checks, and TagHelpers.** The redesign is presentational.
## Working order
1. Land design tokens in `wwwroot/css/site.css` (see README § Design Tokens).
2. Build four Razor partials in `Views/Shared/`: `_StatusChip`, `_Metric`, `_SectionHeader`, `_PowderSwatch`.
3. Rewrite `Views/Dashboard/Index.cshtml` hero + KPI sections.
4. Rewrite `Views/Jobs/Index.cshtml` stats + toolbar + table.
5. Rewrite `Views/Jobs/Board.cshtml` columns and cards.
6. Simplify `Views/Shared/_Layout.cshtml` sidebar (drop 11 of 12 gradient themes).
7. Sweep remaining list views (Customers, Invoices, Equipment, Maintenance, Vendors, PurchaseOrders) to use the new partials.
## Things to delete outright
- `.badge-glow-critical` keyframes and any `class="badge-glow-critical"` usage.
- The `[data-sidebar="forest|crimson|midnight|purple|teal|amber|indigo|emerald|rose|navy|slate"]` CSS blocks. Keep only `ocean` (rename to `ink`) and add `paper`.
- Every instance of `.stats-cards-desktop` + `.mobile-stats-compact` pair on list pages — replace with one `_Metric` row.
- Icon-bubble wrappers: `<div class="rounded-circle p-3" style="background:#dbeafe">…</div>` patterns around KPI icons.
- The `Welcome Back!` gradient banner on the dashboard.
## Things to add
- Google Fonts import for `Inter`, `IBM Plex Mono`, `Fraunces` at the top of `_Layout.cshtml` head.
- A `.mono { font-family: 'IBM Plex Mono', ui-monospace, monospace; font-feature-settings: "zero"; }` utility class.
- Mono numerics on every `.table td` that currently holds a count, currency, or ID (job numbers, invoice numbers, customer balance, etc.). Easiest via a CSS rule: `.table td.num, .table th.num { font-family: var(--font-mono); }`.
## Verification checklist
Before marking each screen done, confirm:
- [ ] No element uses `background: linear-gradient(` except the optional ink sidebar.
- [ ] No `rounded-circle` decorative icon bubbles remain on that screen.
- [ ] All status badges use the new `_StatusChip` partial.
- [ ] All numbers render in mono.
- [ ] Keyboard focus ring is visible on every interactive element (2px ember, 2px offset).
- [ ] Page renders correctly at 1280px, 1024px, and 375px.
- [ ] Dark mode (`[data-surface="ink"]`) still reads.
- [ ] Toastr notifications are still styled consistently.
## Don't touch
- `Controllers/`, `Services/`, `Hubs/`, `BackgroundServices/` — logic is unchanged.
- Database models, DTOs, or migrations.
- `Program.cs`, `Startup.cs`, DI registration.
## Dark mode
There is a dedicated brief: `design_handoff_pcl_redesign/DARK_MODE.md`. Read it before touching the token block, `_StatusChip`, `_PowderSwatch`, Toastr styles, or the top-bar theme toggle. It replaces the `[data-surface="ink"]` block in the README with a full-fidelity version, binds `data-bs-theme` so Bootstrap components flip correctly, and adds a persisted theme toggle.
## If in doubt
Open `design_handoff_pcl_redesign/Design Review.html` in a browser. The "after" screens are the source of truth for layout and spacing. The README covers tokens and partials. Everything else is implementation choice — pick the simplest Razor-native expression.
@@ -0,0 +1,294 @@
# Dark Mode Fix — Powder Coating Logix
Supplemental brief for the in-progress redesign. **Read `README.md` and `CLAUDE.md` in this folder first.** This document only covers the dark-theme gaps.
## Ground rules
1. **Two surfaces, one accent system.** `data-surface="paper"` (default, light) and `data-surface="ink"` (dark). Accent hues stay the same across both; only *lightness* and *chroma* of tints change.
2. **Bind Bootstrap's theme alongside ours.** Whenever `data-surface` changes on `<html>`, `data-bs-theme` must follow (`paper``light`, `ink``dark`). Without this, dropdowns, modals, toasts, form inputs, and tooltips will stay light-on-light or light-on-dark.
3. **User preference, persisted.** Default to `prefers-color-scheme`. Allow override via a toggle. Persist in `localStorage` under key `pcl.surface`.
4. **Every screen must be verified on both surfaces** before marking done.
## Replace the token block
Overwrite the `:root` + `[data-surface="ink"]` block in `wwwroot/css/tokens.css` with this. Key changes: chip tints get dark-surface variants, rule contrast bumps, swatch hairline flips, focus offset uses card color.
```css
:root {
/* Neutrals — warm paper (light surface) */
--pcl-ink: #0F0F10;
--pcl-graphite: #1A1A1C;
--pcl-slate: #3A3A3E;
--pcl-steel: #6B6B70;
--pcl-mute: #9A9A9F;
--pcl-rule: #E4E2DC;
--pcl-rule-soft: #EFEDE7;
--pcl-paper: #FAFAF7;
--pcl-paper-2: #F3F1EB;
--pcl-card: #FFFFFF;
/* Signal — hue preserved across surfaces */
--pcl-ember: oklch(0.68 0.17 50);
--pcl-ok: oklch(0.62 0.11 155);
--pcl-warn: oklch(0.76 0.13 80);
--pcl-bad: oklch(0.60 0.19 25);
--pcl-cool: oklch(0.58 0.09 240);
/* Chip tints — LIGHT surface (high-lightness wash) */
--pcl-ember-tint: oklch(0.95 0.04 55);
--pcl-ember-ink: oklch(0.42 0.15 40);
--pcl-ok-tint: oklch(0.95 0.04 155);
--pcl-ok-ink: oklch(0.35 0.09 155);
--pcl-warn-tint: oklch(0.96 0.05 85);
--pcl-warn-ink: oklch(0.42 0.10 80);
--pcl-bad-tint: oklch(0.95 0.04 25);
--pcl-bad-ink: oklch(0.40 0.15 25);
--pcl-cool-tint: oklch(0.95 0.03 240);
--pcl-cool-ink: oklch(0.38 0.08 240);
/* Hairlines + misc — light */
--pcl-swatch-edge: rgba(0, 0, 0, 0.14);
--pcl-focus-offset: var(--pcl-paper);
/* Bootstrap binding */
--bs-body-bg: var(--pcl-paper);
--bs-body-color: var(--pcl-ink);
--bs-border-color: var(--pcl-rule);
--bs-primary: var(--pcl-ink);
}
/* DARK surface */
[data-surface="ink"] {
--pcl-paper: #0E0E10;
--pcl-paper-2: #17171A;
--pcl-card: #1C1C20;
--pcl-ink: #F4F2EC;
--pcl-graphite: #E8E6DF;
--pcl-slate: #C2C0BA;
--pcl-steel: #8A8883;
--pcl-mute: #6A6864;
/* Rules — bumped from #2B2B2F → better contrast on --pcl-card */
--pcl-rule: #34343A;
--pcl-rule-soft: #26262B;
/* Chip tints — DARK surface (low-lightness wash) */
--pcl-ember-tint: oklch(0.26 0.06 55);
--pcl-ember-ink: oklch(0.82 0.15 55);
--pcl-ok-tint: oklch(0.24 0.05 155);
--pcl-ok-ink: oklch(0.80 0.12 155);
--pcl-warn-tint: oklch(0.26 0.05 85);
--pcl-warn-ink: oklch(0.85 0.13 85);
--pcl-bad-tint: oklch(0.26 0.07 25);
--pcl-bad-ink: oklch(0.80 0.16 25);
--pcl-cool-tint: oklch(0.26 0.04 240);
--pcl-cool-ink: oklch(0.80 0.10 240);
/* Hairlines flip to light */
--pcl-swatch-edge: rgba(255, 255, 255, 0.18);
--pcl-focus-offset: var(--pcl-card);
/* Bootstrap binding */
--bs-body-bg: var(--pcl-paper);
--bs-body-color: var(--pcl-ink);
--bs-border-color: var(--pcl-rule);
--bs-primary: var(--pcl-ink);
}
```
## Update `_StatusChip` to use the paired tokens
Old: chip foreground was `--pcl-{kind}` (the signal color itself). That's fine on light, unreadable on dark. New: chip foreground is `--pcl-{kind}-ink`, which is lightness-adjusted per surface.
```css
.pcl-chip {
display: inline-flex; align-items: center; gap: 6px;
padding: 2px 8px; border-radius: 99px;
font: 500 10.5px/1.2 "IBM Plex Mono", ui-monospace, monospace;
font-feature-settings: "zero";
letter-spacing: 0.02em;
}
.pcl-chip-dot { width: 6px; height: 6px; border-radius: 99px; }
.pcl-chip-ember { background: var(--pcl-ember-tint); color: var(--pcl-ember-ink); }
.pcl-chip-ember .pcl-chip-dot { background: var(--pcl-ember); }
.pcl-chip-ok { background: var(--pcl-ok-tint); color: var(--pcl-ok-ink); }
.pcl-chip-ok .pcl-chip-dot { background: var(--pcl-ok); }
.pcl-chip-warn { background: var(--pcl-warn-tint); color: var(--pcl-warn-ink); }
.pcl-chip-warn .pcl-chip-dot { background: var(--pcl-warn); }
.pcl-chip-bad { background: var(--pcl-bad-tint); color: var(--pcl-bad-ink); }
.pcl-chip-bad .pcl-chip-dot { background: var(--pcl-bad); }
.pcl-chip-cool { background: var(--pcl-cool-tint); color: var(--pcl-cool-ink); }
.pcl-chip-cool .pcl-chip-dot { background: var(--pcl-cool); }
.pcl-chip-neutral{ background: var(--pcl-paper-2); color: var(--pcl-slate); }
.pcl-chip-neutral .pcl-chip-dot { background: var(--pcl-steel); }
```
## Swatch hairline
```css
.pcl-swatch {
display: inline-block; width: 9px; height: 9px;
border-radius: 2px;
box-shadow: inset 0 0 0 1px var(--pcl-swatch-edge);
}
```
No more hardcoded `rgba(0,0,0,0.12)` — it disappears on dark.
## Focus ring
```css
:focus-visible {
outline: 2px solid var(--pcl-ember);
outline-offset: 2px;
box-shadow: 0 0 0 2px var(--pcl-focus-offset); /* "hole" matching surface */
}
```
## Oven meter pulse
Raise the minimum opacity so the dot remains visible on dark:
```css
@keyframes pcl-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; } /* was 0.3 */
}
```
## Bind Bootstrap theme
### `_Layout.cshtml`
Inside `<head>`, **before** the CSS includes, inline this script so first paint is correct:
```html
<script>
(function () {
var saved = localStorage.getItem('pcl.surface');
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
var surface = saved || (prefersDark ? 'ink' : 'paper');
document.documentElement.setAttribute('data-surface', surface);
document.documentElement.setAttribute('data-bs-theme', surface === 'ink' ? 'dark' : 'light');
})();
</script>
```
This avoids the light-flash-then-dark-flip on every page load.
### `wwwroot/js/theme-toggle.js` (new)
```js
(function () {
'use strict';
function apply(surface) {
document.documentElement.setAttribute('data-surface', surface);
document.documentElement.setAttribute('data-bs-theme', surface === 'ink' ? 'dark' : 'light');
localStorage.setItem('pcl.surface', surface);
document.querySelectorAll('[data-theme-toggle]').forEach(function (btn) {
btn.setAttribute('aria-pressed', surface === 'ink');
var icon = btn.querySelector('i');
if (icon) icon.className = surface === 'ink' ? 'bi bi-sun' : 'bi bi-moon';
});
}
document.addEventListener('click', function (e) {
var btn = e.target.closest('[data-theme-toggle]');
if (!btn) return;
e.preventDefault();
var current = document.documentElement.getAttribute('data-surface') || 'paper';
apply(current === 'ink' ? 'paper' : 'ink');
});
// Follow system changes only if user hasn't explicitly chosen.
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (e) {
if (!localStorage.getItem('pcl.surface')) apply(e.matches ? 'ink' : 'paper');
});
}
// Sync toggle UI on load
apply(document.documentElement.getAttribute('data-surface') || 'paper');
})();
```
Include it at the bottom of `_Layout.cshtml`:
```html
<script src="~/js/theme-toggle.js" asp-append-version="true"></script>
```
### Toggle affordance
Add next to the user menu in the top bar:
```html
<button type="button" class="btn btn-sm btn-link pcl-theme-toggle"
data-theme-toggle aria-pressed="false" aria-label="Toggle theme">
<i class="bi bi-moon"></i>
</button>
```
Minimal CSS:
```css
.pcl-theme-toggle {
color: var(--pcl-slate);
padding: 4px 8px;
border: 1px solid transparent;
border-radius: 6px;
}
.pcl-theme-toggle:hover { color: var(--pcl-ink); background: var(--pcl-paper-2); }
```
## Sidebar on dark surface
The ink sidebar was already using dark tokens, so most of it works. Two fixes:
1. Active-item left-bar stays ember (`--pcl-ember`), no change.
2. Group headers: change `rgba(255,255,255,0.35)` to `var(--pcl-steel)` so they track the token and stay consistent in both surfaces.
## Bootstrap components — verify, don't override blindly
Once `data-bs-theme` is bound, these should now render correctly. Spot-check each:
- [ ] Dropdown menus (`.dropdown-menu`) — background, item hover, divider.
- [ ] Modals — header/body/footer backgrounds, close button X visibility.
- [ ] Form controls — `input`, `select`, `textarea` background, border, placeholder color, focus ring.
- [ ] Toasts (Toastr) — Toastr isn't Bootstrap-themed; override its CSS manually:
```css
[data-surface="ink"] .toast-success { background: var(--pcl-ok-tint); color: var(--pcl-ok-ink); }
[data-surface="ink"] .toast-error { background: var(--pcl-bad-tint); color: var(--pcl-bad-ink); }
[data-surface="ink"] .toast-warning { background: var(--pcl-warn-tint); color: var(--pcl-warn-ink); }
[data-surface="ink"] .toast-info { background: var(--pcl-cool-tint); color: var(--pcl-cool-ink); }
```
- [ ] Tooltips — Bootstrap 5.3+ inherits `data-bs-theme` fine.
- [ ] Tables — `.table` uses Bootstrap's dark variables when `data-bs-theme="dark"`. If your rows look striped in a way you don't want, set `.table { --bs-table-bg: transparent; }`.
- [ ] DataTables (if used on list pages) — needs `<table class="table" data-bs-theme="…">` inherited from `<html>`; spot-check pagination + search input colors.
## Per-screen dark-mode checklist
Paste this into your review for each of Dashboard, Jobs list, Jobs board, Customers, Invoices, Equipment, Maintenance, Vendors, PO:
- [ ] Toggle to ink. Everything readable, no white flashes.
- [ ] All status chips legible (check `ok`, `warn`, `bad`, `cool`, `ember`, `neutral`).
- [ ] Powder swatches have a visible hairline.
- [ ] Mono numerics are `--pcl-ink`, not faded.
- [ ] Card rule lines visible against card background.
- [ ] Focus ring visible on `Tab` through all interactive elements.
- [ ] Dropdowns, modals, forms on this screen flip correctly.
- [ ] Toastr notifications triggered on this screen are readable.
- [ ] Oven meter dot pulse visible (Jobs board).
- [ ] Red left-edge on hot jobs readable (Jobs list + Board).
## Order of work
1. Swap the token block. **Commit.**
2. Add the first-paint script + `theme-toggle.js` + toggle button. **Commit.**
3. Update `_StatusChip` and `_PowderSwatch` CSS to use paired tokens. **Commit.**
4. Toastr overrides. **Commit.**
5. Walk every screen in the checklist order above.
~1 dev day if the light-mode implementation is already solid.
@@ -0,0 +1,359 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=1440" />
<title>Powder Coating Logix — Design Review</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=Fraunces:opsz,wght@9..144,500;9..144,600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<style>
:root{
--ink:#0F0F10;
--graphite:#1A1A1C;
--slate:#3A3A3E;
--steel:#6B6B70;
--mute:#9A9A9F;
--rule:#E4E2DC;
--rule-soft:#EFEDE7;
--paper:#FAFAF7;
--paper-2:#F3F1EB;
--card:#FFFFFF;
--ember:oklch(0.68 0.17 50);
--ember-tint:oklch(0.95 0.04 55);
--ember-ink:oklch(0.42 0.15 40);
--ok:oklch(0.62 0.11 155);
--ok-tint:oklch(0.95 0.04 155);
--warn:oklch(0.76 0.13 80);
--warn-tint:oklch(0.96 0.05 85);
--bad:oklch(0.60 0.19 25);
--bad-tint:oklch(0.95 0.04 25);
--cool:oklch(0.58 0.09 240);
--cool-tint:oklch(0.95 0.03 240);
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{
font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
background:var(--paper);
color:var(--ink);
font-size:14px;
line-height:1.5;
-webkit-font-smoothing:antialiased;
font-feature-settings:"ss01","cv11";
}
.mono{font-family:'IBM Plex Mono',ui-monospace,monospace;font-feature-settings:"zero"}
.serif{font-family:'Fraunces',Georgia,serif;font-variation-settings:"opsz" 144}
/* ────────── Document shell ────────── */
.doc{max-width:1320px;margin:0 auto;padding:56px 48px 120px}
.doc-head{
display:grid;grid-template-columns:1fr auto;gap:32px;align-items:end;
padding-bottom:28px;border-bottom:1px solid var(--rule);margin-bottom:40px;
}
.kicker{
font-family:'IBM Plex Mono',monospace;
font-size:11px;letter-spacing:0.14em;text-transform:uppercase;
color:var(--steel);display:flex;align-items:center;gap:10px;
}
.kicker::before{content:"";width:18px;height:1px;background:var(--ember)}
h1.doc-title{
font-family:'Fraunces',Georgia,serif;
font-weight:500;
font-size:64px;
line-height:1.02;
letter-spacing:-0.02em;
margin:14px 0 10px;
color:var(--ink);
}
h1.doc-title em{font-style:italic;color:var(--ember-ink)}
.doc-sub{max-width:640px;color:var(--slate);font-size:16px;line-height:1.55}
.doc-meta{
text-align:right;font-size:12px;color:var(--steel);
display:flex;flex-direction:column;gap:4px;
}
.doc-meta b{color:var(--ink);font-weight:600}
/* ────────── Section ────────── */
section.chapter{margin:88px 0 0}
.chap-head{
display:grid;grid-template-columns:120px 1fr;gap:32px;
padding-bottom:20px;margin-bottom:28px;
border-bottom:1px solid var(--rule);
}
.chap-num{
font-family:'IBM Plex Mono',monospace;font-size:12px;letter-spacing:0.1em;
color:var(--ember-ink);padding-top:6px;
}
.chap-title{
font-family:'Fraunces',Georgia,serif;
font-weight:500;font-size:32px;line-height:1.15;letter-spacing:-0.01em;
margin:0 0 8px;color:var(--ink);
}
.chap-lede{color:var(--slate);max-width:720px;font-size:15px}
/* ────────── Critique list ────────── */
.findings{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--rule);border:1px solid var(--rule);border-radius:2px}
.finding{background:var(--card);padding:22px 22px 24px}
.finding-tag{
font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:0.12em;
color:var(--steel);text-transform:uppercase;display:flex;align-items:center;gap:8px;
}
.finding-tag .sev{
width:8px;height:8px;border-radius:50%;
}
.sev.hi{background:var(--bad)}
.sev.md{background:var(--warn)}
.sev.lo{background:var(--cool)}
.finding h4{margin:10px 0 6px;font-size:15px;font-weight:600;letter-spacing:-0.01em}
.finding p{margin:0;color:var(--slate);font-size:13px;line-height:1.55}
/* ────────── Before/After stage ────────── */
.stage{
display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-top:24px;
}
.stage.single{grid-template-columns:1fr}
.mock{
background:var(--card);
border:1px solid var(--rule);
border-radius:8px;
overflow:hidden;
position:relative;
}
.mock-head{
display:flex;align-items:center;justify-content:space-between;
padding:10px 14px;border-bottom:1px solid var(--rule);
background:var(--paper-2);
}
.mock-label{
font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:0.14em;
text-transform:uppercase;color:var(--steel);
}
.mock-label b{color:var(--ink);font-weight:600}
.mock-label.after b{color:var(--ember-ink)}
.mock-tag{
font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:0.1em;
text-transform:uppercase;color:var(--steel);padding:3px 7px;
border:1px solid var(--rule);border-radius:99px;background:var(--card);
}
.mock-body{
position:relative;
overflow:hidden;
/* content is a fake app at ~1280px wide, scaled to fit column */
}
.scaler{
width:1280px;
transform-origin:top left;
/* transform scale is applied by component based on container width */
}
/* small callout strip under before/after */
.delta{
display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-top:14px;
}
.delta-col{
font-size:12.5px;color:var(--slate);line-height:1.6;
}
.delta-col h5{
font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:0.14em;
text-transform:uppercase;color:var(--steel);margin:0 0 6px;font-weight:500;
}
.delta-col ul{margin:0;padding-left:16px}
.delta-col li{margin:3px 0}
.delta-col li::marker{color:var(--ember)}
/* ────────── System tokens strip ────────── */
.sys{
display:grid;grid-template-columns:repeat(4,1fr);gap:1px;
background:var(--rule);border:1px solid var(--rule);border-radius:2px;
margin-top:28px;
}
.sys-cell{background:var(--card);padding:22px}
.sys-cell h5{
font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:0.14em;
text-transform:uppercase;color:var(--steel);margin:0 0 14px;font-weight:500;
}
.swatch-row{display:flex;flex-direction:column;gap:8px}
.swatch{display:flex;align-items:center;gap:10px;font-size:12px}
.swatch .chip{width:24px;height:24px;border-radius:4px;border:1px solid rgba(0,0,0,0.08);flex-shrink:0}
.swatch .lbl{color:var(--ink);font-weight:500}
.swatch .hex{margin-left:auto;font-family:'IBM Plex Mono',monospace;color:var(--steel);font-size:11px}
.type-sample{margin-bottom:12px}
.type-sample:last-child{margin-bottom:0}
.type-meta{font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--steel);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:2px}
.type-val{color:var(--ink);line-height:1.1}
/* ────────── Tweaks panel ────────── */
.tweaks{
position:fixed;right:20px;bottom:20px;width:280px;
background:var(--card);border:1px solid var(--rule);border-radius:10px;
box-shadow:0 20px 40px -20px rgba(0,0,0,0.25), 0 2px 4px rgba(0,0,0,0.04);
padding:14px 14px 10px;z-index:9999;font-size:12px;
display:none;
}
.tweaks.on{display:block}
.tweaks h5{
font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:0.14em;
text-transform:uppercase;color:var(--steel);margin:0 0 10px;
display:flex;align-items:center;justify-content:space-between;
}
.tweak-row{margin-bottom:10px}
.tweak-row label{display:block;font-size:11px;color:var(--steel);margin-bottom:4px;text-transform:uppercase;letter-spacing:0.06em}
.tweak-opts{display:flex;gap:4px;flex-wrap:wrap}
.tweak-opt{
flex:1;min-width:0;padding:6px 8px;border:1px solid var(--rule);
background:var(--card);border-radius:5px;cursor:pointer;font-size:12px;
color:var(--ink);font-weight:500;transition:all .15s;
}
.tweak-opt:hover{border-color:var(--slate)}
.tweak-opt.on{background:var(--ink);color:var(--paper);border-color:var(--ink)}
.tweak-dot{display:flex;gap:6px}
.tweak-dot button{
width:22px;height:22px;border-radius:50%;border:2px solid transparent;
cursor:pointer;padding:0;
}
.tweak-dot button.on{border-color:var(--ink)}
</style>
</head>
<body>
<div class="doc">
<header class="doc-head">
<div>
<div class="kicker">Design critique · April 2026</div>
<h1 class="doc-title">Powder Coating Logix <em>wants to grow up.</em></h1>
<p class="doc-sub">
The app is functionally dense — billing, jobs, inventory, shop floor, accounting. Visually
it reads as a generic Bootstrap admin: rainbow KPIs, decorative icon bubbles, gradient
welcome banners, duplicated stat strips. Here's what's holding the look back, and a
direction that feels like a tool, not a template.
</p>
</div>
<div class="doc-meta">
<span>File · <b>Design Review.html</b></span>
<span>Screens analyzed · <b>Dashboard · Jobs · Board</b></span>
<span>Source · <b>PowderCoating.Web</b></span>
</div>
</header>
<!-- Mount points -->
<div id="critique-root"></div>
<div id="system-root"></div>
<div id="screen1-root"></div>
<div id="screen2-root"></div>
<div id="screen3-root"></div>
<div id="wrap-root"></div>
<div class="tweaks" id="tweaksPanel">
<h5>Tweaks <span class="mono" style="color:var(--mute)">· v1</span></h5>
<div class="tweak-row">
<label>Accent hue</label>
<div class="tweak-dot" id="tw-accent"></div>
</div>
<div class="tweak-row">
<label>Density</label>
<div class="tweak-opts" id="tw-density">
<button class="tweak-opt" data-v="comfortable">Comfortable</button>
<button class="tweak-opt on" data-v="compact">Compact</button>
</div>
</div>
<div class="tweak-row">
<label>Surface</label>
<div class="tweak-opts" id="tw-surface">
<button class="tweak-opt on" data-v="paper">Paper</button>
<button class="tweak-opt" data-v="white">White</button>
<button class="tweak-opt" data-v="ink">Ink</button>
</div>
</div>
</div>
</div>
<script type="application/json" id="tweak-defaults">/*EDITMODE-BEGIN*/{
"accent": "ember",
"density": "comfortable",
"surface": "ink"
}/*EDITMODE-END*/</script>
<script type="text/babel" src="mock_components.jsx"></script>
<script type="text/babel" src="screens_before.jsx"></script>
<script type="text/babel" src="screens_after.jsx"></script>
<script type="text/babel" src="review_app.jsx"></script>
<script>
// Tweaks wiring — accent hue swatches
const ACCENTS = {
ember: {h:50, c:0.17, label:'Ember'},
signal: {h:25, c:0.19, label:'Signal'},
cobalt: {h:245, c:0.15, label:'Cobalt'},
moss: {h:145, c:0.11, label:'Moss'},
ink: {h:280, c:0.04, label:'Graphite'},
};
const accentWrap = document.getElementById('tw-accent');
Object.entries(ACCENTS).forEach(([k,v])=>{
const b = document.createElement('button');
b.style.background = `oklch(0.68 ${v.c} ${v.h})`;
b.title = v.label;
b.dataset.v = k;
b.addEventListener('click',()=>applyAccent(k));
accentWrap.appendChild(b);
});
function applyAccent(k){
const v = ACCENTS[k];
document.documentElement.style.setProperty('--ember', `oklch(0.68 ${v.c} ${v.h})`);
document.documentElement.style.setProperty('--ember-tint', `oklch(0.95 ${v.c*0.3} ${v.h})`);
document.documentElement.style.setProperty('--ember-ink', `oklch(0.42 ${v.c*0.9} ${v.h-10})`);
[...accentWrap.children].forEach(b=>b.classList.toggle('on',b.dataset.v===k));
post({accent:k});
}
applyAccent('ember');
document.querySelectorAll('#tw-density .tweak-opt').forEach(b=>{
b.addEventListener('click',()=>{
document.querySelectorAll('#tw-density .tweak-opt').forEach(x=>x.classList.remove('on'));
b.classList.add('on');
document.documentElement.dataset.density = b.dataset.v;
post({density:b.dataset.v});
});
});
document.querySelectorAll('#tw-surface .tweak-opt').forEach(b=>{
b.addEventListener('click',()=>{
document.querySelectorAll('#tw-surface .tweak-opt').forEach(x=>x.classList.remove('on'));
b.classList.add('on');
applySurface(b.dataset.v);
post({surface:b.dataset.v});
});
});
function applySurface(v){
const s = document.documentElement.style;
if(v==='paper'){ s.setProperty('--paper','#FAFAF7'); s.setProperty('--paper-2','#F3F1EB'); s.setProperty('--card','#FFFFFF'); s.setProperty('--ink','#0F0F10'); }
if(v==='white'){ s.setProperty('--paper','#FFFFFF'); s.setProperty('--paper-2','#F6F6F6'); s.setProperty('--card','#FFFFFF'); s.setProperty('--ink','#0F0F10'); }
if(v==='ink'){ s.setProperty('--paper','#0E0E10'); s.setProperty('--paper-2','#17171A'); s.setProperty('--card','#1C1C20'); s.setProperty('--ink','#F4F2EC'); s.setProperty('--slate','#C2C0BA'); s.setProperty('--steel','#8A8883'); s.setProperty('--rule','#2B2B2F'); s.setProperty('--rule-soft','#232327'); }
}
// Edit mode protocol
window.addEventListener('message',(e)=>{
if(e.data?.type==='__activate_edit_mode') document.getElementById('tweaksPanel').classList.add('on');
if(e.data?.type==='__deactivate_edit_mode') document.getElementById('tweaksPanel').classList.remove('on');
});
function post(edits){
try{ window.parent.postMessage({type:'__edit_mode_set_keys',edits},'*'); }catch(e){}
}
try{ window.parent.postMessage({type:'__edit_mode_available'},'*'); }catch(e){}
</script>
</body>
</html>
@@ -0,0 +1,224 @@
# Handoff: Powder Coating Logix — Visual Redesign
## Overview
This package redirects Powder Coating Logix from a generic Bootstrap-admin aesthetic to a calmer, more industrial voice — **warm neutrals, a single ember accent, monospace numerics, borders-over-shadows, domain signals on screen**.
Three representative screens are mocked end-to-end (Dashboard, Jobs list, Daily board). The same tokens, type stack, and component vocabulary extend to every other page in the app.
## About the Design Files
The files in this bundle (`Design Review.html`, `mock_components.jsx`, `screens_before.jsx`, `screens_after.jsx`, `review_app.jsx`) are **HTML/React design references**, not production code to copy.
Your task is to **recreate the "after" designs inside `PowderCoating.Web`** — an ASP.NET Core MVC app with Razor `.cshtml` views, Bootstrap 5, and jQuery. Use the codebase's existing patterns (Razor partials, tag helpers, the `_Layout.cshtml` shell, Bootstrap utility classes) — *do not* introduce React.
Where the HTML mocks use inline React-style components, translate them to:
- **CSS custom properties + Bootstrap 5 overrides** in `wwwroot/css/site.css`
- **Razor partial views** in `Views/Shared/` for repeated primitives (status chip, metric tile, section header)
- **Tag helpers** in `TagHelpers/` for things already modeled that way (e.g. the sortable column pattern)
## Fidelity
**High-fidelity.** Colors, typography, spacing, radii, and interaction affordances in the "after" mocks are final. Pixel-match the layout and tokens; use the codebase's existing icons (Bootstrap Icons) and form controls. No emoji.
## Screens
### 1. Dashboard (`Views/Dashboard/Index.cshtml`)
**Replace:**
- The gradient "Welcome Back!" banner (currently `background: var(--sidebar-gradient)`).
- The 6-tile rainbow KPI grid with icon bubbles.
**With, in order:**
1. **Hero brief** — a single card, split 1.6 / 1 columns.
- Left half: small mono kicker with date (`SATURDAY · APRIL 18`), followed by a one-sentence prose summary in Fraunces 500 / 26px, letter-spacing -0.015em, with `--bad` red on the "running hot" substring. Two buttons below: primary "Open daily board" (ink fill), secondary "Oven schedule" (outlined).
- Right half: 2×2 grid of `Metric` tiles with mono numerals, a mono kicker label, and an optional delta in `--ok` / `--bad` with `bi-arrow-up-right` / `bi-arrow-down-right`.
2. **Needs attention row** — a single card with a header ("Needs attention · chip showing count") and 3 equal cells separated by vertical 1px rules. Each cell: status chip, large mono number, one-line label, mono detail.
3. **Two-up** — left 2/3: "Floor activity" feed (time · mono job id · customer · event-chip). Right 1/3: A/R aging with 4px bars, mono dollar amounts aligned right.
No icon bubbles anywhere. No gradient backgrounds.
### 2. Jobs list (`Views/Jobs/Index.cshtml`)
**Replace:**
- Both stat strips (`.stats-cards-desktop` AND `.mobile-stats-compact`).
- The `bg-white shadow-sm` table card chrome.
- The purple-link job number styling.
**With:**
1. **Single metric strip** — one card, 5 columns, each cell a `Metric` primitive. Delete the mobile-stats duplicate; the strip is already compact enough.
2. **Toolbar** — left: search input (with `/` shortcut hint) + pill-row of quick views (`All`, `Due ≤ 48h`, `On floor`, `Ready`, `Mine`). Right: Filters + New job (ink fill).
3. **Table** — borderless rows, 1px soft rules, mono uppercase headers at 10px / 0.12em tracking. Columns: Job (mono), Description (with a tiny dot + customer subline), Customer, Due (mono, red if hot), Status (chip), Priority (small text), Total (right-aligned mono), … menu.
4. **Hot-job indicator** — a 2px red bar on the left edge of the job ID cell, *not* a colored row.
5. **Keyboard hint footer** — mono text: `↑↓ to move · ↵ to open · ⌘F to filter`.
### 3. Daily board (`Views/Jobs/Board.cshtml`)
**Replace:**
- Column tints (every column currently has its own bg color).
- Priority-as-colored-left-border (`border-left: 4px solid {{priorityColor}}`).
**With:**
1. **Column header** — mono uppercase label + small count pill. No fill. A thin 1px `--rule` underline separates header from cards.
2. **Curing column** — gets a small live "oven meter" strip: `<i class="bi-fire"></i> 180°C · 14 min` with a pulsing `--ember` dot. The strip is the *only* saturated color on the board.
3. **Hot jobs** — a single 2px red edge on the card's left, drawn as a pseudo-element so it doesn't change card padding.
4. **Card footer** — 1px `--rule-soft` top border, mono row: **powder swatch** (9px color chip generated from a lookup on `PowderInventory.ColorHex` or a name→hex map) + powder name + parts count + priority dot.
5. **Toolbar** — segmented view switch (Board / List / Oven) + mono stats line (`11 jobs on floor · 2 hot · avg cycle 3.2 d`) + Group/Hide controls.
## Design Tokens
Add these to the top of `wwwroot/css/site.css` (or a new `wwwroot/css/tokens.css` linked from `_Layout.cshtml` before Bootstrap):
```css
:root {
/* Neutrals — warm paper */
--pcl-ink: #0F0F10;
--pcl-graphite: #1A1A1C;
--pcl-slate: #3A3A3E;
--pcl-steel: #6B6B70;
--pcl-mute: #9A9A9F;
--pcl-rule: #E4E2DC;
--pcl-rule-soft: #EFEDE7;
--pcl-paper: #FAFAF7;
--pcl-paper-2: #F3F1EB;
--pcl-card: #FFFFFF;
/* Signal — shared chroma, varied hue */
--pcl-ember: oklch(0.68 0.17 50);
--pcl-ember-tint: oklch(0.95 0.04 55);
--pcl-ember-ink: oklch(0.42 0.15 40);
--pcl-ok: oklch(0.62 0.11 155);
--pcl-ok-tint: oklch(0.95 0.04 155);
--pcl-warn: oklch(0.76 0.13 80);
--pcl-warn-tint: oklch(0.96 0.05 85);
--pcl-bad: oklch(0.60 0.19 25);
--pcl-bad-tint: oklch(0.95 0.04 25);
--pcl-cool: oklch(0.58 0.09 240);
--pcl-cool-tint: oklch(0.95 0.03 240);
/* Bootstrap overrides */
--bs-body-bg: var(--pcl-paper);
--bs-body-color: var(--pcl-ink);
--bs-border-color: var(--pcl-rule);
--bs-primary: var(--pcl-ink); /* primary actions are ink-filled */
}
/* Dark surface variant — applied on data-surface="ink" */
[data-surface="ink"] {
--pcl-paper: #0E0E10;
--pcl-paper-2: #17171A;
--pcl-card: #1C1C20;
--pcl-ink: #F4F2EC;
--pcl-slate: #C2C0BA;
--pcl-steel: #8A8883;
--pcl-rule: #2B2B2F;
--pcl-rule-soft:#232327;
}
```
### Spacing / radius / type scale
| Token | Value | Use |
|---|---|---|
| radius-sm | 4px | swatches, small chips |
| radius | 6px | buttons, inputs, cards within a card |
| radius-lg | 8px | top-level cards, toolbars |
| space-1…6 | 4 / 8 / 12 / 16 / 20 / 24 px | grid gaps |
| font-display | Fraunces 500, -0.015em | hero brief only |
| font-ui | Inter 400/500/600, -0.005em | everything else |
| font-mono | IBM Plex Mono 400/500, feature-settings "zero" | all numerics, IDs, timestamps, keyboard hints, column kickers |
### Type sizes
| Name | Size / line | Weight | Use |
|---|---|---|---|
| display | 26 / 1.2 | Fraunces 500 | dashboard hero sentence |
| h1 | 22 / 1.25 | Inter 600 | page title |
| h2 | 16 / 1.35 | Inter 600 | section header inside card |
| body | 13 / 1.5 | Inter 400 | table rows, card body |
| meta | 12 / 1.5 | Inter 400 `--pcl-slate` | secondary text |
| kicker | 10 / 1.2, 0.14em tracking, uppercase | IBM Plex Mono 500 `--pcl-steel` | column headers, card section labels |
| metric | 26 / 1 | IBM Plex Mono 500 | KPI value |
## Razor Partials to Build
### `_StatusChip.cshtml`
```
@model (string Kind, string Text)
<span class="pcl-chip pcl-chip-@Model.Kind">
<span class="pcl-chip-dot"></span>@Model.Text
</span>
```
CSS: background `--pcl-{kind}-tint`, color `--pcl-{kind}`, dot `--pcl-{kind}`, `IBM Plex Mono` 10.5px, 2px 8px padding, 99px radius. Kinds: `neutral`, `ok`, `warn`, `bad`, `cool`, `ember`.
Use this **everywhere** status badges exist today: Jobs, Invoices, Equipment, Maintenance, Customers. Replace every occurrence of `bg-success bg-warning bg-danger` badges.
### `_Metric.cshtml`
```
@model (string Label, string Value, string? Delta, string? DeltaDir)
```
Renders: mono kicker label, mono 26px value, optional mono delta with arrow. No icon bubble.
### `_SectionHeader.cshtml`
Kicker label + title + optional right-aligned mono meta. Replaces today's `<div class="card-header fw-bold">`.
### `_PowderSwatch.cshtml`
```
@model (string ColorHex, string Name, int? Size)
<span class="pcl-swatch" style="background:@Model.ColorHex" title="@Model.Name"></span>
```
9px color chip + 1px `rgba(0,0,0,0.12)` border. Use it anywhere a powder name appears.
## Interactions & Behavior
- **Hover on table row:** `background: var(--pcl-paper-2)` only. No elevation change.
- **Row keyboard nav:** arrow keys move a focus ring, `enter` opens. The hint footer on Jobs list documents this.
- **Buttons:** primary = `--pcl-ink` bg, `--pcl-paper` fg; ghost = `--pcl-card` bg, `--pcl-rule` border, `--pcl-ink` text. No box shadows. 6px radius.
- **Focus ring:** 2px outline in `--pcl-ember`, 2px offset.
- **Oven meter pulse:** `@keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: 0.3 } }` on the ember dot — 1.5s. This is the **only** loop animation allowed. Kill `.badge-glow-critical`.
- **Toast / alert:** keep existing Toastr wiring; restyle to use the tokens.
## Sidebar
Keep the existing sidebar structure but:
- Ship **only** Paper (default) and Ink sidebar themes. Delete the 12 `[data-sidebar=…]` gradient variants.
- Active item: 2px `--pcl-ember` left bar + icon tinted ember + `--pcl-paper` text. No gradient.
- Add a ⌘K search affordance at the top of the nav (scaffold — wire later).
- Group headers: mono uppercase, 0.12em tracking, `rgba(255,255,255,0.35)` on ink surface.
## Files to Touch
| Layer | File | Change |
|---|---|---|
| Tokens | `wwwroot/css/site.css` | Prepend `:root` token block; delete rainbow KPI colors, glow keyframes, 11 of 12 sidebar palettes |
| Layout | `Views/Shared/_Layout.cshtml` | Swap sidebar theming; load Fraunces + IBM Plex Mono + Inter from Google Fonts; add `_StatusChip` + `_Metric` partials |
| Dashboard | `Views/Dashboard/Index.cshtml` | Rewrite top 60% of the file (welcome banner + KPI grid) |
| Jobs | `Views/Jobs/Index.cshtml` | Collapse both stat strips into one metric row; restyle table; add quick-view chips |
| Board | `Views/Jobs/Board.cshtml` | Kill column tints + priority left-border; add oven meter; card footer redesign |
| Shared | `Views/Shared/_Metric.cshtml`, `_StatusChip.cshtml`, `_SectionHeader.cshtml`, `_PowderSwatch.cshtml` | New partials |
The remaining CRUD pages (Customers, Invoices, Inventory, etc.) inherit the new look automatically once the tokens, `_StatusChip`, and `_Metric` partials land — they all use the same stat-strip + card-table layout today.
## Shipping Order
1. Tokens + font imports — ½ day
2. `_StatusChip` + `_Metric` partials and a codebase-wide find-and-replace of `badge bg-{color}` / stat-card markup — 1 day
3. Dashboard rewrite — ½ day
4. Jobs list rewrite — ½ day
5. Daily board rewrite — 1 day
6. Sidebar simplification + ⌘K scaffold — ½ day
7. Sweep remaining list pages (Customers, Invoices, Equipment, Maintenance, Vendors, PO) using the established partials — 12 days
~56 dev days to ship the visual overhaul.
## Files in This Bundle
- `Design Review.html` — interactive document with before/after comparisons. Open locally.
- `mock_components.jsx`, `screens_before.jsx`, `screens_after.jsx`, `review_app.jsx` — React source for the mocks. Reference only.
- `CLAUDE.md` — brief for Claude Code when run inside `PowderCoating.Web/`.
- `DARK_MODE.md` — supplemental brief that supersedes the dark-surface tokens below and adds Bootstrap theme binding, a persisted toggle, and per-screen verification. **Read this if you're implementing or fixing dark mode.**
## Assets
No new image assets required. Keep `wwwroot/images/pcl-logo.png`. Powder color hex values come from existing `PowderInventory.ColorHex` column (or a name→hex fallback map in a new `Helpers/PowderSwatchHelper.cs` if that column isn't populated).
@@ -0,0 +1,301 @@
/* Shared primitives for fake-app mocks inside the critique doc. */
const { useEffect, useRef, useState } = React;
/* --- Scaled frame: renders a 1280px-wide fake-app and scales to fit its box --- */
function Scaled({ width = 1280, height = 720, children }) {
const ref = useRef(null);
const [scale, setScale] = useState(1);
useEffect(() => {
const el = ref.current;
if (!el) return;
const ro = new ResizeObserver(() => {
const w = el.clientWidth;
setScale(w / width);
});
ro.observe(el);
return () => ro.disconnect();
}, [width]);
return (
<div ref={ref} style={{ position: 'relative', width: '100%', height: height * scale, overflow: 'hidden', background: 'var(--paper)' }}>
<div style={{
width: width, height: height,
transform: `scale(${scale})`, transformOrigin: 'top left',
}}>
{children}
</div>
</div>
);
}
/* --- Before/After stage --- */
function Stage({ beforeLabel = 'Current', afterLabel = 'Proposed', before, after, delta }) {
return (
<>
<div className="stage">
<div className="mock">
<div className="mock-head">
<div className="mock-label"><b>Before</b> · {beforeLabel}</div>
<span className="mock-tag">as shipped</span>
</div>
<div className="mock-body">{before}</div>
</div>
<div className="mock">
<div className="mock-head">
<div className="mock-label after"><b>After</b> · {afterLabel}</div>
<span className="mock-tag" style={{ background: 'var(--ember)', color: '#fff', borderColor: 'var(--ember)' }}>proposed</span>
</div>
<div className="mock-body">{after}</div>
</div>
</div>
{delta && (
<div className="delta">
<div className="delta-col">
<h5>What's wrong</h5>
<ul>{delta.wrong.map((d, i) => <li key={i}>{d}</li>)}</ul>
</div>
<div className="delta-col">
<h5>What changed</h5>
<ul>{delta.change.map((d, i) => <li key={i}>{d}</li>)}</ul>
</div>
</div>
)}
</>
);
}
/* --- Fake Sidebar (before = current Bootstrap) --- */
function SidebarBefore() {
const items = [
['Main Menu', null],
['bi-house-door', 'Dashboard', true],
['Operations', null],
['bi-people', 'Customers'],
['bi-file-text', 'Quotes'],
['bi-briefcase', 'Jobs'],
['bi-receipt', 'Invoices'],
['bi-calendar-event', 'Appointments'],
['bi-clipboard2-check', 'Daily Board'],
['Inventory & Purchasing', null],
['bi-book', 'Product Catalog'],
['bi-box-seam', 'Inventory'],
['bi-truck', 'Vendors'],
];
return (
<aside style={{
width: 240, height: 720, flexShrink: 0,
background: 'linear-gradient(180deg,#1a1a2e,#16213e,#0f3460)',
color: '#fff', display: 'flex', flexDirection: 'column',
}}>
<div style={{ padding: '22px 20px 18px', borderBottom: '1px solid rgba(255,255,255,0.1)', textAlign: 'center' }}>
<div style={{
width: 130, height: 48, margin: '0 auto 6px',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontWeight: 700, fontSize: 14, letterSpacing: '-0.01em', color: '#fff',
background: 'rgba(255,255,255,0.06)', borderRadius: 4,
}}>Acme Powder Co.</div>
<div style={{ fontSize: 11, color: 'rgba(255,255,255,0.7)' }}>Acme Powder Co.</div>
</div>
<div style={{ flex: 1, overflow: 'hidden', padding: '12px 0' }}>
{items.map((it, i) => {
if (it[1] === null) return (
<div key={i} style={{ padding: '14px 20px 6px', fontSize: 10.5, fontWeight: 600, letterSpacing: '0.06em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.4)' }}>{it[0]}</div>
);
const active = it[2];
return (
<a key={i} style={{
display: 'flex', alignItems: 'center', gap: 10,
padding: '9px 20px', color: active ? '#fff' : 'rgba(255,255,255,0.78)',
background: active ? 'rgba(255,255,255,0.07)' : 'transparent',
borderLeft: `3px solid ${active ? '#4fc3f7' : 'transparent'}`,
fontSize: 13, textDecoration: 'none',
}}>
<i className={`bi ${it[0]}`} style={{ fontSize: 15, width: 18, color: 'rgba(255,255,255,0.85)' }} />
<span>{it[1]}</span>
</a>
);
})}
</div>
</aside>
);
}
/* --- Fake Sidebar (after) --- */
function SidebarAfter() {
const sections = [
{ title: 'Today',
items: [
{ icon: 'bi-house-door', label: 'Overview', active: true, badge: null },
{ icon: 'bi-clipboard2-check', label: 'Daily board', badge: '12' },
{ icon: 'bi-fire', label: 'Oven schedule' },
]
},
{ title: 'Pipeline',
items: [
{ icon: 'bi-people', label: 'Customers' },
{ icon: 'bi-file-text', label: 'Quotes', badge: '3' },
{ icon: 'bi-briefcase', label: 'Jobs', badge: '47' },
{ icon: 'bi-receipt', label: 'Invoices' },
{ icon: 'bi-calendar-event', label: 'Appointments' },
]
},
{ title: 'Stock',
items: [
{ icon: 'bi-book', label: 'Catalog' },
{ icon: 'bi-box-seam', label: 'Inventory' },
{ icon: 'bi-truck', label: 'Vendors' },
]
},
];
return (
<aside style={{
width: 240, height: 720, flexShrink: 0,
background: 'var(--ink)', color: 'var(--paper)',
display: 'flex', flexDirection: 'column',
borderRight: '1px solid rgba(255,255,255,0.06)',
}}>
{/* brand */}
<div style={{ padding: '20px 18px 18px', display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
<div style={{
width: 28, height: 28, borderRadius: 6,
background: 'var(--ember)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontWeight: 700, fontSize: 14, fontFamily: 'IBM Plex Mono',
}}>◆</div>
<div style={{ lineHeight: 1.1 }}>
<div style={{ fontWeight: 600, fontSize: 13, letterSpacing: '-0.005em' }}>Acme Powder</div>
<div style={{ fontSize: 10.5, color: 'rgba(255,255,255,0.45)', fontFamily: 'IBM Plex Mono', letterSpacing: '0.04em', textTransform: 'uppercase', marginTop: 2 }}>Coatings · PCL</div>
</div>
</div>
{/* search */}
<div style={{ padding: '12px 12px 6px' }}>
<div style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '7px 10px',
background: 'rgba(255,255,255,0.04)',
border: '1px solid rgba(255,255,255,0.06)',
borderRadius: 6,
fontSize: 12, color: 'rgba(255,255,255,0.45)',
}}>
<i className="bi bi-search" style={{ fontSize: 12 }} />
<span style={{ flex: 1 }}>Jump to…</span>
<span style={{ fontFamily: 'IBM Plex Mono', fontSize: 10, padding: '1px 5px', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 3 }}>⌘K</span>
</div>
</div>
<div style={{ flex: 1, overflow: 'hidden', padding: '6px 0 12px' }}>
{sections.map((sec, si) => (
<div key={si} style={{ marginTop: si === 0 ? 6 : 14 }}>
<div style={{
padding: '4px 18px',
fontFamily: 'IBM Plex Mono',
fontSize: 10, letterSpacing: '0.12em', textTransform: 'uppercase',
color: 'rgba(255,255,255,0.35)',
}}>{sec.title}</div>
{sec.items.map((it, i) => (
<a key={i} style={{
display: 'flex', alignItems: 'center', gap: 10,
padding: '6px 18px', margin: '1px 8px',
color: it.active ? 'var(--paper)' : 'rgba(255,255,255,0.72)',
background: it.active ? 'rgba(255,255,255,0.06)' : 'transparent',
borderRadius: 5,
fontSize: 13, fontWeight: it.active ? 500 : 400,
textDecoration: 'none', position: 'relative',
}}>
{it.active && <span style={{ position: 'absolute', left: -8, top: 6, bottom: 6, width: 2, background: 'var(--ember)', borderRadius: 2 }} />}
<i className={`bi ${it.icon}`} style={{ fontSize: 14, width: 16, color: it.active ? 'var(--ember)' : 'rgba(255,255,255,0.55)' }} />
<span style={{ flex: 1 }}>{it.label}</span>
{it.badge && (
<span style={{
fontFamily: 'IBM Plex Mono', fontSize: 10,
padding: '1px 6px', borderRadius: 99,
background: 'rgba(255,255,255,0.08)', color: 'rgba(255,255,255,0.7)',
}}>{it.badge}</span>
)}
</a>
))}
</div>
))}
</div>
</aside>
);
}
/* --- Fake topbar (before) --- */
function TopbarBefore({ title = 'Dashboard' }) {
return (
<div style={{
padding: '18px 28px',
borderBottom: '1px solid #e5e7eb',
background: '#fff',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
}}>
<h2 style={{ margin: 0, fontSize: 22, fontWeight: 600 }}>{title}</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<button style={{ width: 36, height: 36, borderRadius: '50%', border: 'none', background: '#f3f4f6', color: '#6b7280' }}>
<i className="bi bi-bell" />
</button>
<button style={{ width: 36, height: 36, borderRadius: '50%', border: 'none', background: '#f3f4f6', color: '#6b7280' }}>
<i className="bi bi-gear" />
</button>
<div style={{ width: 40, height: 40, borderRadius: '50%', background: 'linear-gradient(135deg,#4f46e5,#7c3aed)', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 600 }}>JD</div>
</div>
</div>
);
}
/* --- Fake topbar (after) --- */
function TopbarAfter({ title = 'Overview', breadcrumbs = ['Today', 'Overview'] }) {
return (
<div style={{
padding: '14px 28px',
borderBottom: '1px solid var(--rule)',
background: 'var(--card)',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
}}>
<div>
<div style={{
fontFamily: 'IBM Plex Mono', fontSize: 10, letterSpacing: '0.1em',
textTransform: 'uppercase', color: 'var(--steel)', marginBottom: 4,
display: 'flex', alignItems: 'center', gap: 6,
}}>
{breadcrumbs.map((b, i) => (
<React.Fragment key={i}>
{i > 0 && <span style={{ opacity: 0.5 }}>/</span>}
<span>{b}</span>
</React.Fragment>
))}
</div>
<h2 style={{ margin: 0, fontSize: 22, fontWeight: 600, letterSpacing: '-0.015em', color: 'var(--ink)' }}>{title}</h2>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<button style={topBtn}>
<i className="bi bi-plus-lg" style={{ fontSize: 13 }} /> <span>New job</span>
<span style={{ fontFamily: 'IBM Plex Mono', fontSize: 10, padding: '1px 4px', border: '1px solid rgba(255,255,255,0.2)', borderRadius: 3, marginLeft: 6 }}>N</span>
</button>
<button style={ghostBtn}><i className="bi bi-bell" /></button>
<button style={ghostBtn}><i className="bi bi-gear" /></button>
<div style={{ width: 32, height: 32, borderRadius: 6, background: 'var(--ink)', color: 'var(--paper)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 600, fontSize: 12 }}>JD</div>
</div>
</div>
);
}
const topBtn = {
display: 'flex', alignItems: 'center', gap: 6,
padding: '7px 12px', borderRadius: 6, border: 'none',
background: 'var(--ink)', color: 'var(--paper)',
fontSize: 13, fontWeight: 500, cursor: 'pointer',
};
const ghostBtn = {
width: 32, height: 32, borderRadius: 6, border: '1px solid var(--rule)',
background: 'var(--card)', color: 'var(--slate)',
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
};
Object.assign(window, {
Scaled, Stage,
SidebarBefore, SidebarAfter,
TopbarBefore, TopbarAfter,
});
@@ -0,0 +1,278 @@
/* Mount the document sections into their roots. */
const { createRoot } = ReactDOM;
/* ───────── Critique findings ───────── */
function CritiqueSection() {
const findings = [
{ sev: 'hi', tag: 'Color', h: 'Rainbow KPIs carry no hierarchy',
p: 'Six dashboard tiles each use a different hot color (#eb3349, #11998e, #f7971e, #4facfe, #a855f7, #667eea). Everything shouts equally, so nothing reads as urgent. The color palette is a legend, not a signal.' },
{ sev: 'hi', tag: 'Hierarchy', h: 'Decorative icon bubbles everywhere',
p: '48px pastel circles with duplicated iconography sit next to every metric on Customers, Jobs, Invoices, Equipment. They double the card height without carrying new information — the label already says what it is.' },
{ sev: 'hi', tag: 'Layout', h: 'Welcome gradient steals attention',
p: 'The "Welcome Back!" banner uses the full sidebar gradient as background — it dominates the page with a decorative greeting when this is a daily operations tool. People already know their name.' },
{ sev: 'md', tag: 'Type', h: 'No numeric typography',
p: 'All tabular numbers — job counts, A/R amounts, due dates — render in the same proportional Inter as body text. Columns don\'t line up, and currency scanning is slow. A tabular/mono face for numerals would fix this cheaply.' },
{ sev: 'md', tag: 'Density', h: 'Two stat strips on every list page',
p: 'Jobs, Customers, Invoices each render a desktop "stats cards" row AND a mobile "stats compact" row. That\'s 120px of chrome before the actual table. The same counts already belong on the dashboard.' },
{ sev: 'md', tag: 'Consistency', h: 'Twelve sidebar palettes, one default',
p: 'The sidebar ships with 12 preset gradients (ocean, forest, slate, crimson, midnight, purple…) and a light/dark toggle. That\'s surface personalisation where you need identity. Pick one sidebar treatment and invest there.' },
{ sev: 'md', tag: 'Motion', h: 'Glowing critical badges',
p: 'badge-glow-critical pulses a red shadow forever. Motion should bring attention once, not loop. It\'s visual noise that trains users to tune out the red.' },
{ sev: 'lo', tag: 'Forms', h: 'Inputs render with #9ca3af borders always',
p: 'Form controls use a uniform 1.5px gray border regardless of state. There\'s no way for a required-but-empty field to distinguish itself from a focused one until you tab in. Low-cost lift with big payoff.' },
{ sev: 'lo', tag: 'Brand', h: 'No visual ownership of the domain',
p: 'Nothing on screen says "this is for a powder coating shop." Powder color chips, cure temperatures, oven state, batch numbers — the core vocabulary — doesn\'t surface in the UI. The product feels like generic small-business SaaS.' },
];
return (
<section className="chapter">
<div className="chap-head">
<div className="chap-num">01 · Critique</div>
<div>
<h2 className="chap-title">Where the current design leaks value</h2>
<p className="chap-lede">
Nothing below is a bug the app works. But each of these is a small
everyday tax: on legibility, on trust, on the sense that the tool belongs
to a shop that takes craft seriously.
</p>
</div>
</div>
<div className="findings">
{findings.map((f, i) => (
<div key={i} className="finding">
<div className="finding-tag">
<span className={`sev ${f.sev}`} />
{f.tag}
</div>
<h4>{f.h}</h4>
<p>{f.p}</p>
</div>
))}
</div>
</section>
);
}
/* ───────── Design system strip ───────── */
function SystemSection() {
return (
<section className="chapter">
<div className="chap-head">
<div className="chap-num">02 · Direction</div>
<div>
<h2 className="chap-title">A calmer, more industrial voice</h2>
<p className="chap-lede">
Warm neutrals, a single ember accent, Inter + IBM Plex Mono for numerics,
Fraunces for occasional display type. The goal is "shop tool, not admin template."
</p>
</div>
</div>
<div className="sys">
<div className="sys-cell">
<h5>Foundations</h5>
<div className="swatch-row">
<div className="swatch"><span className="chip" style={{ background: '#0F0F10' }} /><span className="lbl">Ink</span><span className="hex">#0F0F10</span></div>
<div className="swatch"><span className="chip" style={{ background: '#3A3A3E' }} /><span className="lbl">Slate</span><span className="hex">#3A3A3E</span></div>
<div className="swatch"><span className="chip" style={{ background: '#6B6B70' }} /><span className="lbl">Steel</span><span className="hex">#6B6B70</span></div>
<div className="swatch"><span className="chip" style={{ background: '#E4E2DC' }} /><span className="lbl">Rule</span><span className="hex">#E4E2DC</span></div>
<div className="swatch"><span className="chip" style={{ background: '#F3F1EB' }} /><span className="lbl">Paper 2</span><span className="hex">#F3F1EB</span></div>
<div className="swatch"><span className="chip" style={{ background: '#FAFAF7' }} /><span className="lbl">Paper</span><span className="hex">#FAFAF7</span></div>
</div>
</div>
<div className="sys-cell">
<h5>Signal</h5>
<div className="swatch-row">
<div className="swatch"><span className="chip" style={{ background: 'var(--ember)' }} /><span className="lbl">Ember · accent</span><span className="hex">oklch 0.68 / 0.17 / 50</span></div>
<div className="swatch"><span className="chip" style={{ background: 'var(--ok)' }} /><span className="lbl">OK · on track</span><span className="hex">0.62 / 0.11 / 155</span></div>
<div className="swatch"><span className="chip" style={{ background: 'var(--warn)' }} /><span className="lbl">Warn · tight</span><span className="hex">0.76 / 0.13 / 80</span></div>
<div className="swatch"><span className="chip" style={{ background: 'var(--bad)' }} /><span className="lbl">Bad · overdue</span><span className="hex">0.60 / 0.19 / 25</span></div>
<div className="swatch"><span className="chip" style={{ background: 'var(--cool)' }} /><span className="lbl">Cool · queued</span><span className="hex">0.58 / 0.09 / 240</span></div>
</div>
<p style={{ margin: '12px 0 0', fontSize: 11.5, color: 'var(--steel)', lineHeight: 1.5 }}>
Shared chroma & lightness across hues. Status chips use the tinted variant + dot,
never a full saturated fill.
</p>
</div>
<div className="sys-cell">
<h5>Type</h5>
<div className="type-sample">
<div className="type-meta">Display · Fraunces 500</div>
<div className="type-val serif" style={{ fontSize: 26, letterSpacing: '-0.015em' }}>12 jobs on the floor</div>
</div>
<div className="type-sample">
<div className="type-meta">UI · Inter 500 / 400</div>
<div className="type-val" style={{ fontSize: 15, fontWeight: 500 }}>Rodriguez Metal</div>
<div className="type-val" style={{ fontSize: 13, color: 'var(--slate)' }}>Fence gate anodized black</div>
</div>
<div className="type-sample">
<div className="type-meta">Numerics · IBM Plex Mono</div>
<div className="type-val mono" style={{ fontSize: 18 }}>$12,450.00 · J-2041</div>
</div>
</div>
<div className="sys-cell">
<h5>Principles</h5>
<ol style={{ margin: 0, paddingLeft: 16, fontSize: 12.5, color: 'var(--slate)', lineHeight: 1.7 }}>
<li><b style={{ color: 'var(--ink)' }}>One accent, one loud.</b> Orange earns attention. Status colors whisper.</li>
<li><b style={{ color: 'var(--ink)' }}>Numerics are monospace.</b> Columns align; currency scans.</li>
<li><b style={{ color: 'var(--ink)' }}>Borders, not shadows.</b> 1px rules on paper reads like a spec sheet.</li>
<li><b style={{ color: 'var(--ink)' }}>Domain on screen.</b> Powder swatches, cure temps, batch IDs.</li>
<li><b style={{ color: 'var(--ink)' }}>No decorative motion.</b> Animation marks change, not state.</li>
</ol>
</div>
</div>
</section>
);
}
/* ───────── Screen chapters ───────── */
function ScreenChapter({ num, title, lede, before, after, delta }) {
return (
<section className="chapter">
<div className="chap-head">
<div className="chap-num">{num}</div>
<div>
<h2 className="chap-title">{title}</h2>
<p className="chap-lede">{lede}</p>
</div>
</div>
<Stage
before={<Scaled width={1280} height={720}>{before}</Scaled>}
after={<Scaled width={1280} height={720}>{after}</Scaled>}
delta={delta}
/>
</section>
);
}
/* ───────── Wrap-up ───────── */
function WrapSection() {
return (
<section className="chapter">
<div className="chap-head">
<div className="chap-num">06 · Next</div>
<div>
<h2 className="chap-title">If you want to ship this, start here</h2>
<p className="chap-lede">
Ordered by impact per hour of work. The first three are the ones that carry 80% of
the visual-quality lift the rest compound over time.
</p>
</div>
</div>
<div style={{
background: 'var(--card)', border: '1px solid var(--rule)', borderRadius: 8,
overflow: 'hidden',
}}>
{[
['01', 'Replace the KPI palette and icon bubbles', 'Kill the six hot colors and the pastel circles. One monospace number, one label. Dashboard + every list page inherits the fix.', '½ day'],
['02', 'Pick one typeface for numerics', 'Drop IBM Plex Mono (or similar) into site.css and apply to .mono, tables, KPIs. Alignment fixes itself.', '2 h'],
['03', 'Demote the welcome banner', 'Turn the gradient block into a single line of kerned text + date. Reclaim 140px of dashboard real estate.', '2 h'],
['04', 'Introduce status chips as a component', 'One ts/cs partial: dot + tinted pill + Plex label. Reuse on Jobs, Invoices, Maintenance, Equipment.', '1 day'],
['05', 'Retire 11 of the 12 sidebar gradients', 'Ship Ink + Paper (dark/light) only. Personalization is a feature for consumer apps, not shop ops.', '1 h'],
['06', 'Add powder swatches to any powder reference', 'Tiny 10px swatch next to every powder name on Jobs, Inventory, Board, Oven. Free branding.', '½ day'],
['07', 'Command bar (⌘K)', 'Sidebar has 40+ items. A fuzzy jump-to turns 2 clicks into 0. Biggest productivity lift on the list.', '2 days'],
].map((r, i) => (
<div key={i} style={{
display: 'grid', gridTemplateColumns: '60px 1fr 100px',
gap: 20, padding: '16px 22px',
borderBottom: i < 6 ? '1px solid var(--rule-soft)' : 'none',
alignItems: 'baseline',
}}>
<span className="mono" style={{ fontSize: 11, color: 'var(--ember-ink)', letterSpacing: '0.06em' }}>{r[0]}</span>
<div>
<div style={{ fontWeight: 600, fontSize: 14, letterSpacing: '-0.005em' }}>{r[1]}</div>
<div style={{ fontSize: 12.5, color: 'var(--slate)', marginTop: 3, lineHeight: 1.55 }}>{r[2]}</div>
</div>
<span className="mono" style={{
fontSize: 10.5, letterSpacing: '0.08em', textTransform: 'uppercase',
color: 'var(--steel)', textAlign: 'right', whiteSpace: 'nowrap',
}}>{r[3]}</span>
</div>
))}
</div>
</section>
);
}
/* ───────── Mount ───────── */
createRoot(document.getElementById('critique-root')).render(<CritiqueSection />);
createRoot(document.getElementById('system-root')).render(<SystemSection />);
createRoot(document.getElementById('screen1-root')).render(
<ScreenChapter
num="03 · Dashboard"
title="From decorative welcome to operational brief"
lede="Today's single most-used screen. The current layout spends its biggest real estate on a gradient greeting and six rainbow KPIs. The proposed layout answers one question first: what needs my attention today?"
before={<DashboardBefore />}
after={<DashboardAfter />}
delta={{
wrong: [
'Gradient "Welcome Back" banner dominates the fold',
'Six KPI tiles in six different accent colors, no ranking',
'Icon bubbles repeat the label',
'A/R aging uses four more unrelated colors',
],
change: [
'Opens with a single prose summary of the day',
'Core finance row sits beside it, monospace numerals',
'"Needs attention" surfaces only items < 48 h or blocked',
'Floor activity feed grounds the dashboard in real events',
],
}}
/>
);
createRoot(document.getElementById('screen2-root')).render(
<ScreenChapter
num="04 · Jobs list"
title="Less chrome, more scanning"
lede="The jobs index is used all day by estimators and shop managers. Every px of chrome is a tax on scan speed. The redesign compresses four stat cards into one strip, adds keyboard-first filtering, and makes job IDs a first-class mono column."
before={<JobsIndexBefore />}
after={<JobsIndexAfter />}
delta={{
wrong: [
'4× stat cards with icon bubbles eat 130px',
'Status and priority pills are hard to tell apart',
'Job IDs styled as purple links, no hierarchy with description',
'No saved/quick views, filters hidden behind a button',
],
change: [
'Single 5-metric strip, monospace numerals, sparkline-less',
'Tabular status chips: dot + tint + Plex label',
'Job ID in mono, description bold, customer secondary, hot jobs carry a left bar',
'Quick views inline (All / Due ≤48h / On floor / Ready / Mine) + ⌘K shortcut hint',
],
}}
/>
);
createRoot(document.getElementById('screen3-root')).render(
<ScreenChapter
num="05 · Daily board"
title="A board that knows it's a shop floor"
lede="The current Kanban is a generic card board. The redesign adds the things this specific tool should know: powder color swatches on every card, a live oven cycle meter on the Curing column, and a hot-job edge treatment instead of colored borders fighting the priority palette."
before={<JobsBoardBefore />}
after={<JobsBoardAfter />}
delta={{
wrong: [
'Every column uses a different pill color (red for Coating, purple for Curing…)',
'Priority encoded as a 4px colored left border — same visual weight as column tint',
'No domain cues: "powder" is just text, due dates aren\'t scannable',
'Kanban chrome (gray column tint) takes space without adding info',
],
change: [
'Columns carry a Plex Mono label and a count badge — no chromatic encoding',
'Hot jobs get a single red edge; everything else is mono',
'Every card shows a powder swatch + parts count + priority dot in a tight footer',
'Curing column carries a live oven meter (180 °C · 14 min)',
],
}}
/>
);
createRoot(document.getElementById('wrap-root')).render(<WrapSection />);
@@ -0,0 +1,488 @@
/* "After" screens — redesigned counterparts. */
/* Shared bits ------------------------------------------------- */
function Metric({ value, label, delta, deltaDir, mono = true }) {
const dcol = deltaDir === 'up' ? 'var(--ok)' : deltaDir === 'down' ? 'var(--bad)' : 'var(--steel)';
return (
<div style={{ padding: '18px 20px', borderRight: '1px solid var(--rule)' }}>
<div style={{
fontFamily: 'IBM Plex Mono', fontSize: 10, letterSpacing: '0.14em',
textTransform: 'uppercase', color: 'var(--steel)', marginBottom: 8,
}}>{label}</div>
<div style={{
fontFamily: mono ? 'IBM Plex Mono' : 'Inter',
fontSize: 26, fontWeight: 500, color: 'var(--ink)',
letterSpacing: '-0.01em', lineHeight: 1, fontFeatureSettings: '"zero"',
}}>{value}</div>
{delta && (
<div style={{ fontSize: 11, color: dcol, marginTop: 8, fontFamily: 'IBM Plex Mono' }}>
<i className={`bi bi-arrow-${deltaDir === 'up' ? 'up' : deltaDir === 'down' ? 'down' : 'right'}-right`} style={{ marginRight: 3 }} />
{delta}
</div>
)}
</div>
);
}
function Chip({ kind = 'neutral', children, dot }) {
const styles = {
neutral: { bg: 'var(--paper-2)', fg: 'var(--slate)', dot: 'var(--steel)' },
ok: { bg: 'var(--ok-tint)', fg: 'var(--ok)', dot: 'var(--ok)' },
warn: { bg: 'var(--warn-tint)', fg: 'oklch(0.4 0.13 80)', dot: 'var(--warn)' },
bad: { bg: 'var(--bad-tint)', fg: 'var(--bad)', dot: 'var(--bad)' },
cool: { bg: 'var(--cool-tint)', fg: 'var(--cool)', dot: 'var(--cool)' },
ember: { bg: 'var(--ember-tint)', fg: 'var(--ember-ink)', dot: 'var(--ember)' },
}[kind];
return (
<span style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
background: styles.bg, color: styles.fg,
padding: '2px 8px 2px 7px', borderRadius: 99,
fontFamily: 'IBM Plex Mono', fontSize: 10.5, fontWeight: 500,
letterSpacing: '0.02em',
}}>
{dot !== false && <span style={{ width: 5, height: 5, borderRadius: 99, background: styles.dot }} />}
{children}
</span>
);
}
/* =======================
DASHBOARD — AFTER
======================= */
function DashboardAfter() {
return (
<div style={{ display: 'flex', background: 'var(--paper)' }}>
<SidebarAfter />
<div style={{ flex: 1, minWidth: 0 }}>
<TopbarAfter title="Good morning, John." breadcrumbs={['Today', 'Overview']} />
<div style={{ padding: '22px 28px' }}>
{/* Hero: today */}
<div style={{
background: 'var(--card)', border: '1px solid var(--rule)', borderRadius: 8,
display: 'grid', gridTemplateColumns: '1.6fr 1fr', marginBottom: 18,
}}>
<div style={{ padding: '22px 24px', borderRight: '1px solid var(--rule)' }}>
<div style={{
fontFamily: 'IBM Plex Mono', fontSize: 10, letterSpacing: '0.14em',
textTransform: 'uppercase', color: 'var(--steel)', marginBottom: 10,
display: 'flex', alignItems: 'center', gap: 8,
}}>
<span style={{ width: 5, height: 5, borderRadius: 99, background: 'var(--ember)' }} />
Saturday · April 18
</div>
<div style={{
fontFamily: 'Fraunces', fontWeight: 500, fontSize: 26,
letterSpacing: '-0.015em', color: 'var(--ink)', lineHeight: 1.2,
}}>
<span>12 jobs on the floor, </span>
<span style={{ color: 'var(--bad)' }}>3 running hot</span>
<span>. One oven cycle left before lunch.</span>
</div>
<div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
<button style={{ background: 'var(--ink)', color: 'var(--paper)', border: 'none', borderRadius: 6, padding: '8px 14px', fontSize: 13, fontWeight: 500, cursor: 'pointer' }}>
Open daily board <i className="bi bi-arrow-right" style={{ marginLeft: 4 }} />
</button>
<button style={{ background: 'var(--card)', color: 'var(--ink)', border: '1px solid var(--rule)', borderRadius: 6, padding: '8px 14px', fontSize: 13, fontWeight: 500, cursor: 'pointer' }}>
Oven schedule
</button>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
<Metric label="Collected · Apr" value="$48,290" delta="+14% vs Mar" deltaDir="up" />
<Metric label="Outstanding A/R" value="$12,450" delta="2 invoices > 30d" deltaDir="down" />
<Metric label="Active customers" value="84" delta="+3 this week" deltaDir="up" />
<div style={{ padding: '18px 20px' }}>
<div style={{ fontFamily: 'IBM Plex Mono', fontSize: 10, letterSpacing: '0.14em', textTransform: 'uppercase', color: 'var(--steel)', marginBottom: 8 }}>Margin · Apr</div>
<div style={{ fontFamily: 'IBM Plex Mono', fontSize: 26, fontWeight: 500, color: 'var(--ink)', lineHeight: 1, letterSpacing: '-0.01em' }}>38.2%</div>
<div style={{ fontSize: 11, color: 'var(--ok)', marginTop: 8, fontFamily: 'IBM Plex Mono' }}><i className="bi bi-arrow-up-right" style={{ marginRight: 3 }} />+2.1 pts</div>
</div>
</div>
</div>
{/* Attention row: only what needs action */}
<div style={{
background: 'var(--card)', border: '1px solid var(--rule)', borderRadius: 8,
marginBottom: 18, overflow: 'hidden',
}}>
<div style={{
padding: '12px 20px', borderBottom: '1px solid var(--rule)',
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontWeight: 600, fontSize: 14, letterSpacing: '-0.01em' }}>Needs attention</span>
<Chip kind="bad">6 items</Chip>
</div>
<span style={{ fontFamily: 'IBM Plex Mono', fontSize: 10, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--steel)' }}>filtered · next 48 h</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)' }}>
{[
{ kind: 'bad', tag: 'OVERDUE', primary: '$3,240', label: '4 invoices past due', detail: 'Oldest: Rodriguez, 42d · $980' },
{ kind: 'warn', tag: 'TIGHT', primary: '7', label: 'jobs due in < 48h', detail: '2 in Prep · 4 in Coating · 1 in Curing' },
{ kind: 'cool', tag: 'LOW', primary: '8', label: 'powders below reorder', detail: 'Gloss black (3.2 lb) · Satin bronze · +6' },
].map((a, i) => (
<div key={i} style={{
padding: '16px 20px',
borderRight: i < 2 ? '1px solid var(--rule)' : 'none',
}}>
<Chip kind={a.kind}>{a.tag}</Chip>
<div style={{
marginTop: 10, fontFamily: 'IBM Plex Mono', fontSize: 22, fontWeight: 500,
color: 'var(--ink)', lineHeight: 1, letterSpacing: '-0.01em',
}}>{a.primary}</div>
<div style={{ fontSize: 12.5, color: 'var(--slate)', marginTop: 4 }}>{a.label}</div>
<div style={{ fontSize: 11, color: 'var(--steel)', marginTop: 6, fontFamily: 'IBM Plex Mono' }}>{a.detail}</div>
</div>
))}
</div>
</div>
{/* Two-up */}
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 16 }}>
<div style={{ background: 'var(--card)', border: '1px solid var(--rule)', borderRadius: 8 }}>
<div style={{ padding: '12px 20px', borderBottom: '1px solid var(--rule)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontWeight: 600, fontSize: 14 }}>Floor activity</span>
<span style={{ fontFamily: 'IBM Plex Mono', fontSize: 11, color: 'var(--steel)' }}>last 6 h</span>
</div>
{[
['09:42', 'J-2041', 'Rodriguez · Fence gate', 'moved to Coating', 'warn'],
['09:28', 'J-2044', 'D&T Auto · Wheel set', 'completed Prep', 'ok'],
['09:05', 'J-2040', 'Summit · Railing', 'flagged rework · pinholing', 'bad'],
['08:51', 'J-2038', 'Heritage · Bumper', 'pulled from oven · 180 °C', 'cool'],
['08:20', 'J-2037', 'Parkside · Chair set', 'received at intake', 'neutral'],
].map((r, i) => (
<div key={i} style={{
display: 'grid', gridTemplateColumns: '52px 70px 1fr auto',
gap: 14, alignItems: 'center', padding: '11px 20px',
borderBottom: i < 4 ? '1px solid var(--rule-soft)' : 'none',
fontSize: 13,
}}>
<span style={{ fontFamily: 'IBM Plex Mono', fontSize: 11.5, color: 'var(--steel)' }}>{r[0]}</span>
<span style={{ fontFamily: 'IBM Plex Mono', fontSize: 11.5, fontWeight: 500 }}>{r[1]}</span>
<span style={{ color: 'var(--slate)' }}>
<span style={{ color: 'var(--ink)' }}>{r[2].split(' · ')[0]}</span>
<span style={{ color: 'var(--steel)' }}> · {r[2].split(' · ')[1]}</span>
</span>
<Chip kind={r[4]}>{r[3]}</Chip>
</div>
))}
</div>
<div style={{ background: 'var(--card)', border: '1px solid var(--rule)', borderRadius: 8 }}>
<div style={{ padding: '12px 20px', borderBottom: '1px solid var(--rule)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontWeight: 600, fontSize: 14 }}>A/R aging</span>
<span style={{ fontFamily: 'IBM Plex Mono', fontSize: 11, color: 'var(--steel)' }}>$12,450</span>
</div>
<div style={{ padding: 18 }}>
{[
['Current', 8420, 'ok'],
['1 30 d', 2100, 'cool'],
['31 60 d', 1430, 'warn'],
['60 + d', 500, 'bad'],
].map(([l, v, k], i) => {
const palette = { ok: 'var(--ok)', cool: 'var(--cool)', warn: 'var(--warn)', bad: 'var(--bad)' };
return (
<div key={i} style={{ marginBottom: 14 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, marginBottom: 5 }}>
<span style={{ color: 'var(--slate)' }}>{l}</span>
<span style={{ fontFamily: 'IBM Plex Mono', color: 'var(--ink)', fontWeight: 500 }}>${v.toLocaleString()}</span>
</div>
<div style={{ height: 4, background: 'var(--rule-soft)', borderRadius: 0, overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${(v / 12450) * 100}%`, background: palette[k] }} />
</div>
</div>
);
})}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
/* =======================
JOBS INDEX — AFTER
======================= */
function JobsIndexAfter() {
const jobs = [
['J-2041', 'Fence gate', 'Anodized black', 'Rodriguez Metal', 'Apr 19', 'Coating', 'bad', 'High', 780, 'hot'],
['J-2040', 'Wheel set (4)', 'Gloss white', 'D&T Auto', 'Apr 19', 'Queued', 'cool', 'Normal', 420, ''],
['J-2039', 'Patio table', 'Matte black', 'Mercer Residential', 'Apr 20', 'Curing', 'ember', 'Normal', 210, ''],
['J-2038', 'Trailer frame', 'Zinc primer', 'Bluegrass Trailers', 'Apr 22', 'Prep', 'warn', 'Low', 1640, ''],
['J-2037', 'Bumper', 'Satin bronze', 'Heritage Restoration', 'Apr 22', 'Ready for pickup', 'ok', 'Normal', 340, ''],
['J-2036', 'Railing', 'Hammered copper', 'Summit Builders', 'Apr 23', 'Queued', 'cool', 'High', 980, ''],
];
return (
<div style={{ display: 'flex', background: 'var(--paper)' }}>
<SidebarAfter />
<div style={{ flex: 1, minWidth: 0 }}>
<TopbarAfter title="Jobs" breadcrumbs={['Pipeline', 'Jobs']} />
<div style={{ padding: '22px 28px' }}>
{/* Single-row metric strip — no cards */}
<div style={{
background: 'var(--card)', border: '1px solid var(--rule)', borderRadius: 8,
display: 'grid', gridTemplateColumns: 'repeat(5,1fr)', marginBottom: 18,
}}>
<Metric label="Open" value="47" delta="+4 wk" deltaDir="up" />
<Metric label="On floor" value="12" />
<Metric label="Due ≤ 48 h" value="7" delta="2 hot" deltaDir="down" />
<Metric label="Completed · Apr" value="28" delta="+11% MoM" deltaDir="up" />
<div style={{ padding: '18px 20px' }}>
<div style={{ fontFamily: 'IBM Plex Mono', fontSize: 10, letterSpacing: '0.14em', textTransform: 'uppercase', color: 'var(--steel)', marginBottom: 8 }}>Open value</div>
<div style={{ fontFamily: 'IBM Plex Mono', fontSize: 26, fontWeight: 500, color: 'var(--ink)', letterSpacing: '-0.01em', lineHeight: 1 }}>$18,420</div>
<div style={{ fontSize: 11, color: 'var(--steel)', marginTop: 8, fontFamily: 'IBM Plex Mono' }}>avg $392 / job</div>
</div>
</div>
{/* Toolbar */}
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
gap: 12, marginBottom: 12,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '7px 10px', background: 'var(--card)',
border: '1px solid var(--rule)', borderRadius: 6, width: 300,
}}>
<i className="bi bi-search" style={{ color: 'var(--steel)', fontSize: 13 }} />
<span style={{ fontSize: 13, color: 'var(--steel)', flex: 1 }}>Search jobs, customers, PO</span>
<span style={{ fontFamily: 'IBM Plex Mono', fontSize: 10, padding: '1px 5px', border: '1px solid var(--rule)', borderRadius: 3, color: 'var(--steel)' }}>/</span>
</div>
{['All', 'Due ≤ 48h', 'On floor', 'Ready', 'Mine'].map((t, i) => (
<button key={i} style={{
padding: '7px 11px', fontSize: 12.5, borderRadius: 6, cursor: 'pointer',
background: i === 0 ? 'var(--ink)' : 'var(--card)',
color: i === 0 ? 'var(--paper)' : 'var(--slate)',
border: `1px solid ${i === 0 ? 'var(--ink)' : 'var(--rule)'}`,
fontWeight: 500,
}}>{t}</button>
))}
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button style={{ padding: '7px 11px', fontSize: 12.5, background: 'var(--card)', color: 'var(--slate)', border: '1px solid var(--rule)', borderRadius: 6 }}>
<i className="bi bi-sliders" /> Filters
</button>
<button style={{ padding: '7px 12px', fontSize: 13, background: 'var(--ink)', color: 'var(--paper)', border: 'none', borderRadius: 6, fontWeight: 500 }}>
<i className="bi bi-plus-lg" /> New job
</button>
</div>
</div>
{/* Table */}
<div style={{ background: 'var(--card)', border: '1px solid var(--rule)', borderRadius: 8, overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
<thead>
<tr>
{['Job', 'Description', 'Customer', 'Due', 'Status', 'Priority', 'Total', ''].map((h, i) => (
<th key={i} style={{
padding: '10px 16px', textAlign: i === 6 ? 'right' : 'left',
background: 'transparent', color: 'var(--steel)',
fontFamily: 'IBM Plex Mono', fontSize: 10, fontWeight: 500,
textTransform: 'uppercase', letterSpacing: '0.12em',
borderBottom: '1px solid var(--rule)',
}}>{h}</th>
))}
</tr>
</thead>
<tbody>
{jobs.map((j, i) => {
const hot = j[9] === 'hot';
return (
<tr key={i} style={{ borderBottom: i < jobs.length - 1 ? '1px solid var(--rule-soft)' : 'none' }}>
<td style={{ padding: '13px 16px', fontFamily: 'IBM Plex Mono', fontWeight: 500, color: 'var(--ink)', position: 'relative' }}>
{hot && <span style={{ position: 'absolute', left: 0, top: 8, bottom: 8, width: 2, background: 'var(--bad)' }} />}
{j[0]}
</td>
<td style={{ padding: '13px 16px' }}>
<div style={{ color: 'var(--ink)', fontWeight: 500 }}>{j[1]}</div>
<div style={{ color: 'var(--steel)', fontSize: 11.5, marginTop: 1, fontFamily: 'IBM Plex Mono' }}>
<span style={{ display: 'inline-block', width: 6, height: 6, background: 'var(--ink)', borderRadius: 99, marginRight: 6, verticalAlign: 'middle' }} />
{j[2]}
</div>
</td>
<td style={{ padding: '13px 16px', color: 'var(--slate)' }}>{j[3]}</td>
<td style={{ padding: '13px 16px', fontFamily: 'IBM Plex Mono', color: hot ? 'var(--bad)' : 'var(--slate)' }}>{j[4]}</td>
<td style={{ padding: '13px 16px' }}><Chip kind={j[6]}>{j[5]}</Chip></td>
<td style={{ padding: '13px 16px', color: 'var(--slate)', fontSize: 12 }}>{j[7]}</td>
<td style={{ padding: '13px 16px', textAlign: 'right', fontFamily: 'IBM Plex Mono', color: 'var(--ink)' }}>${j[8].toLocaleString()}</td>
<td style={{ padding: '13px 16px', width: 30, color: 'var(--steel)' }}><i className="bi bi-three-dots" /></td>
</tr>
);
})}
</tbody>
</table>
<div style={{
padding: '10px 16px', borderTop: '1px solid var(--rule)',
display: 'flex', justifyContent: 'space-between',
fontFamily: 'IBM Plex Mono', fontSize: 11, color: 'var(--steel)',
}}>
<span>Showing 1 6 of 47</span>
<span> to move · to open · F to filter</span>
</div>
</div>
</div>
</div>
</div>
);
}
/* =======================
JOBS BOARD — AFTER
======================= */
function JobsBoardAfter() {
const cols = [
{ title: 'Intake', id: 'INTAKE', count: 3, cards: [
{ id: 'J-2049', cust: 'Pine Ridge Metal', desc: 'Gate hardware set', due: 'Apr 24', days: '+6', powder: 'Gloss black', parts: 12, prio: 'normal' },
{ id: 'J-2048', cust: 'Highway Fabworks', desc: 'Railing brackets', due: 'Apr 25', days: '+7', powder: 'Matte white', parts: 24, prio: 'low' },
{ id: 'J-2047', cust: 'Crestline Custom', desc: 'Bumper set', due: 'Apr 26', days: '+8', powder: 'Satin bronze', parts: 2, prio: 'normal' },
]},
{ title: 'Prep', id: 'PREP', count: 2, cards: [
{ id: 'J-2045', cust: 'Rodriguez Metal', desc: 'Fence gate', due: 'Apr 19', days: '+1', powder: 'Anodized black', parts: 1, prio: 'high', hot: true },
{ id: 'J-2044', cust: 'D&T Auto', desc: 'Wheel set (4)', due: 'Apr 19', days: '+1', powder: 'Gloss white', parts: 4, prio: 'normal' },
]},
{ title: 'Coating', id: 'COAT', count: 3, cards: [
{ id: 'J-2042', cust: 'Mercer', desc: 'Patio table', due: 'Apr 20', days: '+2', powder: 'Matte black', parts: 1, prio: 'normal' },
{ id: 'J-2041', cust: 'Bluegrass', desc: 'Trailer frame', due: 'Apr 22', days: '+4', powder: 'Zinc primer', parts: 1, prio: 'low' },
{ id: 'J-2040', cust: 'Summit', desc: 'Railing', due: 'Apr 23', days: '+5', powder: 'Hammered copper', parts: 8, prio: 'high' },
]},
{ title: 'Curing', id: 'CURE', count: 2, meter: '180°C · 14 min', cards: [
{ id: 'J-2038', cust: 'Heritage', desc: 'Bumper', due: 'Apr 22', days: '+4', powder: 'Satin bronze', parts: 1, prio: 'normal' },
{ id: 'J-2037', cust: 'Parkside', desc: 'Chair set (6)', due: 'Apr 22', days: '+4', powder: 'Gloss red', parts: 6, prio: 'normal' },
]},
{ title: 'Ready', id: 'READY', count: 1, cards: [
{ id: 'J-2034', cust: 'Lakeside Marina', desc: 'Railing kit', due: 'Apr 18', days: '0', powder: 'Matte white', parts: 4, prio: 'low' },
]},
];
const prioDot = { high: 'var(--bad)', normal: 'var(--steel)', low: 'var(--rule)' };
return (
<div style={{ display: 'flex', background: 'var(--paper)' }}>
<SidebarAfter />
<div style={{ flex: 1, minWidth: 0 }}>
<TopbarAfter title="Daily board" breadcrumbs={['Today', 'Daily board']} />
<div style={{ padding: '18px 24px' }}>
{/* compact toolbar */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 14, gap: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
display: 'flex', gap: 0, border: '1px solid var(--rule)', borderRadius: 6, overflow: 'hidden',
}}>
{['Board', 'List', 'Oven'].map((t, i) => (
<button key={i} style={{
padding: '6px 12px', fontSize: 12, fontWeight: 500, cursor: 'pointer',
background: i === 0 ? 'var(--ink)' : 'var(--card)',
color: i === 0 ? 'var(--paper)' : 'var(--slate)',
border: 'none', borderRight: i < 2 ? '1px solid var(--rule)' : 'none',
}}>{t}</button>
))}
</div>
<div style={{ fontFamily: 'IBM Plex Mono', fontSize: 11, color: 'var(--steel)', letterSpacing: '0.05em' }}>
11 jobs on floor · 2 hot · avg cycle 3.2 d
</div>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button style={{ padding: '6px 11px', fontSize: 12, border: '1px solid var(--rule)', background: 'var(--card)', borderRadius: 6, color: 'var(--slate)' }}>Group: <b style={{ color: 'var(--ink)' }}>Stage</b> <i className="bi bi-chevron-down" style={{ fontSize: 10, marginLeft: 4 }} /></button>
<button style={{ padding: '6px 11px', fontSize: 12, border: '1px solid var(--rule)', background: 'var(--card)', borderRadius: 6, color: 'var(--slate)' }}>Hide completed</button>
</div>
</div>
<div style={{ display: 'flex', gap: 10, overflow: 'hidden' }}>
{cols.map((c, i) => (
<div key={i} style={{
width: 228, flexShrink: 0, display: 'flex', flexDirection: 'column',
background: 'transparent',
}}>
{/* column header */}
<div style={{
padding: '8px 10px 10px',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
borderBottom: '1px solid var(--rule)',
marginBottom: 8,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{
fontFamily: 'IBM Plex Mono', fontSize: 10.5, letterSpacing: '0.14em',
textTransform: 'uppercase', color: 'var(--ink)', fontWeight: 500,
}}>{c.id}</span>
<span style={{
fontFamily: 'IBM Plex Mono', fontSize: 10.5, color: 'var(--steel)',
padding: '1px 6px', border: '1px solid var(--rule)', borderRadius: 99,
}}>{c.count}</span>
</div>
<i className="bi bi-plus" style={{ color: 'var(--steel)', fontSize: 14 }} />
</div>
{c.meter && (
<div style={{
background: 'var(--ember-tint)', color: 'var(--ember-ink)',
padding: '6px 10px', borderRadius: 4, marginBottom: 8,
fontFamily: 'IBM Plex Mono', fontSize: 10.5, letterSpacing: '0.04em',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
}}>
<span><i className="bi bi-fire" style={{ marginRight: 6 }} />{c.meter}</span>
<span style={{ width: 6, height: 6, borderRadius: 99, background: 'var(--ember)', animation: 'pulse 1.5s infinite' }} />
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{c.cards.map((card, j) => (
<div key={j} style={{
background: 'var(--card)', border: '1px solid var(--rule)',
borderRadius: 6, padding: '10px 11px',
position: 'relative',
}}>
{card.hot && <span style={{ position: 'absolute', left: -1, top: -1, bottom: -1, width: 2, background: 'var(--bad)', borderTopLeftRadius: 6, borderBottomLeftRadius: 6 }} />}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 4 }}>
<span style={{ fontFamily: 'IBM Plex Mono', fontSize: 11, fontWeight: 500, color: 'var(--ink)' }}>{card.id}</span>
<span style={{ fontFamily: 'IBM Plex Mono', fontSize: 10.5, color: card.hot ? 'var(--bad)' : 'var(--steel)' }}>{card.due} <span style={{ opacity: 0.5 }}>({card.days}d)</span></span>
</div>
<div style={{ fontSize: 12.5, color: 'var(--ink)', fontWeight: 500, marginBottom: 2, lineHeight: 1.3 }}>{card.desc}</div>
<div style={{ fontSize: 11.5, color: 'var(--slate)', marginBottom: 8 }}>{card.cust}</div>
<div style={{
display: 'flex', alignItems: 'center', gap: 8,
paddingTop: 7, borderTop: '1px solid var(--rule-soft)',
fontFamily: 'IBM Plex Mono', fontSize: 10.5, color: 'var(--steel)',
}}>
<span style={{
width: 9, height: 9, borderRadius: 2,
background: powderSwatch(card.powder), border: '1px solid rgba(0,0,0,0.12)',
}} />
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: 'var(--slate)' }}>{card.powder}</span>
<span>{card.parts}p</span>
<span style={{ width: 6, height: 6, borderRadius: 99, background: prioDot[card.prio] }} />
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
<style>{`@keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: 0.3 } }`}</style>
</div>
);
}
function powderSwatch(name) {
const m = {
'Gloss black': '#111',
'Matte white': '#f3f1eb',
'Satin bronze': '#8c6a3d',
'Anodized black': '#1a1a1c',
'Gloss white': '#ffffff',
'Matte black': '#222',
'Zinc primer': '#9aa0a6',
'Hammered copper': '#b87333',
'Gloss red': '#b91c1c',
};
return m[name] || '#777';
}
Object.assign(window, { DashboardAfter, JobsIndexAfter, JobsBoardAfter, Chip, Metric });
@@ -0,0 +1,266 @@
/* "Before" screens — faithful recreations of the current Bootstrap views. */
/* =======================
DASHBOARD — BEFORE
======================= */
function DashboardBefore() {
const kpis = [
{ n: 4, label: 'Overdue Invoices', sub: '$3,240', color: '#eb3349', icon: 'bi-exclamation-circle' },
{ n: 12, label: "Today's Jobs", color: '#11998e', icon: 'bi-calendar-check-fill' },
{ n: 7, label: 'Overdue Jobs', color: '#f7971e', icon: 'bi-exclamation-triangle-fill' },
{ n: 3, label: "Today's Appointments", color: '#4facfe', icon: 'bi-calendar-event-fill' },
{ n: 8, label: 'Low Stock', color: '#a855f7', icon: 'bi-box-seam-fill' },
{ n: 2, label: 'Maintenance Due', color: '#667eea', icon: 'bi-tools' },
];
return (
<div style={{ display: 'flex', background: '#f4f5f7' }}>
<SidebarBefore />
<div style={{ flex: 1, minWidth: 0 }}>
<TopbarBefore title="Dashboard" />
<div style={{ padding: '24px 28px' }}>
{/* gradient welcome banner */}
<div style={{
background: 'linear-gradient(180deg,#1a1a2e,#16213e,#0f3460)',
borderRadius: 12, padding: '22px 26px', marginBottom: 22, color: '#fff',
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
}}>
<div>
<div style={{ fontSize: 18, fontWeight: 700, marginBottom: 4 }}>Welcome Back!</div>
<div style={{ fontSize: 13, color: 'rgba(255,255,255,0.75)' }}>
Here's your shop overview for April 18, 2026 · <span style={{ opacity: 0.85 }}><i className="bi bi-rocket-takeoff" /> What's New</span>
</div>
<div style={{ fontSize: 12, color: 'rgba(255,255,255,0.6)', marginTop: 8 }}>
<i className="bi bi-lightbulb" style={{ color: 'rgba(255,220,80,0.8)' }} /> Tip: Use job templates to speed up quoting for repeat customers.
</div>
</div>
<div style={{ display: 'flex', gap: 28, textAlign: 'right' }}>
<div><div style={{ fontSize: 11, opacity: 0.75 }}>Collected April 2026</div><div style={{ fontWeight: 700, fontSize: 17 }}>$48,290</div></div>
<div><div style={{ fontSize: 11, opacity: 0.75 }}>Outstanding A/R</div><div style={{ fontWeight: 700, fontSize: 17 }}>$12,450</div></div>
<div><div style={{ fontSize: 11, opacity: 0.75 }}>Active Customers</div><div style={{ fontWeight: 700, fontSize: 17 }}>84</div></div>
</div>
</div>
{/* rainbow KPIs */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(6,1fr)', gap: 12, marginBottom: 22 }}>
{kpis.map((k, i) => (
<div key={i} style={{
background: '#fff', borderRadius: 10, padding: 14,
borderTop: `4px solid ${k.color}`,
boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
<div style={{ fontSize: 28, fontWeight: 700, color: k.color, lineHeight: 1 }}>{k.n}</div>
{k.sub && <div style={{ fontSize: 11, color: '#6b7280' }}>{k.sub}</div>}
</div>
<div style={{ fontSize: 11, color: '#6b7280', marginTop: 4 }}>{k.label}</div>
</div>
<i className={`bi ${k.icon}`} style={{ color: k.color, opacity: 0.5, fontSize: 22 }} />
</div>
</div>
))}
</div>
{/* body cards */}
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 16 }}>
<div style={{ background: '#fff', borderRadius: 10, boxShadow: '0 1px 3px rgba(0,0,0,0.06)' }}>
<div style={{ padding: '14px 18px', borderBottom: '1px solid #e5e7eb', fontWeight: 600 }}>Recent Jobs</div>
<div style={{ padding: 6 }}>
{['J-2041 · Fence gate · Rodriguez', 'J-2040 · Wheel set · D&T Auto', 'J-2039 · Patio table · Mercer', 'J-2038 · Trailer frame · Bluegrass', 'J-2037 · Bumper · Heritage'].map((r, i) => (
<div key={i} style={{ padding: '10px 12px', borderBottom: i < 4 ? '1px solid #f3f4f6' : 'none', display: 'flex', justifyContent: 'space-between', fontSize: 13, color: '#374151' }}>
<span>{r}</span>
<span style={{ background: '#fef3c7', color: '#92400e', padding: '2px 8px', borderRadius: 4, fontSize: 10.5, fontWeight: 600 }}>IN PROGRESS</span>
</div>
))}
</div>
</div>
<div style={{ background: '#fff', borderRadius: 10, boxShadow: '0 1px 3px rgba(0,0,0,0.06)' }}>
<div style={{ padding: '14px 18px', borderBottom: '1px solid #e5e7eb', fontWeight: 600 }}>A/R Aging</div>
<div style={{ padding: 18 }}>
{[['Current', 8420, '#11998e'], ['130 days', 2100, '#4facfe'], ['3160 days', 1430, '#f7971e'], ['60+ days', 500, '#eb3349']].map(([l, v, c], i) => (
<div key={i} style={{ marginBottom: 10 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, marginBottom: 4 }}><span>{l}</span><span style={{ fontWeight: 600 }}>${v.toLocaleString()}</span></div>
<div style={{ height: 6, borderRadius: 99, background: '#f3f4f6', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${(v/10000)*100}%`, background: c }} />
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
/* =======================
JOBS INDEX — BEFORE
======================= */
function JobsIndexBefore() {
const jobs = [
['J-2041', 'Fence gate — Anodized black', 'Rodriguez Metal', 'Apr 19', 'IN PROGRESS', 'HIGH', '$780'],
['J-2040', 'Wheel set (4) — Gloss white', 'D&T Auto', 'Apr 19', 'QUEUED', 'NORMAL', '$420'],
['J-2039', 'Patio table — Matte black', 'Mercer Residential', 'Apr 20', 'CURING', 'NORMAL', '$210'],
['J-2038', 'Trailer frame — Zinc primer', 'Bluegrass Trailers', 'Apr 22', 'IN PROGRESS', 'LOW', '$1,640'],
['J-2037', 'Bumper — Satin bronze', 'Heritage Restoration', 'Apr 22', 'READY FOR PICKUP', 'NORMAL', '$340'],
['J-2036', 'Railing — Hammered copper', 'Summit Builders', 'Apr 23', 'QUEUED', 'HIGH', '$980'],
];
const statusStyle = (s) => ({
'IN PROGRESS': { bg: '#fef3c7', fg: '#92400e' },
'QUEUED': { bg: '#dbeafe', fg: '#1e40af' },
'CURING': { bg: '#fee2e2', fg: '#991b1b' },
'READY FOR PICKUP': { bg: '#d1fae5', fg: '#065f46' },
}[s]);
const prioStyle = (p) => ({
'HIGH': { bg: '#fef3c7', fg: '#92400e' },
'NORMAL': { bg: '#dbeafe', fg: '#1e40af' },
'LOW': { bg: '#e5e7eb', fg: '#4b5563' },
}[p]);
return (
<div style={{ display: 'flex', background: '#f4f5f7' }}>
<SidebarBefore />
<div style={{ flex: 1, minWidth: 0 }}>
<TopbarBefore title="Jobs" />
<div style={{ padding: '24px 28px' }}>
{/* stat cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 16, marginBottom: 22 }}>
{[
['Total Jobs', '47', 'bi-briefcase', '#dbeafe', '#1e40af'],
['In Progress', '12', 'bi-hourglass-split', '#fef3c7', '#92400e'],
['Completed', '28', 'bi-check-circle', '#d1fae5', '#065f46'],
['Total Value', '$18,420', 'bi-currency-dollar', '#fee2e2', '#991b1b'],
].map(([l, v, ic, b, f], i) => (
<div key={i} style={{ background: '#fff', borderRadius: 10, padding: 18, boxShadow: '0 1px 3px rgba(0,0,0,0.06)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontSize: 12, color: '#6b7280', marginBottom: 3 }}>{l}</div>
<div style={{ fontSize: 24, fontWeight: 700 }}>{v}</div>
</div>
<div style={{ width: 48, height: 48, borderRadius: '50%', background: b, color: f, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<i className={`bi ${ic}`} style={{ fontSize: 20 }} />
</div>
</div>
))}
</div>
{/* table card */}
<div style={{ background: '#fff', borderRadius: 10, boxShadow: '0 1px 3px rgba(0,0,0,0.06)' }}>
<div style={{ padding: '14px 18px', borderBottom: '1px solid #e5e7eb', display: 'flex', justifyContent: 'space-between', gap: 12 }}>
<div style={{ display: 'flex', gap: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', border: '1.5px solid #9ca3af', borderRadius: 6, padding: '4px 10px', width: 320, gap: 8 }}>
<i className="bi bi-search" style={{ color: '#9ca3af' }} />
<input placeholder="Search jobs…" style={{ border: 'none', outline: 'none', flex: 1, fontSize: 13 }} />
</div>
<button style={{ background: '#4f46e5', color: '#fff', border: 'none', borderRadius: 6, padding: '0 14px', fontSize: 13, fontWeight: 500 }}><i className="bi bi-search" /></button>
<button style={{ background: '#fff', color: '#374151', border: '1.5px solid #9ca3af', borderRadius: 6, padding: '6px 12px', fontSize: 13 }}>Filters</button>
</div>
<button style={{ background: '#4f46e5', color: '#fff', border: 'none', borderRadius: 6, padding: '8px 14px', fontSize: 13, fontWeight: 500 }}><i className="bi bi-plus-lg" /> New Job</button>
</div>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
<thead>
<tr>
{['Job #', 'Description', 'Customer', 'Due', 'Status', 'Priority', 'Total', ''].map((h, i) => (
<th key={i} style={{ padding: '10px 14px', textAlign: 'left', background: '#f3f4f6', color: '#6b7280', fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.05em', borderBottom: '2px solid #e5e7eb' }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{jobs.map((j, i) => {
const st = statusStyle(j[4]); const pr = prioStyle(j[5]);
return (
<tr key={i} style={{ borderBottom: '1px solid #f3f4f6' }}>
<td style={{ padding: '12px 14px', fontWeight: 600, color: '#4f46e5' }}>{j[0]}</td>
<td style={{ padding: '12px 14px' }}>{j[1]}</td>
<td style={{ padding: '12px 14px' }}>{j[2]}</td>
<td style={{ padding: '12px 14px' }}>{j[3]}</td>
<td style={{ padding: '12px 14px' }}><span style={{ background: st.bg, color: st.fg, padding: '4px 10px', borderRadius: 6, fontSize: 10.5, fontWeight: 600 }}>{j[4]}</span></td>
<td style={{ padding: '12px 14px' }}><span style={{ background: pr.bg, color: pr.fg, padding: '4px 10px', borderRadius: 6, fontSize: 10.5, fontWeight: 600 }}>{j[5]}</span></td>
<td style={{ padding: '12px 14px', fontWeight: 600 }}>{j[6]}</td>
<td style={{ padding: '12px 14px', color: '#6b7280' }}><i className="bi bi-three-dots" /></td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
}
/* =======================
JOBS BOARD — BEFORE
======================= */
function JobsBoardBefore() {
const cols = [
{ title: 'INTAKE', count: 3, tint: '#dbeafe', accent: '#1e40af', cards: [
{ id: 'J-2049', cust: 'Pine Ridge Metal', desc: 'Gate hardware set', due: 'Apr 24', prio: '#6c757d', powder: 'Gloss black' },
{ id: 'J-2048', cust: 'Highway Fabworks', desc: 'Railing brackets', due: 'Apr 25', prio: '#ffc107', powder: 'Matte white' },
{ id: 'J-2047', cust: 'Crestline Custom', desc: 'Bumper set', due: 'Apr 26', prio: '#6c757d', powder: 'Satin bronze' },
]},
{ title: 'PREP', count: 2, tint: '#fef3c7', accent: '#92400e', cards: [
{ id: 'J-2045', cust: 'Rodriguez Metal', desc: 'Fence gate', due: 'Apr 19', prio: '#dc3545', powder: 'Anodized black' },
{ id: 'J-2044', cust: 'D&T Auto', desc: 'Wheel set (4)', due: 'Apr 19', prio: '#ffc107', powder: 'Gloss white' },
]},
{ title: 'COATING', count: 3, tint: '#fee2e2', accent: '#991b1b', cards: [
{ id: 'J-2042', cust: 'Mercer', desc: 'Patio table', due: 'Apr 20', prio: '#6c757d', powder: 'Matte black' },
{ id: 'J-2041', cust: 'Bluegrass', desc: 'Trailer frame', due: 'Apr 22', prio: '#198754', powder: 'Zinc primer' },
{ id: 'J-2040', cust: 'Summit', desc: 'Railing', due: 'Apr 23', prio: '#dc3545', powder: 'Hammered copper' },
]},
{ title: 'CURING', count: 2, tint: '#e0e7ff', accent: '#3730a3', cards: [
{ id: 'J-2038', cust: 'Heritage', desc: 'Bumper', due: 'Apr 22', prio: '#6c757d', powder: 'Satin bronze' },
{ id: 'J-2037', cust: 'Parkside', desc: 'Chair set (6)', due: 'Apr 22', prio: '#ffc107', powder: 'Gloss red' },
]},
{ title: 'READY', count: 1, tint: '#d1fae5', accent: '#065f46', cards: [
{ id: 'J-2034', cust: 'Lakeside Marina', desc: 'Railing kit', due: 'Apr 18', prio: '#198754', powder: 'Matte white' },
]},
];
return (
<div style={{ display: 'flex', background: '#f4f5f7' }}>
<SidebarBefore />
<div style={{ flex: 1, minWidth: 0 }}>
<TopbarBefore title="Jobs Board" />
<div style={{ padding: '24px 28px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 14 }}>
<div style={{ display: 'flex', gap: 8 }}>
<button style={{ background: '#fff', border: '1.5px solid #9ca3af', borderRadius: 6, padding: '6px 12px', fontSize: 13 }}>All customers</button>
<button style={{ background: '#fff', border: '1.5px solid #9ca3af', borderRadius: 6, padding: '6px 12px', fontSize: 13 }}>All priorities</button>
</div>
<button style={{ background: '#4f46e5', color: '#fff', border: 'none', borderRadius: 6, padding: '8px 14px', fontSize: 13, fontWeight: 500 }}>+ New Job</button>
</div>
<div style={{ display: 'flex', gap: 10, overflow: 'hidden' }}>
{cols.map((c, i) => (
<div key={i} style={{ width: 220, background: '#e9ecef', borderRadius: 8, overflow: 'hidden', flexShrink: 0 }}>
<div style={{ padding: '10px 12px', display: 'flex', justifyContent: 'space-between', fontSize: 12, fontWeight: 600 }}>
<span style={{ background: c.tint, color: c.accent, padding: '3px 8px', borderRadius: 4, fontSize: 10.5, letterSpacing: '0.04em' }}>{c.title}</span>
<span style={{ color: '#6b7280', fontSize: 11 }}>{c.count}</span>
</div>
<div style={{ padding: '4px 8px 12px' }}>
{c.cards.map((card, j) => (
<div key={j} style={{
background: '#fff', borderRadius: 5, border: '1px solid #e5e7eb',
borderLeft: `4px solid ${card.prio}`, padding: '9px 10px', marginBottom: 7, fontSize: 12,
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ fontWeight: 600, color: '#4f46e5', fontSize: 11.5 }}>{card.id}</span>
<span style={{ color: '#6b7280', fontSize: 10.5 }}>{card.due}</span>
</div>
<div style={{ fontWeight: 500, marginBottom: 2, lineHeight: 1.3 }}>{card.desc}</div>
<div style={{ color: '#6b7280', fontSize: 11, marginBottom: 4 }}>{card.cust}</div>
<div style={{ color: '#6b7280', fontSize: 10.5 }}><i className="bi bi-droplet-fill" style={{ marginRight: 3 }} /> {card.powder}</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}
Object.assign(window, { DashboardBefore, JobsIndexBefore, JobsBoardBefore });