---
name: KeluargaMart ERP Frontend
description: Desk-and-mouse ERP for warehouse, finance, sales and HR staff. Operate mode — density and scanability outrank expression. Brand lives in precise details, never decoration.
mode: Operate
stack: Vue 3 + Vite + Tailwind CSS v4 (tokens in src/assets/base.css @theme)
brand:
50: "#F0F5FF"
100: "#DCE7FC"
200: "#C4D4F5"
300: "#9DB8EA"
400: "#28459A"
500: "#142B80"
600: "#102268"
700: "#071F68"
800: "#051952"
900: "#04123D"
surfaces:
page: "#F5F8FF"
surface: "#FFFFFF"
surface-muted: "#EEF3FA"
border: "#D9E2F0"
border-strong: "#BCCBE4"
ink:
ink: "#14213D"
ink-muted: "#475569"
ink-subtle: "#64748B"
ink-disabled: "#94A3B8"
status:
primary: { fg: "#142B80", soft: "#DCE7FC", line: "#C4D4F5" }
success: { fg: "#15803D", soft: "#ECFAF0", line: "#BBF7D0" }
warning: { fg: "#B45309", soft: "#FFF8EB", line: "#FDE68A" }
danger: { fg: "#C0372C", soft: "#FBE7E4", line: "#F3C6C0" }
info: { fg: "#0E7490", soft: "#ECFEFF", line: "#A5F3FC" }
neutral: { fg: "#475569", soft: "#EEF3FA", line: "#D9E2F0" }
typography:
headline:
fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif"
fontSize: "1.5rem"
lineHeight: 1.25
fontWeight: 600
letterSpacing: "-0.015em"
title:
fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif"
fontSize: "1.125rem"
lineHeight: 1.4
fontWeight: 600
body:
fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif"
fontSize: "0.875rem"
lineHeight: 1.5
fontWeight: 400
body-sm:
fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif"
fontSize: "0.8125rem"
lineHeight: 1.45
fontWeight: 400
label:
fontFamily: "'IBM Plex Mono', ui-monospace, SFMono-Regular, monospace"
fontSize: "0.75rem"
lineHeight: 1.33
fontWeight: 500
letterSpacing: "0.04em"
micro:
fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif"
fontSize: "0.6875rem"
lineHeight: 1.3
fontWeight: 500
badge:
fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif"
fontSize: "0.625rem"
lineHeight: 1.3
fontWeight: 600
tiny:
fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif"
fontSize: "0.5625rem"
lineHeight: 1.3
fontWeight: 600
rounded:
badge: "6px"
button: "8px"
input: "8px"
card: "12px"
modal: "16px"
spacing: [4, 8, 12, 16, 20, 24, 32, 40, 48]
elevation:
card: "0 1px 2px rgba(20,33,61,.04), 0 1px 3px rgba(20,33,61,.06)"
popover: "0 6px 20px rgba(20,33,61,.10), 0 1px 3px rgba(20,33,61,.06)"
modal: "0 16px 48px rgba(20,33,61,.18), 0 4px 12px rgba(20,33,61,.08)"
focus: "outline: 2px solid #142B80; outline-offset: 2px"
components:
field:
height: "44px"
rounded: "8px"
border: "{surfaces.border-strong}"
background: "{surfaces.surface}"
button-primary:
height: "44px"
background: "{brand.500}"
hoverBackground: "{brand.600}"
textColor: "#FFFFFF"
rounded: "8px"
card:
background: "{surfaces.surface}"
border: "1px solid {surfaces.border}"
rounded: "12px"
shadow: "{elevation.card}"
badge:
rounded: "6px"
fontSize: "{typography.micro.fontSize}"
fontWeight: 600
---
> **Family contract:** shared tokens, per-app deltas, and the sync protocol live in `UNIFORMITY.md` — byte-identical across B2B, ERP, and POS. Edit the B2B copy, then run `bash scripts/sync-design.sh`.
# Design system — KeluargaMart ERP
## Mode
**Operate.** This is a tool people sit in front of for eight hours with a mouse
and a keyboard. Every screen is a list, a form, or a number. Density and
scanability beat expression. There are no hero sections, no decorative
gradients, no marketing surfaces. The brand shows up in precise details:
the navy, the mono numerals, the borders, one consistent focus ring.
## North star
A warehouse clerk should be able to find a stock figure in under two seconds,
and a finance user should never have to guess whether a colour means "normal"
or "act now". Nothing on screen is ambiguous, nothing is decorative, and
nothing important is ever truncated.
## Colour roles
The navy scale is the only brand hue. `brand-500` (`#142B80`) is the primary —
buttons, active nav, links, focus. `brand-600` is its hover. `brand-900` is the
scrim behind modals. The lighter steps (`50`–`200`) are soft fills for active
states and primary badges; `300`–`400` exist for charts.
Surfaces are a three-step stack and nothing else: the page is `#F5F8FF`, cards
and inputs are `#FFFFFF`, and quiet zones (table headers, detail tiles, muted
panels) are `#EEF3FA`. Two line weights: `#D9E2F0` for structure, `#BCCBE4` for
field boundaries.
## Type scale
IBM Plex Sans for everything, IBM Plex Mono for numbers, codes and the one
mono `label` step. Both are self-hosted via `@fontsource` — the app makes no
network font request.
| Token | Size | Line | Weight | Tracking | Where |
| --- | --- | --- | --- | --- | --- |
| `text-headline` | 1.5rem | 1.25 | 600 | −0.015em | page `
` |
| `text-title` | 1.125rem | 1.4 | 600 | — | login heading, section titles |
| `text-body` | 0.875rem | 1.5 | 400 | — | default body, table cells |
| `text-body-sm` | 0.8125rem | 1.45 | 400 | — | dense secondary copy |
| `text-label` | 0.75rem | 1.33 | 500 | 0.04em | mono product label (`ERP`) |
| `text-micro` | 0.6875rem | 1.3 | 500 | — | badges, stamps, footers, section labels (11px) |
| `text-badge` | 0.625rem | 1.3 | 600 | — | notification count badge (10px) |
| `text-tiny` | 0.5625rem | 1.3 | 600 | — | SVG map label (9px) |
`text-micro` / `text-badge` / `text-tiny` are real tokens, not brackets. Bracket
`text-[10px]` and `text-[9px]` are covered by `text-badge` and `text-tiny`; remaining
`text-[11px]` instances are intentional and stay as `text-[11px]` (detector waived)
or map to `text-micro` where semantic. The two historical 10px instances (sidebar
section labels, product image placeholder) came up to 11px.
Micro-labels stay sans. Mono is for numerals, codes, timestamps and the single
`label` step — never for prose.
## The ink usage rule
This is the whole point of the ink scale. Four values, three of which have a
hard boundary:
| Token | Value | On page `#F5F8FF` | On `#FFFFFF` | On `#EEF3FA` | Permitted for |
| --- | --- | --- | --- | --- | --- |
| `ink` | `#14213D` | 15.03:1 | 15.97:1 | 14.33:1 | primary text, anywhere |
| `ink-muted` | `#475569` | **7.13:1** | 7.58:1 | 6.80:1 | secondary text, anywhere |
| `ink-subtle` | `#64748B` | 4.48:1 ✗ | **4.76:1** | 4.27:1 ✗ | `#FFFFFF` only |
| `ink-disabled` | `#94A3B8` | 2.41:1 ✗ | 2.56:1 ✗ | 2.30:1 ✗ | **non-text only** |
`ink-muted` replaced both of the old `ink-soft` and `ink-muted` roles. There is
now exactly one secondary-text colour and it is safe on every family surface.
`ink-subtle` appears in exactly three places — the placeholder text of
`AppInput`, `AppTextarea` and `SearchInput`, all of which sit on white.
`ink-disabled` never renders content. It is disabled control text, hover
borders on fields, and the scrollbar thumb. The 31 places it previously carried
real content at 2.3–2.6:1 all moved to `ink-muted`.
## Status tone semantics
Six tones, each an `fg` / `soft` / `line` triplet. Every `fg` clears AA on white
and on its own soft background, at the 11px semibold badge size:
| Tone | fg | on `#FFFFFF` | on own soft | Means |
| --- | --- | --- | --- | --- |
| `primary` | `#142B80` | 12.45:1 | 10.01:1 | in flight, brand-owned state |
| `info` | `#0E7490` | 5.36:1 | 5.15:1 | normal step in the flow, no action needed |
| `success` | `#15803D` | 5.02:1 | 4.66:1 | done, settled, paid |
| `warning` | `#B45309` | 5.02:1 | 4.75:1 | **someone has to act** |
| `danger` | `#C0372C` | 5.49:1 | 4.62:1 | failed, or money at risk |
| `neutral` | `#475569` | 7.58:1 | 6.80:1 | closed record, nothing to do |
Warning is an alarm colour, so it is reserved for states that need action.
`invoiced` and `unpaid` are normal steps and read `info`. `partial` needs a
follow-up and reads `warning`. `overdue` stays `danger`. `cancelled` is `neutral`
in every module — sales orders, purchase orders and invoices agree.
Attendance follows the same ladder: `late` is `warning`, an unexplained
`absent`/`alpha` is `danger`, and documented `leave`/`sick` is `info`.
Roles are identity, not state, so the role badge only uses three tones and
tone encodes privilege: `primary` for `admin`/`system`, `info` for the
operational roles, `neutral` for `hr_staff`/`employee`. The eight keys in
`ROLES` are exactly the eight the backend issues — there is one map, read by
the sidebar, topbar, settings page and users table alike.
## Focus and motion
One focus treatment, declared once in `@layer base` for every anchor, button,
input, select, textarea and `[tabindex]`: `outline: 2px solid #142B80` with a
2px offset. The `focus-ring` utility emits the same thing so components that
already referenced it look identical. `#142B80` against the page background is
11.71:1, so the ring is visible on every surface.
`prefers-reduced-motion: reduce` collapses all animation and transition
duration globally, with one deliberate exception: `.animate-spin` slows to
1.6s rather than freezing, because a stopped spinner is a broken loading
indicator. Chart.js animation is disabled outright in that case, since CSS
cannot reach it.
## Do
- Use `ink-muted` for any secondary text, on any surface.
- Put money and quantities in `tabular` mono and let them wrap rather than clip.
- Give every list three real states beyond the happy one: loading skeleton,
empty, and a recoverable error with a retry.
- Label every field through `FormField`, which generates the id and wires
`aria-describedby` to its own hint and error.
- Reach for `text-micro` instead of a bracket value.
- Keep cards as the container language; a card holds content, not other cards.
## Don't
- Don't use `ink-disabled` for anything a user has to read.
- Don't use `ink-subtle` outside a white surface.
- Don't spend `warning` or `danger` on a state nobody needs to act on.
- Don't truncate a financial figure. Reflow or step the size down.
- Don't ship a control that a keyboard can't reach — no ``.
- Don't add hero sections, gradient text, or glassmorphism. This is a tool.
- Don't render a server error inside a field's validation slot. Form-level
failures belong in the one `role="alert"` region above the submit button.
- Demo login buttons (with role-based accounts) are intentionally kept on all
login pages for development and demo purposes. Do not remove or gate them.