PetCura · Foundation review · v1.0 · May 2026

The WhatsApp-native
ClientOps inbox
for vet clinics.

Direction B (warm sage) — locked. Two surfaces, three locales, one keyboard-first inbox.

View as document — original was a pannable canvas. Sections below preserve the artboard order.

2surfaces
3locales
2themes
6principles
4urgency tiers
Foundation

Six principles

The promises every screen has to keep

P1

Inbox-first, board on demand

List view by default — five-column kanban toggle for batch sweeps.

P2

AI is a draft, never a decision

Every AI artifact ships with source span, confidence, accept/edit/reject — and is never sent without staff confirmation.

P3

Trilingual by default

EE / EN / RU surface as first-class. Translation is one tap, never a separate flow.

P4

Keyboard-equal-to-mouse

Every staff action is reachable via shortcut. ⌘K is the spine.

P5

Audit-grade trace

Every send, edit, and AI use is logged with actor + locale + raw input. Compliance is built in, not bolted on.

P6

Minutes saved, not screens added

If a surface costs more time than it saves, it does not ship.

Brand & tokens

Color, type, the moves we'll keep using

Direction B · Warm sage on cream — locked

Color · Direction B · Light
ink#29261bBody text
ink-2#4d4738Secondary
muted#847e6bMeta
muted-2#a39b86Disabled
paper#f6f4efSurface
soft#ede9dfSoft surface
line#e2dcccBorder
primary#4a6b3fSage · CTA
p-soft#e2ead6Selected row
amber#b07d2cToday / warning
red#a64a3cUrgent
green#4a6b3fResolved
Color · Direction B · Dark
ink#ece8de
ink-2#c9c4b6
muted#8a8270
paper#1c1a16
soft#221f19
line#2a2620
primary#87a87f
p-soft#1f2a1d
amber#d4a872
red#d49080
green#9bb892
Component primitives
Status pills
newwaiting · staffwaiting · ownerresolvedurgent
Badges
WhatsAppAI assistTodayUrgent
Buttons
Urgency dots
Inputs
AI suggestion card
Suggested reply · EE → EEconf 0.86

“Tere Liis! Soovitan tuua Lumi täna kell 14:00. Toite ja liiva muutus võib olla allergia põhjus…”

Type · Montserrat + JetBrains Mono
Display LThe quick brown fox48/600
Display MThe quick brown fox32/600
TitleThe quick brown fox24/600
HeadingThe quick brown fox18/600
BodyThe quick brown fox14/400
Body smThe quick brown fox13/400
CaptionThe quick brown fox11/500
MonoThe quick brown fox11/500
App shell

Shell & navigation

Live primitives — the rail, the active states, the mobile sheet. Regressions in any of these light up here first.

A · Sidebar — three states

Three captioned tiles using the live shadcn Sidebar primitives. If a future PR breaks the active/hover/super-admin rules, one of these tiles shows the broken state immediately.

Sidebar · idle280×720
PetCuratartu loomakliinik

Inbox

Mari Kaskadmin · TL

/inbox active. The parent Inbox row stays neutral (no fill) — only the deepest match (All) carries the sage-soft pill. If both parent and child get fills, the double-active bug is back.

Sidebar · with hover hint280×720
PetCuratartu loomakliinik

Inbox

Mari Kaskadmin · TL

The Mine row is forced into its hover treatment. All idle rows above and below must NOT match this color — if they do, every-row-painted-as-hovered is back.

Sidebar · all routes mapped280×720
PetCuratartu loomakliinik

Inbox

Mari Kaskadmin · TL

Super-admin viewer — the Admin row is visible at the bottom of the rail. Staff-only viewers never see this row; the gate lives in SIDEBAR_NAV[].requires.

B · AppShell skeleton

Sidebar (left, --soft) + main (right, --paper). The surface tones must differ — that's how the rail reads as anchored when content scrolls.

AppShell · sidebar + main1200×600
PetCuratartu loomakliinik

Inbox

Mari Kaskadmin · TL

All · 35

main pane · --paper
Owner thread placeholder #112 : 01
Owner thread placeholder #212 : 02
Owner thread placeholder #312 : 03
Owner thread placeholder #412 : 04
Owner thread placeholder #512 : 05

Skeleton: sidebar (left, --soft surface) · main (right, --paper surface). The surface tones MUST differ so the rail reads as anchored when content scrolls. If both panes share the same background, the rail dissolves into the page.

C · Mobile shell

Below md the rail folds into a Sheet drawer triggered by the MobileShellHeader. The open state here is a visual approximation; the live Sheet renders through a Radix portal.

Mobile · closed390×620

All · 35

Liis · Lumi

Tere! Lumi sügeleb …

Liis · Lumi

Tere! Lumi sügeleb …

Liis · Lumi

Tere! Lumi sügeleb …

Liis · Lumi

Tere! Lumi sügeleb …

Liis · Lumi

Tere! Lumi sügeleb …

Liis · Lumi

Tere! Lumi sügeleb …

