[{"data":1,"prerenderedAt":2795},["ShallowReactive",2],{"project-\u002Fpl\u002Fprojekty\u002F2026\u002Fagrestapp":3,"project-posts-\u002Fpl\u002Fprojekty\u002F2026\u002Fagrestapp":157},{"id":4,"title":5,"agency":6,"body":7,"compare":77,"cover":78,"date":84,"description":85,"draft":86,"extension":87,"facts":88,"featured":86,"gallery":102,"galleryLead":124,"highlights":125,"image":141,"links":142,"meta":144,"navigation":109,"note":145,"ogImage":146,"path":147,"seo":148,"stack":149,"stem":153,"tags":154,"__hash__":156},"projects_pl\u002Fpl\u002Fprojekty\u002F2026\u002Fagrestapp\u002Findex.md","AgrestApp","Pesi",{"type":8,"value":9,"toc":72},"minimark",[10,14,19],[11,12,13],"p",{},"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.",[15,16,18],"h2",{"id":17},"co-zrobiłem","Co zrobiłem",[20,21,22,30,36,42,48,54,60,66],"ul",{},[23,24,25,29],"li",{},[26,27,28],"strong",{},"Nagłówek panelu"," – nowy nagłówek z paskiem „Zaloguj jako”, menu konta i wspólnym komponentem rozwijanej listy",[23,31,32,35],{},[26,33,34],{},"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",[23,37,38,41],{},[26,39,40],{},"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",[23,43,44,47],{},[26,45,46],{},"Analityka"," – główny wykres, wybór zakresu dat z trybem porównania, najlepsi dostawcy, podsumowanie oszczędności i rentowność produktów",[23,49,50,53],{},[26,51,52],{},"Zarządzanie ofertą dostawcy"," – rozbudowa istniejącego ekranu: okno ceny indywidualnej, dodawanie artykułów i przechodzenie Enterem między polami",[23,55,56,59],{},[26,57,58],{},"Zaznaczanie zakresu"," – Shift + klik w 9 listach panelu (surowce, produkty, ceny, powiadomienia i inne)",[23,61,62,65],{},[26,63,64],{},"Sklep"," (dziś zamknięty) – koszyk z minimum logistycznym każdego dostawcy, zakup bez konta, historia zamówień i reklamacje",[23,67,68,71],{},[26,69,70],{},"Migracja na Tailwind CSS v4"," całego projektu",{"title":73,"searchDepth":74,"depth":74,"links":75},"",2,[76],{"id":17,"depth":74,"text":18},null,{"desktop":79,"phone":80,"width":81,"height":82,"alt":83},"\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.",false,"md",[89,93,96,99],{"value":90,"suffix":91,"text":92},"12"," mies.","w projekcie – od października 2025",{"value":94,"text":95},"69","ikon jako komponenty Vue w nowym zestawie UI",{"value":97,"text":98},"4","strony branżowe – piekarnie, cukiernie, lodziarnie i HoReCa – zbudowane z tych samych sekcji",{"value":100,"text":101},"296","klas w szablonach bez zmian przy przejściu na Tailwind v4",[103,107,112,116,120],{"src":104,"width":81,"height":82,"alt":105,"caption":106},"\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":108,"full":109,"alt":110,"caption":111},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Fhome-full.webp",true,"Strona główna AgrestApp","Strona główna – od pierwszego ekranu do newslettera",{"src":113,"full":109,"alt":114,"caption":115},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Fabout-system-full.webp","Strona „O systemie”","„O systemie” – sekcje z opisem modułów",{"src":117,"full":109,"alt":118,"caption":119},"\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":121,"full":109,"alt":122,"caption":123},"\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ć.",[126,129,132,135,138],{"title":127,"text":128},"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":130,"text":131},"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":133,"text":134},"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":136,"text":137},"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":139,"text":140},"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":143},"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\u002Fprojekty\u002F2026\u002Fagrestapp",{"title":5,"description":85},[150,151,152],"Vue 3","Inertia","Tailwind CSS v4","pl\u002Fprojekty\u002F2026\u002Fagrestapp\u002Findex",[155],"vue","dUagLzcAHxQYApCOGRrQojMHy35rm1-7t0FjrUmvKDA",[158,1091,1849],{"id":159,"title":160,"art":161,"body":162,"date":1077,"description":1078,"draft":86,"extension":87,"image":77,"kind":1079,"meta":1080,"navigation":109,"ogImage":1081,"path":1082,"project":1083,"seo":1084,"slug":77,"stem":1085,"tags":1086,"translationKey":1089,"updated":77,"__hash__":1090},"blog_pl\u002Fpl\u002Fwpisy\u002F2026\u002Fnaglowki-agrestapp.md","Nagłówki AgrestApp: 36 px dla myszy, 44 px dla palca","\u002Fart\u002Fblog\u002Fnaglowki-agrestapp.svg",{"type":8,"value":163,"toc":1069},[164,167,171,186,195,202,209,212,493,496,499,507,511,514,521,607,614,618,621,649,652,752,760,764,775,781,938,945,952,959,963,966,1034,1041,1045,1062,1065],[11,165,166],{},"AgrestApp ma dwa nagłówki: na stronie firmowej i w panelu po zalogowaniu. Oba działały, ale każdy był zbudowany inaczej, a rozmiary brały się z przypadkowych pikseli. 15 października przepisałem oba. Wyszło 8 commitów, 40 zmienionych plików i +1424 \u002F −923 linii, a stary nagłówek panelu (643 linie w jednym pliku) zastąpiły trzy mniejsze komponenty. W kolejnych dwóch dniach poprawiłem jeszcze kolory ikon. Zrzuty poniżej są z lokalnej wersji na zmyślonych danych.",[15,168,170],{"id":169},"menu-na-stronie-firmowej","Menu na stronie firmowej",[11,172,173,174,178,179,178,182,185],{},"Stare menu działało tylko po najechaniu myszą. Miało jedną grupę „System Agrest”, a w niej podmenu w podmenu: Branże, Rozwiązania, a nawet Cennik. Szerokości i odstępy były wpisane na sztywno (",[175,176,177],"code",{},"w-[180px]",", ",[175,180,181],{},"gap-[14px]",[175,183,184],{},"gap-[30px]",").",[11,187,188],{},[189,190],"img",{"alt":191,"height":192,"src":193,"width":194},"Pasek menu strony – u góry przed zmianą, na dole po",184,"\u002Fblog\u002F2026\u002Fnaglowki-agrestapp\u002Fsite-header.webp",1400,[11,196,197,198,201],{},"Najważniejsze sekcje wyszły na wierzch jako osobne grupy, a Cennik stał się zwykłym linkiem. Cała struktura jest w jednej liście w ",[175,199,200],{},"mainNav.js",". Z niej powstaje i pasek na komputerze, i menu na telefonie, więc nowa pozycja nie wymaga zmian w dwóch miejscach.",[11,203,204],{},[189,205],{"alt":206,"height":207,"src":208,"width":194},"Rozwinięta grupa „System Agrest” – u góry przed zmianą (podmenu w podmenu), na dole po",736,"\u002Fblog\u002F2026\u002Fnaglowki-agrestapp\u002Fsite-dropdown.webp",[11,210,211],{},"Zamiast osobnej listy dla każdej grupy jest jeden panel. Przy przejściu między grupami przesuwa się pod wskazaną pozycję i dopasowuje szerokość i wysokość do treści. Pozycję i rozmiar liczy JavaScript, a Tailwind tylko je czyta ze zmiennych CSS:",[213,214,217],"pre",{"className":215,"code":216,"language":155,"meta":73,"style":73},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup>\n\u002F\u002F Pozycja i rozmiar panelu – liczone przy otwarciu grupy\nconst panelBox = ref({ x: 0, width: 0, height: 0 })\n\nconst panelStyle = computed(() => ({\n  '--panel-x': `${panelBox.value.x}px`,\n  '--panel-w': `${panelBox.value.width}px`,\n  '--panel-h': `${panelBox.value.height}px`,\n}))\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv\n    :style=\"panelStyle\"\n    class=\"absolute translate-x-(--panel-x) w-(--panel-w) h-(--panel-h)\n           transition-[translate,width,height] duration-200\"\n  >\n    \u003C!-- treść aktywnej grupy -->\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[175,218,219,239,245,281,287,309,342,367,392,398,408,413,423,432,444,455,461,467,473,484],{"__ignoreMap":73},[220,221,224,228,232,236],"span",{"class":222,"line":223},"line",1,[220,225,227],{"class":226},"sVt8B","\u003C",[220,229,231],{"class":230},"s9eBZ","script",[220,233,235],{"class":234},"sScJk"," setup",[220,237,238],{"class":226},">\n",[220,240,241],{"class":222,"line":74},[220,242,244],{"class":243},"sqY6-","\u002F\u002F Pozycja i rozmiar panelu – liczone przy otwarciu grupy\n",[220,246,248,252,256,259,262,265,268,271,273,276,278],{"class":222,"line":247},3,[220,249,251],{"class":250},"szBVR","const",[220,253,255],{"class":254},"sj4cs"," panelBox",[220,257,258],{"class":250}," =",[220,260,261],{"class":234}," ref",[220,263,264],{"class":226},"({ x: ",[220,266,267],{"class":254},"0",[220,269,270],{"class":226},", width: ",[220,272,267],{"class":254},[220,274,275],{"class":226},", height: ",[220,277,267],{"class":254},[220,279,280],{"class":226}," })\n",[220,282,284],{"class":222,"line":283},4,[220,285,286],{"emptyLinePlaceholder":109},"\n",[220,288,290,292,295,297,300,303,306],{"class":222,"line":289},5,[220,291,251],{"class":250},[220,293,294],{"class":254}," panelStyle",[220,296,258],{"class":250},[220,298,299],{"class":234}," computed",[220,301,302],{"class":226},"(() ",[220,304,305],{"class":250},"=>",[220,307,308],{"class":226}," ({\n",[220,310,312,316,319,322,325,328,331,333,336,339],{"class":222,"line":311},6,[220,313,315],{"class":314},"sZZnC","  '--panel-x'",[220,317,318],{"class":226},": ",[220,320,321],{"class":314},"`${",[220,323,324],{"class":226},"panelBox",[220,326,327],{"class":314},".",[220,329,330],{"class":226},"value",[220,332,327],{"class":314},[220,334,335],{"class":226},"x",[220,337,338],{"class":314},"}px`",[220,340,341],{"class":226},",\n",[220,343,345,348,350,352,354,356,358,360,363,365],{"class":222,"line":344},7,[220,346,347],{"class":314},"  '--panel-w'",[220,349,318],{"class":226},[220,351,321],{"class":314},[220,353,324],{"class":226},[220,355,327],{"class":314},[220,357,330],{"class":226},[220,359,327],{"class":314},[220,361,362],{"class":226},"width",[220,364,338],{"class":314},[220,366,341],{"class":226},[220,368,370,373,375,377,379,381,383,385,388,390],{"class":222,"line":369},8,[220,371,372],{"class":314},"  '--panel-h'",[220,374,318],{"class":226},[220,376,321],{"class":314},[220,378,324],{"class":226},[220,380,327],{"class":314},[220,382,330],{"class":226},[220,384,327],{"class":314},[220,386,387],{"class":226},"height",[220,389,338],{"class":314},[220,391,341],{"class":226},[220,393,395],{"class":222,"line":394},9,[220,396,397],{"class":226},"}))\n",[220,399,401,404,406],{"class":222,"line":400},10,[220,402,403],{"class":226},"\u003C\u002F",[220,405,231],{"class":230},[220,407,238],{"class":226},[220,409,411],{"class":222,"line":410},11,[220,412,286],{"emptyLinePlaceholder":109},[220,414,416,418,421],{"class":222,"line":415},12,[220,417,227],{"class":226},[220,419,420],{"class":230},"template",[220,422,238],{"class":226},[220,424,426,429],{"class":222,"line":425},13,[220,427,428],{"class":226},"  \u003C",[220,430,431],{"class":230},"div\n",[220,433,435,438,441],{"class":222,"line":434},14,[220,436,437],{"class":234},"    :style",[220,439,440],{"class":226},"=",[220,442,443],{"class":314},"\"panelStyle\"\n",[220,445,447,450,452],{"class":222,"line":446},15,[220,448,449],{"class":234},"    class",[220,451,440],{"class":226},[220,453,454],{"class":314},"\"absolute translate-x-(--panel-x) w-(--panel-w) h-(--panel-h)\n",[220,456,458],{"class":222,"line":457},16,[220,459,460],{"class":314},"           transition-[translate,width,height] duration-200\"\n",[220,462,464],{"class":222,"line":463},17,[220,465,466],{"class":226},"  >\n",[220,468,470],{"class":222,"line":469},18,[220,471,472],{"class":243},"    \u003C!-- treść aktywnej grupy -->\n",[220,474,476,479,482],{"class":222,"line":475},19,[220,477,478],{"class":226},"  \u003C\u002F",[220,480,481],{"class":230},"div",[220,483,238],{"class":226},[220,485,487,489,491],{"class":222,"line":486},20,[220,488,403],{"class":226},[220,490,420],{"class":230},[220,492,238],{"class":226},[11,494,495],{},"Menu reaguje na sposób obsługi. Mysz otwiera grupę najechaniem, a zamyka ją dopiero 150 ms po zjechaniu, żeby niedokładny ruch nie zamykał panelu. Dotyk przełącza grupę tapnięciem. Klawiatura obsługuje Enter, Esc i wyjście fokusem.",[11,497,498],{},"Na telefonie grupy są akordeonami, a przyciski logowania stoją przyklejone na dole ekranu. Przewija się tylko lista.",[11,500,501],{},[189,502],{"alt":503,"height":504,"src":505,"width":506},"Menu na telefonie – po lewej przed zmianą, po prawej po",844,"\u002Fblog\u002F2026\u002Fnaglowki-agrestapp\u002Fsite-phone-menu.webp",796,[15,508,510],{"id":509},"jedna-zasada-rozmiaru-mysz-36-px-dotyk-44-px","Jedna zasada rozmiaru: mysz 36 px, dotyk 44 px",[11,512,513],{},"W panelu przyciski-ikony miały różne rozmiary: 30, 36, 40, a awatar 56 px. Teraz jest jedna zasada. Przy myszy przyciski mają ok. 36 px, bo kursor jest precyzyjny i gęstszy układ jest wygodny. Przy dotyku mają 44 px, czyli minimum z WCAG 2.5.5.",[11,515,516,517,520],{},"Nie użyłem do tego breakpointów, tylko wariantu ",[175,518,519],{},"pointer-coarse:"," z Tailwinda v4. Rozmiar zależy więc od urządzenia wskazującego, a nie od szerokości ekranu. Tablet z myszą dostaje 36 px, a laptop z ekranem dotykowym 44 px.",[213,522,526],{"className":523,"code":524,"language":525,"meta":73,"style":73},"language-html shiki shiki-themes github-light github-dark","\u003C!-- ikona 18 px + padding: 36 px przy myszy, 44 px przy dotyku -->\n\u003Cbutton class=\"p-2 pointer-coarse:p-3 [&_svg]:size-4.5\" aria-label=\"Powiadomienia\">\n  \u003CBellIcon \u002F>\n\u003C\u002Fbutton>\n\n\u003C!-- pozycja menu: wysokość z paddingu, przy dotyku co najmniej 44 px -->\n\u003Ca class=\"px-3 py-2 pointer-coarse:min-h-11\">Moja firma\u003C\u002Fa>\n","html",[175,527,528,533,558,569,577,581,586],{"__ignoreMap":73},[220,529,530],{"class":222,"line":223},[220,531,532],{"class":243},"\u003C!-- ikona 18 px + padding: 36 px przy myszy, 44 px przy dotyku -->\n",[220,534,535,537,540,543,545,548,551,553,556],{"class":222,"line":74},[220,536,227],{"class":226},[220,538,539],{"class":230},"button",[220,541,542],{"class":234}," class",[220,544,440],{"class":226},[220,546,547],{"class":314},"\"p-2 pointer-coarse:p-3 [&_svg]:size-4.5\"",[220,549,550],{"class":234}," aria-label",[220,552,440],{"class":226},[220,554,555],{"class":314},"\"Powiadomienia\"",[220,557,238],{"class":226},[220,559,560,562,566],{"class":222,"line":247},[220,561,428],{"class":226},[220,563,565],{"class":564},"s7hpK","BellIcon",[220,567,568],{"class":226}," \u002F>\n",[220,570,571,573,575],{"class":222,"line":283},[220,572,403],{"class":226},[220,574,539],{"class":230},[220,576,238],{"class":226},[220,578,579],{"class":222,"line":289},[220,580,286],{"emptyLinePlaceholder":109},[220,582,583],{"class":222,"line":311},[220,584,585],{"class":243},"\u003C!-- pozycja menu: wysokość z paddingu, przy dotyku co najmniej 44 px -->\n",[220,587,588,590,593,595,597,600,603,605],{"class":222,"line":344},[220,589,227],{"class":226},[220,591,592],{"class":230},"a",[220,594,542],{"class":234},[220,596,440],{"class":226},[220,598,599],{"class":314},"\"px-3 py-2 pointer-coarse:min-h-11\"",[220,601,602],{"class":226},">Moja firma\u003C\u002F",[220,604,592],{"class":230},[220,606,238],{"class":226},[11,608,609,610,613],{},"Ta sama zasada działa w menu konta, w menu strony i w polu wyszukiwania (",[175,611,612],{},"h-9 pointer-coarse:h-11","). Przycisk menu na telefonie ma stałe 44 × 44 px, więc zamiana ☰ na × nie przesuwa paska.",[15,615,617],{"id":616},"ikony-w-jednym-kolorze","Ikony w jednym kolorze",[11,619,620],{},"Po pierwszej wersji zobaczyłem, że ikony w nagłówku i w menu konta mają różne kolory. Część brała kolor tekstu, a część szarość z innego miejsca. Ustawiłem więc jedną zasadę:",[20,622,623,633,640,643],{},[23,624,625,626,629,630,327],{},"W spoczynku każda ikona-przycisk w pasku i każda ikona w menu konta ma ten sam drugorzędny kolor: ",[175,627,628],{},"fill-slate-500",", a w trybie ciemnym ",[175,631,632],{},"fill-slate-400",[23,634,635,636,639],{},"Po najechaniu na pozycję w menu konta ikona bierze kolor tekstu (",[175,637,638],{},"group-hover:fill-current","), więc tekst i ikona zmieniają się razem.",[23,641,642],{},"Przycisk aktywnej strony dostaje pełną wersję ikony zamiast samego tła, np. koło ratunkowe na stronie pomocy.",[23,644,645,646,327],{},"Dzwonek z powiadomieniem to osobna ikona, a nie czerwona kropka doklejona przez ",[175,647,648],{},"absolute",[11,650,651],{},"Wariant ikony wybiera mała funkcja. Ta sama logika obsłuży koszyk i inne ikony z licznikiem:",[213,653,657],{"className":654,"code":655,"language":656,"meta":73,"style":73},"language-js shiki shiki-themes github-light github-dark","\u002F\u002F Jedna z czterech nazw: zwykła, pełna, z powiadomieniem, z powiadomieniem pełna\nfunction notificationIcon(icon, notification, hasNotification, active) {\n  const name = hasNotification ? notification : icon\n  return active ? `${name}-solid` : name\n}\n","js",[175,658,659,664,697,722,747],{"__ignoreMap":73},[220,660,661],{"class":222,"line":223},[220,662,663],{"class":243},"\u002F\u002F Jedna z czterech nazw: zwykła, pełna, z powiadomieniem, z powiadomieniem pełna\n",[220,665,666,669,672,675,679,681,684,686,689,691,694],{"class":222,"line":74},[220,667,668],{"class":250},"function",[220,670,671],{"class":234}," notificationIcon",[220,673,674],{"class":226},"(",[220,676,678],{"class":677},"siNPP","icon",[220,680,178],{"class":226},[220,682,683],{"class":677},"notification",[220,685,178],{"class":226},[220,687,688],{"class":677},"hasNotification",[220,690,178],{"class":226},[220,692,693],{"class":677},"active",[220,695,696],{"class":226},") {\n",[220,698,699,702,705,707,710,713,716,719],{"class":222,"line":247},[220,700,701],{"class":250},"  const",[220,703,704],{"class":254}," name",[220,706,258],{"class":250},[220,708,709],{"class":226}," hasNotification ",[220,711,712],{"class":250},"?",[220,714,715],{"class":226}," notification ",[220,717,718],{"class":250},":",[220,720,721],{"class":226}," icon\n",[220,723,724,727,730,732,735,738,741,744],{"class":222,"line":283},[220,725,726],{"class":250},"  return",[220,728,729],{"class":226}," active ",[220,731,712],{"class":250},[220,733,734],{"class":314}," `${",[220,736,737],{"class":226},"name",[220,739,740],{"class":314},"}-solid`",[220,742,743],{"class":250}," :",[220,745,746],{"class":226}," name\n",[220,748,749],{"class":222,"line":289},[220,750,751],{"class":226},"}\n",[11,753,754],{},[189,755],{"alt":756,"height":757,"src":758,"width":759},"Menu konta – po lewej przed zmianą, po prawej po: ikony w jednym kolorze",460,"\u002Fblog\u002F2026\u002Fnaglowki-agrestapp\u002Fpanel-account-menu.webp",1016,[15,761,763],{"id":762},"wysokość-nagłówka-w-jednej-zmiennej","Wysokość nagłówka w jednej zmiennej",[11,765,766,767,770,771,774],{},"Najwięcej porządku dała mała zmiana. Boczne menu i przyklejone paski stron w panelu leżą pod nagłówkiem, więc muszą znać jego wysokość. Wcześniej była wpisana na sztywno: ",[175,768,769],{},"top-[75px]",", a gdy administrator był zalogowany jako inna firma i nad nagłówkiem pojawiał się pasek „Zaloguj jako” – ",[175,772,773],{},"!top-[125px]",". Ten sam warunek powtarzał się w kilku plikach. Jeden pasek i tak chował się 11 px pod nagłówkiem.",[11,776,777,778,718],{},"Teraz pasek „Zaloguj jako” jest częścią nagłówka, a nagłówek sam mierzy swoją wysokość i zapisuje ją w zmiennej CSS na elemencie ",[175,779,780],{},"\u003Chtml>",[213,782,784],{"className":215,"code":783,"language":155,"meta":73,"style":73},"\u003Cscript setup>\nimport { useElementSize } from '@vueuse\u002Fcore'\n\nconst header = ref(null)\nconst { height } = useElementSize(header, undefined, { box: 'border-box' })\n\n\u002F\u002F Wysokość nagłówka dla elementów, które leżą pod nim\nwatch(height, (value) => {\n  document.documentElement.style.setProperty('--panel-header-height', `${value || 72}px`)\n}, { immediate: true })\n\u003C\u002Fscript>\n",[175,785,786,796,810,814,833,864,868,873,891,920,930],{"__ignoreMap":73},[220,787,788,790,792,794],{"class":222,"line":223},[220,789,227],{"class":226},[220,791,231],{"class":230},[220,793,235],{"class":234},[220,795,238],{"class":226},[220,797,798,801,804,807],{"class":222,"line":74},[220,799,800],{"class":250},"import",[220,802,803],{"class":226}," { useElementSize } ",[220,805,806],{"class":250},"from",[220,808,809],{"class":314}," '@vueuse\u002Fcore'\n",[220,811,812],{"class":222,"line":247},[220,813,286],{"emptyLinePlaceholder":109},[220,815,816,818,821,823,825,827,830],{"class":222,"line":283},[220,817,251],{"class":250},[220,819,820],{"class":254}," header",[220,822,258],{"class":250},[220,824,261],{"class":234},[220,826,674],{"class":226},[220,828,829],{"class":254},"null",[220,831,832],{"class":226},")\n",[220,834,835,837,840,842,845,847,850,853,856,859,862],{"class":222,"line":289},[220,836,251],{"class":250},[220,838,839],{"class":226}," { ",[220,841,387],{"class":254},[220,843,844],{"class":226}," } ",[220,846,440],{"class":250},[220,848,849],{"class":234}," useElementSize",[220,851,852],{"class":226},"(header, ",[220,854,855],{"class":254},"undefined",[220,857,858],{"class":226},", { box: ",[220,860,861],{"class":314},"'border-box'",[220,863,280],{"class":226},[220,865,866],{"class":222,"line":311},[220,867,286],{"emptyLinePlaceholder":109},[220,869,870],{"class":222,"line":344},[220,871,872],{"class":243},"\u002F\u002F Wysokość nagłówka dla elementów, które leżą pod nim\n",[220,874,875,878,881,883,886,888],{"class":222,"line":369},[220,876,877],{"class":234},"watch",[220,879,880],{"class":226},"(height, (",[220,882,330],{"class":677},[220,884,885],{"class":226},") ",[220,887,305],{"class":250},[220,889,890],{"class":226}," {\n",[220,892,893,896,899,901,904,906,908,910,913,916,918],{"class":222,"line":394},[220,894,895],{"class":226},"  document.documentElement.style.",[220,897,898],{"class":234},"setProperty",[220,900,674],{"class":226},[220,902,903],{"class":314},"'--panel-header-height'",[220,905,178],{"class":226},[220,907,321],{"class":314},[220,909,330],{"class":226},[220,911,912],{"class":250}," ||",[220,914,915],{"class":254}," 72",[220,917,338],{"class":314},[220,919,832],{"class":226},[220,921,922,925,928],{"class":222,"line":400},[220,923,924],{"class":226},"}, { immediate: ",[220,926,927],{"class":254},"true",[220,929,280],{"class":226},[220,931,932,934,936],{"class":222,"line":410},[220,933,403],{"class":226},[220,935,231],{"class":230},[220,937,238],{"class":226},[11,939,940,941,944],{},"Każdy element pod nagłówkiem ma teraz tę samą klasę ",[175,942,943],{},"top-(--panel-header-height)",". Nie musi wiedzieć, czy pasek „Zaloguj jako” jest widoczny, ani ile nagłówek ma wysokości na danym ekranie.",[11,946,947],{},[189,948],{"alt":949,"height":950,"src":951,"width":81},"Nagłówek panelu – u góry przed zmianą (75 px), na dole po (72 px)",122,"\u002Fblog\u002F2026\u002Fnaglowki-agrestapp\u002Fpanel-header.webp",[11,953,954],{},[189,955],{"alt":956,"height":957,"src":958,"width":506},"Panel na telefonie z otwartym menu bocznym – po lewej przed zmianą, po prawej po: sam znak logo i przycisk zamykania",560,"\u002Fblog\u002F2026\u002Fnaglowki-agrestapp\u002Fpanel-phone.webp",[15,960,962],{"id":961},"wartości-ze-skali-zamiast-pikseli-w-nawiasach","Wartości ze skali zamiast pikseli w nawiasach",[11,964,965],{},"Tailwind v4 przyjmuje każdą wielokrotność 0.25rem, więc większość wartości w nawiasach dało się zamienić na zwykłe klasy:",[967,968,969,982],"table",{},[970,971,972],"thead",{},[973,974,975,979],"tr",{},[976,977,978],"th",{},"Przed",[976,980,981],{},"Po",[983,984,985,999,1012,1024],"tbody",{},[973,986,987,993],{},[988,989,990],"td",{},[175,991,992],{},"h-[75px]",[988,994,995,998],{},[175,996,997],{},"h-18"," (72 px)",[973,1000,1001,1006],{},[988,1002,1003],{},[175,1004,1005],{},"w-[239px]",[988,1007,1008,1011],{},[175,1009,1010],{},"w-60"," (240 px)",[973,1013,1014,1019],{},[988,1015,1016],{},[175,1017,1018],{},"py-[22px]",[988,1020,1021],{},[175,1022,1023],{},"py-5.5",[973,1025,1026,1031],{},[988,1027,1028],{},[175,1029,1030],{},"dark:bg-[#161e30]",[988,1032,1033],{},"kolor z palety projektu",[11,1035,1036,1037,1040],{},"W pasku strony jeden odstęp wymagał płynnej wartości. Między logo, menu i przyciskami jest ",[175,1038,1039],{},"clamp()",", od 16 px przy szerokości 1024 px do 84 px przy 1440 px. Dzięki temu całe menu mieści się już od 1024 px, bez dodatkowych breakpointów.",[15,1042,1044],{"id":1043},"co-z-tego-zostało","Co z tego zostało",[20,1046,1047,1050,1053,1056,1059],{},[23,1048,1049],{},"Jedna lista menu dla komputera i telefonu.",[23,1051,1052],{},"Jedna zasada rozmiaru (36 \u002F 44 px) zamiast kilku przypadkowych.",[23,1054,1055],{},"Jeden kolor ikon w spoczynku i pełna ikona dla aktywnej strony.",[23,1057,1058],{},"Jedna zmienna z wysokością nagłówka zamiast liczb 75 i 125 w kilku plikach.",[23,1060,1061],{},"Menu, które działa myszą, dotykiem i klawiaturą.",[11,1063,1064],{},"Rozwijane menu na stronie ma te same odstępy co menu konta w panelu. Obie części wyglądają teraz jak jeden system, choć są osobnymi aplikacjami.",[1066,1067,1068],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 pre.shiki code .sqY6-, html code.shiki .sqY6-{--shiki-default:#6A737D;--shiki-dark:#90A1B9}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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);}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html pre.shiki code .siNPP, html code.shiki .siNPP{--shiki-default:#CA3500;--shiki-dark:#FFAB70}",{"title":73,"searchDepth":74,"depth":74,"links":1070},[1071,1072,1073,1074,1075,1076],{"id":169,"depth":74,"text":170},{"id":509,"depth":74,"text":510},{"id":616,"depth":74,"text":617},{"id":762,"depth":74,"text":763},{"id":961,"depth":74,"text":962},{"id":1043,"depth":74,"text":1044},"2026-10-17","Jak w jeden dzień przepisałem nagłówek strony i panelu AgrestApp – jedno przesuwane menu, rozmiary zależne od sposobu obsługi i wysokość nagłówka w zmiennej CSS zamiast liczb wpisanych w kilku plikach.","case-study",{},"\u002Fog\u002Fblog\u002Fnaglowki-agrestapp-pl.jpg","\u002Fpl\u002Fwpisy\u002F2026\u002Fnaglowki-agrestapp","\u002Fprojects\u002F2026\u002Fagrestapp",{"title":160,"description":1078},"pl\u002Fwpisy\u002F2026\u002Fnaglowki-agrestapp",[1087,155,1088],"tailwind","accessibility","naglowki-agrestapp","RUI8njbIgnlAA0hKwwyH2FB62PQNRnV3kmvuH59uZDE",{"id":1092,"title":1093,"art":1094,"body":1095,"date":1839,"description":1840,"draft":86,"extension":87,"image":77,"kind":1079,"meta":1841,"navigation":109,"ogImage":1842,"path":1843,"project":1083,"seo":1844,"slug":77,"stem":1845,"tags":1846,"translationKey":1847,"updated":77,"__hash__":1848},"blog_pl\u002Fpl\u002Fwpisy\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":1096,"toc":1832},[1097,1103,1110,1114,1117,1318,1333,1336,1373,1377,1380,1532,1539,1543,1549,1568,1579,1717,1720,1753,1763,1767,1770,1773,1794,1801,1803,1829],[11,1098,1099,1100,1102],{},"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 ",[175,1101,519],{}," – 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,1104,1105,1106,1109],{},"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 (",[175,1107,1108],{},"!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.",[15,1111,1113],{"id":1112},"zasada-nie-przepisywać-szablonów","Zasada: nie przepisywać szablonów",[11,1115,1116],{},"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:",[213,1118,1122],{"className":1119,"code":1120,"language":1121,"meta":73,"style":73},"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",[175,1123,1124,1135,1140,1148,1152,1157,1164,1207,1222,1236,1240,1244,1249,1259,1287,1309,1314],{"__ignoreMap":73},[220,1125,1126,1129,1132],{"class":222,"line":223},[220,1127,1128],{"class":250},"@import",[220,1130,1131],{"class":314}," \"tailwindcss\"",[220,1133,1134],{"class":226},";\n",[220,1136,1137],{"class":222,"line":74},[220,1138,1139],{"class":243},"\u002F* v4 nadal czyta konfigurację JS – kolory i breakpointy bez zmian *\u002F\n",[220,1141,1142,1145],{"class":222,"line":247},[220,1143,1144],{"class":250},"@config",[220,1146,1147],{"class":226}," \"..\u002F..\u002Ftailwind.config.cjs\";\n",[220,1149,1150],{"class":222,"line":283},[220,1151,286],{"emptyLinePlaceholder":109},[220,1153,1154],{"class":222,"line":289},[220,1155,1156],{"class":243},"\u002F* v4 przeskalował te trzy wartości – przywracam te z v3 *\u002F\n",[220,1158,1159,1162],{"class":222,"line":311},[220,1160,1161],{"class":250},"@theme",[220,1163,890],{"class":226},[220,1165,1166,1169,1171,1173,1176,1179,1182,1184,1187,1190,1192,1194,1196,1198,1201,1204],{"class":222,"line":344},[220,1167,1168],{"class":677},"  --shadow-sm",[220,1170,318],{"class":226},[220,1172,267],{"class":254},[220,1174,1175],{"class":254}," 1",[220,1177,1178],{"class":250},"px",[220,1180,1181],{"class":254}," 2",[220,1183,1178],{"class":250},[220,1185,1186],{"class":254}," 0",[220,1188,1189],{"class":254}," rgb",[220,1191,674],{"class":226},[220,1193,267],{"class":254},[220,1195,1186],{"class":254},[220,1197,1186],{"class":254},[220,1199,1200],{"class":226}," \u002F ",[220,1202,1203],{"class":254},"0.05",[220,1205,1206],{"class":226},");\n",[220,1208,1209,1212,1214,1217,1220],{"class":222,"line":369},[220,1210,1211],{"class":677},"  --radius-sm",[220,1213,318],{"class":226},[220,1215,1216],{"class":254},"0.125",[220,1218,1219],{"class":250},"rem",[220,1221,1134],{"class":226},[220,1223,1224,1227,1229,1232,1234],{"class":222,"line":394},[220,1225,1226],{"class":677},"  --default-ring-width",[220,1228,318],{"class":226},[220,1230,1231],{"class":254},"3",[220,1233,1178],{"class":250},[220,1235,1134],{"class":226},[220,1237,1238],{"class":222,"line":400},[220,1239,751],{"class":226},[220,1241,1242],{"class":222,"line":410},[220,1243,286],{"emptyLinePlaceholder":109},[220,1245,1246],{"class":222,"line":415},[220,1247,1248],{"class":243},"\u002F* v4 zmienił domyślny kolor ramki na currentcolor *\u002F\n",[220,1250,1251,1254,1257],{"class":222,"line":425},[220,1252,1253],{"class":250},"@layer",[220,1255,1256],{"class":677}," base",[220,1258,890],{"class":226},[220,1260,1261,1264,1266,1269,1271,1274,1276,1279,1282,1285],{"class":222,"line":434},[220,1262,1263],{"class":230},"  *",[220,1265,178],{"class":226},[220,1267,1268],{"class":234},"::after",[220,1270,178],{"class":226},[220,1272,1273],{"class":234},"::before",[220,1275,178],{"class":226},[220,1277,1278],{"class":234},"::backdrop",[220,1280,1281],{"class":226},", ::",[220,1283,1284],{"class":230},"file-selector-button",[220,1286,890],{"class":226},[220,1288,1289,1292,1294,1297,1299,1302,1304,1307],{"class":222,"line":446},[220,1290,1291],{"class":254},"    border-color",[220,1293,318],{"class":226},[220,1295,1296],{"class":254},"var",[220,1298,674],{"class":226},[220,1300,1301],{"class":677},"--color-gray-200",[220,1303,178],{"class":226},[220,1305,1306],{"class":254},"currentcolor",[220,1308,1206],{"class":226},[220,1310,1311],{"class":222,"line":457},[220,1312,1313],{"class":226},"  }\n",[220,1315,1316],{"class":222,"line":463},[220,1317,751],{"class":226},[11,1319,1320,1321,1324,1325,1328,1329,1332],{},"Te trzy wartości to 296 klas w szablonach: ",[175,1322,1323],{},"shadow-sm"," (226), ",[175,1326,1327],{},"rounded-sm"," (52) i samo ",[175,1330,1331],{},"ring"," (18). Po zmianie wyglądają dokładnie jak w v3, a w szablonach nie zmieniłem w nich ani jednego znaku.",[11,1334,1335],{},"Dwie rzeczy w zależnościach wyszły dopiero przy porównaniu:",[20,1337,1338,1356],{},[23,1339,1340,1343,1344,1347,1348,1351,1352,1355],{},[26,1341,1342],{},"Plugin formularzy."," ",[175,1345,1346],{},"@tailwindcss\u002Fforms"," w wersji 0.5.11 obniża specyficzność swoich selektorów (",[175,1349,1350],{},"input:where([type='text'])"," zamiast ",[175,1353,1354],{},"[type='text']","). Po aktualizacji przesunęło to paddingi wszystkich pól. Przypiąłem wersję 0.5.7, która działa jak wcześniej.",[23,1357,1358,1361,1362,1365,1366,1369,1370,327],{},[26,1359,1360],{},"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 ",[175,1363,1364],{},"engines"," w ",[175,1367,1368],{},"package.json"," i plik ",[175,1371,1372],{},".nvmrc",[15,1374,1376],{"id":1375},"trzy-zmiany-bez-których-v4-nie-działa","Trzy zmiany, bez których v4 nie działa",[11,1378,1379],{},"Kilku rzeczy nie dało się przywrócić w CSS. Te musiałem zmienić w szablonach:",[1381,1382,1383,1444,1517],"ol",{},[23,1384,1385,1391,1392,178,1395,1398,1399],{},[26,1386,1387,1388],{},"Klasy ",[175,1389,1390],{},"*-opacity-*"," (",[175,1393,1394],{},"bg-opacity-50",[175,1396,1397],{},"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.",[213,1400,1402],{"className":523,"code":1401,"language":525,"meta":73,"style":73},"\u003C!-- przed -->\n\u003Cdiv class=\"bg-black bg-opacity-50\">\n\u003C!-- po -->\n\u003Cdiv class=\"bg-black\u002F50\">\n",[175,1403,1404,1409,1424,1429],{"__ignoreMap":73},[220,1405,1406],{"class":222,"line":223},[220,1407,1408],{"class":243},"\u003C!-- przed -->\n",[220,1410,1411,1413,1415,1417,1419,1422],{"class":222,"line":74},[220,1412,227],{"class":226},[220,1414,481],{"class":230},[220,1416,542],{"class":234},[220,1418,440],{"class":226},[220,1420,1421],{"class":314},"\"bg-black bg-opacity-50\"",[220,1423,238],{"class":226},[220,1425,1426],{"class":222,"line":247},[220,1427,1428],{"class":243},"\u003C!-- po -->\n",[220,1430,1431,1433,1435,1437,1439,1442],{"class":222,"line":283},[220,1432,227],{"class":226},[220,1434,481],{"class":230},[220,1436,542],{"class":234},[220,1438,440],{"class":226},[220,1440,1441],{"class":314},"\"bg-black\u002F50\"",[220,1443,238],{"class":226},[23,1445,1446,1455,1456,1458,1459,1462,1463],{},[26,1447,1448,1451,1452,327],{},[175,1449,1450],{},"@reference"," w blokach ",[175,1453,1454],{},"\u003Cstyle>"," W v4 każdy blok ",[175,1457,1454],{}," w komponencie Vue kompiluje się osobno i nie zna kolorów ani klas projektu, więc ",[175,1460,1461],{},"@apply"," kończy się błędem. Dodałem jedną linię w 32 plikach:",[213,1464,1466],{"className":215,"code":1465,"language":155,"meta":73,"style":73},"\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",[175,1467,1468,1479,1486,1490,1497,1505,1509],{"__ignoreMap":73},[220,1469,1470,1472,1474,1477],{"class":222,"line":223},[220,1471,227],{"class":226},[220,1473,1066],{"class":230},[220,1475,1476],{"class":234}," scoped",[220,1478,238],{"class":226},[220,1480,1481,1483],{"class":222,"line":74},[220,1482,1450],{"class":250},[220,1484,1485],{"class":226}," \"..\u002F..\u002Fcss\u002Fapp.css\";\n",[220,1487,1488],{"class":222,"line":247},[220,1489,286],{"emptyLinePlaceholder":109},[220,1491,1492,1495],{"class":222,"line":283},[220,1493,1494],{"class":234},".badge",[220,1496,890],{"class":226},[220,1498,1499,1502],{"class":222,"line":289},[220,1500,1501],{"class":250},"  @apply",[220,1503,1504],{"class":226}," rounded-sm bg-brand-1-100 px-2;\n",[220,1506,1507],{"class":222,"line":311},[220,1508,751],{"class":226},[220,1510,1511,1513,1515],{"class":222,"line":344},[220,1512,403],{"class":226},[220,1514,1066],{"class":230},[220,1516,238],{"class":226},[23,1518,1519,1524,1525,1528,1529,1531],{},[26,1520,1521,327],{},[175,1522,1523],{},"@apply … !important"," v4 nie przyjmuje ",[175,1526,1527],{},"!important"," na końcu ",[175,1530,1461],{},". W jednym miejscu przeniosłem wykrzyknik na poszczególne klasy.",[11,1533,1534,1535,1538],{},"Za to prefiks ",[175,1536,1537],{},"!"," przed klasą działa w v4 bez zmian, więc ponad 1500 takich klas zostało nietkniętych.",[15,1540,1542],{"id":1541},"najtrudniejsze-warstwy-css","Najtrudniejsze: warstwy CSS",[11,1544,1545,1546,1548],{},"Najwięcej czasu zajęła kaskada. W v4 klasy Tailwinda są w warstwach (",[175,1547,1253],{},"), 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ć:",[20,1550,1551,1562],{},[23,1552,1553,1554,1557,1558,1561],{},"reguła ",[175,1555,1556],{},"label { … }"," z marginesem nadpisała klasę ",[175,1559,1560],{},"mb-7.5"," na etykiecie,",[23,1563,1564,1567],{},[175,1565,1566],{},"input { height: 36px }"," rozciągnęła checkboxy i radio do 36 px.",[11,1569,1570,1571,1574,1575,1578],{},"Rozwiązaniem była własna warstwa na reguły elementów. Stoi między ",[175,1572,1573],{},"base"," a ",[175,1576,1577],{},"components",", więc wygrywa z resetem i pluginem formularzy, ale przegrywa z klasami – tak jak w v3:",[213,1580,1582],{"className":1119,"code":1581,"language":1121,"meta":73,"style":73},"\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",[175,1583,1584,1589,1616,1620,1629,1636,1650,1654,1658,1663,1695,1709,1713],{"__ignoreMap":73},[220,1585,1586],{"class":222,"line":223},[220,1587,1588],{"class":243},"\u002F* kolejność warstw – musi być na samym początku arkusza *\u002F\n",[220,1590,1591,1593,1596,1598,1600,1602,1605,1607,1609,1611,1614],{"class":222,"line":74},[220,1592,1253],{"class":250},[220,1594,1595],{"class":677}," theme",[220,1597,178],{"class":226},[220,1599,1573],{"class":677},[220,1601,178],{"class":226},[220,1603,1604],{"class":677},"app-base",[220,1606,178],{"class":226},[220,1608,1577],{"class":677},[220,1610,178],{"class":226},[220,1612,1613],{"class":677},"utilities",[220,1615,1134],{"class":226},[220,1617,1618],{"class":222,"line":247},[220,1619,286],{"emptyLinePlaceholder":109},[220,1621,1622,1624,1627],{"class":222,"line":283},[220,1623,1253],{"class":250},[220,1625,1626],{"class":677}," app-base",[220,1628,890],{"class":226},[220,1630,1631,1634],{"class":222,"line":289},[220,1632,1633],{"class":230},"  label",[220,1635,890],{"class":226},[220,1637,1638,1641,1643,1646,1648],{"class":222,"line":311},[220,1639,1640],{"class":254},"    margin-bottom",[220,1642,318],{"class":226},[220,1644,1645],{"class":254},"0.375",[220,1647,1219],{"class":250},[220,1649,1134],{"class":226},[220,1651,1652],{"class":222,"line":344},[220,1653,1313],{"class":226},[220,1655,1656],{"class":222,"line":369},[220,1657,286],{"emptyLinePlaceholder":109},[220,1659,1660],{"class":222,"line":394},[220,1661,1662],{"class":243},"  \u002F* checkbox i radio mają swój rozmiar z pluginu formularzy *\u002F\n",[220,1664,1665,1668,1671,1674,1677,1679,1682,1685,1687,1689,1692],{"class":222,"line":400},[220,1666,1667],{"class":230},"  input",[220,1669,1670],{"class":234},":not",[220,1672,1673],{"class":226},"([",[220,1675,1676],{"class":234},"type",[220,1678,440],{"class":250},[220,1680,1681],{"class":314},"\"checkbox\"",[220,1683,1684],{"class":226},"], [",[220,1686,1676],{"class":234},[220,1688,440],{"class":250},[220,1690,1691],{"class":314},"\"radio\"",[220,1693,1694],{"class":226},"]) {\n",[220,1696,1697,1700,1702,1705,1707],{"class":222,"line":410},[220,1698,1699],{"class":254},"    height",[220,1701,318],{"class":226},[220,1703,1704],{"class":254},"36",[220,1706,1178],{"class":250},[220,1708,1134],{"class":226},[220,1710,1711],{"class":222,"line":415},[220,1712,1313],{"class":226},[220,1714,1715],{"class":222,"line":425},[220,1716,751],{"class":226},[11,1718,1719],{},"Dwie kolejne różnice wyszły dopiero przy klikaniu:",[20,1721,1722,1739],{},[23,1723,1724,1727,1728,1731,1732,1735,1736,327],{},[26,1725,1726],{},"Okna modalne chowały się pod własnym tłem."," W v3 klasa ",[175,1729,1730],{},"transform"," dodawała pustą transformację, która tworzyła nowy kontekst nakładania. W v4 ta klasa daje ",[175,1733,1734],{},"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: ",[175,1737,1738],{},".transform { transform: translate(0, 0); }",[23,1740,1741,1744,1745,1748,1749,1752],{},[26,1742,1743],{},"Gradient zniknął z pierwszego kafelka na pulpicie."," Własny plugin ",[175,1746,1747],{},"bg-gradient-\u003Ckąt>"," nie ustawiał zmiennej ",[175,1750,1751],{},"--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,1754,1755,1756,1759,1760,327],{},"Poprawiłem też klasę ",[175,1757,1758],{},"container",". v4 nie czyta już z konfiguracji JS wyśrodkowania i paddingu kontenera, więc dodałem je w CSS przez ",[175,1761,1762],{},"@utility container",[15,1764,1766],{"id":1765},"jak-sprawdziłem-że-nic-się-nie-zmieniło","Jak sprawdziłem, że nic się nie zmieniło",[11,1768,1769],{},"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,1771,1772],{},"Wynik:",[20,1774,1775,1778,1781],{},[23,1776,1777],{},"żadna klasa użyta w szablonach nie zniknęła z wynikowego CSS,",[23,1779,1780],{},"prawie wszystkie różnice to tylko inny zapis tej samej wartości,",[23,1782,1783,1784,1351,1787,1790,1791,1793],{},"zostały dwie świadome różnice: domyślna paleta v4 jest w ",[175,1785,1786],{},"oklch",[175,1788,1789],{},"rgb",", a ",[175,1792,1758],{}," ma dodatkowy próg szerokości z jednego z breakpointów.",[11,1795,1796,1797,1800],{},"Na koniec zostawiłem notatkę dla zespołu: po pobraniu zmian trzeba zrobić ",[175,1798,1799],{},"npm ci",", bo bez nowych paczek serwer deweloperski nie wstanie.",[15,1802,1044],{"id":1043},[20,1804,1805,1811,1817,1820],{},[23,1806,1807,1808,1810],{},"Migracja bez przepisywania szablonów – stare wartości w ",[175,1809,1161],{}," i trzy zmiany, których v4 wymaga.",[23,1812,1813,1814,1816],{},"Własna warstwa ",[175,1815,1604],{}," na reguły elementów, żeby kaskada działała jak w v3.",[23,1818,1819],{},"Porównanie wynikowego CSS zamiast przeglądania stron – daje pewność, że nic nie zniknęło.",[23,1821,1822,1823,1825,1826,327],{},"Projekt jest gotowy na ",[175,1824,519],{},", od którego wszystko się zaczęło – użyłem go m.in. w ",[592,1827,1828],{"href":1082},"nagłówkach AgrestApp",[1066,1830,1831],{},"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":73,"searchDepth":74,"depth":74,"links":1833},[1834,1835,1836,1837,1838],{"id":1112,"depth":74,"text":1113},{"id":1375,"depth":74,"text":1376},{"id":1541,"depth":74,"text":1542},{"id":1765,"depth":74,"text":1766},{"id":1043,"depth":74,"text":1044},"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.",{},"\u002Fog\u002Fblog\u002Fmigracja-na-tailwind-v4-pl.jpg","\u002Fpl\u002Fwpisy\u002F2026\u002Fmigracja-na-tailwind-v4",{"title":1093,"description":1840},"pl\u002Fwpisy\u002F2026\u002Fmigracja-na-tailwind-v4",[1087,1121],"migracja-na-tailwind-v4","IOCnHVRK_qhmSRQk22guIepVwo9iCtXlogLmANkDlf8",{"id":1850,"title":1851,"art":1852,"body":1853,"date":2784,"description":2785,"draft":86,"extension":87,"image":77,"kind":1079,"meta":2786,"navigation":109,"ogImage":2787,"path":2788,"project":1083,"seo":2789,"slug":77,"stem":2790,"tags":2791,"translationKey":2793,"updated":77,"__hash__":2794},"blog_pl\u002Fpl\u002Fwpisy\u002F2026\u002Ftabela-jak-arkusz.md","Tabela jak arkusz: Shift + klik i Enter","\u002Fart\u002Fblog\u002Ftabela-jak-arkusz.svg",{"type":8,"value":1854,"toc":2779},[1855,1858,1862,1865,1875,1883,1886,1909,1912,2347,2444,2453,2457,2460,2467,2474,2757,2760,2762,2776],[11,1856,1857],{},"W panelu AgrestApp jest dużo długich list: surowce, produkty, opakowania, ceny, powiadomienia. Przy takich listach najwięcej czasu zabiera klikanie wiersz po wierszu. Klient poprosił o dwa skróty, które każdy zna z arkusza kalkulacyjnego albo z poczty: Shift + klik zaznacza zakres wierszy, a Enter przenosi do tego samego pola w wierszu niżej. Chodziło o to, żeby zaznaczyć kilka pozycji i edytować je naraz, a nie produkt po produkcie, i żeby ceny aktualizować w jednej tabeli, jak w Excelu. Zrzuty poniżej są z lokalnej wersji na zmyślonych danych. Kod we wpisie to uproszczone przykłady napisane do tego tekstu, a nie kod z AgrestApp.",[15,1859,1861],{"id":1860},"shift-klik-zaznaczanie-zakresu","Shift + klik: zaznaczanie zakresu",[11,1863,1864],{},"Działa tak jak w poczcie: klikasz checkbox w jednym wierszu, a potem z wciśniętym Shiftem w innym. Zaznaczają się oba wiersze i wszystko pomiędzy. Jeśli drugie kliknięcie odznacza wiersz, odznacza się cały zakres. W panelu działa to w 9 listach.",[11,1866,1867,1868,1871,1872,1874],{},"Musiałem się podpiąć pod istniejący kod, a każda tabela była zrobiona trochę inaczej. Dlatego przycisk akcji masowej nie przyjmuje tylko tablicy z ",[175,1869,1870],{},"id"," zaznaczonych wierszy – zależnie od miejsca przekazuję mu albo ",[175,1873,1870],{},", albo cały obiekt i dopiero potem przygotowuję dane do wysłania.",[11,1876,1877],{},[189,1878],{"alt":1879,"height":1880,"src":1881,"width":1882},"Po lewej po pierwszym kliknięciu – zaznaczony 1 wiersz, po prawej po Shift + klik w 8. wierszu – zaznaczonych 7",836,"\u002Fblog\u002F2026\u002Ftabela-jak-arkusz\u002Fshift-klik.webp",840,[11,1884,1885],{},"Najprostsza wersja ma trzy pułapki:",[1381,1887,1888,1897,1903],{},[23,1889,1890,1893,1894,1896],{},[26,1891,1892],{},"Numer wiersza zamiast klucza."," Łatwo zapamiętać numer ostatnio klikniętego wiersza. Działa to, dopóki nikt nie posortuje listy, nie zmieni filtra ani nie doładuje kolejnych wierszy. Wtedy numer 3 to już inny wiersz i zaznacza się nie ten zakres. Bezpieczniej zapamiętać ",[175,1895,1870],{}," wiersza, a jego pozycję szukać dopiero przy kliknięciu.",[23,1898,1899,1902],{},[26,1900,1901],{},"Kolejność z danych zamiast z ekranu."," Po sortowaniu i filtrze kolejność na ekranie jest inna niż w danych z serwera. Zakres musi się zgadzać z tym, co widzi użytkownik.",[23,1904,1905,1908],{},[26,1906,1907],{},"Zaznaczony tekst."," Shift + klik w przeglądarce zaznacza też tekst między kliknięciami. Można to zablokować tylko przy wciśniętym Shifcie – wtedy zwykłe zaznaczanie tekstu w tabeli dalej działa.",[11,1910,1911],{},"Uproszczona wersja jako composable, bez tych pułapek:",[213,1913,1915],{"className":654,"code":1914,"language":656,"meta":73,"style":73},"import { ref, toValue } from 'vue'\n\nexport function useRangeSelection(rows, key = 'id') {\n  const selected = ref(new Set())\n  \u002F\u002F klucz ostatnio klikniętego wiersza – nie jego numer\n  const anchor = ref(null)\n\n  function toggle(row, event) {\n    const id = row[key]\n    const select = !selected.value.has(id)\n    const list = toValue(rows) \u002F\u002F wiersze w kolejności, którą widać na ekranie\n    const from = list.findIndex((r) => r[key] === anchor.value)\n    const to = list.findIndex((r) => r[key] === id)\n\n    const ids = event?.shiftKey && from !== -1\n      ? list.slice(Math.min(from, to), Math.max(from, to) + 1).map((r) => r[key])\n      : [id]\n\n    const next = new Set(selected.value)\n    for (const rowId of ids) {\n      if (select) next.add(rowId)\n      else next.delete(rowId)\n    }\n    selected.value = next\n    anchor.value = id\n  }\n\n  return { selected, toggle }\n}\n",[175,1916,1917,1929,1933,1961,1983,1988,2005,2009,2029,2042,2063,2081,2115,2143,2147,2174,2220,2228,2232,2249,2267,2282,2296,2302,2313,2324,2329,2334,2342],{"__ignoreMap":73},[220,1918,1919,1921,1924,1926],{"class":222,"line":223},[220,1920,800],{"class":250},[220,1922,1923],{"class":226}," { ref, toValue } ",[220,1925,806],{"class":250},[220,1927,1928],{"class":314}," 'vue'\n",[220,1930,1931],{"class":222,"line":74},[220,1932,286],{"emptyLinePlaceholder":109},[220,1934,1935,1938,1941,1944,1946,1949,1951,1954,1956,1959],{"class":222,"line":247},[220,1936,1937],{"class":250},"export",[220,1939,1940],{"class":250}," function",[220,1942,1943],{"class":234}," useRangeSelection",[220,1945,674],{"class":226},[220,1947,1948],{"class":677},"rows",[220,1950,178],{"class":226},[220,1952,1953],{"class":677},"key",[220,1955,258],{"class":250},[220,1957,1958],{"class":314}," 'id'",[220,1960,696],{"class":226},[220,1962,1963,1965,1968,1970,1972,1974,1977,1980],{"class":222,"line":283},[220,1964,701],{"class":250},[220,1966,1967],{"class":254}," selected",[220,1969,258],{"class":250},[220,1971,261],{"class":234},[220,1973,674],{"class":226},[220,1975,1976],{"class":250},"new",[220,1978,1979],{"class":234}," Set",[220,1981,1982],{"class":226},"())\n",[220,1984,1985],{"class":222,"line":289},[220,1986,1987],{"class":243},"  \u002F\u002F klucz ostatnio klikniętego wiersza – nie jego numer\n",[220,1989,1990,1992,1995,1997,1999,2001,2003],{"class":222,"line":311},[220,1991,701],{"class":250},[220,1993,1994],{"class":254}," anchor",[220,1996,258],{"class":250},[220,1998,261],{"class":234},[220,2000,674],{"class":226},[220,2002,829],{"class":254},[220,2004,832],{"class":226},[220,2006,2007],{"class":222,"line":344},[220,2008,286],{"emptyLinePlaceholder":109},[220,2010,2011,2014,2017,2019,2022,2024,2027],{"class":222,"line":369},[220,2012,2013],{"class":250},"  function",[220,2015,2016],{"class":234}," toggle",[220,2018,674],{"class":226},[220,2020,2021],{"class":677},"row",[220,2023,178],{"class":226},[220,2025,2026],{"class":677},"event",[220,2028,696],{"class":226},[220,2030,2031,2034,2037,2039],{"class":222,"line":394},[220,2032,2033],{"class":250},"    const",[220,2035,2036],{"class":254}," id",[220,2038,258],{"class":250},[220,2040,2041],{"class":226}," row[key]\n",[220,2043,2044,2046,2049,2051,2054,2057,2060],{"class":222,"line":400},[220,2045,2033],{"class":250},[220,2047,2048],{"class":254}," select",[220,2050,258],{"class":250},[220,2052,2053],{"class":250}," !",[220,2055,2056],{"class":226},"selected.value.",[220,2058,2059],{"class":234},"has",[220,2061,2062],{"class":226},"(id)\n",[220,2064,2065,2067,2070,2072,2075,2078],{"class":222,"line":410},[220,2066,2033],{"class":250},[220,2068,2069],{"class":254}," list",[220,2071,258],{"class":250},[220,2073,2074],{"class":234}," toValue",[220,2076,2077],{"class":226},"(rows) ",[220,2079,2080],{"class":243},"\u002F\u002F wiersze w kolejności, którą widać na ekranie\n",[220,2082,2083,2085,2088,2090,2093,2096,2099,2102,2104,2106,2109,2112],{"class":222,"line":415},[220,2084,2033],{"class":250},[220,2086,2087],{"class":254}," from",[220,2089,258],{"class":250},[220,2091,2092],{"class":226}," list.",[220,2094,2095],{"class":234},"findIndex",[220,2097,2098],{"class":226},"((",[220,2100,2101],{"class":677},"r",[220,2103,885],{"class":226},[220,2105,305],{"class":250},[220,2107,2108],{"class":226}," r[key] ",[220,2110,2111],{"class":250},"===",[220,2113,2114],{"class":226}," anchor.value)\n",[220,2116,2117,2119,2122,2124,2126,2128,2130,2132,2134,2136,2138,2140],{"class":222,"line":425},[220,2118,2033],{"class":250},[220,2120,2121],{"class":254}," to",[220,2123,258],{"class":250},[220,2125,2092],{"class":226},[220,2127,2095],{"class":234},[220,2129,2098],{"class":226},[220,2131,2101],{"class":677},[220,2133,885],{"class":226},[220,2135,305],{"class":250},[220,2137,2108],{"class":226},[220,2139,2111],{"class":250},[220,2141,2142],{"class":226}," id)\n",[220,2144,2145],{"class":222,"line":434},[220,2146,286],{"emptyLinePlaceholder":109},[220,2148,2149,2151,2154,2156,2159,2162,2165,2168,2171],{"class":222,"line":446},[220,2150,2033],{"class":250},[220,2152,2153],{"class":254}," ids",[220,2155,258],{"class":250},[220,2157,2158],{"class":226}," event?.shiftKey ",[220,2160,2161],{"class":250},"&&",[220,2163,2164],{"class":226}," from ",[220,2166,2167],{"class":250},"!==",[220,2169,2170],{"class":250}," -",[220,2172,2173],{"class":254},"1\n",[220,2175,2176,2179,2181,2184,2187,2190,2193,2196,2199,2202,2204,2206,2209,2211,2213,2215,2217],{"class":222,"line":457},[220,2177,2178],{"class":250},"      ?",[220,2180,2092],{"class":226},[220,2182,2183],{"class":234},"slice",[220,2185,2186],{"class":226},"(Math.",[220,2188,2189],{"class":234},"min",[220,2191,2192],{"class":226},"(from, to), Math.",[220,2194,2195],{"class":234},"max",[220,2197,2198],{"class":226},"(from, to) ",[220,2200,2201],{"class":250},"+",[220,2203,1175],{"class":254},[220,2205,185],{"class":226},[220,2207,2208],{"class":234},"map",[220,2210,2098],{"class":226},[220,2212,2101],{"class":677},[220,2214,885],{"class":226},[220,2216,305],{"class":250},[220,2218,2219],{"class":226}," r[key])\n",[220,2221,2222,2225],{"class":222,"line":463},[220,2223,2224],{"class":250},"      :",[220,2226,2227],{"class":226}," [id]\n",[220,2229,2230],{"class":222,"line":469},[220,2231,286],{"emptyLinePlaceholder":109},[220,2233,2234,2236,2239,2241,2244,2246],{"class":222,"line":475},[220,2235,2033],{"class":250},[220,2237,2238],{"class":254}," next",[220,2240,258],{"class":250},[220,2242,2243],{"class":250}," new",[220,2245,1979],{"class":234},[220,2247,2248],{"class":226},"(selected.value)\n",[220,2250,2251,2254,2256,2258,2261,2264],{"class":222,"line":486},[220,2252,2253],{"class":250},"    for",[220,2255,1391],{"class":226},[220,2257,251],{"class":250},[220,2259,2260],{"class":254}," rowId",[220,2262,2263],{"class":250}," of",[220,2265,2266],{"class":226}," ids) {\n",[220,2268,2270,2273,2276,2279],{"class":222,"line":2269},21,[220,2271,2272],{"class":250},"      if",[220,2274,2275],{"class":226}," (select) next.",[220,2277,2278],{"class":234},"add",[220,2280,2281],{"class":226},"(rowId)\n",[220,2283,2285,2288,2291,2294],{"class":222,"line":2284},22,[220,2286,2287],{"class":250},"      else",[220,2289,2290],{"class":226}," next.",[220,2292,2293],{"class":234},"delete",[220,2295,2281],{"class":226},[220,2297,2299],{"class":222,"line":2298},23,[220,2300,2301],{"class":226},"    }\n",[220,2303,2305,2308,2310],{"class":222,"line":2304},24,[220,2306,2307],{"class":226},"    selected.value ",[220,2309,440],{"class":250},[220,2311,2312],{"class":226}," next\n",[220,2314,2316,2319,2321],{"class":222,"line":2315},25,[220,2317,2318],{"class":226},"    anchor.value ",[220,2320,440],{"class":250},[220,2322,2323],{"class":226}," id\n",[220,2325,2327],{"class":222,"line":2326},26,[220,2328,1313],{"class":226},[220,2330,2332],{"class":222,"line":2331},27,[220,2333,286],{"emptyLinePlaceholder":109},[220,2335,2337,2339],{"class":222,"line":2336},28,[220,2338,726],{"class":250},[220,2340,2341],{"class":226}," { selected, toggle }\n",[220,2343,2345],{"class":222,"line":2344},29,[220,2346,751],{"class":226},[213,2348,2350],{"className":215,"code":2349,"language":155,"meta":73,"style":73},"\u003Cinput\n  type=\"checkbox\"\n  :checked=\"selected.has(row.id)\"\n  @mousedown=\"$event.shiftKey && $event.preventDefault()\"\n  @click=\"toggle(row, $event)\"\n>\n",[175,2351,2352,2359,2369,2393,2421,2440],{"__ignoreMap":73},[220,2353,2354,2356],{"class":222,"line":223},[220,2355,227],{"class":226},[220,2357,2358],{"class":230},"input\n",[220,2360,2361,2364,2366],{"class":222,"line":74},[220,2362,2363],{"class":234},"  type",[220,2365,440],{"class":226},[220,2367,2368],{"class":314},"\"checkbox\"\n",[220,2370,2371,2374,2377,2379,2382,2385,2387,2390],{"class":222,"line":247},[220,2372,2373],{"class":226},"  :",[220,2375,2376],{"class":234},"checked",[220,2378,440],{"class":226},[220,2380,2381],{"class":314},"\"",[220,2383,2384],{"class":226},"selected.",[220,2386,2059],{"class":234},[220,2388,2389],{"class":226},"(row.id)",[220,2391,2392],{"class":314},"\"\n",[220,2394,2395,2398,2401,2403,2405,2408,2410,2413,2416,2419],{"class":222,"line":283},[220,2396,2397],{"class":226},"  @",[220,2399,2400],{"class":234},"mousedown",[220,2402,440],{"class":226},[220,2404,2381],{"class":314},[220,2406,2407],{"class":226},"$event.shiftKey ",[220,2409,2161],{"class":250},[220,2411,2412],{"class":226}," $event.",[220,2414,2415],{"class":234},"preventDefault",[220,2417,2418],{"class":226},"()",[220,2420,2392],{"class":314},[220,2422,2423,2425,2428,2430,2432,2435,2438],{"class":222,"line":289},[220,2424,2397],{"class":226},[220,2426,2427],{"class":234},"click",[220,2429,440],{"class":226},[220,2431,2381],{"class":314},[220,2433,2434],{"class":234},"toggle",[220,2436,2437],{"class":226},"(row, $event)",[220,2439,2392],{"class":314},[220,2441,2442],{"class":222,"line":311},[220,2443,238],{"class":226},[11,2445,2446,2449,2450,2452],{},[175,2447,2448],{},"Set"," sprawdza, czy wiersz jest zaznaczony, bez przeszukiwania całej tablicy. Przy każdej zmianie tworzę nowy ",[175,2451,2448],{},", żeby Vue na pewno zauważył zmianę.",[15,2454,2456],{"id":2455},"enter-pole-w-wierszu-niżej","Enter: pole w wierszu niżej",[11,2458,2459],{},"W „Zarządzaniu ofertą” dostawca ustawia ceny wszystkich swoich artykułów: cenę bazową netto, domyślny narzut i swoją cenę netto. Dostawcy co jakiś czas aktualizują ceny i chcieli to robić w jednej tabeli, jak w Excelu. Teraz Enter przenosi do tego samego pola w wierszu niżej, a Shift + Enter – w wierszu wyżej. Zawartość nowego pola jest od razu zaznaczona, więc nową cenę można po prostu wpisać. Tak samo działa okno ceny indywidualnej.",[11,2461,2462],{},[189,2463],{"alt":2464,"height":1880,"src":2465,"width":2466},"U góry po Enterze: kursor w tym samym polu wiersz niżej, a cena jest od razu zaznaczona. Na dole po Shift + Enter: kursor wraca wiersz wyżej","\u002Fblog\u002F2026\u002Ftabela-jak-arkusz\u002Fenter.webp",1168,[11,2468,2469,2470,2473],{},"W przykładzie każde pole dostaje numer wiersza i nazwę kolumny w atrybutach ",[175,2471,2472],{},"data-*",". Po Enterze funkcja szuka pola z tą samą kolumną i sąsiednim numerem:",[213,2475,2477],{"className":215,"code":2476,"language":155,"meta":73,"style":73},"\u003Cscript setup>\nfunction moveOnEnter(event, row) {\n  \u002F\u002F Enter w formularzu normalnie go wysyła – tu ma tylko przenieść kursor\n  event.preventDefault()\n\n  const column = event.currentTarget.dataset.column\n  const next = row + (event.shiftKey ? -1 : 1)\n  const input = document.querySelector(\n    `[data-row=\"${next}\"][data-column=\"${column}\"] input`,\n  )\n\n  input?.focus()\n  input?.select()\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv\n    v-for=\"(item, index) in items\"\n    :key=\"item.id\"\n    :data-row=\"index\"\n    data-column=\"price\"\n    @keydown.enter=\"moveOnEnter($event, index)\"\n  >\n    \u003Cinput v-model=\"item.price\" inputmode=\"decimal\">\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[175,2478,2479,2489,2506,2511,2521,2525,2537,2566,2584,2603,2608,2612,2622,2631,2635,2643,2647,2655,2661,2671,2681,2691,2701,2711,2715,2741,2749],{"__ignoreMap":73},[220,2480,2481,2483,2485,2487],{"class":222,"line":223},[220,2482,227],{"class":226},[220,2484,231],{"class":230},[220,2486,235],{"class":234},[220,2488,238],{"class":226},[220,2490,2491,2493,2496,2498,2500,2502,2504],{"class":222,"line":74},[220,2492,668],{"class":250},[220,2494,2495],{"class":234}," moveOnEnter",[220,2497,674],{"class":226},[220,2499,2026],{"class":677},[220,2501,178],{"class":226},[220,2503,2021],{"class":677},[220,2505,696],{"class":226},[220,2507,2508],{"class":222,"line":247},[220,2509,2510],{"class":243},"  \u002F\u002F Enter w formularzu normalnie go wysyła – tu ma tylko przenieść kursor\n",[220,2512,2513,2516,2518],{"class":222,"line":283},[220,2514,2515],{"class":226},"  event.",[220,2517,2415],{"class":234},[220,2519,2520],{"class":226},"()\n",[220,2522,2523],{"class":222,"line":289},[220,2524,286],{"emptyLinePlaceholder":109},[220,2526,2527,2529,2532,2534],{"class":222,"line":311},[220,2528,701],{"class":250},[220,2530,2531],{"class":254}," column",[220,2533,258],{"class":250},[220,2535,2536],{"class":226}," event.currentTarget.dataset.column\n",[220,2538,2539,2541,2543,2545,2548,2550,2553,2555,2557,2560,2562,2564],{"class":222,"line":344},[220,2540,701],{"class":250},[220,2542,2238],{"class":254},[220,2544,258],{"class":250},[220,2546,2547],{"class":226}," row ",[220,2549,2201],{"class":250},[220,2551,2552],{"class":226}," (event.shiftKey ",[220,2554,712],{"class":250},[220,2556,2170],{"class":250},[220,2558,2559],{"class":254},"1",[220,2561,743],{"class":250},[220,2563,1175],{"class":254},[220,2565,832],{"class":226},[220,2567,2568,2570,2573,2575,2578,2581],{"class":222,"line":369},[220,2569,701],{"class":250},[220,2571,2572],{"class":254}," input",[220,2574,258],{"class":250},[220,2576,2577],{"class":226}," document.",[220,2579,2580],{"class":234},"querySelector",[220,2582,2583],{"class":226},"(\n",[220,2585,2586,2589,2592,2595,2598,2601],{"class":222,"line":394},[220,2587,2588],{"class":314},"    `[data-row=\"${",[220,2590,2591],{"class":226},"next",[220,2593,2594],{"class":314},"}\"][data-column=\"${",[220,2596,2597],{"class":226},"column",[220,2599,2600],{"class":314},"}\"] input`",[220,2602,341],{"class":226},[220,2604,2605],{"class":222,"line":400},[220,2606,2607],{"class":226},"  )\n",[220,2609,2610],{"class":222,"line":410},[220,2611,286],{"emptyLinePlaceholder":109},[220,2613,2614,2617,2620],{"class":222,"line":415},[220,2615,2616],{"class":226},"  input?.",[220,2618,2619],{"class":234},"focus",[220,2621,2520],{"class":226},[220,2623,2624,2626,2629],{"class":222,"line":425},[220,2625,2616],{"class":226},[220,2627,2628],{"class":234},"select",[220,2630,2520],{"class":226},[220,2632,2633],{"class":222,"line":434},[220,2634,751],{"class":226},[220,2636,2637,2639,2641],{"class":222,"line":446},[220,2638,403],{"class":226},[220,2640,231],{"class":230},[220,2642,238],{"class":226},[220,2644,2645],{"class":222,"line":457},[220,2646,286],{"emptyLinePlaceholder":109},[220,2648,2649,2651,2653],{"class":222,"line":463},[220,2650,227],{"class":226},[220,2652,420],{"class":230},[220,2654,238],{"class":226},[220,2656,2657,2659],{"class":222,"line":469},[220,2658,428],{"class":226},[220,2660,431],{"class":230},[220,2662,2663,2666,2668],{"class":222,"line":475},[220,2664,2665],{"class":234},"    v-for",[220,2667,440],{"class":226},[220,2669,2670],{"class":314},"\"(item, index) in items\"\n",[220,2672,2673,2676,2678],{"class":222,"line":486},[220,2674,2675],{"class":234},"    :key",[220,2677,440],{"class":226},[220,2679,2680],{"class":314},"\"item.id\"\n",[220,2682,2683,2686,2688],{"class":222,"line":2269},[220,2684,2685],{"class":234},"    :data-row",[220,2687,440],{"class":226},[220,2689,2690],{"class":314},"\"index\"\n",[220,2692,2693,2696,2698],{"class":222,"line":2284},[220,2694,2695],{"class":234},"    data-column",[220,2697,440],{"class":226},[220,2699,2700],{"class":314},"\"price\"\n",[220,2702,2703,2706,2708],{"class":222,"line":2298},[220,2704,2705],{"class":234},"    @keydown.enter",[220,2707,440],{"class":226},[220,2709,2710],{"class":314},"\"moveOnEnter($event, index)\"\n",[220,2712,2713],{"class":222,"line":2304},[220,2714,466],{"class":226},[220,2716,2717,2720,2723,2726,2728,2731,2734,2736,2739],{"class":222,"line":2315},[220,2718,2719],{"class":226},"    \u003C",[220,2721,2722],{"class":230},"input",[220,2724,2725],{"class":234}," v-model",[220,2727,440],{"class":226},[220,2729,2730],{"class":314},"\"item.price\"",[220,2732,2733],{"class":234}," inputmode",[220,2735,440],{"class":226},[220,2737,2738],{"class":314},"\"decimal\"",[220,2740,238],{"class":226},[220,2742,2743,2745,2747],{"class":222,"line":2326},[220,2744,478],{"class":226},[220,2746,481],{"class":230},[220,2748,238],{"class":226},[220,2750,2751,2753,2755],{"class":222,"line":2331},[220,2752,403],{"class":226},[220,2754,420],{"class":230},[220,2756,238],{"class":226},[11,2758,2759],{},"Dzięki nazwie kolumny w atrybucie jedna funkcja wystarczy dla każdej kolumny, a także dla tabeli i okna ceny indywidualnej. Na ostatnim wierszu Enter nic nie robi – pole niżej nie istnieje, więc kursor zostaje na miejscu.",[15,2761,1044],{"id":1043},[20,2763,2764,2767,2770,2773],{},[23,2765,2766],{},"Shift + klik w 9 listach panelu.",[23,2768,2769],{},"Enter i Shift + Enter w tabeli cen i w oknie ceny indywidualnej – nową cenę wpisuje się bez sięgania po mysz.",[23,2771,2772],{},"Obie rzeczy to mało kodu, a oszczędzają najwięcej klikania tam, gdzie użytkownik przechodzi przez całą listę.",[23,2774,2775],{},"Przy zaznaczaniu zakresu zapamiętuj klucz wiersza, a nie jego numer – wtedy sortowanie i filtry nie psują zakresu.",[1066,2777,2778],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .sqY6-, html code.shiki .sqY6-{--shiki-default:#6A737D;--shiki-dark:#90A1B9}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);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":73,"searchDepth":74,"depth":74,"links":2780},[2781,2782,2783],{"id":1860,"depth":74,"text":1861},{"id":2455,"depth":74,"text":2456},{"id":1043,"depth":74,"text":1044},"2026-03-14","Dwa skróty znane z arkusza kalkulacyjnego w panelu AgrestApp – zaznaczanie zakresu wierszy przez Shift + klik i Enter, który przenosi do tego samego pola w wierszu niżej.",{},"\u002Fog\u002Fblog\u002Ftabela-jak-arkusz-pl.jpg","\u002Fpl\u002Fwpisy\u002F2026\u002Ftabela-jak-arkusz",{"title":1851,"description":2785},"pl\u002Fwpisy\u002F2026\u002Ftabela-jak-arkusz",[155,2792],"javascript","tabela-jak-arkusz","edujmo8eNXdMY_lNu5Ek9lvHKKPKWsXBiIrQC36vKKk",1792530551498]