Project

General

Profile

Feature #3241 » SCR-003-web.html

Bot AI, 08/27/2026 02:05 PM

 
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>SCR-003 — web mockup — Orders list</title>
<!--
SCR-003 · side: web (oms-web) · viewport: 1440 × 900 (desktop, the only breakpoint this
app is built for — app/layout.tsx is a fixed flex row with a w-64 sidebar and ships no
mobile nav).

Screen: app/orders/page.tsx — status "existing-modified". Inventory scope (verbatim):
"app/orders/page.tsx — gate on Orders:View; hide/disable the create entry point when
Orders:Create is absent."
Inventory ambiguity: none declared (`ambiguous: false`). Two genuine open decisions
surfaced while drawing it anyway — row-level data scoping (pin 5) and the master-data
leak through the filter panel (pin 6). Both are drawn with a defensible default and
recorded for N31 rather than treated as blockers.

Artboard A draws the screen in its CHANGED state: a Supplier · Staff user holding
Orders:View and NOT Orders:Create. That is the one permission set where SCR-003's scope
sentence produces visible pixels — the "New Order" button is gone from PageHeader and the
sidebar is filtered down to the single entry this user may open. The unchanged
(Manufacturer · Admin) state is identical to what ships today and is drawn small in pin 3
for comparison rather than given the full artboard.

Every colour, radius, spacing and type size below is transcribed from this repo's real
tokens — tailwind.config.ts (theme.extend.colors) and app/globals.css (@layer components:
.btn* / .field-input / .field-control / .card / .field-label / .field-error / .link).
Nothing is invented. The ONLY non-token styling in this file is the annotation chrome
(dashed outlines, pin numbers, the sheet below the artboard) — deliberately drawn in a
colour that exists nowhere in the design system so it can never be mistaken for a UI
element, and living OUTSIDE the 1440×900 artboard so Artboard A stays a clean, diffable
frame for W21_ui_verify.
-->
<style>
/* ---- design tokens: verbatim from tailwind.config.ts ---- */
:root {
--bg: #fcfcfd;
--surface: #ffffff;
--text: #302828;
--text-muted: #836f6f;
--border: #ede0ce;
--accent: #453028;
--accent-subtle: #f7f1e9;
--status-success: #1f8a4c;
--status-warning: #b8860b;
--status-danger: #c0392b;
--status-neutral: #6b7280;
--status-info: #2563eb;

/* annotation-only — NOT a design token, never ships */
--anno: #7c3aed;
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: #e9e7e4;
color: var(--text);
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
padding: 32px;
}

