Files
felhom.eu/documentation/design/design-system.md
T
admin 43189e8972 D3 Part 4: docs — .tag/.badge addendum, website CHANGELOG, REPORT
- design-system.md: the .tag (outline+dot, state) vs .badge (filled
  chip, static informational label) distinction the D1 unified view
  left implicit, + a Website (TASK-D3) section (shared site.css,
  self-hosted fonts, external sprite, mask-based CSS marks, ?v=
  cache-busting, site_gates.py enforcement).
- website/CHANGELOG.md created (newest-on-top) with the D3 entry.
- REPORT.md overwritten per §15 (baselines, commits, gate outputs +
  all three red-proofs, 84-problem/182-emoji baseline → 0, contact-form
  freeze proof + live TESZT submission resend id, live checks, the
  narrow-viewport limitation, observations).
2026-07-02 22:19:58 +02:00

9.5 KiB
Raw Blame History

Felhom design system v2

Status: canonical reference. Shipped in the controller UI with TASK-D0 (controller v0.96.0, 2026-07-02); D1D4 (settings IA split, Tárhely nav promotion, website, hub) build from this document.

The system was approved via two mockup rounds. Four pillars:

  1. Logo-derived navy palette — a single blue accent on layered navy, replacing the GitHub-dark clone.
  2. Exception-based status color — nominal state is blue/neutral; amber/red appear only on deviation.
  3. Vendored assets — fonts and icons ship inside the binary; no CDN dependency (offline customer nodes).
  4. Flat metadata — no pills, no shadows, no bubbles; one 2px radius everywhere.

The rule set

  • One radius: 2px (--radius). The only exceptions are true circles (state dots, spinners) at 50%.
  • No shadows. No box-shadow anywhere. Depth comes from background layering (--bg-0/1/2) and hairlines (--line, --line-soft).
  • Boxless sections. Section headers are an h3 + muted hint over a 1px hairline (.section-h), not a card. Panels (.panel) exist for genuinely grouped content, flat: bg-1 + 1px line.
  • Color = exception. Anything operating normally is blue or neutral. Green does not exist in the system. Amber (--warn) and red (--crit) are reserved for deviations that need attention. Categorical data series (charts) are exempt — they may use distinct hues; status may not.

Tokens (canonical, verbatim)

:root {
  --bg-0: #0A1220;   --bg-1: #0F1B2E;   --bg-2: #16263F;
  --line: #22344F;   --line-soft: #1A2A42;
  --text-1: #EDF2F9; --text-2: #94A6BF; --text-3: #5E7392;
  --blue: #0083D8;   --blue-bright: #2EA8F5; --blue-dim: rgba(0,131,216,.13);
  --warn: #E0A93E;   --warn-dim: rgba(224,169,62,.12);
  --crit: #E5534B;   --crit-dim: rgba(229,83,75,.12);
  --radius: 2px;
  --font-ui: 'Plus Jakarta Sans', -apple-system, sans-serif;
  --font-data: 'JetBrains Mono', monospace;
}

Semantics: --bg-0 page, --bg-1 panel, --bg-2 elevated/nested surface (buttons, inputs, table stripes). --text-1 primary, --text-2 secondary, --text-3 muted/labels. --line visible hairline, --line-soft row separators and meter tracks. --blue fills and primary actions, --blue-bright text-on-dark accent and hover, --blue-dim selection/hover washes.

Typography

  • UI face: Plus Jakarta Sans (weights 400800). Data face: JetBrains Mono (400500) for every numeric/technical value: sizes, times, percentages, paths, IDs, versions.
  • Both are vendored variable woff2 files embedded in the binary and served from /static/fonts/ (immutable cache). latin-ext is mandatory — Hungarian ő/ű break without it. unicode-range is preserved from the upstream subsets so latin-ext only loads when needed.
  • No CDN font loading anywhere. fonts.googleapis.com is a banned string in the codebase.

Iconography

  • Vendored Lucide sprite (templates/icons.html, 30 icons as <symbol id="i-<name>">), included once at the top of <body>. Stroke = currentColor, stroke-width 2, round caps.
  • Usage convention everywhere: <svg class="ico"><use href="#i-hard-drive"/></svg> (.ico 16px, .ico-sm 14px, .ico-spin for progress).
  • Emoji are banned in templates and JS-built strings (🔒… — see the D0 grep gate list).
  • Standalone pages that don't include the layout (catchall, setup wizard) get inline SVGs or plain text, not the sprite.

Components

Meter (.meter + .meter-track / .meter-fill)

The capacity readout — replaces every zone-colored bar. A 3px hairline track (--line-soft) with a fill; the fill is blue in nominal state. Two 1px neutral ticks are baked into the track at 70% and 85% (.meter-track::before/::after; .no-ticks disables). The wrapper carries the state class from usageColor: nominal (blue), warn (amber fill), crit (red fill). Under warn/crit a .meter-flag renders (dim background chip + triangle-alert icon) with the exact copy: warn → „Fogyóban a hely", crit → „Kritikusan kevés hely" (storage meters). Disconnected/unknown: .meter-empty — empty track, value , no color, no flag. Deploy-page variant .meter-track-stacked: two segments — committed = solid --blue, planned/new = --blue at 35% opacity.

