The shop app looks like a product now, not a prototype

The window a shop assistant stares at all day was the weakest surface in
this system, and it looked improvised because it was: navigation drawn
with text characters (◉ ☺ ▢) that sit on the text baseline and cannot
take a stroke weight, margins set inline per screen, and four large stat
boxes dominating the page while the product's entire reason for existing
- WHO JUST WALKED IN - was a list of "person.seen" rows in the corner.

Rebuilt around the person in front of it: a counter, a cheap monitor,
somebody mid-conversation with a customer.

  - ui/icons.jsx: one drawn icon set, 24-unit grid, 1.6 stroke,
    currentColor, so one icon works on every surface and in every state.
  - styles.css: a real system. Four-step ground→raised palette biased
    blue-green (this product lives in the world of lenses), one spacing
    scale, one type scale, tabular figures wherever digits are compared
    or refreshed in place, and the scrollbars restyled - the default
    light scrollbar on a dark panel is the loudest "web page in a frame"
    tell there is.
  - Live: a status strip that answers "is this working" in one line,
    cameras as pictures with the caption over the image, and arrivals as
    cards big enough to match against the person standing there. The
    four stat boxes became a slim strip at the foot, where numbers that
    nobody acts on belong.
  - State is carried by shape AND colour everywhere - a pill, a dot and
    an edge stripe - because this gets read from two metres away and
    some operators do not see red and green apart.
  - Motion only where it means something: a live camera pulses, a fresh
    arrival slides in once. Nothing loops for decoration; this process
    shares a CPU with recognition.

Two things fixed because the screen showed them, not because a test did:

  - The sidebar read "Stopped" beside a live camera feed and a counter
    ticking up, whenever the engine was running but not started BY the
    app. That is the two-surfaces-disagreeing bug the tray exists to
    avoid. It now reads "Running outside the app" in amber, and Start is
    disabled rather than offering to launch a second engine onto one
    SQLite WAL.
  - The arrivals panel shrank to fit its content and left a hole beside
    a tall camera tile - so the layout looked broken exactly when the
    shop was quiet, which is most of the time. Both panels stretch and
    scroll their own content now.

Every existing class name still resolves, so the screens not rewritten
here pick the system up unchanged. Windows and darwin build; tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
2026-09-15 11:06:57 +05:30
parent a1fe0942e2
commit 3d3775c8be
11 changed files with 751 additions and 382 deletions

View File

