No SKUs yet.
Every event lands on a SKU. The registry syncs from Shopify products.
Five canonical states, three i3 extensions, 18 catalog tiles · every one of the 72 pages copies from here rather than inventing · zero emoji, zero apologies
Empty copy is the voice of the product. Apologetic copy makes a correct system feel broken.
An inventory system tells people things they do not want to hear: your count is wrong, this unit is missing, this page is not trustworthy yet. It has to do that without either panicking or apologising. Direct, present tense, one action.
Five are mandatory. Three fire only where the condition can occur.
| State | Class | Fires when | Must contain | Must never | Where it is required |
|---|---|---|---|---|---|
| default | canonical | the page has data and the user may see it | the page | a placeholder that looks like data | every page |
| loading | canonical | a request is in flight beyond 100ms | skeletons at the exact content height, aria-busy="true" | a spinner, a layout shift, a flash under 100ms | every page that fetches |
| empty | canonical | the query is valid and the answer is genuinely zero | a headline naming the noun, one sub-line, one CTA | an apology, an exclamation mark, an emoji | every list page |
| error | canonical | the request failed | plain cause, stable error id, "Nothing was changed" | a stack trace, a raw HTTP code alone, a blank page | every page that fetches |
| permission | canonical | the caller lacks the role, feature or app grant | the required role, the held role, one request action | a 500, a silently empty page, "contact your administrator" | every page |
| zero-filter | extension | filters hid every row, but rows exist | the active filters as chips, a one-click clear | the word "empty" · this is not empty | every page with filters |
| maintenance | extension | a planned window is running and writes are paused | the window, the job name, what still works | hiding the data that is still valid | every page with writes |
| gate-blocked | i3 only | the feature exists but its reconcile gate has not held 7 days | the gate, its metric, threshold, day count, shadow data | a blank page · that reads as a bug | every page behind an unmet gate |
Copy these verbatim. Deviating is a voice-drift defect, not a preference.
Every event lands on a SKU. The registry syncs from Shopify products.
Until a manager signs it, every number for this location is meaningless.
Either it is genuinely quiet, or an ingest path is silent. Check the sources.
Every location balances. No open cases across 7 locations.
Moving stock between locations posts two rows that net to zero.
Policy is every 30 days per location. HQ_319 was counted 8 days ago.
91% of HQ_319 is tagged. This location is not in the campaign yet.
Nothing unread. 6 rules are muted, so quiet is not the same as healthy.
Try a SKU prefix, an order number, an AWB, an EPC or a case id.
3 filters are hiding all 1,247 rows.
The read timed out after 8 seconds. Nothing was changed.
Muting a rule needs ops_manager. You hold manager on app i3.
The nightly hash-chain verify is running. Reads are accurate to 21:00.
Serial counts are shadow until unit count equals the ledger for 7 days. Day 3.
You sent 240 requests in a minute. The cap is 120. Resumes at 14:44.
72 target pages · how many own each state versus inherit it from this catalog
Every state block follows this grid. Deviate and it stops feeling calm.
One sub-line of why, or what to do next.
icon 40 · stroke 1.5 15px / 500 / 20lh 12.5px muted / 17lh btn-sm 28pxWhich page owns which voice · green cells own their copy, muted cells inherit this catalog, dash means the condition cannot occur there
| Page | Module | default | loading | empty | error | permission | zero-filter | maintenance | gate-blocked |
|---|---|---|---|---|---|---|---|---|---|
| dashboard | 01 | own | own | 03 | 14 | 15 | 12 | 16 | 17 |
| notifications | 01 | own | own | 10 | own | 15 | 12 | 16 | - |
| help | 01 | own | gen | 11 | 14 | 15 | 12 | - | - |
| login | 01 | own | own | - | own | own | - | 16 | - |
| command-palette | 01 | own | own | 11 | own | 15 | 12 | - | - |
| tour | 01 | own | own | own | own | own | - | - | - |
| inventory-live | 02 | own | own | 01 | 14 | 15 | 12 | 16 | 17 |
| inventory-classic | 02 | own | gen | 01 | 14 | 15 | 12 | 16 | - |
| sku-detail | 03 | own | own | own | 14 | 15 | - | 16 | 17 |
| locations | 04 | own | gen | own | 14 | 15 | 12 | - | - |
| cartons | 04 | own | gen | own | 14 | 15 | 12 | 16 | 17 |
| inflows | 05 | own | own | 07 | 14 | 15 | 12 | 16 | 17 |
| production | 05 | own | gen | own | 14 | 15 | 12 | - | 17 |
| grn | 06 | own | own | own | own | 15 | 12 | 16 | 17 |
| rts | 06 | own | gen | own | 14 | 15 | 12 | 16 | - |
| transfers | 07 | own | own | 06 | 14 | 15 | 12 | 16 | 17 |
| returns | 08 | own | own | own | 14 | 15 | 12 | 16 | 17 |
| damages | 09 | own | gen | 05 | 14 | 15 | 12 | 16 | 17 |
| stock-count | 10 | own | own | 08 | 14 | own | 12 | 16 | - |
| opening-stock | 10 | own | own | 02 | own | own | - | 16 | 17 |
| influencer | 11 | own | gen | own | 14 | 15 | 12 | 16 | 17 |
| rfid | 12 | own | own | 09 | own | 15 | 12 | 16 | 17 |
| rfid-recon | 12 | own | own | 04 | 14 | 15 | 12 | 16 | 17 |
| drift | 13 | own | own | 04 | 14 | 15 | 12 | 16 | - |
| classifier | 14 | own | own | own | 14 | own | 12 | 16 | - |
| audit-log | 14 | own | own | 03 | 14 | own | 12 | 16 | - |
| reports | 15 | own | own | own | 14 | own | 12 | 16 | 17 |
| settings | 15 | own | gen | - | 14 | own | - | 16 | - |
Approved strings for toasts, banners, filters and CTAs
Copy into any page, swap the icon, headline, sub and CTA
<div data-state-when="empty" class="state-block" role="status">
<div class="ic" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5">
<path d="M21 16V8l-9-5-9 5v8l9 5 9-5Z"/>
<path d="M3.3 7 12 12l8.7-5"/>
<path d="M12 22V12"/>
</svg>
</div>
<h3>No <span class="noun">transfers</span> in flight.</h3>
<p>Moving stock posts two rows that net to zero.</p>
<div class="actions">
<a class="btn btn-primary btn-sm"
href="transfers.html">Start a transfer</a>
</div>
</div>