Kolory akcentu

View on GitHub

Dodaj nowy kolor akcentu

Przećwiczony przykład: dodanie jedenastej opcji, teal. Ponieważ AccentColor to 'default' | ProjectColors (zobacz sekcję architektury w README), ta jedna nowa nazwa staje się jednocześnie nową opcją akcentu i nowym kolorem odznaki projektu — nie da się dodać jednego bez drugiego, bo to ta sama unia. Cztery osobne, ręcznie utrzymywane miejsca potrzebują nowej nazwy; pomiń jedno, a dostaniesz inny, konkretny rodzaj zepsucia, nie błąd builda.

Krok 1 — Dodaj ją do współdzielonego słownictwa kolorów

Plik: resources/js/types/Projects.ts

export type ProjectColors =
    | 'red'
    | 'orange'
    | 'yellow'
    | 'green'
    | 'lime'
    | 'blue'
    | 'sky'
    | 'violet'
    | 'purple'
    | 'pink'
    | 'teal';

export const AVAILABLE_COLORS: ProjectColors[] = [
    'red',
    'orange',
    'yellow',
    'green',
    'lime',
    'blue',
    'sky',
    'violet',
    'purple',
    'pink',
    'teal',
];

To samo w sobie sprawia, że teal jest poprawnym kolorem odznaki projektu od początku do końca — każdy selektor koloru projektu (NewProjectModal, ProjectDetailsForm, ProjectOnboardingForm i inne) odczytuje z AVAILABLE_COLORS generycznie, żadna zmiana per komponent nie jest potrzebna. Nie sprawia to jeszcze, że teal renderuje się poprawnie (krok 2) ani że jest wybieralny jako akcent (kroki 3–4) — każdy z nich potrzebuje własnej aktualizacji poniżej.

Nazwa musi być prawdziwą nazwą koloru Tailwinda (teal, nie coś wymyślonego) — klasy z kroku 2 to dosłowne stringi, których szuka build Tailwinda; wymyślona nazwa w ogóle nie ma żadnego narzędzia bg-‹nazwa›-500 do wygenerowania.

Krok 2 — Nadaj jej motyw Tailwinda dla odznaki projektu

Plik: resources/js/utils/colors.ts

teal: {
    badgeBg: 'bg-teal-500/10 text-teal-400',
    border: 'hover:border-teal-500/30 shadow-teal-500/5',
    gradient: 'from-teal-500/5 to-transparent',
    accent: 'bg-teal-500',
    textGroupHover: 'group-hover:text-teal-500',
},

Muszą być napisane dosłownie, nie zbudowane z szablonu — skaner Tailwinda generuje klasę narzędziową tylko wtedy, gdy dokładny string pojawia się gdzieś w zeskanowanym źródle (content w tailwind.config.js obejmuje glob resources/js/**/*.{ts,tsx}); wyrażenie szablonowe `bg-${color}-500` jest niewidoczne dla tego skanu i po cichu nie wyprodukowałoby żadnego CSS, niezależnie od tego, jak poprawny okazałby się string w czasie działania. Skopiuj dokładnie kształt pięciu właściwości z istniejącego koloru, zamieniając tylko nazwę koloru.

Krok 3 — Spraw, żeby była poprawną wartością akcentu

Plik: resources/js/context/AccentContext.tsx

const isAccentColor = (value: string | null): value is AccentColor => {
    return (
        value === 'default' ||
        value === 'red' ||
        value === 'orange' ||
        value === 'yellow' ||
        value === 'green' ||
        value === 'lime' ||
        value === 'blue' ||
        value === 'sky' ||
        value === 'violet' ||
        value === 'purple' ||
        value === 'pink' ||
        value === 'teal'
    );
};

Bez tego, wartość teal odczytana z powrotem z localStorage przy przyszłej wizycie nie przechodzi zabezpieczenia i po cichu resetuje się do 'default' — selektor (krok 4) wciąż pozwoliłby ją raz wybrać, ale wybór nie przetrwałby przeładowania strony.

Krok 4 — Nadaj jej wartości hex akcentu i etykietę w selektorze

Plik: resources/js/utils/accentColors.ts

export const ACCENT_COLOR_OPTIONS: AccentColor[] = [
    'default',
    'red',
    'orange',
    'yellow',
    'green',
    'lime',
    'blue',
    'sky',
    'violet',
    'purple',
    'pink',
    'teal',
];

const PROJECT_ACCENT_HEX: Record<
    ProjectColors,
    { base: string; light: string }
> = {
    red: { base: '#ef4444', light: '#f87171' },
    orange: { base: '#f97316', light: '#fb923c' },
    yellow: { base: '#eab308', light: '#facc15' },
    green: { base: '#22c55e', light: '#4ade80' },
    lime: { base: '#84cc16', light: '#a3e635' },
    blue: { base: '#3b82f6', light: '#60a5fa' },
    sky: { base: '#0ea5e9', light: '#38bdf8' },
    violet: { base: '#8b5cf6', light: '#a78bfa' },
    purple: { base: '#a855f7', light: '#c084fc' },
    pink: { base: '#ec4899', light: '#f472b6' },
    teal: { base: '#14b8a6', light: '#2dd4bf' },
};

base/light tutaj to kody hex, nie nazwy klas Tailwinda — to mapa, której krok 2 nie dzieli, mimo że obie są kluczowane tą samą nazwą teal (zobacz notatkę o architekturze w README). Użyj oficjalnych wartości hex -500/-400 palety Tailwinda dla tego koloru (tutaj teal-500/teal-400), żeby próbka akcentu i odznaka projektu czytały się jako "ten sam teal," mimo że nic w kodzie nie wymusza tej zgodności.