@@ -0,0 +1,71 @@
// One icon set, drawn rather than typed.
//
// The navigation used to be text characters — ◉ ☺ ▢ — which render in whatever
// the system decides, sit on the text baseline instead of optical centre, and
// cannot take a stroke weight. On a shop PC that is the difference between
// software somebody trusts with their customers and something that looks
// improvised.
//
// All of these are 24-unit grid, 1.6 stroke, currentColor, no fill. That means
// one icon works on every surface and in every state without a second copy.
const base = {
width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none',
stroke: 'currentColor', strokeWidth: 1.6,
strokeLinecap: 'round', strokeLinejoin: 'round',
'aria-hidden': 'true', focusable: 'false',
}
function Svg({ size, children, ...rest }) {
return <svg {...base} {...rest} width={size ?? base.width} height={size ?? base.height}>{children}</svg>
}
export const Live = p => (
<Svg {...p}><circle cx="12" cy="12" r="3.2" /><path d="M5.6 5.6a9 9 0 0 0 0 12.8M18.4 18.4a9 9 0 0 0 0-12.8" /></Svg>
)
export const People = p => (
<Svg {...p}><circle cx="9" cy="8.5" r="3.2" /><path d="M2.8 19.5a6.4 6.4 0 0 1 12.4 0" /><path d="M16.5 6.2a3.2 3.2 0 0 1 0 6.1M18 19.5a6 6 0 0 0-1.6-4" /></Svg>
)
export const Camera = p => (
<Svg {...p}><path d="M3 8.5h3.4L8 6h8l1.6 2.5H21v10.2H3z" /><circle cx="12" cy="13.2" r="3.1" /></Svg>
)
export const Search = p => (
<Svg {...p}><circle cx="11" cy="11" r="6.4" /><path d="M15.8 15.8 20.5 20.5" /></Svg>
)
export const Plus = p => (<Svg {...p}><path d="M12 5.5v13M5.5 12h13" /></Svg>)
export const Close = p => (<Svg {...p}><path d="M6.5 6.5l11 11M17.5 6.5l-11 11" /></Svg>)
export const Check = p => (<Svg {...p}><path d="M5 12.8l4.4 4.2L19 7" /></Svg>)
export const Play = p => (<Svg {...p}><path d="M8 5.6v12.8L18.5 12z" /></Svg>)
export const Stop = p => (<Svg {...p}><rect x="7" y="7" width="10" height="10" rx="1.6" /></Svg>)
export const Warning = p => (
<Svg {...p}><path d="M12 4.6 21 19.4H3z" /><path d="M12 10v4.1" /><path d="M12 17.1v.01" /></Svg>
)
export const Signal = p => (
<Svg {...p}><path d="M5 19.4v-4.2M10.3 19.4v-7.6M15.7 19.4v-11M21 19.4V4.6" /></Svg>
)
export const Cloud = p => (
<Svg {...p}><path d="M7.2 18.4a4.2 4.2 0 0 1-.6-8.35A6.2 6.2 0 0 1 18.4 9a4.2 4.2 0 0 1 .3 9.4z" /></Svg>
)
export const CloudOff = p => (
<Svg {...p}><path d="M7.2 18.4a4.2 4.2 0 0 1-.6-8.35 6.2 6.2 0 0 1 2-3.2M10.6 5.2A6.2 6.2 0 0 1 18.4 9a4.2 4.2 0 0 1 1.9 7.6" /><path d="M3.6 3.6l16.8 16.8" /></Svg>
)
export const Shield = p => (
<Svg {...p}><path d="M12 3.8 19.4 6.6v5.2c0 4.2-3 7.4-7.4 8.4-4.4-1-7.4-4.2-7.4-8.4V6.6z" /></Svg>
)
export const Link = p => (
<Svg {...p}><path d="M10.2 13.8a3.6 3.6 0 0 0 5.2 0l2.8-2.8a3.7 3.7 0 0 0-5.2-5.2l-1.3 1.3" /><path d="M13.8 10.2a3.6 3.6 0 0 0-5.2 0l-2.8 2.8a3.7 3.7 0 0 0 5.2 5.2l1.3-1.3" /></Svg>
)
export const Logout = p => (
<Svg {...p}><path d="M14.4 7.6V5.4H4.6v13.2h9.8v-2.2" /><path d="M10 12h9.4M16.4 8.8 19.8 12l-3.4 3.2" /></Svg>
)
export const Back = p => (<Svg {...p}><path d="M14.6 5.6 8 12l6.6 6.4" /></Svg>)
export const Chevron = p => (<Svg {...p}><path d="M9.4 5.6 16 12l-6.6 6.4" /></Svg>)
export const Dot = p => (<Svg {...p}><circle cx="12" cy="12" r="4.5" fill="currentColor" stroke="none" /></Svg>)
// Drawn for the empty states rather than an apologetic sentence in grey.
export const NoCamera = p => (
<Svg {...p} strokeWidth="1.2"><path d="M3 8.5h3.4L8 6h8l1.6 2.5H21v10.2H3z" /><circle cx="12" cy="13.2" r="3.1" /><path d="M3.6 3.6l16.8 16.8" /></Svg>
)
export const NoFaces = p => (
<Svg {...p} strokeWidth="1.2"><circle cx="12" cy="9" r="3.4" /><path d="M5.4 20a6.8 6.8 0 0 1 13.2 0" /></Svg>
)