Tag (.tag)

The state chip — replaces pills. Inline-flex, 2px radius, 1px border, 6px round dot. Modifiers: tag-run (blue border/text/dot), tag-progress (blue + pulsing dot; prefers-reduced-motion respected), tag-warn (amber + dim bg), tag-crit (red + dim bg), tag-neutral/tag-off (gray, hollow dot). Protected/locked content uses a neutral tag with the i-lock icon.

Metadata row (.metarow / .metarows)

Informational chips ("~512M", "Pi kompatibilis", "HDD szükséges") become icon + text with no container: 14px muted-stroke icon, --text-2 text, mono for values.

Panel / List (.panel, .list .row, .stack-list)

Panel: --bg-1, 1px --line, 2px radius, no shadow. List: one panel whose rows are separated by border-top: 1px solid var(--line-soft); each row has a 2px left state edge via ::before — blue for run/progress, amber for warn, transparent for neutral/off.

Section header (.section-h)

h3 + right-aligned muted hint over border-bottom: 1px solid var(--line). No box.

Stats (.stats-grid / .stat-card)

Boxless figures with 1px right hairline dividers; numbers in --font-data at 1.9rem. A zero "Leállítva" renders --text-3 (.stat-zero); a nonzero stopped count renders --warn (.stat-warn) — the exception rule applied to numbers.

Buttons

2px radius. Default: --bg-2 + --line border, hover border --blue. .btn-primary: solid blue. .btn-danger: transparent background, --crit border + text — danger is outline until the confirm step (filled red only inside confirmation modals). :focus-visible = 2px --blue-bright outline on all interactive elements.

State truth tables (funcmap, controller internal/web/funcmap.go)

stateColor(ContainerState) → class suffix (consumed as tag-*, stack-state-*, state-text-*):

ContainerState token rendering
running run blue tag + blue row edge — nominal
starting, deploying progress blue tag, pulsing dot, blue edge
unhealthy warn amber tag + amber edge
restarting warn amber (a restart loop is a problem, not progress)
stopped, exited neutral gray tag, hollow dot, no colored edge
paused neutral gray
not_deployed, unknown off gray tag, no row edge

Deliberate semantic change (operator-approved): stopped/exited is neutral, not red. A customer-stopped app is not an emergency; genuine failures surface via unhealthy state + alerts.

usageColor(percent): <70 → nominal, 7085 → warn, ≥85 → crit. tempColor(celsius): >75 → crit, ≥60 → warn, else nominal (thresholds unchanged from v1). Hungarian stateLabel copy is frozen (guarded byte-identical by unit test).

Text color classes: .state-text-{run,progress,warn,crit,neutral,off,nominal}nominal = --text-1 (no color), run/progress = --blue-bright, neutral/off = --text-3.

Chart palette (Chart.js, monitoring)

Categorical series (data, not status): cpu #2EA8F5, memory #8E7CE8, temp #E0A93E, load #5EC4B6. Chrome: tooltip bg #0F1B2E, title #EDF2F9, body #94A6BF, border #22344F, ticks #94A6BF, axis titles #5E7392, grid rgba(34,52,79,.5).

.tag vs .badge (D3 addendum — resolves the D1 parallel-component drift)

Both exist in the controller; they are distinct components, not variants:

  • .tag — outline + dot; expresses state (run/progress/warn/crit/neutral/off). Use it whenever the thing shown can change at runtime: container state, drive activity, health. The dot is part of the semantics (pulsing = in progress, hollow = inert).
  • .badge — filled chip (dim background, no dot); expresses a static informational label: tier names, protocol/type markers (USB, NFS), feature keywords on catalog cards. Nothing about it changes at runtime.
  • If the label has an icon-and-text metadata character ("~512M", "Pi kompatibilis"), prefer .metarow (no container at all) over either chip.
  • On the website, catalog feature keywords are .badge-class chips (filled, 2px, blue-dim) — informational labels, not states.

Website (TASK-D3)

The seven felhom.eu pages share ONE stylesheet (website/assets/site.css?v=N) built on this document's tokens: self-hosted fonts (/assets/fonts/, no CDN — GDPR), a 70-symbol external Lucide sprite (/assets/icons.svg?v=N#i-name; feature tiles = .ico-tile 48px bg-2 squares), the two-tone heading as the brand signature (accent span = solid --blue-bright), and mask-based ✓/✗ marks for CSS-generated content (sprites can't reach ::before). Marketing-only scale addition: hero H1 up to ~3.5rem/800. Assets are cache-busted with ?v=N (nginx expires 7d) — bump every reference together. Enforcement: scripts/site_gates.py (BOM, emoji=0, nav/footer consistency, analytics presence, no-CDN, banned tokens, zero <style> blocks, cache-busting).

Enforcement

The D0 grep gate (all counts must be 0 in controller/internal/{web,setup}): fonts.googleapis.com, border-radius: 999px, box-shadow, the GitHub-dark hexes (#0d1117 #161b22 #1c2128 #30363d #238636 #da3633 #d29922 #db6d28), system-bar-{green,yellow,red}, state-text-{green,red,yellow,orange}, and the emoji list (⚙ 🔧 🔒📦 🔗 🔴 🟡 💾). Run it after any UI change.