State variants

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

One switching mechanism, one vocabulary. States are selected by document.body.dataset.state and blocks are marked data-state-when="<name>". The v2 dashboard shipped two competing mechanisms that spelled the same state zerofilter and zero-filter, so one of them never fired. Why this matters →

The rules

Empty copy is the voice of the product. Apologetic copy makes a correct system feel broken.

binding

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.

Do not
Oh no, there is nothing here yet!
Do
No transfers yet.
Do not
Sorry, we could not find any matches. Try again?
Do
No matches. Clear 3 filters to see all.
Do not
Something went wrong. Please contact your administrator.
Do
Couldn't load drift. Nothing was changed. Error id drf-7a2f91.
Do not
This feature is coming soon!
Do
GATE-6 has not passed. Numbers here are shadow, day 3 of 7.
  • Direct, present tense
  • Headline at most 60 characters
  • Exactly one primary action
  • Icon 40px, outline, muted
  • Name the next action, and who does it
  • State whether anything changed
  • No apology words
  • No exclamation marks
  • No "Oops" or "Uh oh"
  • No emoji, anywhere in the chrome
  • No "contact your administrator" without naming them
  • No spinner where a skeleton fits

The eight states

Five are mandatory. Three fire only where the condition can occur.

closed vocabulary
StateClassFires whenMust containMust neverWhere it is required
defaultcanonicalthe page has data and the user may see itthe pagea placeholder that looks like dataevery page
loadingcanonicala request is in flight beyond 100msskeletons at the exact content height, aria-busy="true"a spinner, a layout shift, a flash under 100msevery page that fetches
emptycanonicalthe query is valid and the answer is genuinely zeroa headline naming the noun, one sub-line, one CTAan apology, an exclamation mark, an emojievery list page
errorcanonicalthe request failedplain cause, stable error id, "Nothing was changed"a stack trace, a raw HTTP code alone, a blank pageevery page that fetches
permissioncanonicalthe caller lacks the role, feature or app grantthe required role, the held role, one request actiona 500, a silently empty page, "contact your administrator"every page
zero-filterextensionfilters hid every row, but rows existthe active filters as chips, a one-click clearthe word "empty" · this is not emptyevery page with filters
maintenanceextensiona planned window is running and writes are pausedthe window, the job name, what still workshiding the data that is still validevery page with writes
gate-blockedi3 onlythe feature exists but its reconcile gate has not held 7 daysthe gate, its metric, threshold, day count, shadow dataa blank page · that reads as a bugevery page behind an unmet gate
Any other value of body.dataset.state renders default and logs a client warning. The vocabulary is closed on purpose: an open one drifts into 14 spellings across 15 modules.

Catalog · 18 tiles

Copy these verbatim. Deviating is a voice-drift defect, not a preference.

10 empty3 filter and search5 system
01 No SKUs/inventory-live

No SKUs yet.

Every event lands on a SKU. The registry syncs from Shopify products.

14 / 60
Run the registry sync
First-run only · disappears after the first Shopify product sync
02 No opening balance/opening-stock

No opening balance signed.

Until a manager signs it, every number for this location is meaningless.

28 / 60
Load and sign
The i2 failure state. Blocks GATE-0 for that location.
03 No ledger rows/audit-log

No ledger writes in 24h.

Either it is genuinely quiet, or an ingest path is silent. Check the sources.

25 / 60
Check sibling health
Silence is ambiguous, so the copy names both readings
04 No drift/drift

Nothing to reconcile.

Every location balances. No open cases across 7 locations.

22 / 60
See resolved cases
Feel-good state · green icon, secondary CTA, never a primary
05 No damages/damages

No damages open.

New reports arrive from ops2, CSD, IWMS QC and RFID.

17 / 60
See closed damages
Names the four intake sources so silence is checkable
06 No transfers/transfers

No transfers in flight.

Moving stock between locations posts two rows that net to zero.

23 / 60
Start a transfer
The sub-line teaches the invariant while the queue is empty
07 No POs/inflows

No open purchase orders.

A PO carries stock through production, transit and GRN.

25 / 60
Raise a PO
Standard list empty · one primary verb-first CTA
08 No count scheduled/stock-count

No cycle count scheduled.

Policy is every 30 days per location. HQ_319 was counted 8 days ago.

26 / 60
Schedule a count
Empty plus context · the number makes the CTA judgeable
09 No serials/rfid

No serials tagged here.

91% of HQ_319 is tagged. This location is not in the campaign yet.

24 / 60
Campaign plan
Not an error · the mass-tagging campaign is phased by location
10 No notifications/notifications

All caught up.

Nothing unread. 6 rules are muted, so quiet is not the same as healthy.

14 / 60
Review muted rules
The only feel-good state that also warns · mutes are disclosed
11 No search matches/search?q=

No matches for "MST-CRM-99".

Try a SKU prefix, an order number, an AWB, an EPC or a case id.

27 / 60
See search grammar
Quotes the query back · teaches the grammar in the sub-line
12 Zero-filterany list

No matches.

3 filters are hiding all 1,247 rows.

location: MCCbelow buffertype: DISPLAY_GOOD
Clear filters
Never says "empty" · the rows exist, the filters hid them
13 Loadingany page
Exact content heights · 36px table row, 28px dense row, no spinner
14 Errorany page

Couldn't load the ledger.

The read timed out after 8 seconds. Nothing was changed.

Error id lgr-7a2f91 · 14:32:01 GST
Try again
Stable id, plain cause, and the assurance that nothing mutated
15 Permissionany page

