From 696a19b142e242e5f71ee3ac72d31a6d586b91f5 Mon Sep 17 00:00:00 2001 From: kbenestad Date: Mon, 8 Jun 2026 08:58:36 +0700 Subject: [PATCH] Add design brief for kBenestad web apps This document serves as a design brief for kBenestad web apps, outlining visual guidelines, typography, color tokens, and UI components. --- DESIGN.md | 339 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 DESIGN.md diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..a021732 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,339 @@ +# kBenestad — app UI brief + +**Drop this into a project's `CLAUDE.md` (or paste it to Claude Code) whenever you build or +restyle a kBenestad web app.** It is the shared visual contract: follow it and any new +screen — invoice, timesheet, reimburse, a dashboard, a settings page — will look like it +belongs to the same family. + +This brief tells you *how things should look and behave*. It does **not** redefine what an +app does. Keep each app's existing purpose, data model, and screens intact — only the +surface (layout, type, colour, components, copy) is governed here. + +--- + +## 0. The one-paragraph version + +Nordic-minimal, light-first, calm. A cool off-white page, pure-white cards held by 1px +hairline borders, near-black cool ink, and **one** blue accent (`#2f6fed`). Schibsted +Grotesk for everything; JetBrains Mono only for numbers, code, and identifiers. 4px +spacing grid, small deliberate radii (8px workhorse), soft shadows used sparingly. No +gradients, no emoji, no bounce. Sentence case. Borders do the structural work; shadow only +when something genuinely floats. Categorical data is coloured with a muted, config-driven +palette (chip + left border + optional row tint), never neon. + +If you only remember five rules: +1. **One accent.** Blue is the only brand colour. Semantic hues are muted and earn their place. +2. **Borders first, shadow last.** Most surfaces have no shadow. +3. **Forms are row-major** (see §6). Never lay a form out as side-by-side column stacks. +4. **Sentence case, no emoji, mono for numbers.** +5. **Colour-code categories** with the chip / border / tint system in §8 — muted, from data. + +--- + +## 1. Setup — wire in the design system + +All tokens are CSS custom properties. Pull them in once, then reference `var(--token)` +everywhere. The canonical source is the kBenestad design system's `styles.css` closure +(`tokens/colors.css`, `typography.css`, `spacing.css`, `elevation.css`, `fonts.css`, +`base.css`). Copy those into the app and `@import` them from a single stylesheet, or copy +the token blocks inline. + +**Fonts** — Schibsted Grotesk (400–800) + JetBrains Mono (400–600). Load from a webfont host +with a `local()` first and a full system fallback stack so the app still renders offline: + +```css +--font-sans: 'Schibsted Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; +--font-mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace; +``` + +**Dark mode** — support both. Honour the OS by default and allow a manual override: +`@media (prefers-color-scheme: dark)` *and* `:root[data-theme="dark"]` both flip the same +tokens. Never hardcode a hex that won't invert — always go through a semantic token. + +**For form-driven apps specifically:** `mockups/kbenestad-forms.css` is a finished, +proven component layer (the `.kb-*` classes used below) built on exactly these tokens. Lift +it wholesale rather than rebuilding inputs/cards/buttons from scratch. + +--- + +## 2. Colour tokens + +Reference the **semantic aliases**, not the raw ramps. The raw ramps exist so the aliases +have something to point at; your code should almost never name a `--blue-500` directly. + +**Surfaces** — `--surface-page` (cool off-white ground), `--surface-card` (pure white), +`--surface-sunken`, `--surface-raised`, `--surface-hover`, `--surface-active`, +`--surface-inverse`. + +**Borders** — `--border-subtle` (hairline, default for cards), `--border-default` +(dividers), `--border-strong` (emphasis), `--border-focus`. + +**Text** — `--text-strong` (headings/primary ink), `--text-body`, `--text-muted`, +`--text-subtle`, `--text-inverse`, `--text-accent`, `--text-link`. + +**Accent / interactive** — `--accent` (`#2f6fed`), `--accent-hover`, `--accent-active`, +`--accent-soft` (pale blue fill), `--accent-soft-text`, `--accent-fg` (text on accent). + +**Semantic feedback (muted, never neon)** — `--success` / `--success-soft`, +`--warning` / `--warning-soft`, `--danger` / `--danger-soft` / `--danger-fg`. Use the soft +variants as fills, the solid as border/text. + +**Focus** — `--focus-ring` (a 3px soft-blue ring). Always visible via `:focus-visible`. + +**Terminal** (CLI / gitxt only) — `--term-bg`, `--term-fg`, `--term-dim`, `--term-accent`, +`--term-green`, `--term-amber`. Don't use these in normal app UI. + +> Light values you'll see most: page `#f8f9fb`, card `#ffffff`, hairline `#e7eaef`, ink +> `#14181e`, accent `#2f6fed` / hover `#1f57cf`. Dark inverts onto `#0d1117` page / +> `#161b22` card with a lighter accent for contrast. + +--- + +## 3. Typography + +- **Families:** `--font-sans` everywhere; `--font-mono` **only** for numbers, code, paths, + package/identifier names, and tabular figures. When showing money, durations, counts, + versions, IDs — mono + `font-variant-numeric: tabular-nums`. +- **Scale:** `--text-display` 52px (splash only), `--text-h1` 36, `--text-h2` 28, + `--text-h3` 22, `--text-h4` 18, `--text-body-lg` 17, `--text-body` 15 (UI default), + `--text-body-sm` 13, `--text-caption` 12, `--text-mono` 14. +- **Weights:** 400 body, 500 medium, 600 semibold (labels/buttons), 700 headings, 800 + reserved for the wordmark/splash. +- **Leading:** `--leading-normal` 1.5 for body; `--leading-tight` 1.1 for big display. +- **Tracking:** `--tracking-tight` (−0.02em) on display/headings; `--tracking-caps` + (+0.08em) on uppercase eyebrow/overline labels. +- **Eyebrow labels** (field labels, table headers, section kickers): 12px, uppercase, + `--tracking-caps`, `--text-muted`, weight 600. This is the recurring small-label motif. + +--- + +## 4. Spacing, shape, elevation, motion + +- **Spacing:** 4px base grid via `--space-1`…`--space-16` (4, 8, 12, 16, 24, 32, 40, 48, 64, + 96, 128). Card padding ~24px (`--space-5`). Comfortable, not airy. +- **Radii:** `--radius-xs` 3, `--radius-sm` 5 (small controls), `--radius-md` 8 (the + workhorse), `--radius-lg` 12 / `--radius-xl` 16 (cards), `--radius-full` (pills/badges + only). **Not bubbly** — never round a card more than 16px. +- **Borders:** `--border-thin` 1px (default hairline), `--border-medium` 1.5px (emphasis / + selection). Borders carry the structure. +- **Elevation:** most surfaces have **no** shadow. `--shadow-md` for menus and hover-lift, + `--shadow-lg` / `--shadow-xl` for dialogs and popovers. Shadows are soft, cool-tinted, + low-opacity. Never use shadow as decoration. +- **Containers:** cap content at `--container-lg` 1080px (forms/docs) to `--container-xl` + 1280px (dashboards). Left-aligned, single-column-leaning. +- **Motion:** 120–280ms (`--duration-fast/base/slow`), `--ease-out` + `cubic-bezier(.22,.61,.36,1)`. Fades and ≤2px translations only. **No bounce, no spring.** + Honour `prefers-reduced-motion`. +- **Hover/press:** filled elements step one shade darker (`--accent-hover`); ghost/secondary + get a faint surface tint. Press adds a 0.5px downward nudge. Disabled ≈ 45% opacity, no + pointer events. + +--- + +## 5. Voice & copy + +The voice is understated, precise, practical. These are working tools, not marketing pages. + +- **Sentence case everywhere** — buttons, headings, menu items ("View source", "Add row", + not Title Case). UPPERCASE only for small tracked-out eyebrow labels. +- **Calm and direct.** Short sentences. Say what a thing does, then stop. No hype + ("seamless", "blazing-fast"), no exclamation marks. +- **Imperative, concrete verbs** for actions: *Add line*, *Generate*, *Validate*, *Export*, + *Remove*, *Publish*. +- **Numbers & identifiers are exact and mono:** `v2.4.0`, `12 kB`, `8.0 h`, `3 items`. +- **No emoji, ever.** Status is a coloured dot, a Lucide icon, or a tinted chip. +- **Errors & empty states are honest and helpful** — what happened, then the next step: + "Time out must be after time in." / "No expenses yet — add your first line to begin." +- **Speak to the user as "you"**; the maker is "I/me". Avoid corporate "we". + +--- + +## 6. Form-driven UIs (invoice, timesheet, reimburse, settings, anything with inputs) + +These apps are the core of the family. The rules below are what make them feel unified. + +### 6.1 Page shell +A centred column (`--container-lg` or narrower), `--surface-page` ground, content in +`--surface-card` cards with `--border-subtle` and at most `--shadow-sm`. Optional top +**utility bar** (language toggle, text-size A−/A/A+, About) as small segmented controls; +then a **document header** where the *customer's* identity leads (logo tile + org name on +the left, document title + period on the right). kBenestad is the quiet signature in the +footer, never the headline. + +### 6.2 Forms are ROW-MAJOR — this is the load-bearing rule +Group fields into **rows**; never build a form as two independent side-by-side column +stacks. Each row is a **top-aligned CSS grid**. When one field grows — a hint appears, a +validation message shows, an explanation expands — **the whole row grows and the rows below +push down**, while the field beside it stays aligned to the top. This keeps labels and +controls in register no matter what expands. + +- Use a `.kb-form` / row container with span helpers (`kb-col-2`, `kb-col-3`, `kb-col-full`) + to control how many columns a field occupies within its row. +- Label above field. Label = eyebrow style (12px, uppercase, tracked, `--text-muted`, 600). +- One thought per row; don't cram unrelated fields together to save vertical space. + +### 6.3 Inputs +1px `--border-strong` border, `--radius-sm`, ~9–11px padding, 15px text. Focus = +`--accent` border + `--focus-ring`. Disabled/readonly = `--surface-sunken` fill, muted +text, `not-allowed`. Numeric inputs: mono, right-aligned, tabular-nums. Selects get a custom +chevron. Error state = `--danger` border; warning = `--warning` border (optional soft fill). + +### 6.4 Buttons +- **Primary:** filled `--accent`, `--accent-fg` text → hover `--accent-hover`. +- **Ghost/secondary:** white surface, `--border-strong` → hover accent border + accent text. +- **Soft:** `--accent-soft` fill, accent text. +- **Dashed:** transparent + dashed accent border — the "add another" affordance. +- **Round add/remove:** 24px circle, `+` (accent) / `−` (danger) — for line-item rows. +- Disabled ≈ 50% opacity. Icons (Lucide, 16px) sit left of the label with a small gap. + +### 6.5 Line items / repeating rows +A header row of eyebrow labels, then data rows on a matching grid. Row hover gets a faint +`--surface-hover`. A left-border slot (`border-left: 3px solid transparent`) is reserved so +rows can be colour-coded (see §8). Trailing add/remove circle buttons. Subtotals and the +running total use mono tabular figures. + +### 6.6 Totals panel +Right-aligned, ~380px. Rows of `label … value` (value mono, tabular). A `grand` row on top +of a `--border-strong` rule, bold, with the figure in `--accent` at ~20px. Notes/derived +explanations below in muted small text. + +### 6.7 Validation & feedback (notes / banners) +A tinted note block: icon + text, `--*-soft` background with matching `--*-border` and text +colour. Four kinds: error (`--danger`), warning (`--warning`), success (`--success`), info +(`--accent`/info). Blocking errors disable the primary action; warnings don't. Mirror +issues inline: tint the offending field's border and show the message next to its row. Keep +messages specific ("Description is required for OTH rows"). + +### 6.8 Config-driven & white-label +Wherever an app has a `config.yml` (org name, logo, accent colour, codes, categories, +holidays, UI strings, language), **drive the UI from it** — don't hardcode. The accent is a +single recolourable token so a customer brand can replace the kBenestad blue without +touching anything else. Strings come from the config's i18n map; support the configured +languages. + +--- + +## 7. Dashboards & data-dense screens + +Dashboards use the same DNA, just wider (`--container-xl` 1280px) and grid-arranged. + +- **Metric / stat cards:** white card, hairline border, no shadow. A small uppercase + eyebrow label, then the figure **large in mono tabular** (`--text-h2`/`h3`), then an + optional delta line. Show change with a small coloured chip or arrow in `--success` / + `--danger` — **muted, not neon**, never a red/green gradient. +- **Card grid:** lay metric cards on a responsive grid with `--space-4`/`--space-5` gaps. + Group related cards under a section header (eyebrow label + thin accent tick). +- **Tables:** the workhorse of dashboards. Eyebrow-label header row, hairline row dividers + (or zebra via `--surface-sunken` at low contrast), generous row height, mono tabular for + any numeric column, right-align numbers. Status/category cells use the chip system (§8). + Sticky header for long tables. Row hover = `--surface-hover`. +- **Charts:** flat fills, no 3D, no drop shadows, no gradients-as-decoration. Use the blue + accent as the primary series; pull additional series from the muted categorical palette + (§8) so chart colours match the chips/legends elsewhere. Thin gridlines in + `--border-subtle`, axis labels in `--text-muted`. A legend reuses the same chip styling. +- **Density:** comfortable, not cramped. Don't add stats, sparklines, or icons that aren't + answering a real question — less is more. No "data slop." +- **Filters / toolbars:** segmented controls and ghost buttons in a row above the content, + separated from it by a hairline. Active segment = `--accent-soft` fill + accent text. +- **Empty & loading states:** a calm centred message in `--text-muted` with one clear + action — never a spinner alone with no context. + +--- + +## 8. Colour-coding categorical data (the timesheet pattern — use it everywhere it fits) + +This is the pattern from the timesheet that works well: any time data falls into **named +categories** — work codes, expense categories, invoice statuses (draft/sent/paid/overdue), +project tags, leave types, priority levels — give each category a **stable, muted colour +identity** and surface it consistently. It turns a wall of rows into something scannable. + +### 8.1 The colour identity +Each category owns a trio, ideally defined in **config/data, not hardcoded**, exposed as CSS +custom properties so they stay configurable: + +``` +--chip-border : a saturated-but-muted hue (the category's "true" colour) +--chip-bg : a soft pale tint of that hue (fills behind text — must keep AA contrast) +--chip-text : a darker shade of the hue (readable label colour) +``` + +You can specify all three for control, or specify just `--chip-border` and derive the tint +with `color-mix(in srgb, var(--chip-border) 16%, var(--surface))`. **Muted, never neon** — +think `#0078d7` blue, `#8cbd18` olive, `#ed616f` coral, `#393939` slate, not pure primaries. + +### 8.2 Three ways to surface it (use together) +1. **Chip / pill** — a `--radius-full` pill with a leading filled **dot** in the category + colour, the code + short name. This is the primary, always-legible token. Used in cells, + filters, and legends. +2. **Left border on the row** — `border-left: 3–4px solid var(--chip-border)`. A quiet, + always-on stripe that lets you scan a long table by category at a glance. +3. **Optional muted row tint** — fill the whole row with `--chip-bg` (or a 45% `color-mix` + of it). Make this **toggleable** (the timesheet's `muted-background` option): some users + want the calm border-only view, others want the colour wash. Default to the quieter one. + +### 8.3 Legend +Whenever colour carries meaning, show a **legend** of chips mapping each colour to its label. +Colour is never the *only* signal — the code/name text is always present, so the system +works for colour-blind users and in print/PDF. + +### 8.4 Full-day vs partial, and state variants +A category can carry more than one colour for sub-states (the timesheet's PPT uses a +red full-day vs amber partial-day pair). Model these as variant classes/props +(`c-PPT` vs `c-PPTp`) rather than inventing ad-hoc colours at the call site. + +### 8.5 Dark mode +Tints are too dark to read directly inverted. Either define dark-mode `--chip-bg`/`--chip-text` +in the dark scope, or in dark mode show the chip as the border colour on a transparent fill +(`color-mix` the hue into the surface) and brighten slightly. Always re-check contrast. + +### 8.6 Reuse across the family +The *same* category palette should drive chips, row borders, row tints, **and** chart +series — so a "Travel" expense is the same colour in the table, the legend, and the pie +chart. Define the palette once per app and pull from it everywhere. + +--- + +## 9. Iconography + +- **System:** Lucide — 2px-stroke outline icons. Size `1em`–18px, round caps/joins, + `currentColor` so they inherit text colour. Bump stroke to 2.2–2.4 at very small sizes. +- **Status** is a small filled dot or a Lucide glyph + colour. **Never emoji.** +- Don't draw bespoke illustrations in SVG; prefer clean icons, real screenshots, or + diagrams. No stock photos, no grain, no duotone. + +--- + +## 10. Dark mode checklist + +- Every colour goes through a semantic token; nothing hardcoded that can't invert. +- Page `#0d1117`, card `#161b22`, raised `#1c232c`; borders lighten, text lightens, accent + shifts lighter (`--blue-400`-ish) for contrast on dark. +- Shadows get heavier/darker (already handled by the dark elevation tokens). +- Re-derive category tints (§8.5). Re-check chip and note contrast. +- Support both auto (`prefers-color-scheme`) and a manual `data-theme` override, persisted. + +--- + +## 11. Do / Don't + +**Do** +- Use one blue accent; let borders and whitespace do the work. +- Keep forms row-major; keep numbers in mono tabular. +- Colour-code categories with the muted chip/border/tint system, with a legend. +- Sentence case, exact numbers, honest empty/error states. +- Cap width, left-align, single-column-lean; comfortable 4px-grid spacing. +- Support light + dark through tokens. + +**Don't** +- No gradients, photographic washes, textures, or glassmorphism. +- No emoji, no exclamation marks, no hype words, no Title Case. +- No neon semantic colours; no red/green gradient deltas. +- No bubbly radii (>16px on cards), no bouncy/springy motion. +- No shadow as decoration — only for things that truly float. +- Don't hardcode colours, strings, or the accent when a config can drive them. +- Don't redefine what the app *is* — restyle the surface, keep the behaviour. + +--- + +*This brief governs surface only. When a real codebase or brand guide exists, reconcile +these tokens against it rather than overriding the product.*