[{"data":1,"prerenderedAt":969},["ShallowReactive",2],{"blog-\u002Fblog\u002F2026\u002Fmigracja-na-tailwind-v4":3,"blog-project-\u002Fblog\u002F2026\u002Fmigracja-na-tailwind-v4":829},{"id":4,"title":5,"art":6,"body":7,"date":814,"description":815,"draft":816,"extension":817,"image":818,"kind":819,"meta":820,"navigation":81,"ogImage":821,"path":822,"project":823,"seo":824,"slug":818,"stem":825,"tags":826,"updated":818,"__hash__":828},"blog_en\u002Fblog\u002F2026\u002Fmigracja-na-tailwind-v4.md","Tailwind v4 in a big project: 76 files instead of 339","\u002Fart\u002Fblog\u002Fmigracja-na-tailwind-v4.svg",{"type":8,"value":9,"toc":807},"minimark",[10,19,26,31,34,272,287,290,331,335,338,503,510,514,520,539,550,688,691,724,734,738,741,744,765,772,776,804],[11,12,13,14,18],"p",{},"AgrestApp was on Tailwind 3, and quite a few things didn't work as they should. At some point I got an audit with a huge list of changes: about 250 buttons to fix, tables and much more. The list said, among other things, that the buttons had too small text and the form fields had 14 px text, so an iPhone zooms in when you tap a field. I suggested making these elements bigger on touch screens with the ",[15,16,17],"code",{},"pointer-coarse:"," variant – something new in Tailwind v4. I warned that it would take time: I could fix everything by eye, or make the changes and clean up the app along the way. I got the go-ahead for the newer Tailwind and for a slow clean-up – e.g. replacing 3–4 buttons from separate components with one.",[11,20,21,22,25],{},"On 26 August I moved AgrestApp from Tailwind 3.4.1 to 4.3.3. It's a big project: the JavaScript config has about 200 colours and 11 breakpoints, and the templates have over 1,500 classes with an exclamation mark (",[15,23,24],{},"!bg-…","). The automatic migration tool suggested changes in 339 files and 1,710 lines. In the end 76 files changed, and most of the changed lines were in two lock files.",[27,28,30],"h2",{"id":29},"the-rule-dont-rewrite-the-templates","The rule: don't rewrite the templates",[11,32,33],{},"Tailwind v4 changed a few default values. I could fix every class in the templates or bring the old values back in one place. I chose the second option. The classes in the templates stayed as they were, and I described the differences between the versions in 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 still reads the JS config – colours and breakpoints stay the same *\u002F\n@config \"..\u002F..\u002Ftailwind.config.cjs\";\n\n\u002F* v4 rescaled these three values – I bring back the ones from 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 changed the default border colour to 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 still reads the JS config – colours and breakpoints stay the same *\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 rescaled these three values – I bring back the ones from 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 changed the default border colour to 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],{},"These three values cover 296 classes in the templates: ",[15,276,277],{},"shadow-sm"," (226), ",[15,280,281],{},"rounded-sm"," (52) and plain ",[15,284,285],{},"ring"," (18). After the change they look exactly like in v3, and I didn't change a single character of them in the templates.",[11,288,289],{},"Two things in the dependencies only showed up when I compared the results:",[291,292,293,313],"ul",{},[294,295,296,300,301,304,305,308,309,312],"li",{},[297,298,299],"strong",{},"The forms plugin."," ",[15,302,303],{},"@tailwindcss\u002Fforms"," 0.5.11 lowers the specificity of its selectors (",[15,306,307],{},"input:where([type='text'])"," instead of ",[15,310,311],{},"[type='text']","). After the update this moved the padding of every form field. I pinned version 0.5.7, which works as before.",[294,314,315,318,319,322,323,326,327,330],{},[297,316,317],{},"Node 20."," The new Tailwind engine needs Node 20 or newer, and on an older version the install fails with an error about a binary, not about Tailwind. I added ",[15,320,321],{},"engines"," to ",[15,324,325],{},"package.json"," and an ",[15,328,329],{},".nvmrc"," file.",[27,332,334],{"id":333},"three-changes-v4-needs","Three changes v4 needs",[11,336,337],{},"Some things couldn't be brought back in CSS. I had to change these in the templates:",[339,340,341,409,487],"ol",{},[294,342,343,349,350,215,353,356,357],{},[297,344,345,348],{},[15,346,347],{},"*-opacity-*"," classes"," (",[15,351,352],{},"bg-opacity-50",[15,354,355],{},"ring-opacity-5"," and so on) don't exist in v4 and generate nothing. I changed them to the slash syntax – 49 places in 36 files. Without this, the overlays under modals would lose their dimming, and a dropdown would get a black ring instead of an almost transparent one.",[35,358,362],{"className":359,"code":360,"language":361,"meta":40,"style":40},"language-html shiki shiki-themes github-light github-dark","\u003C!-- before -->\n\u003Cdiv class=\"bg-black bg-opacity-50\">\n\u003C!-- after -->\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!-- before -->\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!-- after -->\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,421,422,424,425,428,429],{},[297,412,413,416,417,420],{},[15,414,415],{},"@reference"," in ",[15,418,419],{},"\u003Cstyle>"," blocks."," In v4 every ",[15,423,419],{}," block in a Vue component is compiled on its own and doesn't know the project's colours or classes, so ",[15,426,427],{},"@apply"," fails. I added one line in 32 files:",[35,430,434],{"className":431,"code":432,"language":433,"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,435,436,448,455,459,466,474,478],{"__ignoreMap":40},[44,437,438,440,443,446],{"class":46,"line":47},[44,439,373],{"class":58},[44,441,442],{"class":211},"style",[44,444,445],{"class":218}," scoped",[44,447,388],{"class":58},[44,449,450,452],{"class":46,"line":62},[44,451,415],{"class":50},[44,453,454],{"class":58}," \"..\u002F..\u002Fcss\u002Fapp.css\";\n",[44,456,457],{"class":46,"line":69},[44,458,82],{"emptyLinePlaceholder":81},[44,460,461,464],{"class":46,"line":78},[44,462,463],{"class":218},".badge",[44,465,97],{"class":58},[44,467,468,471],{"class":46,"line":85},[44,469,470],{"class":50},"  @apply",[44,472,473],{"class":58}," rounded-sm bg-brand-1-100 px-2;\n",[44,475,476],{"class":46,"line":91},[44,477,183],{"class":58},[44,479,480,483,485],{"class":46,"line":100},[44,481,482],{"class":58},"\u003C\u002F",[44,484,442],{"class":211},[44,486,388],{"class":58},[294,488,489,495,496,499,500,502],{},[297,490,491,494],{},[15,492,493],{},"@apply … !important","."," v4 doesn't accept ",[15,497,498],{},"!important"," at the end of ",[15,501,427],{},". In one place I moved the exclamation mark to the single classes.",[11,504,505,506,509],{},"The ",[15,507,508],{},"!"," prefix before a class, on the other hand, still works in v4, so over 1,500 of those classes stayed untouched.",[27,511,513],{"id":512},"the-hardest-part-css-layers","The hardest part: CSS layers",[11,515,516,517,519],{},"The cascade took the most time. In v4 Tailwind's classes live in layers (",[15,518,200],{},"), and CSS outside a layer always wins over them, whatever the specificity. v3 didn't work like this, so rules that had been losing for years suddenly started to win:",[291,521,522,533],{},[294,523,524,525,528,529,532],{},"a ",[15,526,527],{},"label { … }"," rule with a margin overrode the ",[15,530,531],{},"mb-7.5"," class on a label,",[294,534,535,538],{},[15,536,537],{},"input { height: 36px }"," stretched checkboxes and radio buttons to 36 px.",[11,540,541,542,545,546,549],{},"The fix was a separate layer for element rules. It sits between ",[15,543,544],{},"base"," and ",[15,547,548],{},"components",", so it wins over the reset and the forms plugin but loses to the classes – just like in v3:",[35,551,553],{"className":37,"code":552,"language":39,"meta":40,"style":40},"\u002F* layer order – it has to be at the very top of the stylesheet *\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* checkboxes and radio buttons get their size from the forms plugin *\u002F\n  input:not([type=\"checkbox\"], [type=\"radio\"]) {\n    height: 36px;\n  }\n}\n",[15,554,555,560,587,591,600,607,621,625,629,634,666,680,684],{"__ignoreMap":40},[44,556,557],{"class":46,"line":47},[44,558,559],{"class":65},"\u002F* layer order – it has to be at the very top of the stylesheet *\u002F\n",[44,561,562,564,567,569,571,573,576,578,580,582,585],{"class":46,"line":62},[44,563,200],{"class":50},[44,565,566],{"class":103}," theme",[44,568,215],{"class":58},[44,570,544],{"class":103},[44,572,215],{"class":58},[44,574,575],{"class":103},"app-base",[44,577,215],{"class":58},[44,579,548],{"class":103},[44,581,215],{"class":58},[44,583,584],{"class":103},"utilities",[44,586,59],{"class":58},[44,588,589],{"class":46,"line":69},[44,590,82],{"emptyLinePlaceholder":81},[44,592,593,595,598],{"class":46,"line":78},[44,594,200],{"class":50},[44,596,597],{"class":103}," app-base",[44,599,97],{"class":58},[44,601,602,605],{"class":46,"line":85},[44,603,604],{"class":211},"  label",[44,606,97],{"class":58},[44,608,609,612,614,617,619],{"class":46,"line":91},[44,610,611],{"class":110},"    margin-bottom",[44,613,107],{"class":58},[44,615,616],{"class":110},"0.375",[44,618,160],{"class":50},[44,620,59],{"class":58},[44,622,623],{"class":46,"line":100},[44,624,266],{"class":58},[44,626,627],{"class":46,"line":149},[44,628,82],{"emptyLinePlaceholder":81},[44,630,631],{"class":46,"line":165},[44,632,633],{"class":65},"  \u002F* checkboxes and radio buttons get their size from the forms plugin *\u002F\n",[44,635,636,639,642,645,648,650,653,656,658,660,663],{"class":46,"line":180},[44,637,638],{"class":211},"  input",[44,640,641],{"class":218},":not",[44,643,644],{"class":58},"([",[44,646,647],{"class":218},"type",[44,649,382],{"class":50},[44,651,652],{"class":54},"\"checkbox\"",[44,654,655],{"class":58},"], [",[44,657,647],{"class":218},[44,659,382],{"class":50},[44,661,662],{"class":54},"\"radio\"",[44,664,665],{"class":58},"]) {\n",[44,667,668,671,673,676,678],{"class":46,"line":186},[44,669,670],{"class":110},"    height",[44,672,107],{"class":58},[44,674,675],{"class":110},"36",[44,677,117],{"class":50},[44,679,59],{"class":58},[44,681,682],{"class":46,"line":191},[44,683,266],{"class":58},[44,685,686],{"class":46,"line":197},[44,687,183],{"class":58},[11,689,690],{},"Two more differences only showed up when I clicked through the app:",[291,692,693,710],{},[294,694,695,698,699,702,703,706,707,494],{},[297,696,697],{},"Modals hid under their own backdrop."," In v3 the ",[15,700,701],{},"transform"," class added an empty transform, which created a new stacking context. In v4 the same class gives ",[15,704,705],{},"transform: none"," when no transform is set. After the enter animation the modal fell under the backdrop, and a click on the modal closed it. This affected 33 files. One rule was enough: ",[15,708,709],{},".transform { transform: translate(0, 0); }",[294,711,712,715,716,719,720,723],{},[297,713,714],{},"The gradient disappeared from the first dashboard tile."," A custom ",[15,717,718],{},"bg-gradient-\u003Cangle>"," plugin didn't set the ",[15,721,722],{},"--tw-gradient-position"," variable that v4 uses to build gradients. The whole value was invalid, so the tile had no background, just white text.",[11,725,726,727,730,731,494],{},"I also fixed the ",[15,728,729],{},"container"," class. v4 no longer reads the container's centring and padding from the JS config, so I added them in CSS with ",[15,732,733],{},"@utility container",[27,735,737],{"id":736},"how-i-checked-that-nothing-changed","How I checked that nothing changed",[11,739,740],{},"Clicking through pages isn't enough – it's easy to miss a margin on one subpage. So I built the CSS from the branch with v3 and from the branch with v4 and compared both results: 90 stylesheets against 90, class by class and value by value.",[11,742,743],{},"The result:",[291,745,746,749,752],{},[294,747,748],{},"no class used in the templates is missing from the final CSS,",[294,750,751],{},"almost all differences are just a different notation of the same value,",[294,753,754,755,308,758,761,762,764],{},"two differences are on purpose: v4's default palette is in ",[15,756,757],{},"oklch",[15,759,760],{},"rgb",", and ",[15,763,729],{}," has one extra width step from one of the breakpoints.",[11,766,767,768,771],{},"Finally, I left a note for the team: after pulling the changes you have to run ",[15,769,770],{},"npm ci",", because the dev server won't start without the new packages.",[27,773,775],{"id":774},"what-came-out-of-it","What came out of it",[291,777,778,784,790,793],{},[294,779,780,781,783],{},"A migration without rewriting the templates – the old values in ",[15,782,94],{}," and three changes that v4 needs.",[294,785,786,787,789],{},"A separate ",[15,788,575],{}," layer for element rules, so the cascade works like in v3.",[294,791,792],{},"Comparing the final CSS instead of clicking through pages – it shows for sure that nothing went missing.",[294,794,795,796,798,799,494],{},"The project is ready for ",[15,797,17],{},", which started it all – I used it e.g. in the ",[800,801,803],"a",{"href":802},"\u002Fblog\u002F2026\u002Fnaglowki-agrestapp","AgrestApp headers",[442,805,806],{},"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":808},[809,810,811,812,813],{"id":29,"depth":62,"text":30},{"id":333,"depth":62,"text":334},{"id":512,"depth":62,"text":513},{"id":736,"depth":62,"text":737},{"id":774,"depth":62,"text":775},"2026-08-29","How I moved AgrestApp from Tailwind 3 to 4 without rewriting the templates – the old values in @theme, three class changes and order in the CSS layers.",false,"md",null,"case-study",{},"\u002Fog\u002Fblog\u002Fmigracja-na-tailwind-v4-en.jpg","\u002Fblog\u002F2026\u002Fmigracja-na-tailwind-v4","\u002Fprojects\u002F2026\u002Fagrestapp",{"title":5,"description":815},"blog\u002F2026\u002Fmigracja-na-tailwind-v4",[827,39],"tailwind","Ll9Ocf90OP87MytFATLA2MEHRn9_z2AyVD-gFEi57J4",{"id":830,"title":831,"agency":832,"body":833,"compare":818,"cover":895,"date":901,"description":902,"draft":816,"extension":817,"facts":903,"featured":816,"gallery":917,"galleryLead":938,"highlights":939,"image":955,"links":956,"meta":958,"navigation":81,"note":959,"ogImage":960,"path":823,"seo":961,"stack":962,"stem":966,"tags":967,"__hash__":968},"projects_en\u002Fprojects\u002F2026\u002Fagrestapp\u002Findex.md","AgrestApp","Pesi",{"type":8,"value":834,"toc":892},[835,838,842],[11,836,837],{},"AgrestApp is a purchasing system for food businesses: orders and tenders for raw materials, price monitoring, supplier offers and savings analytics. I've been working on it in the Pesi team since October 2025. Several people build the front-end in Vue 3 and Inertia, so below I only describe the parts where the front-end is mine. What I like most is that I can write many things my own way. I change the app in small steps: a new table here, a new button there, a header somewhere else. Each new element replaces a few older versions, so over time the project has fewer and fewer unnecessary components.",[27,839,841],{"id":840},"what-i-did","What I did",[291,843,844,850,856,862,868,874,880,886],{},[294,845,846,849],{},[297,847,848],{},"Panel header"," – a new header with the \"Log in as\" bar, an account menu and a shared dropdown component",[294,851,852,855],{},[297,853,854],{},"UI component set"," – buttons, an accordion, tooltips, number fields and 69 icons as Vue components; moving the app's buttons to one component and a shared theme toggle",[294,857,858,861],{},[297,859,860],{},"Company website"," – the \"About the system\" page, a set of sections used to build the pages for bakeries, confectioneries, ice cream shops and HoReCa, price tiles and a pricing page with a payment toggle",[294,863,864,867],{},[297,865,866],{},"Analytics"," – the main chart, the date range picker with a compare mode, top suppliers, the savings summary and product profitability",[294,869,870,873],{},[297,871,872],{},"Supplier offer management"," – extending an existing screen: the individual price dialog, adding items and moving between fields with Enter",[294,875,876,879],{},[297,877,878],{},"Range selection"," – Shift-click in 9 lists in the panel (raw materials, products, prices, notifications and more)",[294,881,882,885],{},[297,883,884],{},"Shop"," (now closed) – a basket with each supplier's minimum order, buying without an account, order history and complaints",[294,887,888,891],{},[297,889,890],{},"Migration to Tailwind CSS v4"," for the whole project",{"title":40,"searchDepth":62,"depth":62,"links":893},[894],{"id":840,"depth":62,"text":841},{"desktop":896,"phone":897,"width":898,"height":899,"alt":900},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Fhome-top.webp","\u002Fprojects\u002F2026\u002Fagrestapp\u002Fhome-phone-top.webp",1440,900,"AgrestApp – the first screen of the home page","2026-10-01","A purchasing system for bakeries, confectioneries, ice cream shops and HoReCa. In the Pesi team I work on the front-end: UI components, the company website, analytics and supplier offer management.",[904,908,911,914],{"value":905,"suffix":906,"text":907},"12"," mo.","in the project – since October 2025",{"value":909,"text":910},"69","icons as Vue components in the new UI set",{"value":912,"text":913},"4","industry pages – bakeries, confectioneries, ice cream shops and HoReCa – built from the same sections",{"value":915,"text":916},"296","classes in templates left unchanged when moving to Tailwind v4",[918,922,926,930,934],{"src":919,"width":898,"height":899,"alt":920,"caption":921},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Fanalytics-top.webp","Analytics in compare mode","Analytics – a date range with a slider and fields, two periods compared, savings and a chart (made-up data)",{"src":923,"full":81,"alt":924,"caption":925},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Fhome-full.webp","AgrestApp home page","Home page – from the first screen to the newsletter",{"src":927,"full":81,"alt":928,"caption":929},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Fabout-system-full.webp","The \"About the system\" page","\"About the system\" – sections describing the modules",{"src":931,"full":81,"alt":932,"caption":933},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Findustry-ice-cream-full.webp","The page for ice cream shops","The industry page for ice cream shops – built from the same sections as the others",{"src":935,"full":81,"alt":936,"caption":937},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Findustry-confectionery-full.webp","The page for confectioneries","The industry page for confectioneries","The company website and the app panel – analytics with made-up data. Click a screenshot to enlarge it.",[940,943,946,949,952],{"title":941,"text":942},"A panel header without hard-coded numbers","I rebuilt the panel header. The \"Log in as\" bar is now part of it, and the header measures its own height and saves it in a CSS variable. The sidebar and the sticky page bars use this variable instead of 75 and 125 px written in several files. You can use the account menu with the keyboard and close it with Esc.",{"title":944,"text":945},"One date range in three places","In analytics, the slider, the date fields and the page address show the same range. A change in one place updates the others right away, and the data reloads only a moment after the last change. The calendar won't let you set the start after the end, and the default range starts on the day the company registered.",{"title":947,"text":948},"A price table like a spreadsheet","In supplier offer management, Enter moves to the same field in the row below, and Shift + Enter to the row above. The field's value is already selected, ready to overwrite. In 9 lists in the panel, Shift-click selects a whole range of rows, like in an email inbox.",{"title":950,"text":951},"An individual price without mistakes","The individual price dialog checks the minimum net price, doesn't accept zero or an empty value, and suggests a default value for a single item. The price turns green when it's lower than or equal to the lowest price.",{"title":953,"text":954},"Tailwind v4 without rewriting templates","Instead of changing hundreds of classes, I brought back three v3 values in the config. Then I compared the output CSS with the previous version, went through the pages and fixed regressions the build didn't report – for example dialogs that hid under their own backdrop after the migration.","\u002Fprojects\u002F2026\u002Fagrestapp\u002Fthumbnail.webp",{"live":957},"https:\u002F\u002Fagrestapp.pl",{},"I work in a team – I only describe the parts where the front-end is mine. The app panel is not public.","\u002Fog\u002Fprojects\u002Fagrestapp-en.jpg",{"title":831,"description":902},[963,964,965],"Vue 3","Inertia","Tailwind CSS v4","projects\u002F2026\u002Fagrestapp\u002Findex",[433],"b4XUsEjdYl75DhF_L-GLaNcD-DFiu_gZ517SGy9jGjM",1792358719708]