Jak se web publikovaný na GitHub Pages, Cockpit, stránky, které vykresluje sám spustitelný soubor, a shell Swaggeru začnou jevit jako jeden nástroj: kde se rozhoduje o vizuální identitě, jak dosáhne každého povrchu, jak se rozšiřuje, jak se zachytí drift a co dělat, když stránka vypadá špatně. Chování tak, jak je implementováno a testováno; kde se implementace a tento dokument rozcházejí, má chybu dokument a mění se ve stejném commitu jako oprava. Pravidlo je project.design-tokens-declared-once@2; rozhodnutí jsou ADR 0031 a ADR 0036.
Proč
HTML před člověka staví čtyři povrchy a každý si vyrostl vlastní design. Web byl výchozí motiv Flowbite nad paletou Tailwindu; Cockpit druhý build Tailwindu s vlastní paletou, tmavým motivem v jiném odstínu (který se, jak se ukázalo, nikdy neuplatnil), vlastní favikonou, vlastním klíčem motivu a šesti pixelovými velikostmi v závorkách; stránky reportů sedm hexadecimálních literálů; shell Swaggeru to, co dodala třetí strana. Stavy měly tři palety a to, které slovo znamená který stav, bylo zapsáno v seznamech selektorů stylopisu a ve čtyřech makrech šablon. Nic nedokázalo říct, k čemu je --mj-text-faint.
Nic z toho nebyla špatná hodnota. Byl to návrh bez kanonického domova, takže ho každý povrch rozhodoval znovu a žádná kontrola nedokázala odlišit poslušnost od vymýšlení.
Zdroj pravdy
share/design/tokens.yaml. Jeden soubor ve vlastní podmnožině YAML vrstvy, který drží:
| sekce | o čem rozhoduje |
|---|---|
identity | název a značky pod share/design/brand/ |
font | systémový zásobník a monospace zásobník |
palette | syrové barvy — z Tailwindu, citované; interní, spotřebitel je nikdy nečte |
roles | sémantický povrch: bg, raised, sunken, line, fg, body, muted, faint, accent, accent-strong, accent-soft, accent-line, accent-fill, on-accent, každý položka palety pro každé téma |
status.roles | pět významů — ok, warn, bad, info, neutral — každý s textem, pozadím a ohraničením pro každé téma |
status.states | slovník: každé slovo stavu, které kterýkoli povrch vykresluje, zařazené pod jeden význam |
tones | odstíny pro identitu a nikdy význam — modrá, fialová, zelená, oranžová, růžová, azurová — každý s inkoustem, pozadím, ohraničením a výplní pro každý motiv |
type | pojmenovaná škála (meta, label, small, dense) a prostrkání písmen |
layout, radius, motion | sdílené míry, poloměry rohů a trvání |
theme | třída, která znamená tmavý, a klíč localStorage podle volby čtenáře |
viewports | šířky, při nichž měří každý audit prohlížečem |
alias.flowbite | slovník webu, každý název z Flowbite je synonymem role nebo stavové barvy |
Crate ho čte jako typovaný model (apps/majordomus-cli/src/design/): pořadí deklarací se zachová, každý odkaz se vyřeší, každá nekonzistence se odmítne dřív, než se cokoli zapíše, a nad kanonickou podobou se spočítá otisk SHA-256. Prvních dvanáct číslic tohoto otisku nese každý stylesheet jako --mj-design.
Pipeline
flowchart TD
yaml["share/design/tokens.yaml"]
subgraph sheets["share/design/"]
theme["theme.css<br>Tailwind @theme + Flowbite's names<br>as var(--mj-*)"]
surface["surface.css<br>every role and status colour,<br>light and dark"]
status["status.css<br>one selector group per state word"]
base["base.css<br>(hand-written) focus ring, reduced motion"]
primitives["primitives.css<br>(hand-written) badge, table, card,<br>code, form …"]
end
tailwind["site/tailwind.css"]
appcss["site/static/app.css"]
pages["GitHub Pages"]
cksrc["share/cockpit/src/cockpit.css"]
ckcss["share/cockpit/cockpit.css"]
cockpit["Cockpit"]
webtokens["apps/majordomus-cli/src/web/tokens.css<br>the same tokens, no Tailwind"]
webout["reports, home page, Swagger shell"]
decl["apps/majordomus-cli/src/design/tokens.yaml<br>the declaration, compiled in"]
declout["design.* capabilities,<br>Cockpit shell and Design page"]
logo["apps/majordomus-cli/src/cockpit/logo-mark.svg<br>the mark, compiled in"]
topbar["the UI's hero mark"]
brand["share/cockpit/favicon.svg, site/static/favicon.svg,<br>site/static/images/*.svg<br>every copy of the brand"]
registry["site/data/registry/design.json<br>vocabulary, theme contract, widths"]
registryout["templates, site-probe, ui audit"]
inventory["docs/generated/design.json, design.yaml, design.md<br>the inventory"]
yaml -->|"majordomus generate design (crate::design::render)"| sheets
sheets --> tailwind
tailwind --> appcss
appcss --> pages
sheets --> cksrc
cksrc --> ckcss
ckcss --> cockpit
webtokens --> webout
decl --> declout
logo --> topbar
registry --> registryout
Dva ručně psané soubory leží vedle generovaných a importují je oba vstupní body Tailwindu: base.css (přístupné výchozí hodnoty) a primitives.css (komponenty .mj-*, které vykreslují obě plochy). Jmenují tokeny a nic nevolí. Třetí, figure.css, je jen webu: rám prohlížeče diagramů a grafu (DIAGRAMS.md) nad stejnými tokeny a dvojicemi, které už primitiva používají. Je záměrně bez vrstvy, protože rám je vlastní <pre> diagramu a musí zvítězit nad jakýmikoli utilitami, které mu stránka dala.
Oba buildy Tailwindu importují generované styly ve stejném pořadí, po motivu Flowbite. Web drží ve svých šablonách slovník Flowbite — text-heading, border-default, bg-brand — a generovaný motiv deklaruje každé z těch jmen jako var(--mj-<role>), bez vrstvy a po vlastním bloku .dark Flowbite, takže jméno je synonymum a hodnota je v obou motivech hodnotou deklarace. Cockpit čte --mj-* přímo. Stránka reportu a shell Swaggeru kompilují stejné tokeny dovnitř a nic nestahují.
Stav funguje přes jednu nepřímost. Generovaný stylopis stavů vydává pro každý význam jednu skupinu selektorů nad významem a každým slovem k němu zařazeným — .mj-badge--succeeded, .mj-alert--succeeded, .mj-status--succeeded, … — nastavující --mj-status-fg, --mj-status-bg a --mj-status-line; sdílené primitivy odznaku, upozornění a stavu čtou tyto tři s neutrální záložní hodnotou. Povrch tedy píše mj-badge--<word> a nic jiného a barva je taková, pod čím je slovo zařazeno.
Nic se nespouští ručně. scripts/derive (a just derive) spouští majordomus generate, což zahrnuje cíl designu; scripts/cockpit-assets a scripts/site-build kompilují dva balíky Tailwind z generovaných stylů; hook pre-commit a plán CI odmítnou strom, jehož projekce jsou zastaralé.
Jak ji rozšířit
Každé rozšíření je jedna úprava v share/design/tokens.yaml, pak majordomus generate design (nebo just derive) a nakonec obě sestavení assetů. Nic dalšího se neupravuje.
- Sémantická barva. Přidejte roli pod
rolessabout,lightadarkpojmenovávajícími položky palety (přidejte položky podpalette, pokud je barva nová — hodnota Tailwindu, v uvozovkách). Je to--mj-<name>na každém povrchu, třída swatche pro inspektor, řádek na stránce Design v Cockpitu a v soupisu, a odpovídá na toexplain. - Stavová sémantika. Přidejte význam pod
status.rolessfg,bgalinena motiv a zařaďte jeho slova podstatus.states. Každémj-badge--<word>je ostylováno. - Odstín. Přidejte jej pod
tonessfg,bg,lineafillpro každý motiv. Odstín je barva pro identitu — krok posloupnosti, karta jednoho povrchu — a nikdy verdikt, proto to není stav..mj-tone--<name>nastaví čtyři vlastnosti--mj-tone-*, které čte každá komponenta sady, měření kontrastu páruje odstín jen sám se sebou asite/data/registry/design.jsonjej uvádí pro šablony. - Stavové slovo. Přidejte ho do seznamu pod jeho význam. To je celá změna:
test/cases/109_design_system.shdělá přesně toto, aby prokázal, že se nic jiného nepohne. - Krok typografie. Přidejte ho pod
type.scalessizealeading;text-<name>existuje v obou sestaveních Tailwindu a--text-<name>na každé stránce. - Hodnota odsazení nebo rozvržení. Přidejte ji pod
layout; je--mj-<name>. - Primitivum komponenty. Napište ho jednou v
share/design/primitives.cssnad tokeny a kroky stupnice; mají ho oba povrchy. Vlastní stylesheet Cockpitu drží jen to, co má samotný Cockpit. - Značka. Nahraďte soubor pod
share/design/brand/; každá kopie se vygeneruje znovu. - Název z Flowbite, který web používá a který by měl sledovat roli. Přidejte jej pod
alias.flowbite. Název, který tam chybí, si ponechá hodnotu Flowbite, což je prohlášení, že nejde o sdílené rozhodnutí.
Proces nikdy nezahrnuje úpravu šablon webu a stylopisu Cockpitu ze stejného důvodu. Pokud ano, architektura má díru; založte na to úkol.
Sada
Komponenty, z nichž jsou složeny produktové a dokumentační stránky webu: hero a citát doktríny, panely a fáze, strom souborů, body, řádky rozhraní, dlaždice odkazů, pás tvrzení, terminál se strukturovaným výstupem, příkaz s tlačítkem kopírování, pruh karet ve třech podobách, řádky HTTP API, okno dashboardu s počty, grafem, kanálem, akcemi a seznamem zdraví, stopa, vyhledávací pole, obsah, tok kroků a tři diagramy — zásobník vrstev, vějíř, který spojuje řádky s hubem, a cyklus o čtyřech. /kit/ vykresluje každou z nich z dat tohoto repozitáře.
| soubor | co drží |
|---|---|
share/design/kit.css | jak každá komponenta vypadá, jako třídy .mj-* nad rolemi, nepřímostí stavu a nepřímostí tónu; měřeno kontrolou kontrastu jako primitiva |
site/templates/kit/*.html | co je každá komponenta, jako komponenta Tera: base (icon, glyph, step, button, pill, state, copy), content (hero, quote, section, panel, stages, tree, points, routes, tiles, links, strip, figure, claims, table), code (terminal, term_line, term_out, term_box, cmd, cmd_list, tabs, tab_panel, method, request, endpoint, listing), dashboard (window, sidenav, kpis, chart, legend, feed, actions, health), docs (trail, search, toc, flow), diagrams (stack, fan, cycle) |
site/data/kit/icons.toml | obrysové glyfy, které komponenta jmenuje klíčem; neznámý klíč shodí build |
site/kit.js | tři chování, která značkování neunese: kopírovat, přepínat karty, označit čtenou sekci |
site/kit.tailwind.css, site/templates/partials/kit-assets.html | vlastní sheet a skript sady, které si stránka zvolí |
Konvence, které komponenta dodržuje, aby je stránka mohla skládat, aniž by četla jejich zdroj:
- Položka je mapa a klíč znamená všude jedno.
titleatextse čtou,iconje klíč sady ikon,tonetón,hrefkam vede,stateslovo stavu. - V komponentě se nezapisuje žádná hodnota. Počet, příkaz, přepis nebo release jí předá stránka a stránka ho čte z generované datové sady. Čísla v mockupu nejsou data.
- Stav je atribut. Vybraná karta je
aria-selected, aktuální odkazaria-current, otevřená skupinadetails[open]. Stylesheet stylizuje atribut, takže žádná vázaná třída nemůže prohrát se statickou utilitou (viz Alpine:classprohrává se statickými utilitami). - Nic nevyžaduje, aby se skript četl. Tlačítko kopírování a lišta karet se vykreslí
hiddena odhalí jesite/kit.js; každý panel karty je vykreslen pod nadpisem, který by jeho karta nesla; rozbalovací blok je nativní<details>; křivky vějíře počítá šablona podle toho, kolik řádků má každá strana. - Sada je volbou stránky. Stránka z ní složená zahrnuje
{% block kit %}{% include "partials/kit-assets.html" %}{% endblock kit %}; každá jiná stránka nenačítá nic z ní, což drží domovskou stránku v jejím rozpočtu bajtů.
Obrazovky. Obrazovka je celá stránka složená jen ze sady, která vykresluje jeden z návrhů stránek vlastníka (architektura, integrace, dokumentace, CLI, Cockpit, funkce, jak to funguje, úvod) z dat tohoto repozitáře v /kit/<screen>/. Šablona obrazovky (site/templates/screens/<name>.html) rozšiřuje site/templates/screen.html, která načítá datové sady, jež obrazovka smí číst, pod jednou sadou jmen — design, registry, kinds, oas, cl, term, cli, commands, tones, base — a její stránka je site/content-src/kit/<name>.md. Návrhy nesly hodnoty, které nikdo neměřil (verzi, která nikdy nevyšla, počty neexistujících objektů, latence, číslo úkolu, port); obrazovka neukazuje žádnou z nich a test/cases/791_a_screen_is_composed_of_the_kit_and_of_real_data.sh odmítne tu, která ano, tu, která si volí vlastní barvu, styl nebo třídu, i tu mimo rámec.
Přidání komponenty: napište její třídy do share/design/kit.css, její markup do souboru site/templates/kit/, do něhož patří, a příklad na site/templates/kit-page.html ze skutečných dat; ovládací prvek, který nese, potřebuje specifikaci pod scripts/lib/interaction-specs/. test/cases/790_the_kit_is_one_vocabulary_and_its_page_renders_all_of_it.sh odmítne komponentu, kterou stránka nevykresluje, třídu, kterou nevydává žádná šablona nebo nedefinuje žádný sheet, ikonu, kterou sada nemá, a sadu načítanou každou stránkou; scripts/interaction-probe projde v prohlížeči každé tlačítko kopírování, záložku a pole hledání. Tera 2 v Zole 0.23 typuje argument komponenty podle jeho výchozí hodnoty, bere literální argument jen ve složených závorkách (count={4}), povoluje jedno for na comprehension a pole hluboká dvě úrovně; sada je napsána v těchto mezích.
Navigační lišta: web je reference
majordomus.dev je referencí toho, jak produkt vypadá a chová se, a povrchy, které program servíruje, se z něj odvozují, místo aby ho napodobovaly (project.the-ui-wears-the-sites-navbar). První odvozená věc je lišta v horní části stránky. Má jednu deklaraci (site/data/nav.toml, project.navigation-is-declared-once) a jeden renderer (site/templates/partials/navbar.html); každá obrazovka majordomus ui a každá stránka Cockpitu nosí výstup tohoto rendereru:
- značkování je lišta sestaveného webu, vyříznutá
scripts/ui navbar --writedoapps/majordomus-cli/src/ui/navbar.htmla zkompilovaná do spustitelného souboru — s release v odznaku ponechaným k doplnění spustitelným souborem a s vyřazenými komentáři buildu; - styly jsou tytéž utility nad týmiž tokeny: vstupy Tailwind pro UI a Cockpit (
share/ui/src/ui.css,share/cockpit/src/cockpit.css) importují to, co importuje vstup webu, a kopii jmenují jako zdroj, takže každý zkompilovaný stylesheet nese přesně ta pravidla, v nichž je lišta napsána; - chování je vlastní
nav.jsatheme.jswebu, servírované zshare/ui/ashare/cockpit/jako bajtové kopie, kteréscripts/cockpit-assets --checkdrží shodné s originály.
scripts/ui navbar (spouští ji jako první scripts/ui audit, kontrola ui-audit) odmítne kopii, která není sestavenou lištou, a v prohlížeči porovná lištu na úvodní stránce webu s lištou na /ui a lištu na /cockpit stejně — tag každého prvku, třídy, text, box a vypočtené styly, světlý i tmavý režim, při každé šířce, kterou viewports deklaruje, zavřenou i s otevřenou nabídkou. Spustitelný soubor rozhoduje jen o tom, co ví sám: o verzi a o tom, že položka produktu vede do samotného UI, a ne k jeho publikovanému snímku. Zbytek říká příručka UI (MAJORDOMUS_UI.md).
Validace
| co | kde | kdy |
|---|---|---|
| deklarace je konzistentní | crate::design::DesignSystem::validate, spouštěno generate | dříve, než se cokoli zapíše |
| každá projekce je aktuální | majordomus generate --check (rust-integration / generation-converges) | každý plán, který se dotýká crate, share nebo generovaného stromu |
| žádný zdroj nenese vlastní designové rozhodnutí | scripts/ci/design-check (kontrola design, job structure) | každý plán, který se dotýká stylopisu, šablony, skriptu nebo webového kódu crate |
| zkompilovaný stylesheet Cockpitu odpovídá svému zdroji a paleta odpovídá nainstalovanému Tailwindu | scripts/cockpit-assets --check (kontrola cockpit-assets) | job webu |
| každé slovo odznaku na webu je zařazeno a každá stránka nese otisk | scripts/site-check (kontrola site-build) | job webu |
| stylesheet, stránka a datová sada se shodují při každé deklarované šířce | scripts/site-probe (kontrola site-probe) | job webu |
| stylopis, stránka a spustitelný soubor se shodují; každé slovo odznaku je zaevidováno; žádné přetečení při každé deklarované šířce | scripts/cockpit-probe (kontrola cockpit-probe) | úloha cockpit |
| každá stránka je ovladatelná klávesnicí a prochází kontrastem při každé šířce | scripts/ui audit (kontrola ui-audit) | job webu |
| navbar UI je navbar webu: zkompilovaná kopie je sestavená lišta a obě lišty se shodují prvek po prvku a styl po stylu | scripts/ui navbar, spouštěno jako první pomocí scripts/ui audit (kontrola ui-audit) | job ui |
| každá barva, kterou deklarace spáruje s pozadím, je na něm čitelná, v obou motivech | crate::design::contrast přes design.contrast, otázka 9 pro scripts/ci/design-check; test/cases/120_design_contrast.sh | každý plán, který se dotýká deklarace nebo primitiva |
| model, renderer a vlastnost nulové registrace | testy crate; test/cases/107_design_tokens.sh; test/cases/109_design_system.sh | úlohy rust a suite |
Otázky kontroly jsou nalezené způsoby selhání: barevný literál, syrová utilita palety (text-green-700), velikost v hranatých závorkách (text-[11px]), druhá deklarace kanonické vlastnosti, čtení tokenu --mj-, který nic nevydává, kopie značky, která se liší od kanonického souboru nebo žije jinde, a ručně pojmenovaný klíč nebo třída tématu. Každá výjimka v kontrole je důvod, uvedený tam, kde je kontrola napsána.
Poslední otázka se netýká toho, kde se rozhodlo, ale zda je rozhodnutí čitelné. Které popředí se vykresluje na kterém pozadí, se odvozuje — deklarace přiřazuje text každého stavu jeho vlastnímu pozadí a primitiva říkají zbytek: pravidlo nastavující barvu a pozadí uvádí dvojici, pravidlo nastavující jen barvu uvádí popředí, které dopadá na každé pozadí, jaké kontejner nastaví — takže role přidaná zítra se změří zítra a žádný seznam se nemusí udržovat. Práh je WCAG 2.1 AA, 4,5:1 pro text, stejná konstanta, kterou scripts/lib/ui-contrast.mjs drží pro generované zvýrazňovací palety; ohraničení se měří proti 3:1 a hlásí se, ale nevynucuje, protože primitivum, které jej kreslí, vždy ukazuje vedle něj slovo.
Introspekce
| povrch | jak |
|---|---|
| MCP | majordomus_design, majordomus_design_tokens, majordomus_design_explain, majordomus_design_contrast; zdroj majordomus://design |
| HTTP | GET /api/v1/design, /api/v1/design/tokens?kind=role, /api/v1/design/explain?token=fg, /api/v1/design/contrast |
| OpenAPI, Swagger | stejné čtyři operace, z registru |
| Cockpit | /cockpit/design: každá role s vzorky obou motivů, každý stav se svými slovy jako odznaky, typová škála ve vlastních velikostech, projekce — a odznak, který říká, zda stylesheet načtený stránkou souhlasí se spustitelným souborem |
| reference | docs/generated/design.md a JSON a YAML vedle něj |
explain bere roli, stav, slovo stavu, krok typu, položku palety nebo vlastní vlastnost, kterou se kterákoli z nich stane:
majordomus_design_explain {"token": "--mj-ok"}
→ kind: status, about: healthy, done, passing …,
parts: fg --mj-ok emerald-900 / emerald-300, bg --mj-ok-bg …, line --mj-ok-line …,
states: ok pass passed succeeded completed done …,
aliases: --color-fg-success-strong --color-success-soft --color-success-subtle,
projections: share/design/theme.css share/design/surface.css …Řešení problémů
- Stránka na jednom povrchu vypadá špatně. Přečtěte
--mj-designze stránky (getComputedStyle(document.documentElement).getPropertyValue('--mj-design')) a porovnejte sGET /api/v1/design(design) nebosite/data/registry/design.json. Cockpit to dělá sám a při neshodě ukáže upozornění. Odlišný otisk znamená stylesheet zkompilovaný dřív, než se deklarace posunula: spusťtemajordomus generate design, potéscripts/cockpit-assetsneboscripts/site-builda restartujte server. - Chybí barva. Nedeklarovaná vlastní vlastnost je prázdný řetězec, nikdy chyba.
scripts/ci/design-checkjmenuje každou--mj-, kterou nic nevysílá; spusťte to. - Odznak je šedý, a neměl by být. Jeho slovo není zařazeno pod žádný stav.
explainho; pokud nic neodpoví, zařaďte slovo podstatus.states.scripts/site-checkascripts/cockpit-probenezařazené slovo odmítnou. - Cockpit nemá styly.
share/cockpit/cockpit.csschybí nebo je zastaralé:scripts/cockpit-assets. Shell to oznámí v banneru. - Tmavý režim se nepřepíná. Třída, kterou stránka přepíná, je
theme.classz deklarace, zapsaná na<html>(web) nebo<body>(Cockpit) jakodata-theme-class; skripty ji čtou tam. Stránka bez toho atributu má neplatnou zkompilovanou deklaraci:GET /api/v1/designříká proč. - Selže sonda vizuální regrese. Zjištění jmenuje routu, šířku a tvrzení. Přetečení je vada rozvržení na té stránce; zjištění
designje zastaralý bundle nebo stránka, která nenesedata-design; zjištěnífontznamená jiný stack, než byl deklarován — obvykle stylesheet třetí strany načtený po našem. generatedeklaraci zamítne. Zpráva pojmenuje cestu klíče a důvod (roles.sunken.light: 'gray-51' is not a palette entry). Nic nebylo zapsáno.
Co se záměrně nesdílí
Pod lištou sdílejí povrchy jazyk, ne layout: samotná lišta je webová i na UI a v Cockpitu (výše). Web tvoří komponenty Flowbite v šablonách o hustotě pro čtení; Cockpit jsou sémantické třídy nad týmiž tokeny o provozní hustotě. Vlastní strom komponent Swagger UI si ponechává stylesheet třetí strany uvnitř rámu, který je náš. Názvy Flowbite, které web používá a které nejsou sdílenými rozhodnutími (neutral-tertiary, brand-strong, varianty fg-* bez strong), si ponechávají hodnoty Flowbite, dokud je někdo nezařadí pod roli.