Przejdź do głównej zawartości

Tokeny i zależności

Techniczny: DraftAkceptacja: Approved

Token to nazwana decyzja wyglądu: kolor, odstęp, promień, poziom skali typograficznej, czas animacji. Komponent pyta o rolę (color.brand.primary), nie o konkretny odcień. Dzięki temu ta sama linia kodu wygląda inaczej w pięciu markach i dwóch trybach, a sam kod się nie zmienia.

Jedynym kontraktem jest warstwa semantyczna: --color-*, --spacing-*, --radius-*, --font-*, --shadow-*, --size-*, --border-width-*, --breakpoint-*, --focus-ring, --motion-*. Komponenty w ui/ i strony styleguide’u używają wyłącznie jej.

Tak
<button className="bg-action-primary text-text-on-action rounded-md">Przejdź do wyceny</button>
Nie tak
// Klasa z domyślnej palety Tailwinda i literał. Żadne z nich nie przełączy się
// z data-brand, nawet jeśli dziś trafia w kolor marki.
<button className="bg-blue-600">Przejdź do wyceny</button>
<button style={{ background: "#4A78AD" }}>Przejdź do wyceny</button>

Pierwszej z tych dwóch linii nie da się już skompilować. dist/theme.css gasi domyślny motyw Tailwinda w tych przestrzeniach nazw, w których zbiór tokenów jest całym zbiorem systemu — kolory, typografia, odstępy, promienie, cienie, kontenery i progi układu. bg-blue-600, font-medium, gap-4, max-w-md i wariant 2xl: nie mają po tym żadnej wartości do wzięcia, więc Tailwind nie generuje dla nich klasy. Zostają dwa słowa kluczowe CSS: transparent i currentColor — nie są wartością marki i w tokenach odpowiednika nie mają. Drugą linię, literał w atrybucie style, zatrzymuje bramka no-hardcoded.

Warstwy core, brand i mode są wewnętrzne. Nie mają zmiennych CSS, nie stanowią publicznego kontraktu i zmieniają się bez ostrzeżenia. Jedyny wyjątek to namespace --motion-*, opisany niżej.

Cztery warstwy, jedna droga. Przykład: tło przycisku primary.

WarstwaCo trzymaW tym przykładzie
coreprymitywy wspólne dla wszystkich marek: skala odstępów, promienie, skala typograficzna, rampa navy, statusy, ruchw tym przykładzie nie bierze udziału, bo stopnie rampy marki trzyma warstwa brand
brandpaletę jednej markibrand.role.buttonPrimary wskazuje brand.primary.700 — ten sam stopień w każdej z pięciu marek
modeprzemapowanie ról na tryb jasny albo ciemnytło przycisku nie zależy od trybu, więc mode go nie dotyka; dotyka np. background.page (navy-50 w light, navy-900 w dark)
semanticrolę, o którą pyta komponentcolor.action.primary = {brand.role.buttonPrimary} → --color-action-primary

Stopień znaczy w każdej marce to samo: tło przycisku głównego stoi na 700, stan hover na 800, active na 900 — wszędzie. Numeru stopnia i tak nie wpisujesz nigdzie; pytasz o color.action.primary.

PodglądRola (token)Zmienna CSSKlasa TailwindHex — lightHex — dark
color.brand.primary--color-brand-primarybg-brand-primary#4A78AD#4A78AD
color.action.primary--color-action-primarybg-action-primary#274A74#274A74
color.action.hover--color-action-hoverbg-action-hover#1A3556#1A3556
color.action.active--color-action-activebg-action-active#10243D#10243D
color.brand.subtle--color-brand-subtlebg-brand-subtle#E3ECF5#1A3556
color.brand.on-subtle--color-brand-on-subtletext-brand-on-subtle#1A3556#C5D6E8

Markę i tryb ustawiasz dwoma atrybutami na <html>:

<html data-brand="travel" data-theme="dark">
  • data-brand: insurance, home, health, moto albo travel;
  • data-theme: light albo dark.

Pięć marek razy dwa tryby daje 10 kombinacji. Nazwy ról są w każdej te same, zmieniają się wartości. Bez atrybutów strona dostaje insurance i light.

Tryb ciemny nie jest drugą paletą, tylko innym przemapowaniem tych samych ról. Marka różnicuje dziś wyłącznie kolor: brand.*, action.*, accent.*, text.link, border.focus i focus.ring. Odstępy, promienie, typografia, cienie i ruch są wspólne dla wszystkich pięciu.

brand.primarybrand.subtleaccent.defaulttext.linkbackground.pagetext.primary
insurance.light
insurance.dark
home.light
home.dark
health.light
health.dark
moto.light
moto.dark
travel.light
travel.dark

