Files
felhom.eu/documentation/design/design-system.md
T
admin 80659232bc
gates / gates (push) Successful in 6m58s
website: M3 part A - the controller's shell, and the home page inside it
The direction the operator chose: the controller's FRAME around the website's own
content. Staged at /preview/m3/, noindex, nothing live.

website/assets/shell.css - 117 rules COPIED VERBATIM from felhom-controller
  style.css @ dcd89364a4 (sidebar, mobile top bar, off-canvas drawer, and the
  component atoms the restyled pages use: btn, alert, app-info-card, launch-*,
  stack-detail-card, form-*). Exactly ONE rule is not verbatim and says so:
  body.shell { display:flex; min-height:100vh }, which lives in the controller's
  base rather than in a component, scoped to .shell so other pages are untouched.

  The generator NAMES every rule it copies and REFUSES to write the file if one has
  disappeared upstream. That is not ceremony: it already caught two things this
  session. It refused because .sidebar::-webkit-scrollbar does not exist (only the
  -track override does), and because prefers-reduced-motion is split across FIVE
  separate blocks in the controller - reading only the first would have silently
  lost four rules. The M1 round lost a rule exactly this way.

  The bare `body { flex-direction: column }` that broke M1's phone layout is copied
  this time, by name, as are body.nav-open and the no-js/js pair.

  Load order is deliberate: shell.css AFTER site.css. Thirteen selectors exist in
  both files with different meanings (.nav-links, .page-header, .lang-globe*, .ico)
  - a horizontal site nav there, a vertical sidebar here. Later wins, and a page
  that does not load shell.css is completely unaffected.

website/preview/m3/index.html - the home page in the shell. Content is /preview/'s
  cleaned text; body carries `shell page-index` so the existing .page-index content
  rules (caps, flow, vs, band, who-mini) are reused rather than duplicated. Six
  sections, left-aligned headers, the controller's cards. Head tags (title, meta
  description, canonical, hreflang) are already the FINAL values, so the switch in
  part D needs no head edit.

  Four changes from the operator's mockup, each with its reason in the generator:
    1. the active item follows the page (the mockups show Inditopult active on all)
    2. Teszteles is ALSO a primary button at the top of the lower group - it is the
       one action we want and the mockup buries it at the bottom
    3. the Alkalmazasok categories are anchors on /alkalmazasok: real URLs, no
       JS-only state
    4. ADDED GYIK. It is in today's nav; dropping it silently would take a page out
       of navigation, which is not a change a restyle gets to make.

scripts/site_gates.py - gate 3 extended rather than duplicated, as asked: a SHELL
  set whose pages must carry an IDENTICAL <nav class="sidebar"> after the active
  marker is normalised. RED-PROOFED: a one-character change to one menu label on a
  second shell page convicts. It also prints "DIFFER" rather than a reassuring
  "identical" count when it fails - a false reassurance printed beside a conviction
  is the exact defect this repo files rows about.

documentation/design/design-system.md - new "Shell" section: what is shared (tokens
  + shell + atoms), what each side owns, how a change flows (controller first, the
  website's copy the same week), and that the cross-repo gate is deliberately NOT
  built yet.
2026-10-11 13:48:50 +02:00

223 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Felhom design system v2
**Status:** canonical reference. Shipped in the controller UI with **TASK-D0** (controller v0.96.0,
2026-07-02); D1–D4 (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)
```css
: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 400–800). **Data face:** JetBrains Mono (400–500) 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`, `70–85 → 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).
## Shell (M3, 2026-10-11)
**The website and the controller now share two things: the tokens, and the shell.** Everything else
each owns.
| | Shared | Website owns | Controller owns |
|---|---|---|---|
| Tokens | **yes** — 18 names, verified byte-identical in both `:root` blocks | — | — |
| Shell: sidebar, mobile top bar, off-canvas drawer | **yes** — `website/assets/shell.css` is a COPY | the menu's items, the wordmark under the logo, the sidebar's tester button, the language globe's targets | the menu's items, the language globe's POST form |
| Component atoms: `.btn*`, `.alert*`, `.app-info-card`, `.launch-*`, `.stack-detail-card`, `.form-*` | **yes** — same copy | — | — |
| Page content and its layout | no | all of it (`site.css`) | all of it (`style.css`) |
**`website/assets/shell.css` is a copy, not a library.** It is generated from
`felhom-controller/controller/internal/web/templates/style.css` and carries that commit in its
header. Exactly one rule in it is not verbatim: `body.shell { display: flex; min-height: 100vh }`,
restated because it lives in the controller's base rather than in a component, and scoped to
`.shell` so a page without the shell is untouched.
**Load order matters and is deliberate.** `shell.css` loads AFTER `site.css`. Thirteen selectors
exist in both with different meanings — `.nav-links`, `.nav-links a`, `.page-header`, `.lang-globe*`,
`.ico`, `.ico-sm` — a horizontal site nav in one, a vertical sidebar in the other. Later wins, so a
shell page gets the shell's meaning and a page that does not load `shell.css` is unaffected.
### How a change flows
1. **The controller changes first.** It is the surface a household operates daily; the website
follows it, never the other way round.
2. **The website's copy is re-generated in the same week**, by re-running the generator against the
controller's new commit and updating the header. A copy that silently falls behind is the failure
this section exists to prevent.
3. **A later gate may enforce step 2. One is NOT built today** (operator's brief, 2026-10-11) — the
cross-repo check waits until the shell has settled.
### The trap that makes this a copy with a generator, rather than a copy by hand
Copying components by class name **silently drops the element-level rules those components depend
on.** Measured on 2026-10-11: copying the whole `@media(max-width: 768px)` block *except* its bare
`body { flex-direction: column }` left the sticky top bar as a row flex item, pushed the content
135 px right, collapsed the launcher grid to one column, made a page 15 597 px tall on a phone and
overflowed two others. Everything looked copied. Full account:
`documentation/audits/mockups-2026-10-11/SHARED-DESIGN.md` §2.
So the generator **names every rule it copies and refuses to write the file if one has disappeared
upstream** — an incomplete copy fails loudly instead of rendering wrong.
### Enforcement for the shell
`scripts/site_gates.py` gate 3b: every page in the `SHELL` set must carry a
`<nav class="sidebar">` block **identical** to the others' after the active marker is normalised
away. The site is static with no build step, so the shell is duplicated into each page; this is what
keeps the duplication honest. Red-proofed 2026-10-11: a one-character difference in one menu label
convicts.
## 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.