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
| Principle | What it means | How we check it |
|---|---|---|
| Clarity in the field | Delegates 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 order | Resellers 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 numbers | Money, 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-ready | French 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. |
| Accessible | WCAG 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.
- 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.
- 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).
- 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
Brandrecord (public CDN). - In text, write the brands in capitals, HOCO, ACEFAST, BOROFONE, even where the logo is lowercase (hoco.).
- 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.
- 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.
| Step | HOCO green | Neutral (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
| Token | Value | Use | @hoco/ui variable | Flutter ColorScheme / extension |
|---|---|---|---|---|
color.bg.app | Light Gray #F3F3F3 | Screen background behind cards | --background | surfaceContainerLowest → HocoColors.bgApp |
color.bg.surface | white | Cards, sheets, inputs, tables | --card, --popover | surface |
color.bg.subtle | #EDEDED | Tiles, hovered rows, skeleton, count pills | --muted, --secondary, --accent | surfaceContainerHigh |
color.text.primary | Graphite | Titles, body, values | --foreground | onSurface |
color.text.secondary | #646464 (5.9:1; 5.05:1 on tiles) | Metadata, helper text | --muted-foreground | onSurfaceVariant |
color.primary / .hover | Graphite / #333333 | Primary buttons, selection, links, active tab | --primary | primary |
color.on-primary | white | Text and icons on primary | --primary-foreground | onPrimary |
color.brand / .hover / .on | green 500 / 400 / graphite | Brand button, brand fills, the Signal | --brand, --brand-hover, --brand-foreground | HocoColors.brand |
color.brand.text | green 700 | Brand words on white | --brand-text | HocoColors.brandText |
color.promo / .hover | orange 700 / 800 | Promotional prices and badges | --promo, --promo-hover | tertiary |
color.flash | orange 700 | Flash-sale badge and countdown | --flash | HocoColors.flash |
color.inverse | Graphite / white | Dark banners, tooltips, toasts, selected chips | --surface-inverse | inverseSurface |
color.border.subtle | #E2E2E2 | Card borders, dividers | --border | outlineVariant |
color.border.control | #8C8C8C | Inputs, outline controls, checkboxes | --input | outline |
color.border.focus | green 700, 2 px outline | Focus ring | --ring | HocoColors.focus |
color.danger | #C6361E (5.3:1) | Destructive actions, errors | --destructive, --danger | error |
color.whatsapp | #0F7A40 (white on it 5.4:1) | WhatsApp support button | --whatsapp | HocoColors.whatsapp |
color.overlay | rgb(16 16 21 / 0.28) | Scrim behind sheets and dialogs | --scrim | scrim |
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 ».
| Tone | Meaning | Pill background | Text | Glyph | Examples from enums.json |
|---|---|---|---|---|---|
neutral | Exists, not started | #EDEDED | Graphite | — | Brouillon, Planifiée, À préparer |
muted | Over or out of the flow | #EDEDED | #6E6E6E | minus-circle | Annulée, Archivé, Expirée |
brand | Categories, identity | #ECEDEE | Graphite | — | Super-gros, Collaborateur |
info | Acknowledged, waiting its turn | info blue at 10 % | info blue darkened | info | Envoyée, À visiter, Ouverte |
progress | Moving forward | graphite at 8 % | graphite | circle-dashed | En préparation, Expédiée, En cours |
success | Done or positive | success at 10 % | success darkened | circle-check | Validée, Livrée, Payée, En stock |
warning | Needs attention | warning at 11 % | warning darkened | clock | En attente, À relancer, Impayée |
danger | Failed, refused, blocking | danger at 9 % | danger darkened | circle-x | Refusé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:
| Value | UI label (fr) | Meaning |
|---|---|---|
DRAFT | Brouillon | Assisted order being prepared by a delegate, or a back-office draft. |
SUBMITTED | Envoyée | Placed by the reseller (or an assisted order confirmed by the reseller); waits for the back-office. Orders are never confirmed automatically. |
PENDING | En attente | On hold: needs information, a stock/credit decision, or the reseller's confirmation of an assisted order. |
VALIDATED | Validée | Confirmed by a back-office user (orders.validate); stock reserved (default setting). |
REFUSED | Refusée | Refused with a reason; terminal. |
PREPARING | En préparation | Picking in progress. |
READY | Prête | Packed, waiting for the carrier. |
SHIPPED | Expédiée | Handed to the carrier; stock deducted (default setting). |
DELIVERED | Livrée | Proof of delivery recorded. |
CANCELLED | Annulée | Cancelled by the reseller (before confirmation) or the back-office (with reason); reserved quantities return to stock automatically. |
RETURNED | Retournée | Returned after delivery (accepted return or refused at the door); quantities re-enter stock automatically on reception. |
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
| Slot | Colour | Variable | Note |
|---|---|---|---|
| 1 | HOCO green 600 #5A9F14 | --chart-1 | Primary series (3.2:1) |
| 2 | Graphite #1A1A1A | --chart-2 | Secondary series |
| 3 | Orange 500 #F07D3A | --chart-3 | Warm comparison |
| 4 | HOCO green 300 #ABE066 | --chart-4 | 4th series |
| 5 | Grey #6E6E6E | --chart-5 | 5th series |
| Other | #CFCFCF | --chart-6 | Everything 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
| Role | Family | Weights | Loading |
|---|---|---|---|
| Latin (UI, display, numbers) | Quicksand (variable) | 300–700; in use 400, 500, 600, 700 | next/font (self-hosted), --font-quicksand |
| Arabic (all roles) | IBM Plex Sans Arabic | 400, 500, 600, 700 | next/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)
| Token | Size / line height | Weight | Use |
|---|---|---|---|
type.page-title | 28 / 1.15 (24 in page headers) | 700, −0.02 em | Page title |
type.section-title | 20 / 1.15 | 600 | Section title |
type.card-title | 17 / 1.3 (15 in cards) | 600 | Card, dialog and drawer title |
type.stat | 24 / 1 | 700, tnum | KPI and stat values |
type.body | 14 / 1.45 | 400 | Default body (inputs 16 on phones, against zoom) |
type.body-strong | 14 / 1.45 | 500 | Emphasis, list titles |
type.label | 13 / 1.3 | 500 | Form labels, tabs, table text |
type.caption | 12 / 1.3 | 500 | Helper text, metadata, table headers |
type.button | 13 / 1 | 600 | Buttons (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).
| Token | Value | Typical use |
|---|---|---|
space.1 | 4 | Label-to-input gap, icon-to-text in pills |
space.2 | 8 | Gap between chips and stacked buttons |
space.3 | 12 | Compact padding, list-row padding |
space.4 | 16 | Screen margin (mobile), card gap, form-field gap |
space.5 | 20 | Card padding |
space.6 | 24 | Section gap, page margin (tablet) |
space.8 | 32 | Page margin (desktop), empty-state padding |
space.12 | 48 | Minimum touch target (mobile) |
Grid and breakpoints
| Context | Design at | Grid | Notes |
|---|---|---|---|
| Mobile (RA, DA) | 360 × 800 dp, checked at 390 | 4 columns, 16 dp margins | App bar 56, tab bar 72, sticky CTA bar 72 |
| Back-office | 1440 × 900 | 12 columns responding to their container (480 / 720 / 960 / 1200), gutters 12 → 24 | Sidebar 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 web | 1440, 1024, 768, 390 | Same grid; page width 1440 | Catalogue 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
| Token | Value | Use |
|---|---|---|
radius.pill | 9999 | Every button, input, select, badge, pill and chip, switches, avatars |
radius.sm | 8 | Small chips, skeleton blocks |
radius.md | 10 | Menu items |
radius.lg | 12 | Tiles, rows, image tiles, textareas, callouts, toasts |
radius.xl | 16 | Cards, popovers |
radius.2xl | 20 | Dialogs |
Elevation and shadows
A flat system: cards sit on the gray ground with a hairline and no shadow. Shadows are only for what floats.
| Token | Value | Use |
|---|---|---|
shadow.float | 0 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.window | 0 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
| Token | Value | Use |
|---|---|---|
border.width | 1 | Cards, dividers, inputs at rest |
| selected | graphite border, no extra ring | Choice cards (address, payment method), selected chips are filled graphite |
focus.ring | 2 px solid green 700; offset 2 px (0 on inputs) | Every focusable element on keyboard focus, never removed |
| error | danger border plus a 3 px danger ring at 15 %, and an error glyph | Field in error |
Iconography
- Set: Lucide —
lucide-reacton the web,lucide_icons_flutterin 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).
Iconin@hoco/uiapplies 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 (
fliputility); product, brand, media and status icons never do. - No emoji; icon-only buttons always carry a label (
IconButtonrequires 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
| Token | Duration | Use |
|---|---|---|
dur.micro / fast | 90 / 120 ms | Press, hover, chip and checkbox changes |
dur.base | 200 ms | Buttons, fades, menus |
dur.slow | 320 ms | Scrims, drawer exit |
dur.sheet | 420 ms | Drawers 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
| Component | Variants | Sizes | Extra states | Content rules | Used in |
|---|---|---|---|---|---|
| Button | primary · secondary (outline) · tertiary (text) · danger · accent (offers only) · WhatsApp (whatsapp, share only) | lg 52 · md 44 · sm 36 dp | loading (spinner replaces the label, width kept) | Verb + object, sentence case, 24 characters or fewer (« Valider la commande »). One primary per screen. Optional leading icon | Everywhere |
| Icon button | standard · tonal · with badge | 24 glyph, 48 target | selected (favourite), badge 1–99, 99+ | Always has a semantics label (« Ajouter aux favoris ») | App bars, cards |
| Text field | outlined, label above · prefix (+213) · leading icon · password (show/hide) · multiline with counter · search (pill) | 48 dp | focused, 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 options | empty search, loading, offline (cached list) | Wilayas shown with their code: « 31 — Oran » | RA-05, RA-19, DA-14 |
| Phone field | national display, E.164 storage | 48 dp | invalid, already exists | Displays 0555 12 34 56; accepts +213… and 00213… | RA-03, RA-07, DA-02, DA-14 |
| OTP input | 6 boxes, SMS autofill | 48 × 56 dp boxes | filled, error, expired, resend countdown | « Renvoyer le code dans 0:42 » | RA-04, RA-08, DA-03 |
| Customer-type cards | radio cards ×3 (Super-gros, Demi-gros, Détail) | full width, 72 dp | selected (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 · switch | standard | 24 glyph, 48 target | indeterminate (checkbox), error | The label is clickable | Settings, filters |
| Chip | filter (single or multi) · input (removable ×) · category tile (image and label) · brand (logo and count) | sm 32 · md 36 dp | selected, disabled with (0) | Labels of 20 characters or fewer; the count in brackets is secondary text | RA-12, RA-13, DA-12 |
| Segmented control | 2–4 segments · tone variant (interest level) | 40 dp | selected | Tone variant: Faible muted, Moyen info, Élevé success; the selected segment is filled with its tone | DA-12, DA-21 |
| Tabs | underline, scrollable | 48 dp | active, with count | Active colour is always primary; no tab in a different colour | RA-12, DA-06 |
| Quantity stepper | compact (card) · standard (detail, cart) | 32 / 40 dp | at min (− becomes a delete icon in the cart), at max (+ disabled, helper « Max : 20 pièces par commande »), editing (numeric keyboard), updating (spinner), error | Steps follow the minimum and step of the product. The maximum is the per-order limit, never the stock level | RA-12, RA-14, RA-17, DA-17 |
| Price block | card · detail · promo (struck original price, discount badge −20 %) · total row | price.md / price.lg | locked (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 V1 | RA-12, RA-14, RA-17, DA-17 |
| Availability indicator | pill · inline text | label.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 card | grid · list row (search, assisted order) · mini (carousels) | grid 160 × 260 dp min | skeleton, out of stock (add disabled), in cart (stepper with quantity), guest (price lock), promo badge | 1:1 image on white; optional brand line in capitals; name 2 lines max with ellipsis; price block; availability; add button; favourite | RA-09, RA-12, RA-16, DA-17 |
| Cart line | standard · with error | 88 dp | price changed (old price struck), quantity unavailable (error plus « Ajuster » or « Retirer »), removed (undo) | Unit price × quantity = line total, all visible | RA-17, DA-17 |
| Order summary | cart · checkout · confirmation | — | minimum not reached (progress bar plus remaining amount), free-delivery threshold | Lines: sous-total, remises, livraison (Offerte), Total TTC, dont TVA | RA-17, RA-18, RA-20 |
| Payment method selector | radio cards | 64 dp | unavailable (method disabled in settings: hidden), credit insufficient (error with available credit) | Lists the methods enabled in settings only (FIN-02) | RA-18 |
| Order card | list · compact (home, DA-18) | — | live-changed (motion.highlight), assisted badge, first-order badge | CMD-2026-000158 (no #), date, status pill, first item and « + 2 articles », amount, mini progress bar | RA-21, DA-18 |
| Progress tracker | horizontal 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 screen | RA-21, RA-22, RW-09 |
| Live indicator | inline dot plus label · toast | — | connected (« En direct »), reconnecting, paused (« Mise à jour en pause — tirez pour actualiser ») | See live order status | RA-21, RA-22, RW-08, RW-09 |
| Return and after-sales line picker | order line with checkbox and quantity stepper | 72 dp | selected, quantity above the delivered quantity (error), photo required (error) | Lines of one delivered order only; quantity limited to the delivered quantity | RA-33, RA-40 |
| Status pill | the 8 tones | sm 20 · md 24 dp | — | Label = fr from enums.json; optional 6 dp dot | Everywhere |
| KPI tile | compact (3 per row) · wide (2 per row) · with progress | 104 dp min height | loading, no data (« — ») | Icon in a tinted 40 dp circle, number.kpi value, label of 2 lines max | DA-05, DA-06, DA-20 |
| Objective progress | linear with value/target · compact | 8 dp bar | achieved (success), behind (warning), no objective | « 6 / 12 visites · 50 % » | DA-05, DA-20, DA-22 |
| Finance tile | balance · available credit (with ceiling bar) | 2 per row | overdue warning | No loyalty tile in V1 | RA-09, RA-29 |
| Visit row | planned · next (highlighted) · done (muted) · not done | 72 dp | late (En retard icon and label), priority (Prioritaire flag), pending sync | Sequence number badge, store name, commune, time slot, distance « à 1,2 km », status pill, one action | DA-05, DA-06, DA-08 |
| Map pins and map card | see maps and pins | — | — | — | DA-06, DA-08, DA-13 |
| GPS validation banner | inline card | 56 dp | acquiring (accuracy shown), confirmed (« Position confirmée · ± 12 m »), out of tolerance (« Vous êtes à 420 m du point de vente »), weak accuracy, GPS off, permission denied | Tolerance 150 m and accuracy 100 m are settings; the copy shows the actual values | DA-08, DA-10 |
| Checklist item | standard · required | 56 dp | checked, required missing (error), locked (visit done) | The whole row is the target | DA-10 |
| Photo capture tile | add tile (dashed) · thumbnail | 96 dp | queued, 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 field | label, selected chips, « + Ajouter » | — | empty, error (required) | Opens a searchable sheet | DA-12 |
| QR card | QR code plus code plus copy | QR 200 dp min with a 4-module quiet zone | offline (cannot create) | Code shown as AB12-CD34; screen brightness raised while shown | DA-16 |
| Share row | WhatsApp (primary) · SMS · copy link · more (system sheet) | 52 dp | copied (« Lien copié » toast) | Share text from DQA-10 | DA-16, RA-14 |
| Activation funnel | vertical stepper | — | done (time), current, upcoming | The six InvitationStep values with timestamps | DA-16, BO-11 |
| Sync status badge | Synchronisé (success) · En attente (n) (warning) · Erreur (n) (danger) · Hors ligne (neutral) | app-bar badge | — | Tap opens DA-27 | DA-05, DA-06, DA-27 |
| Offline banner | persistent, under the app bar | 40 dp | — | Delegate: « Hors ligne — vos actions seront synchronisées ». Reseller: « Hors ligne — catalogue en cache, commande impossible » | RA-36, DA-28 |
| WhatsApp support button | floating (FAB) · app-bar icon · card (RA-32) | FAB 56 dp | region resolved, closed hours, region unknown (picker), offline | See regional WhatsApp | RA-09, RA-12, RA-14, RA-32, RW all |
| Sign-up prompt | bottom sheet · inline card · locked tab | — | — | See guest mode | RA-41 |
| Filter sheet parts | category tree · brand list · range slider · attribute chips · toggles · sort list · sticky footer | — | — | See advanced filters | RA-13 |
| Active-filter chips row | horizontal scroll | 32 dp chips | — | Each chip removes one filter; « Tout effacer » at the end | RA-12, RW-03 |
| List row | 1, 2 or 3 lines · leading icon, avatar or thumbnail · trailing chevron, pill or value | 56 / 72 / 88 dp | swipe actions (cart only), unread (bold plus dot) | — | Everywhere |
| Section header | title plus « Voir tout » | — | — | — | RA-09, DA-05 |
| Banner carousel | slides 2:1 with dots | full width minus margins | auto-advance 5 s (pauses on touch), single slide (no dots) | Live text; countdown chip | RA-09, RA-24 |
| Countdown chip | — | 24 dp | under 1 h (seconds shown), ended | « Se termine dans 12 h 45 min » | RA-09, RA-24, RA-25 |
| App bar | tab root (greeting or title) · pushed (back plus title, start-aligned) · search · contextual (selection) | 56 dp | scrolled (elevation 1) | The greeting « Bonjour, … » appears on home only | Everywhere |
| Bottom navigation | 5 items | 64 dp | active, badge, guest (locked items open the sign-up prompt) | RA: Accueil · Catalogue · Panier · Commandes · Compte. DA: Accueil · Tournée · Prospects · Rapports · Compte | Tab roots |
| Bottom sheet | modal · full-height · with sticky footer | top radius 24 | — | A drag handle; Esc / back closes it | RA-13, pickers |
| Dialog | confirm · destructive (with reason) · info | 312 dp wide | — | Buttons name the action (« Annuler la commande » / « Garder la commande ») | RA-22, DA-20 |
| Snackbar | info · success · error · with action | 48 dp, 4 s; with undo 6 s | — | Never stacks more than one | Everywhere |
| Inline alert | info · success · warning · danger | — | dismissible or persistent | Title plus one sentence plus an optional action | RA-06, RA-17, DA-20 |
| Empty state | inline · full screen | 96 / 160 dp illustration | first use, no results (with « Réinitialiser les filtres »), cleared | Title, one sentence, one action | All lists |
| Error state | inline (retry) · full page | — | — | Message plus « Réessayer » plus a small request id | RA-36, DA-28 |
| Skeleton | card · list row · text lines | — | static (reduce motion) | Shown after 300 ms of loading | All |
| Progress | linear determinate (uploads) · circular (buttons, pull to refresh) | — | — | — | Uploads |
| Avatar | photo · initials · store icon | 32 / 40 / 64 dp | — | — | RA-09, DA-24 |
| Date picker | day · range | — | min and max dates | French month names; week starts on Saturday or Sunday (decide on Day 2) | DA-12, DA-21 |
| Timeline | vertical, with actor and time | — | — | « Validée par HOCO · 22 mai 2026 à 10:32 » | RA-22, DA-09 |
| Message bubble | mine · theirs · system · resolution card | — | sending, failed (retry) | — | RA-34 |
| Document row | PDF · image | 56 dp | downloading, failed | Number, date, size | RA-30, RA-22 |
| Scanner overlay | viewfinder · torch · manual entry | full screen | found, not found | — | RA-11 |
| Permission rationale | location · camera · notifications | full screen | denied, permanently denied (link to settings) | What, why, retention, who sees it | DA-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>.
| Component | File | Variants and sizes | Rules | Used in |
|---|---|---|---|---|
| Button | button | default (graphite) · brand (green) · promo · secondary · outline · ghost · destructive · link; sm 28 · md 36 · lg 48 px; icon 32 / 36 / 40 | Pill; loading keeps the width and blocks a second press; destructive only for irreversible actions | All |
| IconButton | icon-button | same variants, icon sizes | label required (screen readers and tooltip) | All |
| Input, Textarea, NativeSelect, Field | input, form-controls | 36 px pill; textarea 12 px corners | 3:1 boundary; error glyph and message; the money input shows DA as a suffix and stores centimes | Forms |
| PasswordInput | password-input | — | Show / hide button | Sign-in, reset, settings |
| SearchInput | search-input | — | Clear button | Lists, header |
| Checkbox, RadioCard, Switch | checkbox, radio-card, switch | Switch 36 × 22 | Graphite when on; choiceCardClassName for a label around a native radio | Forms, checkout |
| FilterChip, SegmentedControl | filter-chip, segmented-control | 32 px | Selected chips are filled graphite | Filters, view switches |
| Pill (status), Badge | feedback, badge | 24 px pill, 8 tones | A glyph per status tone; EnumPill renders categories as neutral tags | All |
| Tabs, nav tabs | tabs, page-header | 44 px, rounded 2 px underline | Link-based tabs use navTabClassName | Detail pages, sections |
| Page, PageHeader, Grid, Split, WithAside | layout, page-header | 12 columns on container widths | One primary action on the right | All pages |
| Card, ListRow, DescriptionList, IconTile | card, list-row, description-list, icon-tile | Card 16 px, 20 px padding, no shadow | — | All |
| StatCard, MetricTile, DeltaChip | stat | — | No count-up; red and green only for direction | Dashboards, details |
| Dialog, Sheet (drawer), Menu, Popover | dialog, sheet, menu, popover | Dialog 20 px corners; sheet up to 560 px, 420 ms | Scrim at 28 %; focus trapped; close labelled in the page language | All |
| Toast | feedback (Toaster, toast) | Graphite bar, top centre | The tone shows in the glyph | All |
| Callout, EmptyState | callout, empty-state | info · warning · danger · success · brand · muted; empty · search · error · offline · restricted · denied · done | One next action | Forms, lists |
| Pagination, Stepper, StatusTimeline | pagination, stepper, status-timeline | — | — | Tables, wizards, histories |
| Avatar, Brand, SignalBars | avatar, brand, signal-bars | — | Avatars are neutral (initials), never colour-coded | Shells, sign-in |
| Commerce | commerce/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 caller | Reseller web |
| ConfirmDialog | confirm-dialog | default · destructive | Question title naming the record, consequences in the body, the verb on the button; a destructive dialog starts on « Annuler » | All |
| FormSection, FormFull, FormActions | form-section | 2 or 1 columns | Title and help beside the fields on wide screens; the save bar sticks to the bottom with the unsaved-changes note | Reseller form (BO) |
| SystemState | system-state | maintenance · update · session · offline · error · notFound | One primary action; errors show the copyable request reference | Error and not-found pages |
| OrderProgress | order-progress | default · compact; hold · refused · cancelled · returned | Track fills to the current step, three pulses at most; advance on a server event only | Orders (RW) |
| DecisionBar | decision-bar | refuse · hold · validate | Primary last; the audit line under it, or why validation is blocked | Reseller validation (BO) |
| ReturnLinePicker | return-line-picker | — | Only returnable lines; quantity capped; per-line photos | Return request (RW) |
| OtpInput | otp-input | 6 boxes | Paste and autofill fill every box; digits stay LTR; optional resend countdown | Password reset, sign-up |
| GeoPicker | geo-picker | — | Wilaya then commune in one field; search ignores accents and takes wilaya numbers | Addresses (RW), reseller form (BO) |
| DateField | date-field | with or without presets | The browser's date control in the pill; presets « Demain », « Dans 3 / 7 jours » | Date filters (BO) |
| FilterBar | filter-bar | — | Search, filters, actions; applied filters as removable chips with « Effacer » | Data tables (BO) |
| EvidenceUploader | evidence-uploader | — | Drop, paste or pick; uploading, done and failed states with retry | Returns, tickets, payment proofs (RW) |
| ChecklistItem | checklist-item | editable · read-only · locked | 48 px target; done items stay readable (no strike-through) | Visit detail (BO) |
| NotificationRow, NotificationGroupChip | notification-row | default · compact | Group glyph in its tone; unread dot; one action | Notifications (RW) |
| WhatsAppButton | whatsapp-button | fab · bar · button | Graphite with an amber dot outside the hours | Every page (RW) |
| PaymentOption | payment-option | — | One native radio per method; a disabled method stays visible with its reason | Checkout (RW) |
| ChartFrame, Heatmap, Sparkline | chart-frame, heatmap, sparkline | — | Every chart has a table view; heatmap on the six-step heat ramp | Dashboards, reports (BO) |
| Reveal | reveal | rise · fade | Staggered entrance, capped at ten items; off under reduced motion | Dashboard 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 colours | Cart, product page, promotions (RW) |
| Sidebar (back-office) | apps/dashboard app shell | 208 px, light, on the app ground | Wordmark, FR / ع / EN, « Aller à… », items filtered by permission, user at the foot | BO-03 |
| Header (reseller web) | apps/web site shell | 64 px, white | Wordmark, search, notifications, cart badge, account | RW 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.
| Marker | Shape and fill | Glyph | Colour | Size |
|---|---|---|---|---|
| Current position | Dot with white ring and accuracy halo; heading cone when moving | — | primary (graphite), halo at 15 % | 16 dp |
| Next destination | Filled pin | Sequence number, white | primary (graphite) | 40 dp |
| Planned stop | White pin, 2 dp outline | Sequence number | primary (graphite) | 32 dp |
| Done stop | Filled pin | ✓ | success dot colour | 32 dp |
| Not done | White pin, 2 dp outline | × | danger | 32 dp |
| Late or priority | Planned pin plus a small badge at the top end | ! | badge warning | badge 14 dp |
| Prospect / reseller (DA-13, BO-10) | Circle marker | Store icon (reseller) or none (prospect) | Tone of the ResellerStatus at 600 | 24 dp |
| Cluster | Circle with count | Number | ink on white, 2 dp border | 32–48 dp |
| Delegate (BO live map) | Avatar marker with a direction arrow | Initials | info ring; greyed when the position is older than 15 minutes | 36 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.
| Section | Control | Rules |
|---|---|---|
| Catégorie | Tree: category, then sub-category, expandable, with counts | Selecting a sub-category selects only that sub-category; the attribute sections then show that category's attributes |
| Marque | Multi-select list with logo and count | HOCO, ACEFAST, BOROFONE |
| Prix | Range slider with two handles plus min/max number inputs in DA | Signed-in only. Hidden for guests (no price facet). Bounds come from the facets |
| Attributes, list type | Chips, multi-select (USB-C, Lightning, Micro-USB; colour swatches with name) | Values with 0 results show (0) and are disabled, not hidden |
| Attributes, number type | Range 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 type | Toggle (« Charge rapide ») | — |
| Disponibilité · Promotion · Nouveautés | Toggles | En stock uniquement hides En rupture products |
| Tri | Single choice | Pertinence, 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.
| Element | Behaviour |
|---|---|
| Live indicator | A 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 screen | The 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 |
| Accessibility | Status 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.whatsappbackground, white WhatsApp glyph,elevation.2. Accessible label « Assistance WhatsApp — région Ouest ». - Resolution order:
- The region of the wilaya on the reseller's account when signed in.
- Otherwise the device location, asked just in time on first tap (« Utiliser ma position »): nearest commune, then its wilaya, then the region.
- Otherwise a wilaya picker, or the default number.
- Tap. Opens WhatsApp (
wa.melink) 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.
| Element | Spec |
|---|---|
| Price lock | Replaces 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 prompt | Bottom 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 » |
| Triggers | Add 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-up | Prices 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).
| Situation | Message |
|---|---|
| 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_FAILED | Inline field errors |
INVALID_CREDENTIALS | Inline under the form: « Numéro ou mot de passe incorrect. » |
RATE_LIMITED, TOO_MANY_ATTEMPTS, OTP_RESEND_TOO_SOON | Alert with a countdown; the action is disabled until it ends |
ACCOUNT_SUSPENDED | Blocking page with reason and contact (WhatsApp) |
ACCOUNT_PENDING_VALIDATION | Banner; ordering actions locked with an explanation |
PHONE_ALREADY_EXISTS | Field error plus action (sign in, or account claim; in the delegate app, open the existing record) |
ORDER_MIN_AMOUNT_NOT_MET | Cart alert with progress to the minimum |
STOCK_INSUFFICIENT | Line error with « Ajuster » / « Retirer », never the stock figure |
CREDIT_LIMIT_EXCEEDED | Payment-section error with the available credit and other methods |
GPS_OUT_OF_TOLERANCE | GPS validation banner with the distance |
VISIT_LOCKED | Read-only visit with a lock note |
VERSION_CONFLICT | Dialog: « Cet élément a été modifié entre-temps » plus « Recharger » |
ORDER_TRANSITION_NOT_ALLOWED | Toast; 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 |
UNAUTHENTICATED | Mobile: back to sign-in. Web: « Session expirée » dialog, then return to the same URL |
PERMISSION_DENIED, OUT_OF_SCOPE, NOT_FOUND | Back-office 403 / 404 page; in apps, « Cet élément n'est plus disponible » |
OCCURRED_AT_OUT_OF_RANGE | Sync-centre item rejected: « Heure du téléphone incorrecte — vérifiez la date et l'heure. » |
INTERNAL_ERROR | Error state with « Réessayer » and the requestId in small text |
Empty, loading, error and offline states
| State | Mobile | Web | Rule |
|---|---|---|---|
| Loading | Skeletons shaped like the content after 300 ms; spinner inside buttons; pull to refresh | Skeleton rows and cards; top progress bar for page changes | Never block the whole screen except during submit |
| Empty, first use | Illustration 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 » | Same | Different copy from first use |
| Error | Inline retry in the failing block; full page only if nothing can show | Card-level error with retry | Keep what loaded |
| Offline, reseller app | Banner; cached catalog and orders readable; cart, checkout and claims disabled with « Connexion requise » | — | The reseller app is online-first |
| Offline, delegate app | Banner plus sync badge; everything in the tour works; actions queue in the outbox | — | Offline-first (SYN-04) |
| Live connection lost | Live indicator turns to « Reconnexion… », then « Mise à jour en pause » | Top-bar indicator | — |
Confirmation versus undo
| Action | Pattern |
|---|---|
| Remove a cart line, remove a favourite, mark notifications as read, remove a filter | Undo snackbar (6 s), no dialog |
| Clear the whole cart | Confirmation dialog |
| Submit an order | The checkout screen is the confirmation; no extra dialog |
| Cancel an order, reject an assisted order, request a return | Dialog with a reason |
| Close a tour | Dialog: « 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 claim | Modal with a required reason from the reference list |
| Back-office: confirm an order, approve an account | One 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 DAonly 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 DAand1.700,00 DA(dot separator). All become the format above.
Dates and times
| Context | Format |
|---|---|
| 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 exports | 22/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's05 58 12 34 56grouping 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 isAB12-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-*, thertl:variant, anddir="rtl"onhtml. - 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
- Source of truth for code: the CSS custom properties on
:rootinpackages/ui/src/styles/globals.css, set from the designer's HOCO PRO design system (delivery of 31 Oct 2026). A value changes there first. - 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. - Flutter:
pnpm --filter @hoco/ui tokensresolves the variables (aliases andcolor-mixtints included) intopackages/ui/tokens/hoco.tokens.jsonin the W3C Design Tokens (DTCG) format. The file is copied intohoco-mobile, where the Dart generator writes theThemeExtensions. Widgets readcontext.hocoColors.tone.success.bg, never hex. - No raw values in code: hex colours outside
globals.cssfail review. The two exceptions are data, not theme: the default colours offered for a new zone and a new segment.
Naming conventions
| Layer | Example | Rule |
|---|---|---|
| CSS variable | --tone-success-bg, --muted-foreground | shadcn role names kept; HOCO groups by role |
| Tailwind class | bg-tone-success-bg, text-muted-foreground, bg-brand | Generated through @theme inline |
| JSON path | color.tone.success.bg | The CSS name split on its groups |
| Dart | context.hocoColors.tone.success.bg | camelCase fields; one extension per group |
Changes from the proposed v0
| Area | Proposed v0 | Shipped (HOCO PRO) |
|---|---|---|
| Brand | Blue #1D4ED8, teal, orange, navy ink | HOCO Green #7ED321 (sparingly), Graphite #1A1A1A, Light Gray #F3F3F3 |
| Primary action | Blue | Graphite; green only for the brand button |
| Neutrals | Navy-tinted greys | The brand's neutral grays |
| Status tones | 8 tones × bg, fg, border, dot | 8 tones × bg, ring, fg, plus a glyph per status tone; categories as neutral tags |
| Fonts | Inter, Plus Jakarta Sans, IBM Plex Sans Arabic | Quicksand (Montserrat until 1 Nov 2026), IBM Plex Sans Arabic |
| Shape | 8 px controls, 12–16 px cards | Pill controls, 16 px cards, 12 px tiles, 20 px dialogs |
| Shadows | Ink-tinted elevation on cards | Flat cards; one float shadow and the drawer shadow |
| Brand motif | Tri-colour band | The Signal |
| Dark mode | Variables kept, not designed | Removed |
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.
Design plan (21 days)
How Akram designs the four HOCO interfaces in 21 working days — design system first, inventory validation on day 5, then every V1 screen — with inputs, outputs, reviews, client checkpoints and the work that follows handoff.
Client reference screens
The 16 reference screens HOCO attached to the cahier — what each one asks for, what we keep, what we change and why, and which inventory screens it maps to.