HOCO B2B
Design

Design system

Principles, brand rules, tokens, components and patterns shared by the reseller app, the delegate app, the back-office and the reseller web — the contract between Claude Design, hoco_ui (Flutter) and @hoco/ui (Next.js).

One design system serves four interfaces: the reseller app and the delegate app (Flutter, package hoco_ui in hoco-mobile), and the back-office and reseller web (Next.js, package @hoco/ui in this monorepo). Akram defines it on Days 1–4 of the design plan (DES-01 to DES-04). Imad implements it in MOB-04 and Anis in DSH-01.

Status: HOCO PRO, shipped on the web (updated 1 Nov 2026)

The values below are the HOCO PRO design system as delivered by the designer on 31 Oct 2026 and updated on 1 Nov 2026 (brand board of 28 Oct: HOCO Green, Graphite, Light Gray; Quicksand since 1 Nov), as implemented in @hoco/ui. They replace the proposed v0 (blue, teal and orange; Inter). The binding product decisions of 17 October are unchanged. Contrast ratios were computed, not estimated.

Principles

PrincipleWhat it meansHow we check it
Clarity in the fieldDelegates work outdoors, often with one hand, in sunlight and on a weak network. The state that matters is always visible: tour status, GPS, sync.Touch targets of 48 dp or more. Text of 14 sp or more in the delegate app. One primary action per screen, in the thumb zone. Sync and GPS badges on every tour screen. Outdoor test in DQA-03.
Speed to orderResellers reorder the same references often. Finding, adding and sending must be fast.Three taps or fewer from home to cart. Stepper on cards. Search, barcode scan and filters with live counts. Reorder from an order. A one-screen checkout.
Trust in numbersMoney, credit and KPIs must add up and say where they come from.One money format everywhere. Tabular figures. Unit × quantity = line total, shown. TTC stated. KPI tiles show « Mis à jour à 10:32 ». Re-validation messages say what changed. Live order status, never a stale one.
French-first, RTL-readyFrench copy in V1. Arabic (RTL) and English are prepared, not designed (NFR-06).Start/end spacing only, never left/right. Directional icons mirrored. 30 % text-expansion room. No text baked into images. Arabic type tokens defined.
AccessibleWCAG 2.2 AA as the target (NFR-07).Text contrast of 4.5:1 or more, UI boundaries 3:1 or more. Labels are never placeholders. Colour is never the only signal. Layouts hold at 130 % text size. Screen-reader labels on every icon button.

Brand

