Resources
15Install
npx skillscat add cyclops8833/bep-app Install via the SkillsCat registry.
Bep Design System — SKILL.md
Read this file before writing any UI code. Apply every rule here to every component, page, and layout you build.
What Bep is
Bep (Bếp) is a Vietnamese-native F&B operations and accounting tool for small restaurant and cafe owners. The visual language must feel warm, credible, and culturally grounded — like a trusted tool used in a real Vietnamese kitchen, not a generic SaaS dashboard.
The single design question to ask before every UI decision:
Would a Vietnamese cafe owner opening this at 7pm after a long shift find it calm, readable, and immediately useful?
What Bep is NOT
Never produce these patterns under any circumstances:
- No corporate blue (no #1565C0, no #006AFF, no Xero/Misa blue palettes)
- No tech purple (no Sapo-style purple dominance)
- No dark sidebars (no Toast/KiotViet dark nav panels)
- No aggressive orange-on-dark (no high-contrast fiery POS aesthetics)
- No icon overload (max one icon per nav item, never icon-only navigation without labels)
- No dense ERP layouts (no 8-column data grids, no sidebar-within-sidebar)
- No generic SaaS "clean white with blue accents" — this is the default Claude Code will reach for without instruction. Resist it entirely.
- No gradients, mesh backgrounds, glassmorphism, or decorative shadows
- No hardcoded English strings in JSX — all text must use
t()from react-i18next
Colour Palette
Primary — Warm earth tones
These are the identity colours of Bep. They come from Vietnamese kitchen culture: lacquerware, turmeric, clay pots, lantern light.
--bep-lacquer: #7C2D12 /* deep red-brown — primary brand, headings, logo */
--bep-turmeric: #B45309 /* warm amber-brown — primary accent, active states */
--bep-amber: #D97706 /* lighter amber — hover states, secondary accents */
--bep-cream: #FEF3C7 /* warm cream — highlighted backgrounds, callouts */
--bep-rice: #FAFAF9 /* off-white — page background */
--bep-charcoal: #1C1917 /* near-black — body text, headings */
--bep-stone: #78716C /* warm grey — secondary text, labels, captions */
--bep-pebble: #E7E5E4 /* light warm grey — borders, dividers */
--bep-surface: #FFFFFF /* card backgrounds */Semantic — Financial indicators
These are the only colours that carry meaning in the UI. Use them exclusively for financial data — not for decoration.
--bep-profit: #059669 /* green — profit, positive margin, healthy state */
--bep-profit-bg: #D1FAE5 /* green tint — profit badge backgrounds */
--bep-loss: #DC2626 /* red — loss, negative margin, critical state */
--bep-loss-bg: #FEE2E2 /* red tint — loss badge backgrounds */
--bep-warning: #D97706 /* amber — watch this, margin below threshold */
--bep-warning-bg: #FEF3C7 /* amber tint — warning badge backgrounds */Usage rules
--bep-lacqueris for the wordmark, page headings (h1), and primary CTA buttons only--bep-turmericis the workhorse accent — active nav items, links, focus rings, selected states--bep-amberis for hover states on turmeric elements only- Never use
--bep-profit/--bep-loss/--bep-warningfor anything except financial data - Background hierarchy:
--bep-rice(page) →--bep-surface(cards) →--bep-cream(highlighted sections) - Border default:
1px solid var(--bep-pebble)— always warm grey, never cool grey
Tailwind config
Add to tailwind.config.js:
theme: {
extend: {
colors: {
bep: {
lacquer: '#7C2D12',
turmeric: '#B45309',
amber: '#D97706',
cream: '#FEF3C7',
rice: '#FAFAF9',
charcoal: '#1C1917',
stone: '#78716C',
pebble: '#E7E5E4',
surface: '#FFFFFF',
profit: '#059669',
'profit-bg':'#D1FAE5',
loss: '#DC2626',
'loss-bg': '#FEE2E2',
warning: '#D97706',
'warning-bg':'#FEF3C7',
}
}
}
}Typography
Font stack
--font-ui: 'Be Vietnam Pro', 'Inter', sans-serif;
--font-brand: Georgia, 'Times New Roman', serif; /* wordmark only */
--font-mono: 'JetBrains Mono', 'Courier New', monospace; /* financial numbers */Be Vietnam Pro is the primary font. Install it:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@400;500;600&display=swap" rel="stylesheet">Why Be Vietnam Pro: designed specifically for Vietnamese diacriticals (ắ, ộ, ử, etc.). Renders Vietnamese text correctly and elegantly. Critical for user trust.
Type scale
Brand wordmark: font-family: 'Be Vietnam Pro'; font-size: 24px; font-weight: 500; color: --bep-lacquer; letter-spacing: -0.02em
Note: Georgia was originally specified but does not support Vietnamese diacritics — always use Be Vietnam Pro for the wordmark.
Page heading h1: 18px / 500 / --bep-charcoal
Section heading h2: 15px / 500 / --bep-charcoal
Label / caption: 11px / 500 / --bep-stone; text-transform: uppercase; letter-spacing: 0.07em
Body text: 14px / 400 / --bep-charcoal; line-height: 1.6
Secondary text: 13px / 400 / --bep-stone
Financial value: font-family: --font-mono; font-variant-numeric: tabular-numsRules
- Two weights only: 400 and 500. Never use 600 or 700 — they feel heavy in Vietnamese text.
- All headings are sentence case. Never title case, never all caps (except labels).
- Labels (column headers, field names, section dividers) use the uppercase 11px style exclusively.
- All financial numbers (VND amounts, percentages, quantities) use
font-family: var(--font-mono)andfont-variant-numeric: tabular-numsso digits align vertically in tables.
Spacing & Layout
Scale
4px — micro gaps (icon-to-label, badge padding)
8px — tight gaps (within a component)
12px — standard gap (between sibling components)
16px — component padding (card inner padding)
24px — section spacing (between card rows)
32px — page section spacing (between major sections)Page structure
Page background: bg-bep-rice (--bep-rice, #FAFAF9)
Max content width: 1200px, centred, px-6 on mobile
Top navigation: height 56px, bg-bep-surface, border-b border-bep-pebble
Left sidebar: width 220px (desktop), bg-bep-surface, border-r border-bep-pebble
Content area: p-6 (24px), gap-6 between sectionsGrid
Use CSS grid for dashboard layouts. Standard patterns:
/* Metric card row (3 up) */
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
/* Two-column layout */
grid-template-columns: 2fr 1fr;
gap: 24px;
/* Responsive collapse */
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));Components
Navigation sidebar
// Sidebar item — inactive
<div className="flex items-center gap-3 px-4 py-2.5 text-sm text-bep-stone hover:bg-bep-cream hover:text-bep-turmeric rounded-lg cursor-pointer transition-colors">
<Icon size={16} />
<span>{t('nav.recipes')}</span>
</div>
// Sidebar item — active
<div className="flex items-center gap-3 px-4 py-2.5 text-sm text-bep-turmeric bg-bep-cream rounded-lg font-medium cursor-pointer">
<Icon size={16} className="text-bep-turmeric" />
<span>{t('nav.recipes')}</span>
</div>Rules:
- Active state:
bg-bep-cream text-bep-turmeric— never a dark background, never white text - Icons must always have a visible text label alongside them
- Section dividers in the sidebar use a 1px border in
--bep-pebblewith an uppercase 11px label
Metric card
Used on the dashboard for Revenue, Costs, Net Profit, and any top-line KPI.
<div className="bg-bep-surface border border-bep-pebble rounded-xl p-4">
<p className="text-xs font-medium text-bep-stone uppercase tracking-wider mb-1">
{t('dashboard.revenue')}
</p>
<p className="text-2xl font-medium text-bep-charcoal font-mono tabular-nums">
{formatVND(value)}
</p>
<p className="text-xs text-bep-stone mt-1">
{t('dashboard.this_month')}
</p>
</div>Rules:
- Label: uppercase 11px stone — always
- Value: 24px mono charcoal — always
- Subtext: 12px stone — optional context (period, comparison)
- Profit values get
text-bep-profit, loss values gettext-bep-loss - Cards in a row use
gap-3, nevergap-6(too much separation for related metrics)
Data table
<table className="w-full text-sm">
<thead>
<tr className="border-b border-bep-pebble">
<th className="text-left text-xs font-medium text-bep-stone uppercase tracking-wider py-2 px-3">
{t('recipes.dish_name')}
</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-bep-pebble hover:bg-bep-rice transition-colors">
<td className="py-3 px-3 text-bep-charcoal">{item.name}</td>
</tr>
</tbody>
</table>Rules:
- Header: uppercase 11px stone — matches the label style
- Row hover:
bg-bep-riceonly — never a coloured hover - Row border:
border-bep-pebble— always warm grey - Numeric columns:
text-right font-mono tabular-nums - Never use zebra striping — row hover is sufficient
Margin health badge
The single most important recurring component in Bep. Used on every recipe card and table row.
// Healthy margin (>30%)
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-bep-profit-bg text-bep-profit">
{margin}%
</span>
// Warning margin (15–30%)
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-bep-warning-bg text-bep-warning">
{margin}%
</span>
// Critical margin (<15%)
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-bep-loss-bg text-bep-loss">
{margin}%
</span>Threshold logic (configurable per user, these are defaults):
const getMarginVariant = (margin: number) => {
if (margin >= 30) return 'profit'
if (margin >= 15) return 'warning'
return 'loss'
}Health indicator banner
Used on the P&L dashboard to give the owner a plain-language summary.
// Profitable state
<div className="flex items-center gap-2 px-4 py-2.5 bg-bep-profit-bg rounded-lg">
<div className="w-2 h-2 rounded-full bg-bep-profit" />
<span className="text-sm font-medium text-bep-profit">
{t('dashboard.health.profitable')}
</span>
</div>
// Warning state
<div className="flex items-center gap-2 px-4 py-2.5 bg-bep-warning-bg rounded-lg">
<div className="w-2 h-2 rounded-full bg-bep-warning" />
<span className="text-sm font-medium text-bep-warning">
{t('dashboard.health.watch_this')}
</span>
</div>
// Loss state
<div className="flex items-center gap-2 px-4 py-2.5 bg-bep-loss-bg rounded-lg">
<div className="w-2 h-2 rounded-full bg-bep-loss" />
<span className="text-sm font-medium text-bep-loss">
{t('dashboard.health.at_a_loss')}
</span>
</div>Card
Standard raised surface for content sections.
<div className="bg-bep-surface border border-bep-pebble rounded-xl p-4">
{/* content */}
</div>Rules:
- Always
rounded-xl(12px) — neverrounded-mdorrounded-lgfor cards - Always
border border-bep-pebble— never boxShadow as a substitute - Card padding:
p-4(16px) standard,p-6(24px) for large content cards - Never nest cards (card-within-card creates visual noise)
Button
// Primary CTA
<button className="bg-bep-lacquer hover:bg-bep-turmeric text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors">
{t('action.save')}
</button>
// Secondary
<button className="bg-transparent border border-bep-pebble hover:border-bep-turmeric hover:text-bep-turmeric text-bep-stone text-sm font-medium px-4 py-2 rounded-lg transition-colors">
{t('action.cancel')}
</button>
// Destructive
<button className="bg-transparent border border-bep-pebble hover:border-bep-loss hover:text-bep-loss text-bep-stone text-sm font-medium px-4 py-2 rounded-lg transition-colors">
{t('action.delete')}
</button>Rules:
- Primary:
bg-bep-lacquerbase,hover:bg-bep-turmeric— always white text - Never use
bg-blue-*orbg-indigo-*for any button - Icon-only buttons are not allowed — always include a text label
Form input
<div className="flex flex-col gap-1.5">
<label className="text-xs font-medium text-bep-stone uppercase tracking-wider">
{t('field.selling_price')}
</label>
<input
type="number"
className="w-full bg-bep-surface border border-bep-pebble rounded-lg px-3 py-2 text-sm text-bep-charcoal placeholder:text-bep-stone focus:outline-none focus:border-bep-turmeric transition-colors font-mono tabular-nums"
placeholder="75000"
/>
</div>Rules:
- Label: uppercase 11px stone — consistent with all other labels
- Focus state:
border-bep-turmeric— never a blue ring - Number inputs: always
font-mono tabular-nums - Currency fields: plain number input in VND — do not use masked currency inputs (causes confusion with Vietnamese number formatting)
Drawer / modal
All add/edit flows use a right-side drawer, not a centred modal. This preserves context — the user can still see the list they're editing.
// Drawer structure
<div className="fixed inset-y-0 right-0 w-[480px] bg-bep-surface border-l border-bep-pebble flex flex-col z-50">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-bep-pebble">
<h2 className="text-base font-medium text-bep-charcoal">{t('recipes.add_recipe')}</h2>
<button onClick={onClose} className="text-bep-stone hover:text-bep-charcoal">
<X size={18} />
</button>
</div>
{/* Scrollable content */}
<div className="flex-1 overflow-y-auto px-6 py-4 flex flex-col gap-4">
{/* form fields */}
</div>
{/* Footer actions */}
<div className="px-6 py-4 border-t border-bep-pebble flex justify-end gap-3">
<button className="...">{t('action.cancel')}</button>
<button className="...">{t('action.save')}</button>
</div>
</div>Empty state
Every list and table must have an empty state. No blank white space.
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="w-12 h-12 rounded-full bg-bep-cream flex items-center justify-center mb-4">
<Icon size={20} className="text-bep-turmeric" />
</div>
<p className="text-sm font-medium text-bep-charcoal mb-1">
{t('recipes.empty.title')}
</p>
<p className="text-sm text-bep-stone mb-4 max-w-xs">
{t('recipes.empty.body')}
</p>
<button className="bg-bep-lacquer hover:bg-bep-turmeric text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors">
{t('recipes.empty.cta')}
</button>
</div>Loading skeleton
<div className="animate-pulse">
<div className="h-4 bg-bep-pebble rounded w-3/4 mb-2" />
<div className="h-4 bg-bep-pebble rounded w-1/2" />
</div>Rules:
- Always
bg-bep-pebblefor skeleton fills — never grey-300 or blue-100 - Skeletons must match the approximate layout of the content they replace
Toast notifications
Using Sonner. Configure once in the app root:
<Toaster
toastOptions={{
style: {
background: 'var(--bep-surface, #FFFFFF)',
border: '1px solid #E7E5E4',
color: '#1C1917',
borderRadius: '10px',
},
classNames: {
success: 'border-l-4 border-l-[#059669]',
error: 'border-l-4 border-l-[#DC2626]',
}
}}
/>VND Currency Formatting
Always format Vietnamese Dong amounts consistently. Use this utility:
// src/lib/format.ts
export const formatVND = (amount: number): string => {
return new Intl.NumberFormat('vi-VN', {
style: 'currency',
currency: 'VND',
maximumFractionDigits: 0,
}).format(amount)
// Output: 75.000 ₫
}
export const formatVNDShort = (amount: number): string => {
if (amount >= 1_000_000) return `${(amount / 1_000_000).toFixed(1)}M ₫`
if (amount >= 1_000) return `${(amount / 1_000).toFixed(0)}k ₫`
return `${amount} ₫`
// Output: 4.2M ₫ / 75k ₫
}Rules:
- Full amounts in tables and forms:
formatVND()— always - Summary values in metric cards:
formatVNDShort()— fits the large number display - Never display raw numbers without a currency unit in financial contexts
- Never use
$orAUDanywhere in the UI
Recharts Config
Standard chart colours for Bep. Always use these — never Recharts defaults.
// src/lib/chartConfig.ts
export const CHART_COLORS = {
revenue: '#B45309', // turmeric — revenue bars
cost: '#DC2626', // loss red — cost bars
profit: '#059669', // profit green — profit line
neutral: '#78716C', // stone — neutral/reference lines
}
export const chartDefaults = {
style: { fontFamily: 'Be Vietnam Pro, Inter, sans-serif' },
tick: { fill: '#78716C', fontSize: 12 },
axisLine: { stroke: '#E7E5E4' },
grid: { stroke: '#E7E5E4', strokeDasharray: '3 3' },
}Usage in a bar chart:
<BarChart data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="#E7E5E4" vertical={false} />
<XAxis dataKey="date" tick={{ fill: '#78716C', fontSize: 12 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: '#78716C', fontSize: 12 }} axisLine={false} tickLine={false} tickFormatter={formatVNDShort} />
<Bar dataKey="revenue" fill="#B45309" radius={[4, 4, 0, 0]} />
<Bar dataKey="cost" fill="#DC2626" radius={[4, 4, 0, 0]} />
</BarChart>Internationalisation Rules
- Every string in JSX must use
t('namespace.key')— zero hardcoded text - Namespace structure:
common·auth·onboarding·dashboard·recipes·invoices·suppliers·revenue·vat - Vietnamese is always the default (
lng: 'vi'in i18n config) - When adding a new string, add it to both
vi.jsonanden.jsonsimultaneously — never leave a key missing in either file - Vietnamese text is often 20–30% longer than English equivalent — design all components with Vietnamese text length as the baseline, not English
Supabase + RLS Reminder
Every database query must be scoped to the authenticated user. Every table has a user_id column with an RLS policy. Never query without RLS enabled. Pattern:
// Always — Supabase RLS handles the user_id filter automatically
const { data } = await supabase.from('recipes').select('*')
// Never manually add .eq('user_id', user.id) — RLS does this
// But always confirm RLS policy exists on the table before shippingFile & Folder Conventions
src/
components/
ui/ — reusable primitives (Badge, Card, Button, Input, Drawer)
features/ — feature-specific components (RecipeCard, InvoiceRow, MetricCard)
pages/ — route-level components
lib/
supabase.ts — Supabase client singleton
i18n.ts — i18next config
format.ts — formatVND, formatVNDShort, date formatters
chartConfig.ts — Recharts colour + style constants
hooks/ — useProfile, useRecipes, useInvoices, etc.
types/ — TypeScript interfaces (Recipe, Ingredient, Invoice, etc.)
locales/
vi.json — Vietnamese strings (default)
en.json — English stringsQuick Reference — Do / Don't
| Do | Don't |
|---|---|
bg-bep-lacquer for primary CTAs |
bg-blue-600 for anything |
text-bep-stone for secondary text |
text-gray-500 (cool grey) |
border-bep-pebble for all borders |
border-gray-200 (cool grey) |
font-mono tabular-nums for numbers |
Plain numbers in financial displays |
Be Vietnam Pro for all UI text |
Inter / Roboto / system-ui |
t('key') for every string |
Hardcoded "Save" or "Lưu" in JSX |
| Right-side drawer for add/edit | Centred modals for forms |
rounded-xl for cards |
rounded-md for cards |
formatVND() for currency |
Raw number display |
Warm grey (--bep-pebble) for dividers |
Cool grey or blue-grey dividers |
Bep Design System · Northset Advisory · Last updated April 2026