[{"data":1,"prerenderedAt":968},["ShallowReactive",2],{"blog-\u002Fpl\u002Fblog\u002F2026\u002Fmigracja-na-tailwind-v4":3,"blog-project-\u002Fpl\u002Fblog\u002F2026\u002Fmigracja-na-tailwind-v4":827},{"id":4,"title":5,"art":6,"body":7,"date":812,"description":813,"draft":814,"extension":815,"image":816,"kind":817,"meta":818,"navigation":81,"ogImage":819,"path":820,"project":821,"seo":822,"slug":816,"stem":823,"tags":824,"updated":816,"__hash__":826},"blog_pl\u002Fpl\u002Fblog\u002F2026\u002Fmigracja-na-tailwind-v4.md","Tailwind v4 w dużym projekcie: 76 plików zamiast 339","\u002Fart\u002Fblog\u002Fmigracja-na-tailwind-v4.svg",{"type":8,"value":9,"toc":805},"minimark",[10,19,26,31,34,272,287,290,331,335,338,501,508,512,518,537,548,686,689,722,732,736,739,742,763,770,774,802],[11,12,13,14,18],"p",{},"AgrestApp był na Tailwindzie 3 i sporo elementów nie działało tak, jak powinno. W pewnym momencie dostałem audyt z ogromną listą zmian: ok. 250 przycisków do poprawy, tabele i wiele innych rzeczy. Na liście było m.in. to, że przyciski mają za mały tekst, a pola formularzy – 14 px, przez co iPhone przybliża stronę po kliknięciu w pole. Zaproponowałem, żeby na ekranach dotykowych powiększać te elementy wariantem ",[15,16,17],"code",{},"pointer-coarse:"," – to nowość z Tailwinda v4. Uprzedziłem, że to potrwa: mogę poprawić wszystko „na oko” albo zmieniać i przy okazji porządkować serwis. Dostałem zgodę na nowszą wersję Tailwinda i na powolne porządki – np. zamianę 3–4 przycisków z osobnych komponentów na jeden.",[11,20,21,22,25],{},"26 sierpnia przeniosłem AgrestApp z Tailwinda 3.4.1 na 4.3.3. To duży projekt: konfiguracja w JavaScripcie ma ok. 200 kolorów i 11 breakpointów, a w szablonach jest ponad 1500 klas z wykrzyknikiem (",[15,23,24],{},"!bg-…","). Narzędzie do automatycznej migracji zaproponowało zmiany w 339 plikach i 1710 liniach. Weszło 76 plików, a większość zmienionych linii to dwa pliki lock.",[27,28,30],"h2",{"id":29},"zasada-nie-przepisywać-szablonów","Zasada: nie przepisywać szablonów",[11,32,33],{},"Tailwind v4 zmienił kilka domyślnych wartości. Mogłem poprawić każdą klasę w szablonach albo przywrócić stare wartości w jednym miejscu. Wybrałem to drugie. Klasy w szablonach zostały takie, jak były, a różnice między wersjami opisałem w CSS:",[35,36,41],"pre",{"className":37,"code":38,"language":39,"meta":40,"style":40},"language-css shiki shiki-themes github-light github-dark","@import \"tailwindcss\";\n\u002F* v4 nadal czyta konfigurację JS – kolory i breakpointy bez zmian *\u002F\n@config \"..\u002F..\u002Ftailwind.config.cjs\";\n\n\u002F* v4 przeskalował te trzy wartości – przywracam te z v3 *\u002F\n@theme {\n  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 \u002F 0.05);\n  --radius-sm: 0.125rem;\n  --default-ring-width: 3px;\n}\n\n\u002F* v4 zmienił domyślny kolor ramki na currentcolor *\u002F\n@layer base {\n  *, ::after, ::before, ::backdrop, ::file-selector-button {\n    border-color: var(--color-gray-200, currentcolor);\n  }\n}\n","css","",[15,42,43,60,67,76,83,89,98,147,163,178,184,189,195,206,238,261,267],{"__ignoreMap":40},[44,45,48,52,56],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"szBVR","@import",[44,53,55],{"class":54},"sZZnC"," \"tailwindcss\"",[44,57,59],{"class":58},"sVt8B",";\n",[44,61,63],{"class":46,"line":62},2,[44,64,66],{"class":65},"sqY6-","\u002F* v4 nadal czyta konfigurację JS – kolory i breakpointy bez zmian *\u002F\n",[44,68,70,73],{"class":46,"line":69},3,[44,71,72],{"class":50},"@config",[44,74,75],{"class":58}," \"..\u002F..\u002Ftailwind.config.cjs\";\n",[44,77,79],{"class":46,"line":78},4,[44,80,82],{"emptyLinePlaceholder":81},true,"\n",[44,84,86],{"class":46,"line":85},5,[44,87,88],{"class":65},"\u002F* v4 przeskalował te trzy wartości – przywracam te z v3 *\u002F\n",[44,90,92,95],{"class":46,"line":91},6,[44,93,94],{"class":50},"@theme",[44,96,97],{"class":58}," {\n",[44,99,101,105,108,112,115,118,121,123,126,129,132,134,136,138,141,144],{"class":46,"line":100},7,[44,102,104],{"class":103},"siNPP","  --shadow-sm",[44,106,107],{"class":58},": ",[44,109,111],{"class":110},"sj4cs","0",[44,113,114],{"class":110}," 1",[44,116,117],{"class":50},"px",[44,119,120],{"class":110}," 2",[44,122,117],{"class":50},[44,124,125],{"class":110}," 0",[44,127,128],{"class":110}," rgb",[44,130,131],{"class":58},"(",[44,133,111],{"class":110},[44,135,125],{"class":110},[44,137,125],{"class":110},[44,139,140],{"class":58}," \u002F ",[44,142,143],{"class":110},"0.05",[44,145,146],{"class":58},");\n",[44,148,150,153,155,158,161],{"class":46,"line":149},8,[44,151,152],{"class":103},"  --radius-sm",[44,154,107],{"class":58},[44,156,157],{"class":110},"0.125",[44,159,160],{"class":50},"rem",[44,162,59],{"class":58},[44,164,166,169,171,174,176],{"class":46,"line":165},9,[44,167,168],{"class":103},"  --default-ring-width",[44,170,107],{"class":58},[44,172,173],{"class":110},"3",[44,175,117],{"class":50},[44,177,59],{"class":58},[44,179,181],{"class":46,"line":180},10,[44,182,183],{"class":58},"}\n",[44,185,187],{"class":46,"line":186},11,[44,188,82],{"emptyLinePlaceholder":81},[44,190,192],{"class":46,"line":191},12,[44,193,194],{"class":65},"\u002F* v4 zmienił domyślny kolor ramki na currentcolor *\u002F\n",[44,196,198,201,204],{"class":46,"line":197},13,[44,199,200],{"class":50},"@layer",[44,202,203],{"class":103}," base",[44,205,97],{"class":58},[44,207,209,213,216,220,222,225,227,230,233,236],{"class":46,"line":208},14,[44,210,212],{"class":211},"s9eBZ","  *",[44,214,215],{"class":58},", ",[44,217,219],{"class":218},"sScJk","::after",[44,221,215],{"class":58},[44,223,224],{"class":218},"::before",[44,226,215],{"class":58},[44,228,229],{"class":218},"::backdrop",[44,231,232],{"class":58},", ::",[44,234,235],{"class":211},"file-selector-button",[44,237,97],{"class":58},[44,239,241,244,246,249,251,254,256,259],{"class":46,"line":240},15,[44,242,243],{"class":110},"    border-color",[44,245,107],{"class":58},[44,247,248],{"class":110},"var",[44,250,131],{"class":58},[44,252,253],{"class":103},"--color-gray-200",[44,255,215],{"class":58},[44,257,258],{"class":110},"currentcolor",[44,260,146],{"class":58},[44,262,264],{"class":46,"line":263},16,[44,265,266],{"class":58},"  }\n",[44,268,270],{"class":46,"line":269},17,[44,271,183],{"class":58},[11,273,274,275,278,279,282,283,286],{},"Te trzy wartości to 296 klas w szablonach: ",[15,276,277],{},"shadow-sm"," (226), ",[15,280,281],{},"rounded-sm"," (52) i samo ",[15,284,285],{},"ring"," (18). Po zmianie wyglądają dokładnie jak w v3, a w szablonach nie zmieniłem w nich ani jednego znaku.",[11,288,289],{},"Dwie rzeczy w zależnościach wyszły dopiero przy porównaniu:",[291,292,293,313],"ul",{},[294,295,296,300,301,304,305,308,309,312],"li",{},[297,298,299],"strong",{},"Plugin formularzy."," ",[15,302,303],{},"@tailwindcss\u002Fforms"," w wersji 0.5.11 obniża specyficzność swoich selektorów (",[15,306,307],{},"input:where([type='text'])"," zamiast ",[15,310,311],{},"[type='text']","). Po aktualizacji przesunęło to paddingi wszystkich pól. Przypiąłem wersję 0.5.7, która działa jak wcześniej.",[294,314,315,318,319,322,323,326,327,330],{},[297,316,317],{},"Node 20."," Nowy silnik Tailwinda wymaga Node 20 lub nowszego, a na starszym instalacja kończy się błędem o binarce, nie o Tailwindzie. Dodałem ",[15,320,321],{},"engines"," w ",[15,324,325],{},"package.json"," i plik ",[15,328,329],{},".nvmrc",".",[27,332,334],{"id":333},"trzy-zmiany-bez-których-v4-nie-działa","Trzy zmiany, bez których v4 nie działa",[11,336,337],{},"Kilku rzeczy nie dało się przywrócić w CSS. Te musiałem zmienić w szablonach:",[339,340,341,409,486],"ol",{},[294,342,343,349,350,215,353,356,357],{},[297,344,345,346],{},"Klasy ",[15,347,348],{},"*-opacity-*"," (",[15,351,352],{},"bg-opacity-50",[15,354,355],{},"ring-opacity-5"," itd.) w v4 nie istnieją i nic nie generują. Zamieniłem je na zapis z ukośnikiem – 49 miejsc w 36 plikach. Bez tego nakładki pod oknami modalnymi straciłyby przyciemnienie, a lista rozwijana dostałaby czarny pierścień zamiast prawie przezroczystego.",[35,358,362],{"className":359,"code":360,"language":361,"meta":40,"style":40},"language-html shiki shiki-themes github-light github-dark","\u003C!-- przed -->\n\u003Cdiv class=\"bg-black bg-opacity-50\">\n\u003C!-- po -->\n\u003Cdiv class=\"bg-black\u002F50\">\n","html",[15,363,364,369,389,394],{"__ignoreMap":40},[44,365,366],{"class":46,"line":47},[44,367,368],{"class":65},"\u003C!-- przed -->\n",[44,370,371,374,377,380,383,386],{"class":46,"line":62},[44,372,373],{"class":58},"\u003C",[44,375,376],{"class":211},"div",[44,378,379],{"class":218}," class",[44,381,382],{"class":58},"=",[44,384,385],{"class":54},"\"bg-black bg-opacity-50\"",[44,387,388],{"class":58},">\n",[44,390,391],{"class":46,"line":69},[44,392,393],{"class":65},"\u003C!-- po -->\n",[44,395,396,398,400,402,404,407],{"class":46,"line":78},[44,397,373],{"class":58},[44,399,376],{"class":211},[44,401,379],{"class":218},[44,403,382],{"class":58},[44,405,406],{"class":54},"\"bg-black\u002F50\"",[44,408,388],{"class":58},[294,410,411,420,421,423,424,427,428],{},[297,412,413,416,417,330],{},[15,414,415],{},"@reference"," w blokach ",[15,418,419],{},"\u003Cstyle>"," W v4 każdy blok ",[15,422,419],{}," w komponencie Vue kompiluje się osobno i nie zna kolorów ani klas projektu, więc ",[15,425,426],{},"@apply"," kończy się błędem. Dodałem jedną linię w 32 plikach:",[35,429,433],{"className":430,"code":431,"language":432,"meta":40,"style":40},"language-vue shiki shiki-themes github-light github-dark","\u003Cstyle scoped>\n@reference \"..\u002F..\u002Fcss\u002Fapp.css\";\n\n.badge {\n  @apply rounded-sm bg-brand-1-100 px-2;\n}\n\u003C\u002Fstyle>\n","vue",[15,434,435,447,454,458,465,473,477],{"__ignoreMap":40},[44,436,437,439,442,445],{"class":46,"line":47},[44,438,373],{"class":58},[44,440,441],{"class":211},"style",[44,443,444],{"class":218}," scoped",[44,446,388],{"class":58},[44,448,449,451],{"class":46,"line":62},[44,450,415],{"class":50},[44,452,453],{"class":58}," \"..\u002F..\u002Fcss\u002Fapp.css\";\n",[44,455,456],{"class":46,"line":69},[44,457,82],{"emptyLinePlaceholder":81},[44,459,460,463],{"class":46,"line":78},[44,461,462],{"class":218},".badge",[44,464,97],{"class":58},[44,466,467,470],{"class":46,"line":85},[44,468,469],{"class":50},"  @apply",[44,471,472],{"class":58}," rounded-sm bg-brand-1-100 px-2;\n",[44,474,475],{"class":46,"line":91},[44,476,183],{"class":58},[44,478,479,482,484],{"class":46,"line":100},[44,480,481],{"class":58},"\u003C\u002F",[44,483,441],{"class":211},[44,485,388],{"class":58},[294,487,488,493,494,497,498,500],{},[297,489,490,330],{},[15,491,492],{},"@apply … !important"," v4 nie przyjmuje ",[15,495,496],{},"!important"," na końcu ",[15,499,426],{},". W jednym miejscu przeniosłem wykrzyknik na poszczególne klasy.",[11,502,503,504,507],{},"Za to prefiks ",[15,505,506],{},"!"," przed klasą działa w v4 bez zmian, więc ponad 1500 takich klas zostało nietkniętych.",[27,509,511],{"id":510},"najtrudniejsze-warstwy-css","Najtrudniejsze: warstwy CSS",[11,513,514,515,517],{},"Najwięcej czasu zajęła kaskada. W v4 klasy Tailwinda są w warstwach (",[15,516,200],{},"), a CSS spoza warstwy wygrywa z nimi zawsze, niezależnie od specyficzności. W v3 tak nie było, więc reguły, które latami przegrywały, nagle zaczęły wygrywać:",[291,519,520,531],{},[294,521,522,523,526,527,530],{},"reguła ",[15,524,525],{},"label { … }"," z marginesem nadpisała klasę ",[15,528,529],{},"mb-7.5"," na etykiecie,",[294,532,533,536],{},[15,534,535],{},"input { height: 36px }"," rozciągnęła checkboxy i radio do 36 px.",[11,538,539,540,543,544,547],{},"Rozwiązaniem była własna warstwa na reguły elementów. Stoi między ",[15,541,542],{},"base"," a ",[15,545,546],{},"components",", więc wygrywa z resetem i pluginem formularzy, ale przegrywa z klasami – tak jak w v3:",[35,549,551],{"className":37,"code":550,"language":39,"meta":40,"style":40},"\u002F* kolejność warstw – musi być na samym początku arkusza *\u002F\n@layer theme, base, app-base, components, utilities;\n\n@layer app-base {\n  label {\n    margin-bottom: 0.375rem;\n  }\n\n  \u002F* checkbox i radio mają swój rozmiar z pluginu formularzy *\u002F\n  input:not([type=\"checkbox\"], [type=\"radio\"]) {\n    height: 36px;\n  }\n}\n",[15,552,553,558,585,589,598,605,619,623,627,632,664,678,682],{"__ignoreMap":40},[44,554,555],{"class":46,"line":47},[44,556,557],{"class":65},"\u002F* kolejność warstw – musi być na samym początku arkusza *\u002F\n",[44,559,560,562,565,567,569,571,574,576,578,580,583],{"class":46,"line":62},[44,561,200],{"class":50},[44,563,564],{"class":103}," theme",[44,566,215],{"class":58},[44,568,542],{"class":103},[44,570,215],{"class":58},[44,572,573],{"class":103},"app-base",[44,575,215],{"class":58},[44,577,546],{"class":103},[44,579,215],{"class":58},[44,581,582],{"class":103},"utilities",[44,584,59],{"class":58},[44,586,587],{"class":46,"line":69},[44,588,82],{"emptyLinePlaceholder":81},[44,590,591,593,596],{"class":46,"line":78},[44,592,200],{"class":50},[44,594,595],{"class":103}," app-base",[44,597,97],{"class":58},[44,599,600,603],{"class":46,"line":85},[44,601,602],{"class":211},"  label",[44,604,97],{"class":58},[44,606,607,610,612,615,617],{"class":46,"line":91},[44,608,609],{"class":110},"    margin-bottom",[44,611,107],{"class":58},[44,613,614],{"class":110},"0.375",[44,616,160],{"class":50},[44,618,59],{"class":58},[44,620,621],{"class":46,"line":100},[44,622,266],{"class":58},[44,624,625],{"class":46,"line":149},[44,626,82],{"emptyLinePlaceholder":81},[44,628,629],{"class":46,"line":165},[44,630,631],{"class":65},"  \u002F* checkbox i radio mają swój rozmiar z pluginu formularzy *\u002F\n",[44,633,634,637,640,643,646,648,651,654,656,658,661],{"class":46,"line":180},[44,635,636],{"class":211},"  input",[44,638,639],{"class":218},":not",[44,641,642],{"class":58},"([",[44,644,645],{"class":218},"type",[44,647,382],{"class":50},[44,649,650],{"class":54},"\"checkbox\"",[44,652,653],{"class":58},"], [",[44,655,645],{"class":218},[44,657,382],{"class":50},[44,659,660],{"class":54},"\"radio\"",[44,662,663],{"class":58},"]) {\n",[44,665,666,669,671,674,676],{"class":46,"line":186},[44,667,668],{"class":110},"    height",[44,670,107],{"class":58},[44,672,673],{"class":110},"36",[44,675,117],{"class":50},[44,677,59],{"class":58},[44,679,680],{"class":46,"line":191},[44,681,266],{"class":58},[44,683,684],{"class":46,"line":197},[44,685,183],{"class":58},[11,687,688],{},"Dwie kolejne różnice wyszły dopiero przy klikaniu:",[291,690,691,708],{},[294,692,693,696,697,700,701,704,705,330],{},[297,694,695],{},"Okna modalne chowały się pod własnym tłem."," W v3 klasa ",[15,698,699],{},"transform"," dodawała pustą transformację, która tworzyła nowy kontekst nakładania. W v4 ta klasa daje ",[15,702,703],{},"transform: none",", gdy żadna transformacja nie jest ustawiona. Po animacji wejścia okno wpadało pod przyciemnienie, a kliknięcie w okno zamykało je. Dotyczyło to 33 plików. Wystarczyła jedna reguła: ",[15,706,707],{},".transform { transform: translate(0, 0); }",[294,709,710,713,714,717,718,721],{},[297,711,712],{},"Gradient zniknął z pierwszego kafelka na pulpicie."," Własny plugin ",[15,715,716],{},"bg-gradient-\u003Ckąt>"," nie ustawiał zmiennej ",[15,719,720],{},"--tw-gradient-position",", z której v4 składa gradient. Cała wartość była nieprawidłowa, więc kafelek został bez tła, z samym białym tekstem.",[11,723,724,725,728,729,330],{},"Poprawiłem też klasę ",[15,726,727],{},"container",". v4 nie czyta już z konfiguracji JS wyśrodkowania i paddingu kontenera, więc dodałem je w CSS przez ",[15,730,731],{},"@utility container",[27,733,735],{"id":734},"jak-sprawdziłem-że-nic-się-nie-zmieniło","Jak sprawdziłem, że nic się nie zmieniło",[11,737,738],{},"Przeglądanie stron nie wystarczy – łatwo przeoczyć margines na jednej podstronie. Zbudowałem więc CSS z gałęzi z v3 i z gałęzi z v4 i porównałem oba wyniki: 90 arkuszy wobec 90, klasa po klasie i wartość po wartości.",[11,740,741],{},"Wynik:",[291,743,744,747,750],{},[294,745,746],{},"żadna klasa użyta w szablonach nie zniknęła z wynikowego CSS,",[294,748,749],{},"prawie wszystkie różnice to tylko inny zapis tej samej wartości,",[294,751,752,753,308,756,759,760,762],{},"zostały dwie świadome różnice: domyślna paleta v4 jest w ",[15,754,755],{},"oklch",[15,757,758],{},"rgb",", a ",[15,761,727],{}," ma dodatkowy próg szerokości z jednego z breakpointów.",[11,764,765,766,769],{},"Na koniec zostawiłem notatkę dla zespołu: po pobraniu zmian trzeba zrobić ",[15,767,768],{},"npm ci",", bo bez nowych paczek serwer deweloperski nie wstanie.",[27,771,773],{"id":772},"co-z-tego-zostało","Co z tego zostało",[291,775,776,782,788,791],{},[294,777,778,779,781],{},"Migracja bez przepisywania szablonów – stare wartości w ",[15,780,94],{}," i trzy zmiany, których v4 wymaga.",[294,783,784,785,787],{},"Własna warstwa ",[15,786,573],{}," na reguły elementów, żeby kaskada działała jak w v3.",[294,789,790],{},"Porównanie wynikowego CSS zamiast przeglądania stron – daje pewność, że nic nie zniknęło.",[294,792,793,794,796,797,330],{},"Projekt jest gotowy na ",[15,795,17],{},", od którego wszystko się zaczęło – użyłem go m.in. w ",[798,799,801],"a",{"href":800},"\u002Fpl\u002Fblog\u002F2026\u002Fnaglowki-agrestapp","nagłówkach AgrestApp",[441,803,804],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sqY6-, html code.shiki .sqY6-{--shiki-default:#6A737D;--shiki-dark:#90A1B9}html pre.shiki code .siNPP, html code.shiki .siNPP{--shiki-default:#CA3500;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":40,"searchDepth":62,"depth":62,"links":806},[807,808,809,810,811],{"id":29,"depth":62,"text":30},{"id":333,"depth":62,"text":334},{"id":510,"depth":62,"text":511},{"id":734,"depth":62,"text":735},{"id":772,"depth":62,"text":773},"2026-08-29","Jak przeniosłem AgrestApp z Tailwinda 3 na 4 bez przepisywania szablonów – stare wartości w @theme, trzy zmiany w klasach i porządek w warstwach CSS.",false,"md",null,"case-study",{},"\u002Fog\u002Fblog\u002Fmigracja-na-tailwind-v4-pl.jpg","\u002Fpl\u002Fblog\u002F2026\u002Fmigracja-na-tailwind-v4","\u002Fprojects\u002F2026\u002Fagrestapp",{"title":5,"description":813},"pl\u002Fblog\u002F2026\u002Fmigracja-na-tailwind-v4",[825,39],"tailwind","zjVFH9JytvMkebNNHc7U_8FUXtyNblhBX_7cnqfbTiw",{"id":828,"title":829,"agency":830,"body":831,"compare":816,"cover":893,"date":899,"description":900,"draft":814,"extension":815,"facts":901,"featured":814,"gallery":915,"galleryLead":936,"highlights":937,"image":953,"links":954,"meta":956,"navigation":81,"note":957,"ogImage":958,"path":959,"seo":960,"stack":961,"stem":965,"tags":966,"__hash__":967},"projects_pl\u002Fpl\u002Fprojects\u002F2026\u002Fagrestapp\u002Findex.md","AgrestApp","Pesi",{"type":8,"value":832,"toc":890},[833,836,840],[11,834,835],{},"AgrestApp to system zakupów dla firm z branży spożywczej: zamówienia i przetargi na surowce, monitoring cen, oferty dostawców i analityka oszczędności. Pracuję przy nim w zespole Pesi od października 2025. Front-end w Vue 3 i Inertia tworzy kilka osób, dlatego poniżej opisuję tylko części, przy których front-end jest mój. Najbardziej cieszy mnie to, że wiele rzeczy mogę tu napisać po swojemu. Serwis zmieniam małymi etapami: tu nowa tabela, tam nowy przycisk, gdzie indziej nagłówek. Każdy nowy element zastępuje kilka starszych wersji, więc z czasem w projekcie jest coraz mniej zbędnych komponentów.",[27,837,839],{"id":838},"co-zrobiłem","Co zrobiłem",[291,841,842,848,854,860,866,872,878,884],{},[294,843,844,847],{},[297,845,846],{},"Nagłówek panelu"," – nowy nagłówek z paskiem „Zaloguj jako”, menu konta i wspólnym komponentem rozwijanej listy",[294,849,850,853],{},[297,851,852],{},"Zestaw komponentów UI"," – przyciski, akordeon, podpowiedzi, pola liczbowe i 69 ikon jako komponenty Vue; przeniesienie przycisków aplikacji na jeden komponent i wspólny przełącznik motywu",[294,855,856,859],{},[297,857,858],{},"Strona firmowa"," – strona „O systemie”, zestaw sekcji, z których powstały strony dla piekarni, cukierni, lodziarni i HoReCa, kafelki cenowe i cennik z przełącznikiem płatności",[294,861,862,865],{},[297,863,864],{},"Analityka"," – główny wykres, wybór zakresu dat z trybem porównania, najlepsi dostawcy, podsumowanie oszczędności i rentowność produktów",[294,867,868,871],{},[297,869,870],{},"Zarządzanie ofertą dostawcy"," – rozbudowa istniejącego ekranu: okno ceny indywidualnej, dodawanie artykułów i przechodzenie Enterem między polami",[294,873,874,877],{},[297,875,876],{},"Zaznaczanie zakresu"," – Shift + klik w 9 listach panelu (surowce, produkty, ceny, powiadomienia i inne)",[294,879,880,883],{},[297,881,882],{},"Sklep"," (dziś zamknięty) – koszyk z minimum logistycznym każdego dostawcy, zakup bez konta, historia zamówień i reklamacje",[294,885,886,889],{},[297,887,888],{},"Migracja na Tailwind CSS v4"," całego projektu",{"title":40,"searchDepth":62,"depth":62,"links":891},[892],{"id":838,"depth":62,"text":839},{"desktop":894,"phone":895,"width":896,"height":897,"alt":898},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Fhome-top.webp","\u002Fprojects\u002F2026\u002Fagrestapp\u002Fhome-phone-top.webp",1440,900,"AgrestApp – pierwszy ekran strony głównej","2026-10-01","System zakupów surowców dla piekarni, cukierni, lodziarni i gastronomii – w zespole Pesi robię front-end: komponenty UI, stronę firmową, analitykę i zarządzanie ofertą dostawcy.",[902,906,909,912],{"value":903,"suffix":904,"text":905},"12"," mies.","w projekcie – od października 2025",{"value":907,"text":908},"69","ikon jako komponenty Vue w nowym zestawie UI",{"value":910,"text":911},"4","strony branżowe – piekarnie, cukiernie, lodziarnie i HoReCa – zbudowane z tych samych sekcji",{"value":913,"text":914},"296","klas w szablonach bez zmian przy przejściu na Tailwind v4",[916,920,924,928,932],{"src":917,"width":896,"height":897,"alt":918,"caption":919},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Fanalytics-top.webp","Analityka w trybie porównania","Analityka – zakres dat z suwakiem i polami, porównanie dwóch okresów, oszczędności i wykres (zmyślone dane)",{"src":921,"full":81,"alt":922,"caption":923},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Fhome-full.webp","Strona główna AgrestApp","Strona główna – od pierwszego ekranu do newslettera",{"src":925,"full":81,"alt":926,"caption":927},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Fabout-system-full.webp","Strona „O systemie”","„O systemie” – sekcje z opisem modułów",{"src":929,"full":81,"alt":930,"caption":931},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Findustry-ice-cream-full.webp","Strona dla lodziarni","Strona branżowa dla lodziarni – zbudowana z tych samych sekcji co pozostałe",{"src":933,"full":81,"alt":934,"caption":935},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Findustry-confectionery-full.webp","Strona dla cukierni","Strona branżowa dla cukierni","Strona firmowa i panel aplikacji – analityka na zmyślonych danych. Kliknij zrzut, żeby go powiększyć.",[938,941,944,947,950],{"title":939,"text":940},"Nagłówek panelu bez liczb na sztywno","Przebudowałem nagłówek panelu. Pasek „Zaloguj jako” jest teraz jego częścią, a nagłówek sam mierzy swoją wysokość i zapisuje ją w zmiennej CSS. Menu boczne i przyklejone paski stron korzystają z tej zmiennej zamiast liczb 75 i 125 px wpisanych w kilku plikach. Menu konta obsłużysz z klawiatury i zamkniesz klawiszem Esc.",{"title":942,"text":943},"Zakres dat w trzech miejscach naraz","W analityce suwak, pola dat i adres strony pokazują ten sam zakres. Zmiana w jednym miejscu od razu zmienia pozostałe, a dane odświeżają się dopiero chwilę po ostatniej zmianie. Kalendarz nie pozwala ustawić początku po końcu, a domyślny zakres zaczyna się w dniu rejestracji firmy.",{"title":945,"text":946},"Tabela cen jak arkusz","W zarządzaniu ofertą dostawcy Enter przenosi do tego samego pola w wierszu niżej, a Shift + Enter – wyżej. Zawartość pola jest od razu zaznaczona do nadpisania. W 9 listach panelu Shift z kliknięciem zaznacza cały zakres wierszy, jak w poczcie.",{"title":948,"text":949},"Cena indywidualna bez pomyłek","Okno ceny indywidualnej pilnuje minimalnej ceny netto, nie przyjmuje zera ani pustej wartości i podpowiada wartość domyślną dla pojedynczej pozycji. Cena podświetla się na zielono, gdy jest niższa lub równa najniższej cenie.",{"title":951,"text":952},"Tailwind v4 bez przepisywania szablonów","Zamiast zmieniać setki klas przywróciłem w konfiguracji trzy wartości z v3. Potem porównałem wynikowy CSS z poprzednią wersją, przejrzałem strony i poprawiłem regresje, których nie zgłosił build – m.in. okna, które po migracji chowały się pod własnym przyciemnieniem.","\u002Fprojects\u002F2026\u002Fagrestapp\u002Fthumbnail.webp",{"live":955},"https:\u002F\u002Fagrestapp.pl",{},"Pracuję w zespole – opisuję tylko części, przy których front-end jest mój. Panel aplikacji nie jest publiczny.","\u002Fog\u002Fprojects\u002Fagrestapp-pl.jpg","\u002Fpl\u002Fprojects\u002F2026\u002Fagrestapp",{"title":829,"description":900},[962,963,964],"Vue 3","Inertia","Tailwind CSS v4","pl\u002Fprojects\u002F2026\u002Fagrestapp\u002Findex",[432],"9K9-hLbbaGH3H17k_QO2mHzbXXsCfmWEW5rHxmSri_M",1792358954455]