/* ================= mockup document chrome (outside the artboard) ================= */
.doc-head { max-width: 1440px; margin: 0 auto 20px; }
.doc-head h1 { font-size: 20px; line-height: 28px; margin: 0 0 6px; font-weight: 600; }
.doc-head p { font-size: 13px; line-height: 20px; color: #4b4b4b; margin: 0 0 4px; max-width: 1100px; }
.doc-meta { font-size: 12px; color: #5c5c5c; margin-top: 8px; }
.doc-meta code { background: #fff; border: 1px solid #d8d5d1; border-radius: 4px; padding: 1px 5px; }
.frame-label {
max-width: 1440px; margin: 28px auto 8px; font-size: 12px; font-weight: 600;
text-transform: uppercase; letter-spacing: .06em; color: #4b4b4b;
}
.frame-label span { font-weight: 400; text-transform: none; letter-spacing: 0; color: #6b6b6b; }

/* ================= ARTBOARD A — exactly 1440 × 900, no annotation inside ================= */
.artboard {
width: 1440px; height: 900px; margin: 0 auto;
overflow: hidden; background: var(--bg);
outline: 1px solid #cfcbc6; /* outline, not border — does not alter the 1440×900 box */
box-shadow: 0 8px 24px rgba(0,0,0,.10);
}

/* ---- app/layout.tsx: body.flex.min-h-screen > Sidebar + main.flex-1.p-6 ---- */
.shell { display: flex; width: 100%; height: 100%; }
.main { flex: 1 1 auto; padding: 24px; min-width: 0; overflow: hidden; }

/* ---- components/layout/Sidebar.tsx: aside.w-64.shrink-0.border-r.bg-surface.flex.flex-col ---- */
.sidebar {
width: 256px; flex: 0 0 256px;
border-right: 1px solid var(--border); background: var(--surface);
display: flex; flex-direction: column;
}
.sidebar-brand { padding: 16px 20px; border-bottom: 1px solid var(--border); } /* px-5 py-4 */
.sidebar-brand span { font-size: 18px; line-height: 28px; font-weight: 600; color: var(--text); } /* text-lg */
.sidebar-nav { flex: 1 1 auto; overflow-y: auto; padding: 16px 0; } /* py-4 */
.nav-group { margin-bottom: 20px; padding: 0 12px; } /* mb-5 px-3 */
.nav-group-label {
padding: 0 8px; margin-bottom: 4px; /* px-2 mb-1 */
font-size: 12px; line-height: 16px; font-weight: 600; /* text-xs font-semibold */
text-transform: uppercase; letter-spacing: .025em; color: var(--text-muted);
}
.nav-list { list-style: none; margin: 0; padding: 0; }
.nav-list li + li { margin-top: 2px; } /* space-y-0.5 */
.nav-item {
display: flex; align-items: center; gap: 8px; /* gap-2 */
border-radius: 6px; padding: 6px 8px; /* rounded-md px-2 py-1.5 */
font-size: 14px; line-height: 20px; color: var(--text-muted); text-decoration: none;
}
.nav-item.active { background: var(--accent-subtle); color: var(--text); font-weight: 500; }
.nav-item svg { width: 16px; height: 16px; flex: 0 0 16px; } /* h-4 w-4 */
.sidebar-foot { border-top: 1px solid var(--border); padding-top: 12px; } /* pt-3 */
.signout {
display: flex; align-items: center; gap: 8px; margin: 0 12px 16px; /* px-3 pb-4 */
width: calc(100% - 24px);
border-radius: 6px; padding: 6px 8px; font-size: 14px; line-height: 20px;
color: var(--text-muted); background: none; border: 0; font-family: inherit; text-align: left;
}
.signout svg { width: 16px; height: 16px; flex: 0 0 16px; }

/* ---- app/globals.css @layer components ---- */
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; } /* rounded-lg */
.btn {
display: inline-flex; align-items: center; justify-content: center; border-radius: 6px; /* rounded-md */
padding: 8px 16px; /* px-4 py-2 */
font-size: 14px; line-height: 20px; /* text-sm */
font-weight: 500; /* font-medium */
font-family: inherit; border: 0; cursor: default; text-decoration: none;
}
.btn-primary { background: var(--accent); color: #fff; }
.btn-secondary { background: var(--surface); border: 1px solid var(--border); color: var(--text); }
.btn-sm { padding: 4px 12px; } /* Pagination's px-3 py-1 */
.btn-dim { opacity: .4; } /* Pagination disabled edge: opacity-40 */
.field-input {
display: block; width: 100%; border: 1px solid var(--border); border-radius: 6px;
background: var(--surface); color: var(--text);
font-size: 14px; line-height: 20px; /* sm:text-sm */
padding: 8px 12px; font-family: inherit;
}
.field-control {
display: block; width: 100%; border: 1px solid var(--border); border-radius: 6px;
background: var(--surface); color: var(--text);
font-size: 14px; line-height: 20px; padding: 8px 12px; min-height: 42px; font-family: inherit;
}
.field-label { display: block; font-size: 14px; line-height: 20px; font-weight: 500; color: var(--text); margin-bottom: 4px; }
.field-error { margin-top: 4px; font-size: 14px; line-height: 20px; color: var(--status-danger); }
.link { color: var(--status-info); text-decoration: underline; }

/* ---- PageHeader.tsx: div.flex.items-center.justify-between.mb-6 ---- */
.page-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; }
.page-head h1 { font-size: 20px; line-height: 28px; font-weight: 600; color: var(--text); margin: 0; } /* text-xl */

/* ---- orders toolbar: div.flex.items-center.justify-between.mb-4.gap-4.flex-wrap ---- */
.toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
.search-wrap { position: relative; width: 100%; max-width: 320px; } /* max-w-xs */
.search-wrap svg { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; }
.search-wrap .field-input { padding-left: 32px; } /* pl-8 */
.search-wrap .placeholder {
position: absolute; left: 32px; top: 50%; transform: translateY(-50%);
font-size: 14px; line-height: 20px; color: var(--text-muted); pointer-events: none;
}
.filters-btn svg { width: 16px; height: 16px; margin-right: 6px; } /* mr-1.5 */

/* ---- DataTable.tsx ---- */
.table-card { overflow: hidden; }
table.dt { width: 100%; border-collapse: collapse; font-size: 14px; line-height: 20px; } /* text-sm */
table.dt thead th {
padding: 12px 16px; text-align: left; font-weight: 500; /* px-4 py-3 font-medium */
color: var(--text-muted); white-space: nowrap;
}
table.dt thead th .sort { display: inline-flex; align-items: center; gap: 4px; color: inherit; text-decoration: none; }
table.dt thead th .sort svg { width: 14px; height: 14px; } /* h-3.5 w-3.5 */
table.dt thead tr { border-bottom: 1px solid var(--border); }
table.dt tbody tr { border-top: 1px solid var(--border); } /* divide-y divide-border */
table.dt tbody tr:first-child { border-top: 0; }
table.dt tbody td { padding: 12px 16px; vertical-align: middle; white-space: nowrap; color: var(--text); }
table.dt td.empty { padding: 32px 16px; text-align: center; color: var(--text-muted); } /* px-4 py-8 */

/* ---- StatusBadge.tsx: rounded-full px-2 py-0.5 text-xs font-medium, tone at /10 ---- */
.badge { display: inline-flex; align-items: center; border-radius: 9999px; padding: 2px 8px; font-size: 12px; line-height: 16px; font-weight: 500; }
.badge-warning { background: rgba(184,134,11,.1); color: var(--status-warning); }
.badge-success { background: rgba(31,138,76,.1); color: var(--status-success); }
.badge-danger { background: rgba(192,57,43,.1); color: var(--status-danger); }
.badge-neutral { background: rgba(107,114,128,.1); color: var(--status-neutral); }
.badge-info { background: rgba(37,99,235,.1); color: var(--status-info); }

/* ---- Pagination.tsx: flex justify-between mt-4 text-sm text-text-muted ---- */
.pager { display: flex; align-items: center; justify-content: space-between; margin-top: 16px; font-size: 14px; line-height: 20px; color: var(--text-muted); gap: 12px; flex-wrap: wrap; }
.pager .right { display: flex; align-items: center; gap: 12px; }
.pager label { display: flex; align-items: center; gap: 8px; }
.pager select.field-input { width: auto; padding: 4px 28px 4px 12px; } /* py-1 pr-7 w-auto */

/* ================= annotation sheet (never ships) ================= */
.sheet { max-width: 1440px; margin: 0 auto; }
.sheet section {
background: #fff; border: 1px solid #d8d5d1; border-radius: 8px;
padding: 20px 24px; margin-top: 16px;
}
.sheet h2 { font-size: 15px; margin: 0 0 4px; font-weight: 600; }
.sheet h2 .pin { margin-right: 8px; }
.sheet .lede { font-size: 13px; line-height: 20px; color: #4b4b4b; margin: 0 0 16px; max-width: 1180px; }
.pin {
display: inline-flex; align-items: center; justify-content: center;
width: 20px; height: 20px; border-radius: 9999px; background: var(--anno);
color: #fff; font-size: 11px; font-weight: 700; line-height: 20px; vertical-align: middle;
}
.anno-note { font-size: 12px; line-height: 18px; color: var(--anno); margin-top: 8px; }
.anno-box { outline: 2px dashed var(--anno); outline-offset: 3px; }
.sheet table.info { width: 100%; border-collapse: collapse; font-size: 13px; }
.sheet table.info thead th { border-bottom: 1px solid #d8d5d1; color: #4b4b4b; padding: 8px 10px; text-align: left; font-weight: 500; }
.sheet table.info tbody td { padding: 8px 10px; vertical-align: top; border-top: 1px solid #ece9e6; }
.sheet code { background: #f5f3f1; border-radius: 4px; padding: 1px 5px; font-size: 12px; }
.states { display: flex; gap: 28px; flex-wrap: wrap; align-items: flex-start; }
.state-cap { font-size: 12px; font-weight: 600; color: #3b3b3b; margin-bottom: 6px; }
.state-sub { font-size: 12px; line-height: 17px; color: #6b6b6b; margin-bottom: 10px; max-width: 400px; }
.mini-frame {
width: 400px; background: var(--bg); border: 1px solid #d8d5d1; border-radius: 8px; padding: 16px;
}
.swatches { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 6px; }
.sw { font-size: 11px; color: #4b4b4b; text-align: center; }
.sw i { display: block; width: 56px; height: 28px; border-radius: 4px; border: 1px solid #d8d5d1; margin-bottom: 4px; }
.decisions li { font-size: 13px; line-height: 20px; margin-bottom: 10px; }
.flag { color: var(--anno); font-weight: 600; }
.chip {
display: inline-block; font-size: 11px; font-weight: 600; letter-spacing: .04em;
text-transform: uppercase; border-radius: 4px; padding: 2px 6px; vertical-align: middle;
}
.chip-drawn { background: rgba(31,138,76,.12); color: var(--status-success); }
.chip-declined { background: rgba(192,57,43,.10); color: var(--status-danger); }
.chip-approx { background: rgba(184,134,11,.14); color: var(--status-warning); }
.ladder { list-style: none; margin: 0; padding: 0; }
.ladder li {
font-size: 13px; line-height: 20px; color: #3b3b3b;
border-left: 2px solid var(--anno); padding: 0 0 14px 14px; margin: 0; position: relative;
}
.ladder li:last-child { border-left-color: transparent; padding-bottom: 0; }
.ladder li::before {
content: ""; position: absolute; left: -5px; top: 6px; width: 8px; height: 8px;
border-radius: 9999px; background: var(--anno);
}
.ladder b { color: #1f1f1f; }
</style>
</head>
<body>

<div class="doc-head">
<h1>SCR-003 — Orders list · web (oms-web) · <code>app/orders/page.tsx</code></h1>
<p>
The screen is unchanged in layout. What this ticket adds is a <em>gate</em> in front of it and a
<em>conditional</em> inside it: the route requires <code>Orders:View</code>, and the one create entry point in
<code>PageHeader</code> requires <code>Orders:Create</code>. Nothing else on this page is permission-sensitive —
search, sort, filter and pagination are deliberately left alone (pin <span class="pin">4</span>).
</p>
<p>
<strong>Artboard A draws the changed state, not the shipping one.</strong> A Manufacturer · Admin sees exactly
today's page, so drawing that would produce a mockup with nothing in it to review. The frame instead shows a
Supplier · Staff user holding <code>Orders:View</code> only: <strong>no "New Order" button</strong>, and a sidebar
filtered to the single entry they may open. Pin <span class="pin">3</span> puts the two header states side by side
so the delta is one comparison, not two files.
</p>
<p>
Two open decisions came out of drawing this and are flagged rather than silently resolved:
<strong>row-level data scoping</strong> — should a Supplier user see <em>all</em> orders or only their own
(<span class="pin">5</span>) — and <strong>the supplier/project master lists leaking through the filter panel</strong>
to a user without <code>Supplier:View</code> (<span class="pin">6</span>). Neither blocks the frame; both change
what the frame means.
</p>
<div class="doc-meta">
Viewport <code>1440 × 900</code> (desktop only) ·
tokens from <code>tailwind.config.ts</code> + <code>app/globals.css</code> ·
shell from <code>app/layout.tsx</code> + <code>components/layout/Sidebar.tsx</code> ·
page transcribed from <code>app/orders/page.tsx</code> with
<code>PageHeader</code>/<code>SearchInput</code>/<code>FilterBar</code>/<code>DataTable</code>/<code>StatusBadge</code>/<code>Pagination</code> ·
data shape from <code>lib/queries/orders.ts</code>, paging defaults from <code>lib/params.ts</code>
· <code>fields["SCR-003"]</code> is <code>[]</code> — the list has no form of its own; its only inputs are the
shared search box and the <code>FilterBar</code> panel, both drawn from the real components (see
<span class="pin">6</span>)
</div>
</div>

<div class="frame-label">Artboard A — 1440 × 900 · <span>Supplier · Staff, permissions = { Orders:View }. Clean frame, no annotation overlay — diffable by W21_ui_verify.</span></div>

<div class="artboard">
<div class="shell">

<!-- components/layout/Sidebar.tsx rendered from the resolved permission set (SCR-027).
This user holds Orders:View only, so Expenses and Expense Summary are absent from the
OMS group and the whole Admin group is dropped (every item denied). Hide, not disable —
the default drawn in SCR-000 pin 5. -->
<aside class="sidebar">
<div class="sidebar-brand"><span>OMS</span></div>
<nav class="sidebar-nav">
<div class="nav-group">
<div class="nav-group-label">OMS</div>
<ul class="nav-list">
<li>
<a class="nav-item active" href="#">
<!-- lucide ClipboardList -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M12 11h4"/><path d="M12 16h4"/><path d="M8 11h.01"/><path d="M8 16h.01"/></svg>
Orders
</a>
</li>
</ul>
</div>
</nav>
<div class="sidebar-foot">
<button type="button" class="signout">
<!-- lucide LogOut -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/></svg>
Sign out
</button>
</div>
</aside>

<!-- main.flex-1.p-6 rendering app/orders/page.tsx -->
<main class="main">

<!-- PageHeader title="Orders" action={...}. Orders:Create is ABSENT, so `action` is not
passed at all — PageHeader already renders nothing when action is undefined, so this
needs no new markup and leaves no gap. This is the single visible delta on the page. -->
<div class="page-head">
<h1>Orders</h1>
</div>

<div class="toolbar">
<div class="search-wrap">
<!-- lucide Search -->
<svg viewBox="0 0 24 24" fill="none" stroke="#836f6f" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<input class="field-input" type="search" value="" />
<span class="placeholder">Search by order no...</span>
</div>
<span class="btn btn-secondary filters-btn">
<!-- lucide SlidersHorizontal -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 4H14"/><path d="M10 4H3"/><path d="M21 12H12"/><path d="M8 12H3"/><path d="M21 20H16"/><path d="M12 20H3"/><path d="M14 2v4"/><path d="M8 10v4"/><path d="M16 18v4"/></svg>
Filters
</span>
</div>

<div class="card table-card">
<table class="dt">
<thead>
<tr>
<th style="width:170px">
<span class="sort">Order No
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 16-4 4-4-4"/><path d="M17 20V4"/><path d="m3 8 4-4 4 4"/><path d="M7 4v16"/></svg>
</span>
</th>
<th style="width:130px">
<span class="sort">Date
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 16-4 4-4-4"/><path d="M17 20V4"/><path d="m3 8 4-4 4 4"/><path d="M7 4v16"/></svg>
</span>
</th>
<th>Supplier</th>
<th>Project</th>
<th style="width:190px">
<span class="sort">Status
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 16-4 4-4-4"/><path d="M17 20V4"/><path d="m3 8 4-4 4 4"/><path d="M7 4v16"/></svg>
</span>
</th>
<th style="width:140px">Due Date</th>
</tr>
</thead>
<tbody>
<tr><td><a class="link" href="#">OMS-2026-000047</a></td><td>26/08/2026</td><td>Meridian Steel Co.</td><td>Riverside Clinic Fit-out</td><td><span class="badge badge-warning">Pending</span></td><td>09/09/2026</td></tr>
<tr><td><a class="link" href="#">OMS-2026-000046</a></td><td>25/08/2026</td><td>Ardent Fasteners</td><td>Northgate Warehouse</td><td><span class="badge badge-info">Partially Fulfilled</span></td><td>05/09/2026</td></tr>
<tr><td><a class="link" href="#">OMS-2026-000045</a></td><td>24/08/2026</td><td>Northgate Timber</td><td>Depot Cold Storage</td><td><span class="badge badge-success">Fulfilled</span></td><td>01/09/2026</td></tr>
<tr><td><a class="link" href="#">OMS-2026-000044</a></td><td>21/08/2026</td><td>Meridian Steel Co.</td><td>Harbour Point Retrofit</td><td><span class="badge badge-warning">Pending</span></td><td></td></tr>
<tr><td><a class="link" href="#">OMS-2026-000043</a></td><td>20/08/2026</td><td>Coastline Electricals</td><td>Riverside Clinic Fit-out</td><td><span class="badge badge-neutral">Cancelled</span></td><td>03/09/2026</td></tr>
<tr><td><a class="link" href="#">OMS-2026-000042</a></td><td>19/08/2026</td><td>Ardent Fasteners</td><td>Depot Cold Storage</td><td><span class="badge badge-success">Fulfilled</span></td><td>28/08/2026</td></tr>
<tr><td><a class="link" href="#">OMS-2026-000041</a></td><td>18/08/2026</td><td>Northgate Timber</td><td>Northgate Warehouse</td><td><span class="badge badge-info">Partially Fulfilled</span></td><td>02/09/2026</td></tr>
<tr><td><a class="link" href="#">OMS-2026-000040</a></td><td>17/08/2026</td><td>Meridian Steel Co.</td><td>Riverside Clinic Fit-out</td><td><span class="badge badge-success">Fulfilled</span></td><td>27/08/2026</td></tr>
<tr><td><a class="link" href="#">OMS-2026-000039</a></td><td>14/08/2026</td><td>Coastline Electricals</td><td>Harbour Point Retrofit</td><td><span class="badge badge-warning">Pending</span></td><td></td></tr>
<tr><td><a class="link" href="#">OMS-2026-000038</a></td><td>13/08/2026</td><td>Ardent Fasteners</td><td>Depot Cold Storage</td><td><span class="badge badge-success">Fulfilled</span></td><td>25/08/2026</td></tr>
<tr><td><a class="link" href="#">OMS-2026-000037</a></td><td>12/08/2026</td><td>Northgate Timber</td><td>Northgate Warehouse</td><td><span class="badge badge-neutral">Cancelled</span></td><td>24/08/2026</td></tr>
<tr><td><a class="link" href="#">OMS-2026-000036</a></td><td>11/08/2026</td><td>Meridian Steel Co.</td><td>Riverside Clinic Fit-out</td><td><span class="badge badge-info">Partially Fulfilled</span></td><td>26/08/2026</td></tr>
</tbody>
</table>
</div>

<div class="pager">
<div>Showing 1–12 of 12</div>
<div class="right">
<label>Rows
<select class="field-input"><option>20</option></select>
</label>
<div style="display:flex; align-items:center; gap:4px;">
<span class="btn btn-secondary btn-sm btn-dim">Prev</span>
<span style="padding:0 8px;">Page 1 of 1</span>
<span class="btn btn-secondary btn-sm btn-dim">Next</span>
</div>
</div>
</div>
</main>

</div>
</div>

<!-- ======================= annotation sheet — reference only, never ships ======================= -->
<div class="sheet">

<section>
<h2><span class="pin">1</span>What Artboard A asserts</h2>
<p class="lede">
Five claims, each checkable against the frame above. If any one of them is wrong the frame is wrong — that is the
point of listing them rather than describing the screen in prose.
</p>
<table class="info">
<thead>
<tr>
<th style="width:210px">Element</th>
<th style="width:170px">Permission</th>
<th style="width:330px">Today</th>
<th>Drawn in Artboard A</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Sidebar "Orders" entry</strong></td>
<td><code>Orders:View</code></td>
<td><code>NAV_GROUPS</code> is a hardcoded literal in
<code>components/layout/Sidebar.tsx</code>; every user sees all seven entries.</td>
<td>Present and <em>active</em> (<code>bg-accent-subtle</code>, <code>font-medium</code>) — this user holds
the permission. Everything else is gone, including the entire Admin group label. SCR-027 owns that
behaviour; it is drawn here because a filtered page under an unfiltered menu would be incoherent.</td>
</tr>
<tr>
<td><strong>The route itself</strong></td>
<td><code>Orders:View</code></td>
<td><code>middleware.ts</code> only checks that a session cookie exists. Any authenticated user may
<code>GET /orders</code>.</td>
<td>Renders. The denied case has no pixels on this screen — it is SCR-028's outcome, drawn small in
<span class="pin">3</span>.</td>
</tr>
<tr>
<td><strong>"New Order" button</strong></td>
<td><code>Orders:Create</code></td>
<td>Always rendered:
<code>&lt;PageHeader title="Orders" action={&lt;Link href="/orders/new" className="btn-primary"&gt;} /&gt;</code>
(<code>app/orders/page.tsx:43-50</code>).</td>
<td><strong>Absent.</strong> The <code>action</code> prop is not passed, and
<code>PageHeader</code> already renders <code>null</code> in that case
(<code>PageHeader.tsx:9</code>) — so the title stays left-aligned, the <code>mb-6</code> gap is unchanged,
and no placeholder or disabled ghost is left behind.</td>
</tr>
<tr>
<td><strong>Order No cell → detail</strong></td>
<td><code>Orders:View</code></td>
<td><code>&lt;Link href={`/orders/${r.id}`} className="link"&gt;</code> — always a link.</td>
<td>Still a link. <strong>Drawn assumption:</strong> <code>Orders:View</code> covers both the list and the
per-record detail (SCR-005), so a user who can see the row can open it. See <span class="pin">7</span>.</td>
</tr>
<tr>
<td><strong>Rows returned</strong></td>
<td><em>(none)</em></td>
<td><code>listOrders()</code> builds its <code>where</code> from search/filter params only — no ownership or
tenancy predicate anywhere (<code>lib/queries/orders.ts:5-19</code>).</td>
<td><strong>Unscoped: all 12 orders, from four different suppliers.</strong> This is the frame's most
consequential assumption and the easiest to miss — <span class="pin">5</span>.</td>
</tr>
</tbody>
</table>
</section>

<section>
<h2><span class="pin">2</span>Where the gate lives — four layers, one screen</h2>
<p class="lede">
The acceptance criterion is "the user should not be able to access the functionality directly through a URL", so
hiding the button is the <em>least</em> important of these four. Layers 1 and 2 are the only ones drawn (they are
the only ones with pixels); 3 and 4 are listed because a mockup that showed only the hidden button would be read
as the whole change.
</p>
<ul class="ladder">
<li><b>1 · Menu (SCR-027)</b> — the Orders entry is rendered only if the resolved permission set contains
<code>Orders:View</code>. Cosmetic. Prevents nothing on its own.</li>
<li><b>2 · Route guard</b><code>app/orders/page.tsx</code> resolves the session's permission set and denies
without <code>Orders:View</code>, <em>before</em> <code>listOrders()</code> runs. This is what a typed URL hits.
The outcome of the denial is SCR-028's open decision — the default carried from SCR-002 is "redirect to the
first permitted menu entry".</li>
<li><b>3 · Create entry point</b> — the <code>action</code> prop here, <em>and</em> the same
<code>Orders:Create</code> check as a route guard on <code>app/orders/new/page.tsx</code> (SCR-004). Hiding a
link does not protect the page it points at; both are required, and only the second is security.</li>
<li><b>4 · Server action</b><code>createOrder()</code> in <code>lib/actions/orders.ts:18</code> currently calls
<code>requireSession()</code> and nothing more. A Server Action is a POST to the same route URL and is
invocable without ever loading the page, so it needs its own <code>Orders:Create</code> assertion. No pixels,
and the only layer an attacker actually has to get past.</li>
</ul>
<p class="anno-note">
<strong>One registry, not four lists.</strong> The menu entry, the route guard and the action check must read the
same permission constant. If the sidebar says a user may open <code>/orders</code> but the route guard demands a
differently-named permission, the user gets bounced off a menu item they can see — and under SCR-002's
redirect-to-first-permitted default that bounce can loop. SCR-002 <span class="pin">2</span> records the same
constraint from the other end.
</p>
</section>

<section>
<h2><span class="pin">3</span>The four states of this screen</h2>
<p class="lede">
Only one is a full artboard because only one differs from what ships today. The rest are drawn at component
scale — that is genuinely all there is to them.
</p>
<div class="states">

<div>
<div class="state-cap">PageHeader — with <code>Orders:Create</code> (Manufacturer · Admin)</div>
<div class="state-sub">
Identical to today. <strong>No behaviour change for the privileged case</strong>, which is what makes this
ticket low-risk to ship.
</div>
<div class="mini-frame">
<div class="page-head" style="margin:0;">
<h1>Orders</h1>
<div><span class="btn btn-primary">New Order</span></div>
</div>
</div>
</div>

<div>
<div class="state-cap">PageHeader — without <code>Orders:Create</code> <span class="chip chip-drawn">Artboard A</span></div>
<div class="state-sub">
Hidden, not disabled. A greyed-out button advertises a capability the user cannot have and invites a support
ticket; nothing is left in its place.
</div>
<div class="mini-frame anno-box">
<div class="page-head" style="margin:0;">
<h1>Orders</h1>
</div>
</div>
</div>

<div>
<div class="state-cap">Empty result — unchanged by permissions</div>
<div class="state-sub">
<code>DataTable</code>'s existing empty row, with the caller's
<code>emptyLabel="No orders found."</code> (<code>app/orders/page.tsx:78</code>). Reached today by a search or
filter that matches nothing — and, <em>if</em> row-scoping is adopted (<span class="pin">5</span>), by a
Supplier user with no orders at all.
</div>
<div class="mini-frame" style="padding:0; overflow:hidden;">
<table class="dt" style="background:var(--surface);">
<thead><tr><th>Order No</th><th>Date</th><th>Supplier</th><th>Status</th></tr></thead>
<tbody><tr><td class="empty" colspan="4">No orders found.</td></tr></tbody>
</table>
</div>
<p class="anno-note" style="max-width:400px;">
<strong>Same label for two different situations.</strong> "No orders found." reads as "your filter is too
narrow". If it also becomes the answer to "you have no orders", a Supplier user's first ever visit looks
like a broken filter. Only matters if <span class="pin">5</span> is overturned — flagged so it is not
discovered afterwards.
</p>
</div>

<div>
<div class="state-cap">Denied — no <code>Orders:View</code>, typed URL</div>
<div class="state-sub">
This screen renders nothing at all. Outcome owned by SCR-028; the default inherited from SCR-000/SCR-002 is
drawn.
</div>
<div class="mini-frame">
<div style="font-size:12px; line-height:20px; color:#3b3b3b;">
<div><code>GET /orders</code> (typed, or a stale bookmark)</div>
<div style="color:var(--text-muted)">↓ session valid — <code>middleware.ts</code> passes</div>
<div style="color:var(--text-muted)">↓ permission set lacks <code>Orders:View</code></div>
<div><code>307 → /expenses</code> <span style="color:var(--text-muted)">(first permitted menu entry)</span></div>
<div style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<span class="badge badge-neutral">never rendered</span>
<span style="margin-left:6px;">no order data is queried</span>
</div>
</div>
</div>
<p class="anno-note" style="max-width:400px;">
The guard must sit <em>above</em> <code>listOrders()</code>, not filter its result. Denying after the query
has run still puts every order row into the server's memory and into any log or trace on that path.
</p>
</div>

</div>
</section>

<section>
<h2><span class="pin">4</span>What is deliberately <em>not</em> gated</h2>
<p class="lede">
Recorded so a later reviewer does not read these as oversights. Each was considered and left alone.
</p>
<table class="info">
<thead>
<tr><th style="width:230px">Control</th><th style="width:120px">Decision</th><th>Reasoning</th></tr>
</thead>
<tbody>
<tr>
<td>Search box (<code>SearchInput</code>, <code>?q=</code>)</td>
<td><span class="chip chip-drawn">not gated</span></td>
<td>Searches <code>orderNo</code> within rows the user is already permitted to see. Gating it would remove a
usability affordance without removing any access.</td>
</tr>
<tr>
<td>Sortable headers (<code>?sort=</code>, <code>?dir=</code>)</td>
<td><span class="chip chip-drawn">not gated</span></td>
<td>Reordering a permitted result set discloses nothing new. <code>orderBy()</code> already allow-lists the
sortable fields (<code>lib/params.ts</code>), so an arbitrary <code>?sort=</code> cannot reach an
un-exposed column.</td>
</tr>
<tr>
<td>Pagination + rows-per-page</td>
<td><span class="chip chip-drawn">not gated</span></td>
<td>Same set, different window. <code>pageSize</code> is clamped to
<code>PAGE_SIZE_OPTIONS</code> so <code>?pageSize=100000</code> is already inert.</td>
</tr>
<tr>
<td>Row-level Edit / Delete controls</td>
<td><span class="chip chip-approx">nothing to gate</span></td>
<td><strong>The Orders list has no actions column and the app has no order edit or delete route at all</strong>
— there is no <code>OrderRowActions.tsx</code> and no <code>deleteOrder</code> action anywhere in
<code>lib/actions/</code>. An <code>Orders:Edit</code> or <code>Orders:Delete</code> permission would
therefore have <em>no surface to control</em> on this screen or any other. Flagged in
<span class="pin">7</span>: the permission registry should not grow entries that gate nothing.</td>
</tr>
<tr>
<td>Supplier / Project / Status / Date filters</td>
<td><span class="chip chip-approx">flagged</span></td>
<td>Not gated — but the <em>options</em> inside two of them are master data. See <span class="pin">6</span>.</td>
</tr>
</tbody>
</table>
</section>

<section>
<h2><span class="pin">5</span>Row-level scoping — the decision Artboard A silently makes <span class="chip chip-drawn">unscoped drawn</span></h2>
<p class="lede">
The requirement introduces <strong>Supplier</strong> and <strong>Manufacturer</strong> as user <em>types</em>, not
just roles. That naming strongly implies a Supplier user is <em>a supplier</em> — in which case a list showing
Meridian, Ardent, Northgate and Coastline orders side by side shows one supplier its competitors' order book.
Artboard A draws exactly that, and it is a choice, not an accident.
</p>
<div class="states">
<div style="max-width: 620px;">
<div class="state-cap">Drawn: no row-level scoping <span class="chip chip-drawn">drawn</span></div>
<ul style="font-size:13px; line-height:20px; color:#4b4b4b; margin:0 0 10px; padding-left:18px;">
<li><strong>No acceptance criterion asks for it.</strong> Every stated criterion is about menus, routes,
actions and direct-URL access — the vocabulary is capability, never ownership. Inventing record ownership
would be building beyond the requirement, on a security question, without being asked.</li>
<li><strong>The schema cannot express it.</strong> <code>prisma/schema.prisma</code> has no
<code>User</code> model at all (SCR-029), so there is nothing to hang a
<code>user.supplierId</code> off. Row-scoping is not a filter this ticket forgot; it is a data model this
ticket does not have.</li>
<li><strong>It is additive later.</strong> Adding a <code>where</code> predicate to
<code>listOrders()</code> changes no pixels and no components — only which rows appear. The reverse
(shipping a scoping rule and then relaxing it) is the migration that hurts.</li>
</ul>
<p class="anno-note">
<strong>The counter-argument, recorded honestly.</strong> If "Supplier user type" does mean an external
supplier login, then shipping this unscoped is a cross-tenant data disclosure that no amount of correct
permission gating catches — the user has <code>Orders:View</code> and is being shown orders, so every check
in <span class="pin">2</span> passes and the audit is clean. <strong>This is the single highest-severity
question on the screen and it is not visible in the pixels</strong>, which is precisely why it is written
here. N31 should answer "does a Supplier user see other suppliers' orders?" explicitly, in words, before
this is built.
</p>
</div>
<div>
<div class="state-cap">If overturned — what changes</div>
<div class="state-sub">Small in code, large in meaning. No component changes; the frame's row count does.</div>
<div class="mini-frame">
<div style="font-size:12px; line-height:20px; color:#3b3b3b;">
<div><b>Schema</b><code>User.supplierId</code> (nullable; null = Manufacturer)</div>
<div><b>Query</b><code>listOrders()</code> takes the session and adds
<code>supplierId</code> to its <code>where</code></div>
<div><b>Detail</b> — SCR-005 needs the same predicate, or
<code>/orders/&lt;id&gt;</code> re-opens the hole one record at a time</div>
<div><b>Filter</b> — the Supplier dropdown collapses to one option, or disappears</div>
<div style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border); color:var(--anno);">
Artboard A would show 3 rows, all "Meridian Steel Co."
</div>
</div>
</div>
</div>
</div>
</section>

<section>
<h2><span class="pin">6</span>The filter panel leaks master data — and gating it would be theatre</h2>
<p class="lede">
<code>app/orders/page.tsx:18-22</code> calls <code>activeSuppliers()</code> and
<code>activeProjects()</code> to populate the filter dropdowns. Every supplier name and every project name is
therefore delivered to anyone who can open the Orders list — including a user with no
<code>Supplier:View</code> and no <code>Project:View</code>, for whom the entire Admin group is hidden from the
menu. The panel is drawn here in its open state because it is the only part of this screen a checklist sweep
would miss.
</p>
<div class="states">
<div>
<div class="state-cap">FilterBar panel, open — <code>w-80 card p-4 shadow-lg</code></div>
<div class="state-sub">
Drawn from <code>components/ui/FilterBar.tsx</code>. Closed by default (it auto-opens only when a filter is
already active), which is why Artboard A shows the button, not the panel.
</div>
<div class="mini-frame" style="width:320px; background:var(--surface); border-color:var(--border);">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:12px;">
<span style="font-size:14px; font-weight:500; color:var(--text);">Filters</span>
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="#836f6f" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</div>
<div style="margin-bottom:12px;">
<span class="field-label">Supplier</span>
<span class="field-control anno-box" style="color:var(--text-muted);">All</span>
</div>
<div style="margin-bottom:12px;">
<span class="field-label">Project</span>
<span class="field-control anno-box" style="color:var(--text-muted);">All</span>
</div>
<div style="margin-bottom:12px;">
<span class="field-label">Status</span>
<span class="field-control" style="color:var(--text-muted);">All</span>
</div>
<div>
<span class="field-label">Order Date</span>
<span class="field-control" style="color:var(--text-muted);">dd/mm/yyyy</span>
</div>
</div>
</div>
<div style="max-width: 620px;">
<div class="state-cap">Drawn: do not gate the dropdowns <span class="chip chip-drawn">drawn</span></div>
<ul style="font-size:13px; line-height:20px; color:#4b4b4b; margin:0 0 10px; padding-left:18px;">
<li><strong>The table already shows the same names.</strong> Artboard A's Supplier and Project columns print
"Meridian Steel Co." and "Riverside Clinic Fit-out" in plain text. Hiding a dropdown that lists the names
visible three columns to the left protects nothing and costs the screen its two most useful filters.</li>
<li><strong>Master <em>data</em> is not the master <em>screen</em>.</strong> <code>Supplier:View</code> gates
<code>/admin/suppliers</code> — the place you manage suppliers, with contact details, supplier numbers and
an edit path. A name rendered inside an order is order data.</li>
<li><strong>The real containment is <span class="pin">5</span>.</strong> If a Supplier user should not know
who the other suppliers are, the fix is scoping the rows — at which point the dropdown narrows to match on
its own. Gating the dropdown while leaving the column is the worst of both: less usable, equally leaky.</li>
</ul>
<p class="anno-note">
<strong>What would change the answer.</strong> This reasoning holds only because the leak is limited to
<em>names of records already on screen</em>. If the filter ever lists suppliers with no orders — i.e. names
that appear nowhere in the table — it becomes a genuine enumeration of the supplier master, and
<code>activeSuppliers()</code> must be narrowed to suppliers present in the permitted result set. Worth
checking against the real data before build: <code>activeSuppliers()</code> selects on
<code>isActive</code>, <strong>not</strong> on "has orders", so today it very likely does.
</p>
</div>
</div>
</section>

<section>
<h2><span class="pin">7</span>Approximations — flagged, not silently drawn</h2>
<p class="lede">
Per this node's instruction: anything drawn without a real control behind it is called out here. An unflagged
approximation is what gets built.
</p>
<table class="info">
<thead>
<tr><th style="width:250px">Item</th><th style="width:120px">Kind</th><th>Detail</th></tr>
</thead>
<tbody>
<tr>
<td>Unscoped rows in Artboard A</td>
<td><span class="chip chip-approx">assumption</span></td>
<td>Twelve orders across four suppliers, shown to a Supplier · Staff user. The drawn default from
<span class="pin">5</span>. If overturned, this frame is re-cut with three rows and a single supplier
name.</td>
</tr>
<tr>
<td>Order No stays a link</td>
<td><span class="chip chip-approx">assumption</span></td>
<td>Assumes <code>Orders:View</code> covers list <em>and</em> detail. If SCR-005 splits them, the cell becomes
conditional — plain text when detail is denied, which is a visual change (the blue underline disappears from
every row) and needs its own frame.</td>
</tr>
<tr>
<td>Filter panel drawn closed in Artboard A</td>
<td><span class="chip chip-drawn">accurate</span></td>
<td>Not an omission. <code>FilterBar</code> initialises <code>open</code> to
<code>activeCount &gt; 0</code>, and the frame draws an unfiltered first load. The open state is
<span class="pin">6</span>.</td>
</tr>
<tr>
<td><code>Listbox</code> / <code>DatePicker</code> controls</td>
<td><span class="chip chip-approx">approximation</span></td>
<td>Drawn as static <code>.field-control</code> boxes at their real geometry (1px border, 8×12px padding,
<code>min-height: 2.625rem</code>) showing their real placeholders ("All" —
<code>placeholderSelectable</code>; <code>dd/mm/yyyy</code>). They are custom popover components, not native
selects; their open/expanded states are outside this screen's scope.</td>
</tr>
<tr>
<td>Order numbers, supplier and project names, dates, statuses</td>
<td><span class="chip chip-approx">sample data</span></td>
<td>Invented content at realistic shape. <code>OMS-2026-000047</code> follows
<code>nextOrderNo()</code>'s real <code>OMS-YYYY-######</code> format
(<code>lib/queries/orders.ts:47-52</code>); dates use
<code>toLocaleDateString()</code>; the <code></code> in Due Date is the literal em-dash fallback from
<code>app/orders/page.tsx:30</code>. All four order statuses are exercised so every
<code>StatusBadge</code> tone on this screen is visible at once.</td>
</tr>
<tr>
<td>Twelve rows, "Showing 1–12 of 12"</td>
<td><span class="chip chip-approx">sample data</span></td>
<td>Row count chosen to fill 900px without clipping the pager. Real default
<code>pageSize</code> is 20 (<code>lib/params.ts:7</code>), so a full page would overflow the viewport and
scroll — as it does in the shipped app. Prev/Next are drawn at <code>opacity-40</code>, the real
single-page state.</td>
</tr>
<tr>
<td>Icon glyphs</td>
<td><span class="chip chip-approx">approximation</span></td>
<td>Inline SVG traces of the <code>lucide-react</code> icons the app imports (ClipboardList, LogOut, Search,
SlidersHorizontal, ArrowUpDown, X) at their real <code>h-4 w-4</code> / <code>h-3.5 w-3.5</code> sizes.
Same glyphs, hand-transcribed paths — sub-pixel differences from the shipped icons are expected.</td>
</tr>
<tr>
<td>Sidebar shown filtered</td>
<td><span class="chip chip-approx">borrowed</span></td>
<td>SCR-027's design, not this screen's. Drawn because a page gated on <code>Orders:View</code> under a menu
offering six routes this user cannot open would misrepresent the change. If SCR-027's hide-vs-disable
default flips, this frame is re-cut with it.</td>
</tr>
<tr>
<td>Hover / focus states</td>
<td><span class="chip chip-drawn">not drawn</span></td>
<td>The real components carry <code>hover:bg-accent-subtle/60</code> on rows,
<code>hover:text-text</code> on sort links and <code>focus:ring-accent</code> on inputs. A static artboard
has no pointer; unchanged by this ticket and deliberately omitted.</td>
</tr>
</tbody>
</table>
</section>

<section>
<h2><span class="pin">8</span>Open decisions carried into N31 — and the default this mockup drew</h2>
<p class="lede">
None of these blocked the mockup. A defensible default was drawn for each and is recorded here so the reviewer
overturns it explicitly rather than discovering it after it ships. The inventory marks SCR-003
<code>ambiguous: false</code> — the first two below are why that is optimistic.
</p>
<ul class="decisions">
<li>
<span class="flag">Does a Supplier user see other suppliers' orders?</span>
<strong>Drawn: yes — no row-level scoping (<span class="pin">5</span>).</strong> Highest-severity item on the
screen, invisible in the pixels, and passes every permission check even if wrong. Needs an explicit yes/no,
not silence. Overturning it adds a <code>User.supplierId</code> and a <code>where</code> predicate to
<code>listOrders()</code> <em>and</em> to SCR-005's <code>getOrder()</code>.
</li>
<li>
<span class="flag">Do the filter dropdowns leak the supplier/project masters?</span>
<strong>Drawn: not gated (<span class="pin">6</span>)</strong> — the same names already print in the table's
own columns, so hiding the dropdown costs usability and buys nothing. Revisit if
<code>activeSuppliers()</code> can return suppliers that appear in no visible row, which on today's query it
can.
</li>
<li>
<span class="flag">Is detail a separate permission from list?</span>
<strong>Drawn: no — <code>Orders:View</code> covers both</strong>, so the Order No cell stays a link. Coupled
to SCR-005's own Approve-vs-Fulfil ambiguity; if that screen gains a view-level permission, this cell becomes
conditional and every row loses its blue underline.
</li>
<li>
<span class="flag">Denial outcome for a typed <code>/orders</code> URL.</span>
<strong>Drawn: redirect to first permitted menu entry</strong>, inherited from SCR-002 and SCR-000 rather than
re-decided here. Owned by SCR-028 — this screen only needs the answer to be consistent with the other two
mockups.
</li>
<li>
<span class="flag">Hide vs disable.</span> <strong>Drawn: hide</strong>, matching SCR-000
<span class="pin">5</span>. If N31 prefers disable-with-tooltip, this artboard's PageHeader is re-cut and so is
every list screen's.
</li>
<li>
<span class="flag"><code>Orders:Edit</code> / <code>Orders:Delete</code> have no surface.</span>
<strong>Not drawn, flagged (<span class="pin">4</span>).</strong> The app has no order edit route and no order
delete action; the only post-create mutation is SCR-005's fulfilment form. Defining permissions that gate
nothing makes the registry a list of promises the UI does not keep — the same "data/configuration driven"
registry is what the requirement asks to be extensible later, so an unused entry is cheap to add then and
misleading to add now.
</li>
<li>
<span class="flag">SCR-029 dependency.</span> <code>prisma/schema.prisma</code> has no <code>User</code> model
and <code>lib/auth.ts</code> is a single shared credential (<code>AUTH_USERNAME</code>/<code>AUTH_PASSWORD</code>),
so the Supplier · Staff user in Artboard A cannot exist yet. Not a design question — but it is why this frame
cannot be verified end-to-end until the operator rules on how users get created.
</li>
</ul>
</section>

<section>
<h2><span class="pin">9</span>Tokens used — no new colour, radius or type scale is introduced</h2>
<div class="swatches">
<div class="sw"><i style="background:#fcfcfd"></i>bg #fcfcfd</div>
<div class="sw"><i style="background:#ffffff"></i>surface #ffffff</div>
<div class="sw"><i style="background:#302828"></i>text #302828</div>
<div class="sw"><i style="background:#836f6f"></i>text-muted #836f6f</div>
<div class="sw"><i style="background:#ede0ce"></i>border #ede0ce</div>
<div class="sw"><i style="background:#453028"></i>accent #453028</div>
<div class="sw"><i style="background:#f7f1e9"></i>accent-subtle<br/>#f7f1e9</div>
<div class="sw"><i style="background:#1f8a4c"></i>success #1f8a4c</div>
<div class="sw"><i style="background:#b8860b"></i>warning #b8860b</div>
<div class="sw"><i style="background:#c0392b"></i>danger #c0392b</div>
<div class="sw"><i style="background:#6b7280"></i>neutral #6b7280</div>
<div class="sw"><i style="background:#2563eb"></i>info #2563eb</div>
<div class="sw"><i style="background:#7c3aed"></i>annotation<br/>(NOT a token)</div>
</div>
<p class="anno-note">
Classes reproduced verbatim: <code>.card</code> (surface + <code>--border</code> + <code>rounded-lg</code> 8px),
<code>.btn-primary</code> (<code>--accent</code>, white text, <code>px-4 py-2</code>, <code>rounded-md</code> 6px,
14px/500), <code>.btn-secondary</code> (surface + 1px <code>--border</code>), <code>.field-input</code>
(<code>@tailwindcss/forms</code> base geometry: 1px border, 8px×12px padding, 14px),
<code>.field-control</code> (the same geometry restored for non-form elements, <code>min-height: 2.625rem</code>),
<code>.field-label</code>, <code>.link</code> (<code>--status-info</code> + persistent underline — the fix from
the "orders can't be clicked" report, and the reason every Order No in Artboard A is blue rather than brown).
Layout: sidebar <code>w-64</code> = 256px with <code>px-5 py-4</code> brand and <code>px-3</code>/<code>mb-5</code>
nav groups; <code>main.flex-1.p-6</code> = 24px gutter; <code>PageHeader</code> <code>mb-6</code> = 24px;
toolbar <code>mb-4</code> = 16px; <code>DataTable</code> cells <code>px-4 py-3</code> with an empty row at
<code>px-4 py-8</code>; <code>Pagination</code> <code>mt-4</code>; <code>FilterBar</code> panel
<code>w-80 p-4</code>. <code>StatusBadge</code> tones are the token colour at <code>/10</code> alpha on a
<code>rounded-full px-2 py-0.5 text-xs</code> pill, exactly as
<code>components/ui/StatusBadge.tsx</code> defines them.
</p>
<p class="anno-note">
All five status tones are exercised on this screen except <code>danger</code> (no <code>REJECTED</code> value
exists on <code>OrderStatus</code> — it belongs to <code>ExpenseStatus</code>); it is shown in the swatch row
only. This screen introduces <strong>zero new components, zero new strings and zero new tokens</strong> — the
entire visible change is one prop that stops being passed.
</p>
</section>

</div>

</body>
</html>
(4-4/5)