Strony styleguide’u nie ustawiają data-brand. Dokumentacja żyje na stałej, neutralnej palecie. Komponent w konkretnej marce i trybie oglądasz na stronach /preview/*, które mają własny przełącznik.

CSS własny
.card {
background: var(--color-background-surface);
padding: var(--spacing-stack-lg);
border-radius: var(--radius-lg);
}
Klasa narzędziowa Tailwind — ta sama wartość
<div className="bg-background-surface p-stack-lg rounded-lg">…</div>
Agent i walidator
// tokens/dist/tokens.json — nazwa tokenu jest kluczem mapy `tokens`
data.tokens["color.brand.primary"].cssVar // "--color-brand-primary"
data.tokens["color.brand.primary"].values["health.dark"]

Build wydaje sześć artefaktów w tokens/dist/: css/tokens.css (zmienne do importu), tokens.json (płaska mapa z wartością w każdej z 10 kombinacji), theme.css (rejestracja w Tailwind v4), type-scale.css (klasy text-<rola>) i type-scale.json (ta sama skala jako dane). Szósty plik, palette.json, pokazuje pełne rampy na stronie koloru i podaje barwy narzędziom (generator znaku, walidator literałów). Nie jest interfejsem dla kodu produktu. Nazw nie zgadujemy, czytamy je z tokens.json. Mechanika budowania: README paczki — źródło w repo: packages/design/tokens/README.md.

core.motion.* jest stałe: nie zależy ani od marki, ani od trybu, więc trafia do dist/ bezpośrednio, pod własnym prefiksem --motion-*. To jedyny wyjątek od reguły „nigdy core”.

TokenZmienna CSSWartośćPodgląd
core.motion.duration.slow--motion-duration-slow320ms
core.motion.duration.base--motion-duration-base200ms
core.motion.duration.fast--motion-duration-fast120ms
core.motion.easing.accelerate--motion-easing-acceleratecubic-bezier(0.4, 0, 1, 1)
core.motion.easing.decelerate--motion-easing-deceleratecubic-bezier(0, 0, 0.2, 1)
core.motion.easing.standard--motion-easing-standardcubic-bezier(0.4, 0, 0.2, 1)

Kiedy którego czasu i krzywej użyć, opisuje Ruch i reduced motion.

Kolejność jest stała: źródło → build → bramka → strona.

  1. Źródło. Wybierz warstwę. Nowy prymityw wspólny dla marek idzie do core.tokens.json, wartość jednej marki do brands/<marka>.tokens.json, różnica między trybami do modes/{light,dark}.tokens.json, nowa rola dla komponentów do semantic.tokens.json. Wartości są statyczne i commitowane.
  2. Build. pnpm -F @polisoteka/design-tokens build przelicza wszystkie 10 kombinacji naraz. Token bez wartości w którejkolwiek z nich nie przejdzie.
  3. Bramka. pnpm -F @polisoteka/design-tokens check:tokens sprawdza kompletność i kontrast par, które realnie renderują komponenty. Nową parę tekst/tło dopisujesz do listy par zanim jej użyjesz, nie po pierwszym audycie.
  4. Strona. Rola widoczna w dokumentacji ma trafić do tabeli na właściwej stronie fundamentów, a nie do prozy. Tabele czytają tokens.json, więc same podążają za wartością.

Gdy kontrast nie przechodzi, korygujesz odcień w tokens/src. Progu w bramce nie obniżasz.

  • Komponent nie sięga do core, brand ani mode. Wyjątkiem jest --motion-*.
  • W komponentach i na stronach nie ma literałów: ani hex, ani rgb(), ani pikseli odstępu, ani nazw krojów spoza font.family.*.
  • tokens/dist/ jest wynikiem budowania. Ręczna poprawka tam znika przy najbliższym przebiegu.
  • Nowy token powstaje dla roli, która powtarza się w systemie. Jedno miejsce użycia to znak, że rola już istnieje pod inną nazwą.
  • Marka nie rozgałęzia kodu komponentu. Różnicę między portalami niesie wartość tokenu, nie warunek w JSX.

Token działa, gdy:

  1. build i check kończą się zielono, a token ma wartość w każdej z 10 kombinacji w dist/tokens.json;
  2. komponent zmienia wygląd po przestawieniu data-brand i data-theme, bez zmiany w kodzie komponentu;
  3. bramka pnpm design:check przechodzi, łącznie z kontrolą literałów w zbudowanym styleguide;
  4. nowa para tekst/tło ma wpis w liście par bramki kontrastu.

Kontrakt obszaru — źródło w repo: packages/design/AGENTS.md, README paczki tokenów — źródło w repo: packages/design/tokens/README.md, bramka kontrastu — źródło w repo: packages/design/tokens/src/artifact.ts, Kolor, Typografia, Odstępy i rytm, Standard dostępności.