Persistent rail is hidden below md. The MobileShellHeader carries the trigger and current page title.

Mobile · open (sheet)390×620

All · 35

Liis · Lumi

Tere! Lumi sügeleb …

Liis · Lumi

Tere! Lumi sügeleb …

Liis · Lumi

Tere! Lumi sügeleb …

PetCuratartu loomakliinik

Inbox

Mari Kaskadmin · TL

Sheet drawer slides in over the content with a scrim. The real implementation uses Radix Sheet via shadcn — visual approximation here since the portal lifts out of the tile.

D · Active-state matrix

Static chips rendered with the exact same cn classes SidebarMenuSubButton applies in each state. The contract: this table mirrors the live row — break the classes, break this row.

StateLightDark

Idle

No fill, --muted ink

Mine
Mine

Hover

sidebar-accent fill (primary-soft in light)

Mine
Mine

Active (current page)

primary-soft fill, primary-strong ink, sage badge

Mine
Mine

Focused (keyboard)

primary ring · sidebar-ring

Mine
Mine

E · AI confidence bucket

Three buckets surface across AI reply drafts, urgency suggestions, and intake category guesses. The text label carries the meaning (WCAG 1.4.1); color is reinforcement only.

conf 0.32LOWconf 0.60MEDIUMconf 0.92HIGH

AI confidence bucket — the text label is the WCAG 1.4.1 signal; color is reinforcement. The same chip pattern surfaces on AI reply drafts, urgency suggestions, and intake category guesses. Thresholds live in the shared bucketConfidence() helper so the showcase and production stay in sync.

  • LOW

    Below 0.40 — staff must review before sending

  • MEDIUM

    0.40–0.75 — scan for tone and facts before send

  • HIGH

    0.75 and above — safe default, staff confirms

F · Nav config

The sidebar is data-driven. One typed entry per top-level feature, gated to roles when needed.

Adding a new top-level feature is one line in apps/web/lib/nav/sidebar-nav.ts:

// apps/web/lib/nav/sidebar-nav.ts
export const SIDEBAR_NAV: NavItem[] = [
  {
    id: "inbox",
    labelKey: "nav.inbox",
    href: "/inbox",
    icon: Inbox,
    countSource: "inboxTotal",
    children: INBOX_CHILDREN
  },
  {
    id: "reminders",
    labelKey: "nav.reminders",
    href: "/reminders",
    icon: Bell,
    countSource: "remindersTotal"
  },
  {
    id: "admin",
    labelKey: "nav.admin",
    href: "/admin",
    icon: ShieldCheck,
    requires: "super_admin"  // ← gated to super-admins
  }
];

Counts are NEVER computed inside this module — they're resolved server-side and threaded through the typed NavCounts map.

Surface 1

Clinic inbox

Live preview — light + dark, three viewports

Desktop · Light1320×820
Desktop · Dark1320×820
Tablet · Light820×1080
Phone · Dark420×860
Surface 2

Owner intake

Mobile-first · WhatsApp + Web · Estonian primary

WhatsApp · Light420×860
WhatsApp · Dark420×860
Web intake · Step 2420×860
Web intake · Review420×860
Cross-surface

Where the system shows up consistently

Same pattern across clinic, intake, audit log

AI is a draft

P2
Every AI artifact carries:

• Source span (which message)
• Confidence (0.0–1.0)
• Locale (in/out)
• Accept · Edit · Reject

Nothing reaches the owner without a staff send.
Every use is logged with actor + raw input + final output.

EE · EN · RU

P3
Source language detected on every inbound message.
Translation is a per-bubble toggle, not a separate flow.
AI replies generated in source language.
Audit log preserves source AND translation.

Note: EE strings can run ~25% longer than EN — pad row min-heights.

⌘K is the spine

P4
⌘K  Command palette
J/K Navigate threads
E   Resolve
A   Assign
R   Reply (focus composer)
T   Translate thread
⌘↵  Send reply
?   Show all shortcuts

Four tiers, never five

contracts/request-lifecycle
urgent  · within 1h
today   · within 8h
week    · within 5d
routine · best effort

Urgency is a request property, not a status.
Staff can override AI urgency at any time.
Open & next

What's locked, what's still up for review

Locked for v1
  • DirectionB · Warm sage on cream
  • TypeMontserrat (UI) + JetBrains Mono (meta)
  • ThemesLight & Dark — both v1
  • LayoutInbox-first list, kanban toggle
  • Selected rowDot only (no avatars in row)
  • UrgencyFour tiers
  • LocalesEE · EN · RU
  • AIDraft-only, with source/conf/accept-edit-reject
Open questions
  1. Q1Reminder model — opt-in per request, or rules-based per pet?
  2. Q2Multi-clinic switcher — top-level org rail or per-account?
  3. Q3AI categories — fixed taxonomy or learned per clinic?
  4. Q4Web intake → WhatsApp handoff — automatic or staff-triggered?
  5. Q5Owner-side notifications when staff are typing?
  6. Q6Bulk-resolve flow — kanban-only, or list multi-select too?