HOCO is the distributor and one of the three product brands it sells (HOCO, ACEFAST, BOROFONE; cahier §4.3). The rules below keep the two roles apart.

  1. The interface belongs to HOCO the distributor. App icons, splash, headers, primary colour and documents use the HOCO identity. ACEFAST and BOROFONE never recolour the interface and never appear as app chrome.
  2. Product brands appear only in content: brand chips in filters (logo and count), an optional brand line on product cards and detail, brand banners supplied by marketing, and charts (where each brand has a fixed colour, see charts).
  3. Equal treatment. The three logos use the same optical height: 12 dp on cards, 20 dp in filter chips, 24 dp on product detail. Each sits on white with clear space equal to half its height. Never stretched, recoloured or outlined. Single-colour (ink) versions are used under 16 dp; colour versions elsewhere. Logo files come from the Brand record (public CDN).
  4. In text, write the brands in capitals, HOCO, ACEFAST, BOROFONE, even where the logo is lowercase (hoco.).
  5. The Signal (vertical bars rising like the logo's) is the HOCO PRO motif, for the splash, sign-in, onboarding, empty brand states, documents (DQA-09) and the field kit (DQA-07). It is decorative only: never behind text, never a loader, never inside dense screens.
  6. App names. Assumption: HOCO Revendeur (dz.hoco.reseller) and HOCO Délégué (dz.hoco.delegate). To confirm with HOCO and log in the decision register.

Tokens

Tokens are named by role, not by value. Components use semantic tokens (primary, muted-foreground, tone-success-fg); palette steps are only referenced by semantic tokens. The web values below are the ones shipped in packages/ui/src/styles/globals.css; ratios are computed against white unless stated.

Colour palette

The HOCO PRO brand board (28 Oct 2026) sets four colours: HOCO Green #7ED321, Graphite #1A1A1A, Light Gray #F3F3F3 and white. Green is used sparingly: it is a fill (1.9:1 on white), never text or a thin line on white.

StepHOCO greenNeutral (graphite scale)
50#F3FAE8#F3F3F3 app background (Light Gray)
100#E4F5CC chart 1 soft#EDEDED tiles, hover, table header, muted fills
200#C9EB99#E2E2E2 hairlines, card borders
300#ABE066#CFCFCF strong hairline, switch track off
400#94DA43 brand hover#8C8C8C input and checkbox boundaries (3.2:1)
500#7ED321 ★ brand fill (graphite text on it, 9.3:1)#6E6E6E placeholders (5.1:1)
600#5A9F14 chart series 1 (3.2:1)#4A4A4A
700#44800F text-safe green: focus ring, brand text (4.8:1)#333333 primary hover
800#33610B#222222
900#223F08#1A1A1A ★ Graphite: headings, primary actions, dark grounds (17.4:1)

Offers: one orange from the brand board. Orange 700 #C2410C carries white text (5.2:1) and is promo and flash; orange 800 #9A3412 is its hover; orange 500 #F07D3A is a fill or chart colour only (warm). The storefront ramps (violet, coral, tangerine, crimson, lime, aurora) were removed on 1 Nov 2026. progress is graphite. Blue means info and nothing else: never selection, links, done steps or highlights.

Rules that follow from the ratios.

  • Graphite carries the interface: primary buttons, selection, radios, checks, links and the active tab are graphite. Green never marks a state, a category or a selection, so it cannot be confused with Terminée / Actif (success green).
  • Green appears only in the logo and the Signal, the brand button (bg-brand, graphite text), chart series 1 and the focus ring (green 700).
  • Input boundaries use #8C8C8C (3.2:1, WCAG 1.4.11). Secondary text is #646464, which passes 4.5:1 on white, the app ground and tiles alike.

Semantic colour roles

TokenValueUse@hoco/ui variableFlutter ColorScheme / extension
color.bg.appLight Gray #F3F3F3Screen background behind cards--backgroundsurfaceContainerLowest → HocoColors.bgApp
color.bg.surfacewhiteCards, sheets, inputs, tables--card, --popoversurface
color.bg.subtle#EDEDEDTiles, hovered rows, skeleton, count pills--muted, --secondary, --accentsurfaceContainerHigh
color.text.primaryGraphiteTitles, body, values--foregroundonSurface
color.text.secondary#646464 (5.9:1; 5.05:1 on tiles)Metadata, helper text--muted-foregroundonSurfaceVariant
color.primary / .hoverGraphite / #333333Primary buttons, selection, links, active tab--primaryprimary
color.on-primarywhiteText and icons on primary--primary-foregroundonPrimary
color.brand / .hover / .ongreen 500 / 400 / graphiteBrand button, brand fills, the Signal--brand, --brand-hover, --brand-foregroundHocoColors.brand
color.brand.textgreen 700Brand words on white--brand-textHocoColors.brandText
color.promo / .hoverorange 700 / 800Promotional prices and badges--promo, --promo-hovertertiary
color.flashorange 700Flash-sale badge and countdown--flashHocoColors.flash
color.inverseGraphite / whiteDark banners, tooltips, toasts, selected chips--surface-inverseinverseSurface
color.border.subtle#E2E2E2Card borders, dividers--borderoutlineVariant
color.border.control#8C8C8CInputs, outline controls, checkboxes--inputoutline
color.border.focusgreen 700, 2 px outlineFocus ring--ringHocoColors.focus
color.danger#C6361E (5.3:1)Destructive actions, errors--destructive, --dangererror
color.whatsapp#0F7A40 (white on it 5.4:1)WhatsApp support button--whatsappHocoColors.whatsapp
color.overlayrgb(16 16 21 / 0.28)Scrim behind sheets and dialogs--scrimscrim

Dark theme is not designed in V1. @hoco/ui has no dark values; the delegate app stays light, which reads better in sunlight.

Status tones

Every status in enums.json carries a tone. Status pills take their colour from the tone, never from the status itself, and each status tone shows its glyph, so colour is never the only signal. A designer never picks a colour for « Expédiée »: it is progress because the enum says so.

Categories are not statuses. Customer type, order channel, payment method, reseller source, potential, delivery mode, ticket type and visit priority render as neutral tags without a glyph (isCategoryEnum() in @hoco/shared), so « Détail » never shares a colour with « Terminée ».

ToneMeaningPill backgroundTextGlyphExamples from enums.json
neutralExists, not started#EDEDEDGraphite—Brouillon, Planifiée, À préparer
mutedOver or out of the flow#EDEDED#6E6E6Eminus-circleAnnulée, Archivé, Expirée
brandCategories, identity#ECEDEEGraphite—Super-gros, Collaborateur
infoAcknowledged, waiting its turninfo blue at 10 %info blue darkenedinfoEnvoyée, À visiter, Ouverte
progressMoving forwardgraphite at 8 %graphitecircle-dashedEn préparation, Expédiée, En cours
successDone or positivesuccess at 10 %success darkenedcircle-checkValidée, Livrée, Payée, En stock
warningNeeds attentionwarning at 11 %warning darkenedclockEn attente, À relancer, Impayée
dangerFailed, refused, blockingdanger at 9 %danger darkenedcircle-xRefusée, En rupture, En retard

The tints are mixed in OKLab (color-mix(in oklab, …)); a 1 px ring at about 20 % of the hue goes around icon tiles. Variables: --tone-<tone>-bg, -ring, -fg; Tailwind bg-tone-success-bg, text-tone-success-fg. Example, the order lifecycle:

ValueUI label (fr)Meaning
DRAFTBrouillonAssisted order being prepared by a delegate, or a back-office draft.
SUBMITTEDEnvoyéePlaced by the reseller (or an assisted order confirmed by the reseller); waits for the back-office. Orders are never confirmed automatically.
PENDINGEn attenteOn hold: needs information, a stock/credit decision, or the reseller's confirmation of an assisted order.
VALIDATEDValidéeConfirmed by a back-office user (orders.validate); stock reserved (default setting).
REFUSEDRefuséeRefused with a reason; terminal.
PREPARINGEn préparationPicking in progress.
READYPrêtePacked, waiting for the carrier.
SHIPPEDExpédiéeHanded to the carrier; stock deducted (default setting).
DELIVEREDLivréeProof of delivery recorded.
CANCELLEDAnnuléeCancelled by the reseller (before confirmation) or the back-office (with reason); reserved quantities return to stock automatically.
RETURNEDRetournéeReturned after delivery (accepted return or refused at the door); quantities re-enter stock automatically on reception.
Envoyée En attente Validée Expédiée Refusée Annulée

Several statuses of one lifecycle share a tone. Validée and Livrée are both success; En préparation, Prête and Expédiée are all progress. The label tells them apart, and the progress tracker shows the position. Do not invent extra colours per step; the pill label is always the label from enums.json; the tone colours never encode anything else (a chart series, a category, a brand).

Data-visualisation colours

SlotColourVariableNote
1HOCO green 600 #5A9F14--chart-1Primary series (3.2:1)
2Graphite #1A1A1A--chart-2Secondary series
3Orange 500 #F07D3A--chart-3Warm comparison
4HOCO green 300 #ABE066--chart-44th series
5Grey #6E6E6E--chart-55th series
Other#CFCFCF--chart-6Everything folded into « Autres »

Ordered steps (heat by wilaya, density) use the green heat ramp --heat-1 … --heat-5 (green 100, 200, 400, 600, 800); --heat-0 is no data. Grid --chart-grid, axis --chart-axis, ticks --chart-tick, tooltip graphite (--chart-tooltip), bar radius 6 px, strokes 2 px.

Chart rules:

  • Series take the fixed order and are never cycled; three series per chart at most, else fold into « Autres » or split. A colour follows its entity.
  • Red and green only say direction (--chart-positive, --chart-negative), never a series.
  • One y-axis per chart; a legend for two series or more; a hover and keyboard tooltip; a Tableau view on every chart card.
  • Statuses are never chart colours. "Orders by status" is a ranked bar list with status pills.

Typography

RoleFamilyWeightsLoading
Latin (UI, display, numbers)Quicksand (variable)300–700; in use 400, 500, 600, 700next/font (self-hosted), --font-quicksand
Arabic (all roles)IBM Plex Sans Arabic400, 500, 600, 700next/font, --font-plex-arabic; first in the stack under dir="rtl"

Both are SIL OFL fonts. Quicksand is rounded and light on screen, so body text never goes under 400 and 11–12 px text is 500 or 600 (text-xs carries 500 by default). Labels, table heads and section captions are sentence case and untracked: no uppercase labels. Flutter bundles them as assets (the delegate app is offline-first). The logo wordmark is artwork, never set in a font.

Web scale (px)

TokenSize / line heightWeightUse
type.page-title28 / 1.15 (24 in page headers)700, −0.02 emPage title
type.section-title20 / 1.15600Section title
type.card-title17 / 1.3 (15 in cards)600Card, dialog and drawer title
type.stat24 / 1700, tnumKPI and stat values
type.body14 / 1.45400Default body (inputs 16 on phones, against zoom)
type.body-strong14 / 1.45500Emphasis, list titles
type.label13 / 1.3500Form labels, tabs, table text
type.caption12 / 1.3500Helper text, metadata, table headers
type.button13 / 1600Buttons (12 for sm, 14 for lg)

Mobile scale (sp) keeps the roles and sizes defined for Flutter in MOB-04 (display 28, headline 22, title 18 / 16 / 14, body 16 / 14 / 13, label 16 / 14 / 12, caption 12, price 24 / 16, KPI 28) with Quicksand in place of Inter and Plus Jakarta Sans (Arabic stays IBM Plex Sans Arabic). Nothing below 12 sp; delegate-app content 14 sp or more; layouts survive 130 % text size.

Rules: sentence case everywhere; no capitals-and-tracking eyebrows (the design system rejects them); headings take −0.02 em tracking in Latin and none in Arabic; Arabic uses no letter-spacing and no capitals transform; digits stay Latin (0–9).

Spacing

A 4-point scale (--space-* in the design system, Tailwind's 4 px steps on the web). Cards pad 20 px with a 16 px gap; rows inside cards are 12 px tiles with 12–14 px padding; page margins follow the viewport (16 → 24 → 32 px, --page-pad).

TokenValueTypical use
space.14Label-to-input gap, icon-to-text in pills
space.28Gap between chips and stacked buttons
space.312Compact padding, list-row padding
space.416Screen margin (mobile), card gap, form-field gap
space.520Card padding
space.624Section gap, page margin (tablet)
space.832Page margin (desktop), empty-state padding
space.1248Minimum touch target (mobile)

Grid and breakpoints

ContextDesign atGridNotes
Mobile (RA, DA)360 × 800 dp, checked at 3904 columns, 16 dp marginsApp bar 56, tab bar 72, sticky CTA bar 72
Back-office1440 × 90012 columns responding to their container (480 / 720 / 960 / 1200), gutters 12 → 24Sidebar 208 px, top bar 64 px, page capped at 1440 px. Split (340 px list + detail, stacked under 900 px) and WithAside (360 px aside, stacked under 1100 px) in @hoco/ui/components/layout
Reseller web1440, 1024, 768, 390Same grid; page width 1440Catalogue 4 / 3 / 2 columns; filter column at 1024 and up

Wide content never pushes the page sideways: a grid is one shrinkable column until a grid-cols-* utility sets its columns, and tables and boards scroll inside their box.

Radii

TokenValueUse
radius.pill9999Every button, input, select, badge, pill and chip, switches, avatars
radius.sm8Small chips, skeleton blocks
radius.md10Menu items
radius.lg12Tiles, rows, image tiles, textareas, callouts, toasts
radius.xl16Cards, popovers
radius.2xl20Dialogs

Elevation and shadows

A flat system: cards sit on the gray ground with a hairline and no shadow. Shadows are only for what floats.

TokenValueUse
shadow.float0 12px 32px -8px rgb(16 16 21 / .18), 0 1px 2px rgb(16 16 21 / .06)Menus, popovers, dialogs, toasts, sticky save bars, the WhatsApp button
shadow.drawer-16px 0 48px -16px rgb(16 16 21 / .25)Side sheets (mirrored in RTL)
shadow.window0 24px 60px -20px rgb(16 16 21 / .25), 0 2px 6px rgb(16 16 21 / .06)The sign-in card on graphite

Borders and focus

TokenValueUse
border.width1Cards, dividers, inputs at rest
selectedgraphite border, no extra ringChoice cards (address, payment method), selected chips are filled graphite
focus.ring2 px solid green 700; offset 2 px (0 on inputs)Every focusable element on keyboard focus, never removed
errordanger border plus a 3 px danger ring at 15 %, and an error glyphField in error

Iconography

  • Set: Lucide — lucide-react on the web, lucide_icons_flutter in Flutter, same glyph names.
  • Sizes and optical stroke: 12 caption (2.1), 14 button and chip (2), 16 default (1.75), 20 list row and tile (1.75), 24 app bar and empty states (1.5). Icon in @hoco/ui applies them.
  • Icon tiles: 32–44 px round tiles in a status tone: a 10 % tint, a 1 px ring and the glyph in the darkened hue (IconTile).
  • RTL: chevrons and arrows mirror (flip utility); product, brand, media and status icons never do.
  • No emoji; icon-only buttons always carry a label (IconButton requires it).

Illustration and imagery

  • Product photos: square 1:1, product centred. Shown on a gray tile with 10 % padding and multiply blending, so white photo backgrounds melt into the tile; cards, gallery and line thumbnails use the same treatment.
  • Banners and daily offers: 2:1 with live text, never baked into the image.
  • The Signal (SignalBars): HOCO PRO's signature bars, decorative only, for brand moments (splash, sign-in, empty brand states, documents). Never behind text, never a loader, never looped.
  • Logos: the official artwork only (Brand): the hoco. wordmark in app chrome and documents, the HOCO PRO DZ lockup on splash and sign-in. Never redrawn, recoloured or stretched.

Motion

TokenDurationUse
dur.micro / fast90 / 120 msPress, hover, chip and checkbox changes
dur.base200 msButtons, fades, menus
dur.slow320 msScrims, drawer exit
dur.sheet420 msDrawers and sheets entering

Easings: ease-out cubic-bezier(.23, 1, .32, 1) (feedback, entrances), ease-in-out cubic-bezier(.77, 0, .175, 1) (on-screen moves), ease-emphasized (large entrances), ease-sheet (sheets), ease-exit (dismiss). Nothing bounces, no count-up numbers, no fly-to-cart. Buttons settle to 0.97 on press. Nothing enters from scale 0: entrances start at 0.95, badges and checks at 0.8. Hover tints fire only for a mouse (Tailwind hover: sits inside @media (hover: hover)), so a tap never leaves a stuck hover. With reduce motion on, every duration becomes 1 ms.

Components

All interactive components support default, pressed (hover on web), focused and disabled. The States column lists the extra states each one needs. Used in lists representative screen ids from the inventory.

Mobile components

ComponentVariantsSizesExtra statesContent rulesUsed in
Buttonprimary · secondary (outline) · tertiary (text) · danger · accent (offers only) · WhatsApp (whatsapp, share only)lg 52 · md 44 · sm 36 dploading (spinner replaces the label, width kept)Verb + object, sentence case, 24 characters or fewer (« Valider la commande »). One primary per screen. Optional leading iconEverywhere
Icon buttonstandard · tonal · with badge24 glyph, 48 targetselected (favourite), badge 1–99, 99+Always has a semantics label (« Ajouter aux favoris »)App bars, cards
Text fieldoutlined, label above · prefix (+213) · leading icon · password (show/hide) · multiline with counter · search (pill)48 dpfocused, filled, error, read-only, success (OTP)The label stays visible above. Helper below; the error replaces the helper with an icon. Placeholder = example only (« Ex : Bazar du Centre »)RA-03, RA-05, DA-12, DA-14
Picker (bottom sheet)single · multi · searchable (communes) · cascading (wilaya → commune)full-height sheet over 7 optionsempty search, loading, offline (cached list)Wilayas shown with their code: « 31 — Oran »RA-05, RA-19, DA-14
Phone fieldnational display, E.164 storage48 dpinvalid, already existsDisplays 0555 12 34 56; accepts +213… and 00213…RA-03, RA-07, DA-02, DA-14
OTP input6 boxes, SMS autofill48 × 56 dp boxesfilled, error, expired, resend countdown« Renvoyer le code dans 0:42 »RA-04, RA-08, DA-03
Customer-type cardsradio cards ×3 (Super-gros, Demi-gros, Détail)full width, 72 dpselected (2 px primary border plus check), error (nothing chosen)Title plus one line of description; note « Vérifié par HOCO lors de la validation ». Read-only variant with pill and « Contactez HOCO pour modifier »RA-05, RA-28
Checkbox · radio · switchstandard24 glyph, 48 targetindeterminate (checkbox), errorThe label is clickableSettings, filters
Chipfilter (single or multi) · input (removable ×) · category tile (image and label) · brand (logo and count)sm 32 · md 36 dpselected, disabled with (0)Labels of 20 characters or fewer; the count in brackets is secondary textRA-12, RA-13, DA-12
Segmented control2–4 segments · tone variant (interest level)40 dpselectedTone variant: Faible muted, Moyen info, Élevé success; the selected segment is filled with its toneDA-12, DA-21
Tabsunderline, scrollable48 dpactive, with countActive colour is always primary; no tab in a different colourRA-12, DA-06
Quantity steppercompact (card) · standard (detail, cart)32 / 40 dpat min (− becomes a delete icon in the cart), at max (+ disabled, helper « Max : 20 pièces par commande »), editing (numeric keyboard), updating (spinner), errorSteps follow the minimum and step of the product. The maximum is the per-order limit, never the stock levelRA-12, RA-14, RA-17, DA-17
Price blockcard · detail · promo (struck original price, discount badge −20 %) · total rowprice.md / price.lglocked (guest): lock icon and « Créez un compte pour voir les prix »Detail shows « Votre prix · Demi-gros » (TTC, decision to confirm) and « Prix de vente conseillé » as secondary. No quantity tiers in V1RA-12, RA-14, RA-17, DA-17
Availability indicatorpill · inline textlabel.sm—Exactly two values from StockAvailability: En stock (success) or En rupture (danger). Never a quantity, never "Plus que N pièces", never "Stock limité" (STK-07)RA-12, RA-14, RA-16, DA-17
Product cardgrid · list row (search, assisted order) · mini (carousels)grid 160 × 260 dp minskeleton, out of stock (add disabled), in cart (stepper with quantity), guest (price lock), promo badge1:1 image on white; optional brand line in capitals; name 2 lines max with ellipsis; price block; availability; add button; favouriteRA-09, RA-12, RA-16, DA-17
Cart linestandard · with error88 dpprice changed (old price struck), quantity unavailable (error plus « Ajuster » or « Retirer »), removed (undo)Unit price × quantity = line total, all visibleRA-17, DA-17
Order summarycart · checkout · confirmation—minimum not reached (progress bar plus remaining amount), free-delivery thresholdLines: sous-total, remises, livraison (Offerte), Total TTC, dont TVARA-17, RA-18, RA-20
Payment method selectorradio cards64 dpunavailable (method disabled in settings: hidden), credit insufficient (error with available credit)Lists the methods enabled in settings only (FIN-02)RA-18
Order cardlist · compact (home, DA-18)—live-changed (motion.highlight), assisted badge, first-order badgeCMD-2026-000158 (no #), date, status pill, first item and « + 2 articles », amount, mini progress barRA-21, DA-18
Progress trackerhorizontal 5 steps (Envoyée → Validée → En préparation → Expédiée → Livrée) · vertical timeline (detail)—done, current, upcoming, on hold (En attente), terminal branch (Refusée, Annulée, Retournée)Completed segments filled; dates under steps on the detail screenRA-21, RA-22, RW-09
Live indicatorinline dot plus label · toast—connected (« En direct »), reconnecting, paused (« Mise à jour en pause — tirez pour actualiser »)See live order statusRA-21, RA-22, RW-08, RW-09
Return and after-sales line pickerorder line with checkbox and quantity stepper72 dpselected, quantity above the delivered quantity (error), photo required (error)Lines of one delivered order only; quantity limited to the delivered quantityRA-33, RA-40
Status pillthe 8 tonessm 20 · md 24 dp—Label = fr from enums.json; optional 6 dp dotEverywhere
KPI tilecompact (3 per row) · wide (2 per row) · with progress104 dp min heightloading, no data (« — »)Icon in a tinted 40 dp circle, number.kpi value, label of 2 lines maxDA-05, DA-06, DA-20
Objective progresslinear with value/target · compact8 dp barachieved (success), behind (warning), no objective« 6 / 12 visites · 50 % »DA-05, DA-20, DA-22
Finance tilebalance · available credit (with ceiling bar)2 per rowoverdue warningNo loyalty tile in V1RA-09, RA-29
Visit rowplanned · next (highlighted) · done (muted) · not done72 dplate (En retard icon and label), priority (Prioritaire flag), pending syncSequence number badge, store name, commune, time slot, distance « à 1,2 km », status pill, one actionDA-05, DA-06, DA-08
Map pins and map cardsee maps and pins———DA-06, DA-08, DA-13
GPS validation bannerinline card56 dpacquiring (accuracy shown), confirmed (« Position confirmée · ± 12 m »), out of tolerance (« Vous êtes à 420 m du point de vente »), weak accuracy, GPS off, permission deniedTolerance 150 m and accuracy 100 m are settings; the copy shows the actual valuesDA-08, DA-10
Checklist itemstandard · required56 dpchecked, required missing (error), locked (visit done)The whole row is the targetDA-10
Photo capture tileadd tile (dashed) · thumbnail96 dpqueued, uploading (%), uploaded, failed (retry), required (error)« JPG, PNG — 5 Mo max »DA-10, DA-14, RA-31, RA-33, RA-40
Signature pad———Not in V1: proof of delivery is a photo and a recipient name (DLV-03), captured in the back-office—
Multi-select chips fieldlabel, selected chips, « + Ajouter »—empty, error (required)Opens a searchable sheetDA-12
QR cardQR code plus code plus copyQR 200 dp min with a 4-module quiet zoneoffline (cannot create)Code shown as AB12-CD34; screen brightness raised while shownDA-16
Share rowWhatsApp (primary) · SMS · copy link · more (system sheet)52 dpcopied (« Lien copié » toast)Share text from DQA-10DA-16, RA-14
Activation funnelvertical stepper—done (time), current, upcomingThe six InvitationStep values with timestampsDA-16, BO-11
Sync status badgeSynchronisé (success) · En attente (n) (warning) · Erreur (n) (danger) · Hors ligne (neutral)app-bar badge—Tap opens DA-27DA-05, DA-06, DA-27
Offline bannerpersistent, under the app bar40 dp—Delegate: « Hors ligne — vos actions seront synchronisées ». Reseller: « Hors ligne — catalogue en cache, commande impossible »RA-36, DA-28
WhatsApp support buttonfloating (FAB) · app-bar icon · card (RA-32)FAB 56 dpregion resolved, closed hours, region unknown (picker), offlineSee regional WhatsAppRA-09, RA-12, RA-14, RA-32, RW all
Sign-up promptbottom sheet · inline card · locked tab——See guest modeRA-41
Filter sheet partscategory tree · brand list · range slider · attribute chips · toggles · sort list · sticky footer——See advanced filtersRA-13
Active-filter chips rowhorizontal scroll32 dp chips—Each chip removes one filter; « Tout effacer » at the endRA-12, RW-03
List row1, 2 or 3 lines · leading icon, avatar or thumbnail · trailing chevron, pill or value56 / 72 / 88 dpswipe actions (cart only), unread (bold plus dot)—Everywhere
Section headertitle plus « Voir tout »———RA-09, DA-05
Banner carouselslides 2:1 with dotsfull width minus marginsauto-advance 5 s (pauses on touch), single slide (no dots)Live text; countdown chipRA-09, RA-24
Countdown chip—24 dpunder 1 h (seconds shown), ended« Se termine dans 12 h 45 min »RA-09, RA-24, RA-25
App bartab root (greeting or title) · pushed (back plus title, start-aligned) · search · contextual (selection)56 dpscrolled (elevation 1)The greeting « Bonjour, … » appears on home onlyEverywhere
Bottom navigation5 items64 dpactive, badge, guest (locked items open the sign-up prompt)RA: Accueil · Catalogue · Panier · Commandes · Compte. DA: Accueil · Tournée · Prospects · Rapports · CompteTab roots
Bottom sheetmodal · full-height · with sticky footertop radius 24—A drag handle; Esc / back closes itRA-13, pickers
Dialogconfirm · destructive (with reason) · info312 dp wide—Buttons name the action (« Annuler la commande » / « Garder la commande »)RA-22, DA-20
Snackbarinfo · success · error · with action48 dp, 4 s; with undo 6 s—Never stacks more than oneEverywhere
Inline alertinfo · success · warning · danger—dismissible or persistentTitle plus one sentence plus an optional actionRA-06, RA-17, DA-20
Empty stateinline · full screen96 / 160 dp illustrationfirst use, no results (with « Réinitialiser les filtres »), clearedTitle, one sentence, one actionAll lists
Error stateinline (retry) · full page——Message plus « Réessayer » plus a small request idRA-36, DA-28
Skeletoncard · list row · text lines—static (reduce motion)Shown after 300 ms of loadingAll
Progresslinear determinate (uploads) · circular (buttons, pull to refresh)———Uploads
Avatarphoto · initials · store icon32 / 40 / 64 dp——RA-09, DA-24
Date pickerday · range—min and max datesFrench month names; week starts on Saturday or Sunday (decide on Day 2)DA-12, DA-21
Timelinevertical, with actor and time——« Validée par HOCO · 22 mai 2026 à 10:32 »RA-22, DA-09
Message bubblemine · theirs · system · resolution card—sending, failed (retry)—RA-34
Document rowPDF · image56 dpdownloading, failedNumber, date, sizeRA-30, RA-22
Scanner overlayviewfinder · torch · manual entryfull screenfound, not found—RA-11
Permission rationalelocation · camera · notificationsfull screendenied, permanently denied (link to settings)What, why, retention, who sees itDA-04

Web and back-office components

The back-office and the reseller web are built on @hoco/ui (Base UI primitives, Tailwind 4, class-variance-authority, Lucide), restyled to HOCO PRO. Every component below exists; import it from @hoco/ui/components/<file>.

ComponentFileVariants and sizesRulesUsed in
Buttonbuttondefault (graphite) · brand (green) · promo · secondary · outline · ghost · destructive · link; sm 28 · md 36 · lg 48 px; icon 32 / 36 / 40Pill; loading keeps the width and blocks a second press; destructive only for irreversible actionsAll
IconButtonicon-buttonsame variants, icon sizeslabel required (screen readers and tooltip)All
Input, Textarea, NativeSelect, Fieldinput, form-controls36 px pill; textarea 12 px corners3:1 boundary; error glyph and message; the money input shows DA as a suffix and stores centimesForms
PasswordInputpassword-input—Show / hide buttonSign-in, reset, settings
SearchInputsearch-input—Clear buttonLists, header
Checkbox, RadioCard, Switchcheckbox, radio-card, switchSwitch 36 × 22Graphite when on; choiceCardClassName for a label around a native radioForms, checkout
FilterChip, SegmentedControlfilter-chip, segmented-control32 pxSelected chips are filled graphiteFilters, view switches
Pill (status), Badgefeedback, badge24 px pill, 8 tonesA glyph per status tone; EnumPill renders categories as neutral tagsAll
Tabs, nav tabstabs, page-header44 px, rounded 2 px underlineLink-based tabs use navTabClassNameDetail pages, sections
Page, PageHeader, Grid, Split, WithAsidelayout, page-header12 columns on container widthsOne primary action on the rightAll pages
Card, ListRow, DescriptionList, IconTilecard, list-row, description-list, icon-tileCard 16 px, 20 px padding, no shadow—All
StatCard, MetricTile, DeltaChipstat—No count-up; red and green only for directionDashboards, details
Dialog, Sheet (drawer), Menu, Popoverdialog, sheet, menu, popoverDialog 20 px corners; sheet up to 560 px, 420 msScrim at 28 %; focus trapped; close labelled in the page languageAll
Toastfeedback (Toaster, toast)Graphite bar, top centreThe tone shows in the glyphAll
Callout, EmptyStatecallout, empty-stateinfo · warning · danger · success · brand · muted; empty · search · error · offline · restricted · denied · doneOne next actionForms, lists
Pagination, Stepper, StatusTimelinepagination, stepper, status-timeline——Tables, wizards, histories
Avatar, Brand, SignalBarsavatar, brand, signal-bars—Avatars are neutral (initials), never colour-codedShells, sign-in
Commercecommerce/price, commerce/quantity-control, commerce/product-card, commerce/favorite-button, commerce/countdown, commerce/order-summary, progress-bar—Prices arrive formatted by formatDzd; quantity rules stay with the callerReseller web
ConfirmDialogconfirm-dialogdefault · destructiveQuestion title naming the record, consequences in the body, the verb on the button; a destructive dialog starts on « Annuler »All
FormSection, FormFull, FormActionsform-section2 or 1 columnsTitle and help beside the fields on wide screens; the save bar sticks to the bottom with the unsaved-changes noteReseller form (BO)
SystemStatesystem-statemaintenance · update · session · offline · error · notFoundOne primary action; errors show the copyable request referenceError and not-found pages
OrderProgressorder-progressdefault · compact; hold · refused · cancelled · returnedTrack fills to the current step, three pulses at most; advance on a server event onlyOrders (RW)
DecisionBardecision-barrefuse · hold · validatePrimary last; the audit line under it, or why validation is blockedReseller validation (BO)
ReturnLinePickerreturn-line-picker—Only returnable lines; quantity capped; per-line photosReturn request (RW)
OtpInputotp-input6 boxesPaste and autofill fill every box; digits stay LTR; optional resend countdownPassword reset, sign-up
GeoPickergeo-picker—Wilaya then commune in one field; search ignores accents and takes wilaya numbersAddresses (RW), reseller form (BO)
DateFielddate-fieldwith or without presetsThe browser's date control in the pill; presets « Demain », « Dans 3 / 7 jours »Date filters (BO)
FilterBarfilter-bar—Search, filters, actions; applied filters as removable chips with « Effacer »Data tables (BO)
EvidenceUploaderevidence-uploader—Drop, paste or pick; uploading, done and failed states with retryReturns, tickets, payment proofs (RW)
ChecklistItemchecklist-itemeditable · read-only · locked48 px target; done items stay readable (no strike-through)Visit detail (BO)
NotificationRow, NotificationGroupChipnotification-rowdefault · compactGroup glyph in its tone; unread dot; one actionNotifications (RW)
WhatsAppButtonwhatsapp-buttonfab · bar · buttonGraphite with an amber dot outside the hoursEvery page (RW)
PaymentOptionpayment-option—One native radio per method; a disabled method stays visible with its reasonCheckout (RW)
ChartFrame, Heatmap, Sparklinechart-frame, heatmap, sparkline—Every chart has a table view; heatmap on the six-step heat rampDashboards, reports (BO)
Revealrevealrise · fadeStaggered entrance, capped at ten items; off under reduced motionDashboard cards (BO)
Commerce (more)commerce/cart-line, commerce/product-gallery, commerce/flash-sale-card, commerce/color-swatches, commerce/brand-tile—Cart issues shown in place with their fix; gallery never mirrors in RTL; swatches strike unavailable coloursCart, product page, promotions (RW)
Sidebar (back-office)apps/dashboard app shell208 px, light, on the app groundWordmark, FR / ع / EN, « Aller à… », items filtered by permission, user at the footBO-03
Header (reseller web)apps/web site shell64 px, whiteWordmark, search, notifications, cart badge, accountRW all

| Orders-to-confirm row | queue variant of the data table | 56 px | control failed (icon plus label), assisted, first order, live new row | Control icons: stock, credit, price, minimum amount, address; age « depuis 3 h » | BO-41 | | Kanban column | one per DeliveryStatus | 280 px wide | drag over (allowed / forbidden), empty, count | Card: order number, reseller, wilaya, items, age, carrier | BO-42 | | Calendar | week (delegates × days) · month | — | drag a tour (allowed / conflict), today | Tour chips coloured by TourStatus tone | BO-21 | | Map panel | markers · clusters · zone polygons · live delegates · track | — | stale position (more than 15 min, greyed with time), loading | A legend is always present; the side list stays in sync with the map | BO-06, BO-10, BO-19, BO-22, BO-23 | | Uploader | dropzone · image with crop ratio (1:1 product, 2:1 banner) · document | — | uploading, error (type, size), uploaded | Shows the type and size limits | BO-28, BO-51, BO-45 | | Wizard stepper | horizontal | — | step error | Upload → mapping → validation report → confirm → result | BO-32, BO-67 | | Permission matrix | modules × actions grid | — | changed (highlight), saving | 18 modules × 7 actions; row and column toggles; confirmation before saving | BO-62 | | Attribute definition editor | list grouped by category plus drawer | — | in use (warning on change) | Key, French label, type (list, number, yes/no), unit, allowed values, filter widget (chips, range, toggle), order | BO-71 | | Three-price editor | per variant: Super-gros, Demi-gros, Détail (HT), VAT, recommended price, purchase price | — | purchase price masked without permission, history | Shows the TTC equivalent next to each HT price | BO-28 | | Description list | 1 or 2 columns | — | empty value (« — ») | — | Detail pages | | Entity header | reseller · order · delegate · product | — | — | Name, business number, status pills (ResellerStatus, CustomerType), quick actions | BO-11, BO-39 | | Composer | reply · internal note (amber background, « Note interne » label) | — | sending | — | BO-56, BO-11 | | Proof viewer | image · PDF | — | zoom, rotate, download | — | BO-45, BO-42, BO-69 | | Command palette | global search | 640 px | no result | Grouped: revendeurs, commandes, produits, factures | BO-03 | | Notification panel | popover | 400 px | unread, live | « Tout marquer comme lu » | BO-03 | | Settings section | label plus description plus control plus default value | — | changed, saving, saved | Each setting shows its unit and default (« 150 m par défaut ») | BO-64 | | System pages | 403 · 404 · session expired (dialog) · maintenance | — | — | — | All | | Reseller-web header | guest · signed-in | 64 px | cart badge, live dot | Logo, search, categories menu, favourites, cart, account | RW all | | Catalog filter sidebar | see advanced filters | 280 px | — | — | RW-03 | | WhatsApp support button (web) | floating | 56 px | as mobile | Bottom right on every reseller-web page | RW all |

Maps and pins

One pin language for the delegate app and the back-office. The palette checker showed that colour alone cannot separate the pin states: red and green fall to ΔE 5.0 for deuteranopes, and orange and red are only ΔE 8.7 apart for everyone. So fill style and glyph come first, colour second.

MarkerShape and fillGlyphColourSize
Current positionDot with white ring and accuracy halo; heading cone when moving—primary (graphite), halo at 15 %16 dp
Next destinationFilled pinSequence number, whiteprimary (graphite)40 dp
Planned stopWhite pin, 2 dp outlineSequence numberprimary (graphite)32 dp
Done stopFilled pin✓success dot colour32 dp
Not doneWhite pin, 2 dp outline×danger32 dp
Late or priorityPlanned pin plus a small badge at the top end!badge warningbadge 14 dp
Prospect / reseller (DA-13, BO-10)Circle markerStore icon (reseller) or none (prospect)Tone of the ResellerStatus at 60024 dp
ClusterCircle with countNumberink on white, 2 dp border32–48 dp
Delegate (BO live map)Avatar marker with a direction arrowInitialsinfo ring; greyed when the position is older than 15 minutes36 dp

The route polyline is 4 dp primary (graphite) (done legs neutral.400, dashed). Map controls (recentre, zoom) are white 44 dp round buttons with elevation.2. Every map has a list alternative showing the same order (FLD-06).

Advanced filters

Advanced, combinable filters on mobile and web (CAT-12). Attributes are defined per category in the back-office (CAT-13, BO-71). Counts come from catalog.facets.

SectionControlRules
CatégorieTree: category, then sub-category, expandable, with countsSelecting a sub-category selects only that sub-category; the attribute sections then show that category's attributes
MarqueMulti-select list with logo and countHOCO, ACEFAST, BOROFONE
PrixRange slider with two handles plus min/max number inputs in DASigned-in only. Hidden for guests (no price facet). Bounds come from the facets
Attributes, list typeChips, multi-select (USB-C, Lightning, Micro-USB; colour swatches with name)Values with 0 results show (0) and are disabled, not hidden
Attributes, number typeRange slider with unit (W, mAh, m) and optional preset chips (« 20 W et plus »)The unit always follows the value: 20 W, 10 000 mAh
Attributes, yes/no typeToggle (« Charge rapide »)—
Disponibilité · Promotion · NouveautésTogglesEn stock uniquement hides En rupture products
TriSingle choicePertinence, prix croissant, prix décroissant, nouveautés, popularité (price sorts hidden for guests)
  • Mobile (RA-13): a full-height sheet. Sections are collapsible, and the first two are open. The sticky footer has « Réinitialiser » and « Voir 128 produits », updated live (debounced 300 ms). The Filtres button on RA-12 shows the active count, « Filtres (3) ».
  • Web (RW-03): a 280 px sidebar at 1024 px and up, applied instantly, synced to the URL. Each list shows « Afficher plus » after 6 options. Below 1024 px the sidebar becomes a drawer with the mobile footer.
  • Both: an active-filter chip row above the results (« USB-C ✕ », « 20–65 W ✕ », « Tout effacer »), the result count, and a filtered-empty state.

Live order status

Every status change reaches the reseller live while the app or page is open (ORD-15, events.stream), and as a notification otherwise.

ElementBehaviour
Live indicatorA small success dot plus « En direct » in the RA-21 and RA-22 headers while the connection is open. Grey dot plus « Reconnexion… » while retrying. « Mise à jour en pause — tirez pour actualiser » after 3 failed retries
Changed card (list)The pill crossfades (200 ms), the mini tracker advances, and the card gets one motion.highlight pulse. The order of the list does not jump while the user is scrolling
Detail screenThe tracker step animates; a new history line appears; toast « Commande CMD-2026-000158 : Validée »
Back-office modification« Votre commande a été modifiée par HOCO » banner with the changes listed
AccessibilityStatus changes are announced to screen readers (polite live region)

The back-office uses the same pattern on its own stream (/v1/admin/events): new rows highlighted in BO-41 and BO-38, sidebar counters updated.

Regional WhatsApp button

A fixed WhatsApp button in both reseller interfaces opens the support number of the user's support region (SUP-05, support.whatsapp, BO-70).

  • Placement, mobile. A 56 dp round button, bottom end, 16 dp above the bottom navigation, on Accueil, Catalogue and the order screens. On screens with a sticky bottom action (product detail, cart, checkout, forms) it moves into the app bar as an icon button, so it never covers a CTA. It is always on RA-32 as a card (region, number, hours).
  • Placement, web. Bottom right on every page, at the same size.
  • Look. color.whatsapp background, white WhatsApp glyph, elevation.2. Accessible label « Assistance WhatsApp — région Ouest ».
  • Resolution order:
    1. The region of the wilaya on the reseller's account when signed in.
    2. Otherwise the device location, asked just in time on first tap (« Utiliser ma position »): nearest commune, then its wilaya, then the region.
    3. Otherwise a wilaya picker, or the default number.
  • Tap. Opens WhatsApp (wa.me link) with a pre-filled greeting (copy from DQA-10): « Bonjour, je suis Bazar du Centre (REV-000123)… »; guests get a generic greeting.
  • Outside opening hours. A sheet first: « Notre équipe répond de 8 h 30 à 17 h — envoyer quand même ? ». If WhatsApp is not installed, show the number with « Appeler » and « Copier ».

Guest mode

Visitors can browse the catalog, product pages and promotions, and use the WhatsApp button, without an account (AUTH-10, RA-41). Prices are hidden.

ElementSpec
Price lockReplaces the price block: lock icon plus « Créez un compte pour voir les prix » (body.sm, link colour). Tapping it opens the sign-up prompt
Sign-up promptBottom sheet: title « Commandez aux prix revendeur », three benefits (prix selon votre profil, suivi en direct, assistance), primary « Créer mon compte », secondary « J'ai déjà un compte »
TriggersAdd to cart, favourite, the Panier, Commandes and Compte tabs, the price filter, and « Commander maintenant »
Guest banner (home)A dismissible card: « Vous naviguez en invité » plus « Créer un compte »
After sign-upPrices of the declared customer type appear right away (default, decision to confirm). Ordering stays locked until validation (RA-06 banner)

Patterns

Forms and validation

  • Label above the field, always visible. Mark optional fields with « (facultatif) » rather than starring required ones; most fields are required.
  • Validate on blur. After the first error, re-validate on each change. On submit, scroll to and focus the first error, and show a summary alert when there are 3 errors or more.
  • The submit button stays enabled; it is disabled only while submitting, which also prevents double submits. A slow submit (over 1 s) shows a spinner inside the button.
  • Server field errors (details.fields) map onto the fields. Other errors map onto the error-code table.
  • French copy: address the user as vous; sentence case; verbs on buttons; specific and actionable messages, no « Erreur ! ». No exclamation marks, except in success titles. Use « 1re commande », not « 1ère ». Put a no-break space before : ; ! ? and inside « ». Write units after numbers with a no-break space (20 W, 5 Mo).
SituationMessage
Phone invalid« Saisissez un numéro mobile à 10 chiffres commençant par 05, 06 ou 07. »
Phone already used« Ce numéro est déjà associé à un compte. Connectez-vous ou récupérez votre accès. »
Required field« Indiquez le nom du magasin. » (named, not « Champ obligatoire »)
Customer type missing« Choisissez votre type de client : super-gros, demi-gros ou détail. »
Password too weak« 8 caractères minimum, dont au moins un chiffre. » (rule shown before typing; final rule from AUTH-07)
File too large« Ce fichier dépasse 5 Mo. Choisissez une photo plus légère. »
Quantity unavailable« La quantité demandée n'est plus disponible. Réduisez la quantité ou retirez l'article. » (no stock figure)

Error codes and their UI

Error code (API)UI pattern
VALIDATION_FAILEDInline field errors
INVALID_CREDENTIALSInline under the form: « Numéro ou mot de passe incorrect. »
RATE_LIMITED, TOO_MANY_ATTEMPTS, OTP_RESEND_TOO_SOONAlert with a countdown; the action is disabled until it ends
ACCOUNT_SUSPENDEDBlocking page with reason and contact (WhatsApp)
ACCOUNT_PENDING_VALIDATIONBanner; ordering actions locked with an explanation
PHONE_ALREADY_EXISTSField error plus action (sign in, or account claim; in the delegate app, open the existing record)
ORDER_MIN_AMOUNT_NOT_METCart alert with progress to the minimum
STOCK_INSUFFICIENTLine error with « Ajuster » / « Retirer », never the stock figure
CREDIT_LIMIT_EXCEEDEDPayment-section error with the available credit and other methods
GPS_OUT_OF_TOLERANCEGPS validation banner with the distance
VISIT_LOCKEDRead-only visit with a lock note
VERSION_CONFLICTDialog: « Cet élément a été modifié entre-temps » plus « Recharger »
ORDER_TRANSITION_NOT_ALLOWEDToast; the action list refreshes
UPGRADE_REQUIRED (426)Full page with store link; unsynced items are kept
MAINTENANCE (503)Full page with the expected end time
UNAUTHENTICATEDMobile: back to sign-in. Web: « Session expirée » dialog, then return to the same URL
PERMISSION_DENIED, OUT_OF_SCOPE, NOT_FOUNDBack-office 403 / 404 page; in apps, « Cet élément n'est plus disponible »
OCCURRED_AT_OUT_OF_RANGESync-centre item rejected: « Heure du téléphone incorrecte — vérifiez la date et l'heure. »
INTERNAL_ERRORError state with « Réessayer » and the requestId in small text

Empty, loading, error and offline states

StateMobileWebRule
LoadingSkeletons shaped like the content after 300 ms; spinner inside buttons; pull to refreshSkeleton rows and cards; top progress bar for page changesNever block the whole screen except during submit
Empty, first useIllustration plus a sentence plus one action (« Découvrir le catalogue »)Same, in the table area—
Empty, filtered« Aucun résultat pour ces filtres » plus « Réinitialiser les filtres »SameDifferent copy from first use
ErrorInline retry in the failing block; full page only if nothing can showCard-level error with retryKeep what loaded
Offline, reseller appBanner; cached catalog and orders readable; cart, checkout and claims disabled with « Connexion requise »—The reseller app is online-first
Offline, delegate appBanner plus sync badge; everything in the tour works; actions queue in the outbox—Offline-first (SYN-04)
Live connection lostLive indicator turns to « Reconnexion… », then « Mise à jour en pause »Top-bar indicator—

Confirmation versus undo

ActionPattern
Remove a cart line, remove a favourite, mark notifications as read, remove a filterUndo snackbar (6 s), no dialog
Clear the whole cartConfirmation dialog
Submit an orderThe checkout screen is the confirmation; no extra dialog
Cancel an order, reject an assisted order, request a returnDialog with a reason
Close a tourDialog: « La clôture est définitive, sauf réouverture par HOCO. »
Sign out with unsynced items (delegate)Blocking dialog: synchronise first
Back-office: refuse an order, reject a payment, reject a return or claimModal with a required reason from the reference list
Back-office: confirm an order, approve an accountOne click plus a toast with « Annuler » for 6 s (the action is sent when the toast closes)

Destructive actions

  • Red is used only for destructive or failing things. Finishing a visit or closing a tour is a normal completion and uses primary (the reference's red « Terminer la visite » is corrected).
  • The button names the object: « Annuler la commande » / « Garder la commande », never « Oui / Non ».
  • The destructive button is never the default focus. In dialogs it sits on the end side, with « Garder… » as the secondary.
  • When a reason is required by a business rule (refusal, cancellation, rejection, visit not done), the reason field is part of the dialog.
  • In the back-office, bulk deletion of more than 10 rows asks the user to type the count.

Numbers and money

  • Canonical money format: 1 250,00 DA. Narrow no-break space as the thousands separator, comma as the decimal separator, two decimals, a no-break space before DA. Amounts are integer centimes in the API (125000).
  • Compact format 1 250 DA only on product cards, carousels and KPI tiles, and only when the centimes are zero. It is never used in carts, checkout, invoices, payments, credit or tables.
  • Large KPIs: « 1,25 M DA »; chart axes: « 250 000 » or « 1,2 M ».
  • Negative amounts (credit notes, adjustments) use the minus sign −1 250,00 DA. Percentages: 19 %; discounts: −20 %.
  • Quantities: « 12 pièces ». Distances: « 1,2 km », « 450 m ». Durations: « 12 min », « 1 h 05 ».
  • Use the shared formatters (MOB-07 in Flutter, the equivalent helper in the web apps), never hand-formatting. Numerals use tabular figures wherever values line up.
  • The client references use three formats: 1 250 DA, 850,00 DA and 1.700,00 DA (dot separator). All become the format above.

Dates and times

ContextFormat
Full timestamp« 22 mai 2026 à 10:32 »
Same day / previous day« Aujourd'hui, 10:32 » · « Hier, 18:05 »
Under an hour (notifications, live updates)« Il y a 5 min »
Current year« 22 mai »; weekday « mercredi 22 mai » (lowercase)
Tables and exports22/05/2026 and 22/05/2026 10:32
Ranges« du 3 au 9 nov. 2026 »

24-hour clock, business time zone Africa/Algiers. Abbreviated months: janv., févr., mars, avr., mai, juin, juil., août, sept., oct., nov., déc.

Phones, identifiers and addresses

  • Phones: displayed 0555 12 34 56 (4-2-2-2), entered in any form, stored E.164 +213555123456. Call and WhatsApp links use E.164. The reference's 05 58 12 34 56 grouping is not used.
  • Business numbers are shown exactly as issued, without #, with a copy button on detail screens: REV-000123, CMD-2026-000158, FAC-2026-000123, BL-2026-000123, PAY-2026-000077, TRN-2026-000045, VIS-2026-001234, TKT-2026-000045, RET-2026-000012. The invitation code is AB12-CD34.
  • Addresses: « 15, rue Didouche Mourad, Alger-Centre, 16000 Alger » (line, commune, postcode and wilaya). Pickers show wilayas with their code.
  • Coordinates: signed decimal degrees, « 35.6969, −0.6331 », with the accuracy (« ± 12 m »).

Accessibility checklist

  • Text contrast 4.5:1 or more; large text, icons and UI boundaries 3:1 or more (values in the palette).
  • Touch targets 48 × 48 dp, with 8 dp between targets; web targets 24 px minimum, 36 px in practice.
  • Every icon button has an accessible label; decorative images have none; product images use the product name.
  • Colour is never the only signal: pills have text, pins have glyphs, chart series have labels, deltas have arrows and signs.
  • Reading and focus order follow the visual order. The focus ring is visible on web and with keyboard or switch access on mobile.
  • Layouts hold at 130 % text size; checkout, visit and closing hold at 200 %.
  • Errors are identified in text, with an icon, and announced. Countdowns (OTP, offers, assisted-order expiry) are not the only way to know a deadline.
  • Reduce motion is respected; nothing flashes more than 3 times a second.
  • Web pages set lang="fr"; skip link to the main content; tables have headers; modals trap focus.
  • Outdoor check for the delegate app (DQA-03): 100 % brightness, direct sun, one hand.

RTL rules

Arabic is prepared, not designed, in V1. Everything is built so that switching dir produces a correct layout.

  • Use start and end, never left and right. Flutter: EdgeInsetsDirectional, AlignmentDirectional, Directionality. Tailwind: ps-*, pe-*, ms-*, me-*, start-*, end-*, the rtl: variant, and dir="rtl" on html.
  • Mirror: navigation arrows, chevrons, progress and stepper direction, carousels and swipe directions, the sidebar (it moves to the right), drawers and sheets that slide from a side.
  • Do not mirror: logos, product images, QR codes, check marks, clocks, media icons, charts' time axes, phone numbers, business numbers, codes and coordinates. Inside RTL text, these sit in bidi isolates.
  • Text aligns to the start; truncation happens at the end. Allow about 30 % expansion; Arabic labels are often shorter, English ones longer.
  • Digits stay Latin. The font switches to IBM Plex Sans Arabic with the Arabic adjustments.

Token delivery

  1. Source of truth for code: the CSS custom properties on :root in packages/ui/src/styles/globals.css, set from the designer's HOCO PRO design system (delivery of 31 Oct 2026). A value changes there first.
  2. Web: Tailwind reads them through @theme inline. The shadcn role names stay (--primary, --muted-foreground, --border, --ring), so every component follows; the HOCO groups are --brand*, --tone-*, --chart-*, --heat-*, --promo, --flash, --whatsapp, plus type, size, layout and motion tokens.
  3. Flutter: pnpm --filter @hoco/ui tokens resolves the variables (aliases and color-mix tints included) into packages/ui/tokens/hoco.tokens.json in the W3C Design Tokens (DTCG) format. The file is copied into hoco-mobile, where the Dart generator writes the ThemeExtensions. Widgets read context.hocoColors.tone.success.bg, never hex.
  4. No raw values in code: hex colours outside globals.css fail review. The two exceptions are data, not theme: the default colours offered for a new zone and a new segment.

Naming conventions

LayerExampleRule
CSS variable--tone-success-bg, --muted-foregroundshadcn role names kept; HOCO groups by role
Tailwind classbg-tone-success-bg, text-muted-foreground, bg-brandGenerated through @theme inline
JSON pathcolor.tone.success.bgThe CSS name split on its groups
Dartcontext.hocoColors.tone.success.bgcamelCase fields; one extension per group

Changes from the proposed v0

AreaProposed v0Shipped (HOCO PRO)
BrandBlue #1D4ED8, teal, orange, navy inkHOCO Green #7ED321 (sparingly), Graphite #1A1A1A, Light Gray #F3F3F3
Primary actionBlueGraphite; green only for the brand button
NeutralsNavy-tinted greysThe brand's neutral grays
Status tones8 tones × bg, fg, border, dot8 tones × bg, ring, fg, plus a glyph per status tone; categories as neutral tags
FontsInter, Plus Jakarta Sans, IBM Plex Sans ArabicQuicksand (Montserrat until 1 Nov 2026), IBM Plex Sans Arabic
Shape8 px controls, 12–16 px cardsPill controls, 16 px cards, 12 px tiles, 20 px dialogs
ShadowsInk-tinted elevation on cardsFlat cards; one float shadow and the drawer shadow
Brand motifTri-colour bandThe Signal
Dark modeVariables kept, not designedRemoved

Versioning. After the 31 Oct delivery, changes follow semantic versioning: a value change is a patch, a new token is a minor, a renamed or removed token is a major. See the release process.

On this page