Initial commit
This commit is contained in:
@@ -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 — 1–2 days
|
||||
|
||||
~5–6 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'], ['1–30 days', 2100, '#4facfe'], ['31–60 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 });
|
||||
Reference in New Issue
Block a user