Plik: resources/js/Components/Organisms/AccountSettingsContent/AccountSettingsPreferencesTab.tsx

const accentLabels: Record<AccentColor, string> = {
    default: 'Default',
    red: 'Red',
    orange: 'Orange',
    yellow: 'Yellow',
    green: 'Green',
    lime: 'Lime',
    blue: 'Blue',
    sky: 'Sky',
    violet: 'Violet',
    purple: 'Purple',
    pink: 'Pink',
    teal: 'Teal',
};

To trzeci, niezależny, ręcznie utrzymywany rekord kluczowany tymi samymi nazwami — Record<AccentColor, string> oznacza, że sam TypeScript odmówi kompilacji, gdy tylko krok 3/4 rozszerzy AccentColor, dopóki ta mapa też nie dostanie nowego klucza, co jest jedynym elementem w tym całym łańcuchu, który ma siatkę bezpieczeństwa kompilatora.

Krok 5 — Zaktualizuj skrypt sprzed pierwszego renderu (nie pomijaj tego)

Plik: resources/views/app.blade.php

Jest czwarta, niezależna kopia pary hex base/light, wewnątrz inline <script> w <head>, który stosuje zapisany motyw/akcent zanim React w ogóle się zamontuje (żeby uniknąć błysku złego koloru przy pierwszym renderze — zobacz ../theme-colors/01-how-theme-switching-works.md po tę samą troskę "przed pierwszym renderem" po stronie motywu). Ten skrypt nie może zaimportować z accentColors.ts — uruchamia się, zanim jakikolwiek bundle to zrobi — więc koduje na sztywno własną kopię w zwykłym JS:

<script>
    (function () {
        try {
            var stored = localStorage.getItem('theme');
            var mode = (stored === 'light' || stored === 'dark' || stored === 'system') ? stored : 'dark';
            var resolved = mode === 'system'
                ? (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark')
                : mode;
            document.documentElement.setAttribute('data-theme', resolved);

            var accentHex = {
                red: ['#ef4444', '#f87171'],
                orange: ['#f97316', '#fb923c'],
                yellow: ['#eab308', '#facc15'],
                green: ['#22c55e', '#4ade80'],
                lime: ['#84cc16', '#a3e635'],
                blue: ['#3b82f6', '#60a5fa'],
                sky: ['#0ea5e9', '#38bdf8'],
                violet: ['#8b5cf6', '#a78bfa'],
                purple: ['#a855f7', '#c084fc'],
                pink: ['#ec4899', '#f472b6'],
                teal: ['#14b8a6', '#2dd4bf']
            };
            var accent = localStorage.getItem('accentColor');
            if (accent && accent !== 'default' && accentHex[accent]) {
                var base = accentHex[accent][0];
                var light = accentHex[accent][1];
                var r = parseInt(base.slice(1, 3), 16);
                var g = parseInt(base.slice(3, 5), 16);
                var b = parseInt(base.slice(5, 7), 16);
                var alpha = resolved === 'light' ? 0.12 : 0.2;
                var root = document.documentElement.style;
                root.setProperty('--accent-color', base);
                root.setProperty('--accent-light-color', light);
                root.setProperty('--accent-color-opacity', 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')');
            }
        } catch (e) {}
    })();
</script>

Pomiń ten krok, a teal wciąż działa poprawnie — ale dopiero po tym, jak efekt AccentProvider uruchomi się przy montowaniu. Do tego czasu sam pierwszy render renderuje się bez żadnego nadpisania akcentu (zwykła wartość --accent-color, jaką definiuje global.css), a chwilę później widocznie przeskakuje na teal — dokładnie ten błysk złego koloru, któremu ten skrypt ma zapobiegać dla każdego innego akcentu. Trzymaj tę parę hex w zgodzie z PROJECT_ACCENT_HEX z kroku 4 ręcznie — nic nie wymusza, żeby się zgadzały.

Testy

  • resources/js/utils/colors.test.ts — jego test.each(AVAILABLE_COLORS)('returns a complete, color-matched theme for "%s"', ...) już iteruje po tablicy generycznie; gdy krok 1 doda 'teal', a krok 2 doda jego wpis, ten test pokryje to bez żadnych zmian, o ile klasy z kroku 2 są zgodne z dokładnym wzorcem nazewnictwa bg-${color}-500, względem którego test asercuje.
  • resources/js/utils/accentColors.ts nie ma dziś dedykowanego pliku testów — dodaj resources/js/utils/accentColors.test.ts pokrywający getAccentSwatch('teal') (zwraca hex base) oraz getAccentCssVariables('teal', 'dark')/getAccentCssVariables('teal', 'light') (zwraca właściwe accentColor/accentLightColor oraz alfa accentColorOpacity równe 0.2 dla dark vs. 0.12 dla light), na wzór istniejących asercji resources/js/context/AccentContext.test.tsx na kształt getAccentCssVariables dla kolorów, które już ćwiczy.
  • resources/js/context/AccentContext.test.tsx — żadna zmiana nie jest wymagana; jego test 'setAccentColor updates state, persists to localStorage, and sets the CSS variables' używa jako fixture ustalonego, istniejącego koloru i nie jest sparametryzowany po każdym AccentColor, więc nowa nazwa nie potrzebuje tu nowego przypadku, chyba że chcesz go dla dodatkowej pewności.