You can read this, not change it.

Muting a rule needs ops_manager. You hold manager on app i3.

31 / 60
Request access
Names both roles and the app code · one action, never a 500
16 Maintenancewrite pages

Read-only for 25 minutes.

The nightly hash-chain verify is running. Reads are accurate to 21:00.

21:00 to 21:25 GST · job chain-verify-0729
Job status
States what still works · hiding valid reads is worse than a banner
17 Gate-blockedi3 only

GATE-6 has not passed.

Serial counts are shadow until unit count equals the ledger for 7 days. Day 3.

gate catch 97.2% · threshold 99%
See shadow data
Never blank · shows what it has, marked as not yet authoritative
18 Rate limited/api, /dev-console

Slowed to protect the ledger.

You sent 240 requests in a minute. The cap is 120. Resumes at 14:44.

30 / 60
Export instead
Names the number, the cap and the resume time · offers the right tool

State coverage across the wave

72 target pages · how many own each state versus inherit it from this catalog

576 states mapped
default · 72 pages own it · 100% loading · 58 own a page-specific skeleton loading · 14 inherit the generic skeleton empty · 62 own copy specific to their noun empty · 10 inherit tile 01 error · 41 own a page-specific cause line error · 31 inherit tile 14 permission · 24 name a page-specific role permission · 48 inherit tile 15 zero-filter · 34 filtered pages own it zero-filter · 12 inherit tile 12 zero-filter · 26 pages have no filters, not applicable maintenance · 15 write pages own it maintenance · 9 inherit tile 16 maintenance · 48 read-only pages, not applicable gate-blocked · 18 pages sit behind an unmet gate today gate-blocked · 54 pages are behind a passed gate or no gate
default
72
loading
58+14
empty
62+10
error
41+31
permission
24+48
zero-filter
34+12
maintenance
15+9
gate-blocked
18
defaultpage owns its copyinherits this cataloggate-blocked todaynot applicable

Anatomy of one tile

Every state block follows this grid. Deviate and it stops feeling calm.

canonical

Headline sentence.

One sub-line of why, or what to do next.

icon 40 · stroke 1.5 15px / 500 / 20lh 12.5px muted / 17lh btn-sm 28px
Measurements
Icon
40 x 40 · 1.5
Headline
15 / 500 / 20
Sub
12.5 / muted / 17
CTA
btn-sm 28px
Gap
4px between lines
Min-H
200px
Text max-W
240px
Voice check
  • 60 chars maximum
  • No apology, no "Oops"
  • One CTA, verb first
  • No emoji, ever
  • Outline icon, never filled
  • Say if anything changed

Coverage matrix

Which page owns which voice · green cells own their copy, muted cells inherit this catalog, dash means the condition cannot occur there

20 of 72 pages shown
PageModuledefaultloadingemptyerrorpermissionzero-filtermaintenancegate-blocked
dashboard01ownown031415121617
notifications01ownown10own151216-
help01owngen11141512--
login01ownown-ownown-16-
command-palette01ownown11own1512--
tour01ownownownownown---
inventory-live02ownown011415121617
inventory-classic02owngen0114151216-
sku-detail03ownownown1415-1617
locations04owngenown141512--
cartons04owngenown1415121617
inflows05ownown071415121617
production05owngenown141512-17
grn06ownownownown15121617
rts06owngenown14151216-
transfers07ownown061415121617
returns08ownownown1415121617
damages09owngen051415121617
stock-count10ownown0814own1216-
opening-stock10ownown02ownown-1617
influencer11owngenown1415121617
rfid12ownown09own15121617
rfid-recon12ownown041415121617
drift13ownown0414151216-
classifier14ownownown14own1216-
audit-log14ownown0314own1216-
reports15ownownown14own121617
settings15owngen-14own-16-
"gen" is the generic skeleton template. A page that inherits every state is not lazy, it is consistent. A page that owns a state must justify why its noun needs different words.

Micro-copy library

Approved strings for toasts, banners, filters and CTAs

copy verbatim
Toasts
good"Saved to draft. Undo (8s)."
good"12 rows posted. View the ledger."
good"Rule muted until 03-08. 2 people notified."
bad"Success! Your data has been saved."
Banners and errors
good"Shopify is slow. Cross-check retries at 14:32."
good"Couldn't load drift. Error drf-7a2f91."
good"GATE-1 day 6 of 7. Divergence 0.04%."
bad"Oops! Please try again later."
Filters and search
good"No matches. Clear 3 filters to see all 1,247."
good"Try a SKU prefix, an AWB, an EPC or a case id."
bad"No results found. Please try again!"
CTA verbs
good"Load and sign" · "Raise a PO" · "Start a transfer"
good"Schedule a count" · "Post a correction" · "Acknowledge"
bad"Click here" · "Submit" · "OK"
Never say "adjust the balance". i3 has no such action. Say "post a correction".

Drop-in markup

Copy into any page, swap the icon, headline, sub and CTA

no new CSS needed
<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>
Switching is one line: document.body.dataset.state = 'empty'. The .state-bar buttons are the only writers. Blocks hide by CSS, never by setting hidden from JavaScript, so a page cannot end up with two mechanisms disagreeing.
CI enforces four things: exactly one state mechanism per page, exactly one shortcut dialog per page, every internal href resolves, and no em dash or en dash anywhere.
Reference implementation lives in shared/i3.js · owned by the lead agent
Connected to Dashboard Help Notifications Login and access Command palette Guided tour Live inventory Opening stock Drift 3-way reconcile Transfers GRN Stock count Settings