[{"data":1,"prerenderedAt":1090},["ShallowReactive",2],{"blog-\u002Fblog\u002F2026\u002Fnaglowki-agrestapp":3,"blog-project-\u002Fblog\u002F2026\u002Fnaglowki-agrestapp":950},{"id":4,"title":5,"art":6,"body":7,"date":934,"description":935,"draft":936,"extension":937,"image":938,"kind":939,"meta":940,"navigation":137,"ogImage":941,"path":942,"project":943,"seo":944,"slug":938,"stem":945,"tags":946,"updated":938,"__hash__":949},"blog_en\u002Fblog\u002F2026\u002Fnaglowki-agrestapp.md","AgrestApp headers: 36 px for a mouse, 44 px for a finger","\u002Fart\u002Fblog\u002Fnaglowki-agrestapp.svg",{"type":8,"value":9,"toc":926},"minimark",[10,14,19,34,43,50,57,60,345,348,351,359,363,366,373,459,466,470,473,504,507,607,615,619,630,637,794,801,809,816,820,823,891,898,902,919,922],[11,12,13],"p",{},"AgrestApp has two headers: one on the company website and one in the panel after logging in. Both worked, but each was built in a different way, and the sizes came from random pixels. On 15 October I rewrote both. It took 8 commits, 40 changed files and +1424 \u002F −923 lines, and the old panel header (643 lines in one file) was replaced by three smaller components. In the next two days I also fixed the icon colours. The screenshots below are from a local version with made-up data.",[15,16,18],"h2",{"id":17},"the-menu-on-the-company-website","The menu on the company website",[11,20,21,22,26,27,26,30,33],{},"The old menu only worked on mouse hover. It had one group, \"System Agrest\", with submenus inside a submenu: Industries, Solutions and even Pricing. Widths and gaps were hard-coded (",[23,24,25],"code",{},"w-[180px]",", ",[23,28,29],{},"gap-[14px]",[23,31,32],{},"gap-[30px]",").",[11,35,36],{},[37,38],"img",{"alt":39,"height":40,"src":41,"width":42},"The website menu bar – before the change on top, after at the bottom",184,"\u002Fblog\u002F2026\u002Fnaglowki-agrestapp\u002Fsite-header.webp",1400,[11,44,45,46,49],{},"The most important sections moved to the top level as separate groups, and Pricing became a normal link. The whole structure is in one list in ",[23,47,48],{},"mainNav.js",". Both the desktop bar and the phone menu are built from it, so a new item doesn't need changes in two places.",[11,51,52],{},[37,53],{"alt":54,"height":55,"src":56,"width":42},"The open \"System Agrest\" group – before the change on top (a submenu inside a submenu), after at the bottom",736,"\u002Fblog\u002F2026\u002Fnaglowki-agrestapp\u002Fsite-dropdown.webp",[11,58,59],{},"Instead of a separate list for each group, there is one panel. When you move between groups, it slides under the selected item and changes its width and height to fit the content. JavaScript calculates the position and size, and Tailwind only reads them from CSS variables:",[61,62,67],"pre",{"className":63,"code":64,"language":65,"meta":66,"style":66},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup>\n\u002F\u002F Panel position and size – calculated when a group opens\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!-- content of the active group -->\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n","vue","",[23,68,69,89,96,132,139,161,194,219,244,250,260,265,275,284,296,307,313,319,325,336],{"__ignoreMap":66},[70,71,74,78,82,86],"span",{"class":72,"line":73},"line",1,[70,75,77],{"class":76},"sVt8B","\u003C",[70,79,81],{"class":80},"s9eBZ","script",[70,83,85],{"class":84},"sScJk"," setup",[70,87,88],{"class":76},">\n",[70,90,92],{"class":72,"line":91},2,[70,93,95],{"class":94},"sqY6-","\u002F\u002F Panel position and size – calculated when a group opens\n",[70,97,99,103,107,110,113,116,119,122,124,127,129],{"class":72,"line":98},3,[70,100,102],{"class":101},"szBVR","const",[70,104,106],{"class":105},"sj4cs"," panelBox",[70,108,109],{"class":101}," =",[70,111,112],{"class":84}," ref",[70,114,115],{"class":76},"({ x: ",[70,117,118],{"class":105},"0",[70,120,121],{"class":76},", width: ",[70,123,118],{"class":105},[70,125,126],{"class":76},", height: ",[70,128,118],{"class":105},[70,130,131],{"class":76}," })\n",[70,133,135],{"class":72,"line":134},4,[70,136,138],{"emptyLinePlaceholder":137},true,"\n",[70,140,142,144,147,149,152,155,158],{"class":72,"line":141},5,[70,143,102],{"class":101},[70,145,146],{"class":105}," panelStyle",[70,148,109],{"class":101},[70,150,151],{"class":84}," computed",[70,153,154],{"class":76},"(() ",[70,156,157],{"class":101},"=>",[70,159,160],{"class":76}," ({\n",[70,162,164,168,171,174,177,180,183,185,188,191],{"class":72,"line":163},6,[70,165,167],{"class":166},"sZZnC","  '--panel-x'",[70,169,170],{"class":76},": ",[70,172,173],{"class":166},"`${",[70,175,176],{"class":76},"panelBox",[70,178,179],{"class":166},".",[70,181,182],{"class":76},"value",[70,184,179],{"class":166},[70,186,187],{"class":76},"x",[70,189,190],{"class":166},"}px`",[70,192,193],{"class":76},",\n",[70,195,197,200,202,204,206,208,210,212,215,217],{"class":72,"line":196},7,[70,198,199],{"class":166},"  '--panel-w'",[70,201,170],{"class":76},[70,203,173],{"class":166},[70,205,176],{"class":76},[70,207,179],{"class":166},[70,209,182],{"class":76},[70,211,179],{"class":166},[70,213,214],{"class":76},"width",[70,216,190],{"class":166},[70,218,193],{"class":76},[70,220,222,225,227,229,231,233,235,237,240,242],{"class":72,"line":221},8,[70,223,224],{"class":166},"  '--panel-h'",[70,226,170],{"class":76},[70,228,173],{"class":166},[70,230,176],{"class":76},[70,232,179],{"class":166},[70,234,182],{"class":76},[70,236,179],{"class":166},[70,238,239],{"class":76},"height",[70,241,190],{"class":166},[70,243,193],{"class":76},[70,245,247],{"class":72,"line":246},9,[70,248,249],{"class":76},"}))\n",[70,251,253,256,258],{"class":72,"line":252},10,[70,254,255],{"class":76},"\u003C\u002F",[70,257,81],{"class":80},[70,259,88],{"class":76},[70,261,263],{"class":72,"line":262},11,[70,264,138],{"emptyLinePlaceholder":137},[70,266,268,270,273],{"class":72,"line":267},12,[70,269,77],{"class":76},[70,271,272],{"class":80},"template",[70,274,88],{"class":76},[70,276,278,281],{"class":72,"line":277},13,[70,279,280],{"class":76},"  \u003C",[70,282,283],{"class":80},"div\n",[70,285,287,290,293],{"class":72,"line":286},14,[70,288,289],{"class":84},"    :style",[70,291,292],{"class":76},"=",[70,294,295],{"class":166},"\"panelStyle\"\n",[70,297,299,302,304],{"class":72,"line":298},15,[70,300,301],{"class":84},"    class",[70,303,292],{"class":76},[70,305,306],{"class":166},"\"absolute translate-x-(--panel-x) w-(--panel-w) h-(--panel-h)\n",[70,308,310],{"class":72,"line":309},16,[70,311,312],{"class":166},"           transition-[translate,width,height] duration-200\"\n",[70,314,316],{"class":72,"line":315},17,[70,317,318],{"class":76},"  >\n",[70,320,322],{"class":72,"line":321},18,[70,323,324],{"class":94},"    \u003C!-- content of the active group -->\n",[70,326,328,331,334],{"class":72,"line":327},19,[70,329,330],{"class":76},"  \u003C\u002F",[70,332,333],{"class":80},"div",[70,335,88],{"class":76},[70,337,339,341,343],{"class":72,"line":338},20,[70,340,255],{"class":76},[70,342,272],{"class":80},[70,344,88],{"class":76},[11,346,347],{},"The menu reacts to how you use it. A mouse opens a group on hover and closes it only 150 ms after leaving, so an imprecise movement doesn't close the panel. Touch toggles a group with a tap. The keyboard supports Enter, Esc and moving the focus out.",[11,349,350],{},"On the phone the groups are accordions, and the login buttons stay fixed at the bottom of the screen. Only the list scrolls.",[11,352,353],{},[37,354],{"alt":355,"height":356,"src":357,"width":358},"The phone menu – before the change on the left, after on the right",844,"\u002Fblog\u002F2026\u002Fnaglowki-agrestapp\u002Fsite-phone-menu.webp",796,[15,360,362],{"id":361},"one-size-rule-mouse-36-px-touch-44-px","One size rule: mouse 36 px, touch 44 px",[11,364,365],{},"In the panel, the icon buttons had different sizes: 30, 36, 40, and the avatar 56 px. Now there is one rule. With a mouse the buttons are about 36 px, because the cursor is precise and a denser layout is comfortable. With touch they are 44 px, which is the minimum from WCAG 2.5.5.",[11,367,368,369,372],{},"I didn't use breakpoints for this, but the ",[23,370,371],{},"pointer-coarse:"," variant from Tailwind v4. So the size depends on the pointing device, not on the screen width. A tablet with a mouse gets 36 px, and a laptop with a touch screen gets 44 px.",[61,374,378],{"className":375,"code":376,"language":377,"meta":66,"style":66},"language-html shiki shiki-themes github-light github-dark","\u003C!-- 18 px icon + padding: 36 px with a mouse, 44 px with touch -->\n\u003Cbutton class=\"p-2 pointer-coarse:p-3 [&_svg]:size-4.5\" aria-label=\"Notifications\">\n  \u003CBellIcon \u002F>\n\u003C\u002Fbutton>\n\n\u003C!-- menu item: height from padding, at least 44 px with touch -->\n\u003Ca class=\"px-3 py-2 pointer-coarse:min-h-11\">My company\u003C\u002Fa>\n","html",[23,379,380,385,410,421,429,433,438],{"__ignoreMap":66},[70,381,382],{"class":72,"line":73},[70,383,384],{"class":94},"\u003C!-- 18 px icon + padding: 36 px with a mouse, 44 px with touch -->\n",[70,386,387,389,392,395,397,400,403,405,408],{"class":72,"line":91},[70,388,77],{"class":76},[70,390,391],{"class":80},"button",[70,393,394],{"class":84}," class",[70,396,292],{"class":76},[70,398,399],{"class":166},"\"p-2 pointer-coarse:p-3 [&_svg]:size-4.5\"",[70,401,402],{"class":84}," aria-label",[70,404,292],{"class":76},[70,406,407],{"class":166},"\"Notifications\"",[70,409,88],{"class":76},[70,411,412,414,418],{"class":72,"line":98},[70,413,280],{"class":76},[70,415,417],{"class":416},"s7hpK","BellIcon",[70,419,420],{"class":76}," \u002F>\n",[70,422,423,425,427],{"class":72,"line":134},[70,424,255],{"class":76},[70,426,391],{"class":80},[70,428,88],{"class":76},[70,430,431],{"class":72,"line":141},[70,432,138],{"emptyLinePlaceholder":137},[70,434,435],{"class":72,"line":163},[70,436,437],{"class":94},"\u003C!-- menu item: height from padding, at least 44 px with touch -->\n",[70,439,440,442,445,447,449,452,455,457],{"class":72,"line":196},[70,441,77],{"class":76},[70,443,444],{"class":80},"a",[70,446,394],{"class":84},[70,448,292],{"class":76},[70,450,451],{"class":166},"\"px-3 py-2 pointer-coarse:min-h-11\"",[70,453,454],{"class":76},">My company\u003C\u002F",[70,456,444],{"class":80},[70,458,88],{"class":76},[11,460,461,462,465],{},"The same rule works in the account menu, in the website menu and in the search field (",[23,463,464],{},"h-9 pointer-coarse:h-11","). The menu button on the phone is always 44 × 44 px, so switching ☰ to × doesn't move the bar.",[15,467,469],{"id":468},"icons-in-one-colour","Icons in one colour",[11,471,472],{},"After the first version I saw that the icons in the header and in the account menu had different colours. Some took the text colour, and some took a grey from another place. So I set one rule:",[474,475,476,488,495,498],"ul",{},[477,478,479,480,483,484,487],"li",{},"At rest, every icon button in the bar and every icon in the account menu has the same secondary colour: ",[23,481,482],{},"fill-slate-500",", and ",[23,485,486],{},"fill-slate-400"," in dark mode.",[477,489,490,491,494],{},"When you hover an item in the account menu, the icon takes the text colour (",[23,492,493],{},"group-hover:fill-current","), so the text and the icon change together.",[477,496,497],{},"The button of the active page gets the solid version of the icon instead of only a background, for example the life ring on the help page.",[477,499,500,501,179],{},"The bell with a notification is a separate icon, not a red dot added with ",[23,502,503],{},"absolute",[11,505,506],{},"A small function chooses the icon variant. The same logic will work for the cart and other icons with a counter:",[61,508,512],{"className":509,"code":510,"language":511,"meta":66,"style":66},"language-js shiki shiki-themes github-light github-dark","\u002F\u002F One of four names: normal, solid, with a notification, with a notification and solid\nfunction notificationIcon(icon, notification, hasNotification, active) {\n  const name = hasNotification ? notification : icon\n  return active ? `${name}-solid` : name\n}\n","js",[23,513,514,519,552,577,602],{"__ignoreMap":66},[70,515,516],{"class":72,"line":73},[70,517,518],{"class":94},"\u002F\u002F One of four names: normal, solid, with a notification, with a notification and solid\n",[70,520,521,524,527,530,534,536,539,541,544,546,549],{"class":72,"line":91},[70,522,523],{"class":101},"function",[70,525,526],{"class":84}," notificationIcon",[70,528,529],{"class":76},"(",[70,531,533],{"class":532},"siNPP","icon",[70,535,26],{"class":76},[70,537,538],{"class":532},"notification",[70,540,26],{"class":76},[70,542,543],{"class":532},"hasNotification",[70,545,26],{"class":76},[70,547,548],{"class":532},"active",[70,550,551],{"class":76},") {\n",[70,553,554,557,560,562,565,568,571,574],{"class":72,"line":98},[70,555,556],{"class":101},"  const",[70,558,559],{"class":105}," name",[70,561,109],{"class":101},[70,563,564],{"class":76}," hasNotification ",[70,566,567],{"class":101},"?",[70,569,570],{"class":76}," notification ",[70,572,573],{"class":101},":",[70,575,576],{"class":76}," icon\n",[70,578,579,582,585,587,590,593,596,599],{"class":72,"line":134},[70,580,581],{"class":101},"  return",[70,583,584],{"class":76}," active ",[70,586,567],{"class":101},[70,588,589],{"class":166}," `${",[70,591,592],{"class":76},"name",[70,594,595],{"class":166},"}-solid`",[70,597,598],{"class":101}," :",[70,600,601],{"class":76}," name\n",[70,603,604],{"class":72,"line":141},[70,605,606],{"class":76},"}\n",[11,608,609],{},[37,610],{"alt":611,"height":612,"src":613,"width":614},"The account menu – before the change on the left, after on the right: icons in one colour",460,"\u002Fblog\u002F2026\u002Fnaglowki-agrestapp\u002Fpanel-account-menu.webp",1016,[15,616,618],{"id":617},"the-header-height-in-one-variable","The header height in one variable",[11,620,621,622,625,626,629],{},"A small change brought the most order. The sidebar and the sticky page bars in the panel sit under the header, so they need to know its height. Before, it was hard-coded: ",[23,623,624],{},"top-[75px]",", and when an administrator was logged in as another company and the \"Log in as\" bar appeared above the header, ",[23,627,628],{},"!top-[125px]",". The same condition was repeated in several files. One bar still hid 11 px under the header.",[11,631,632,633,636],{},"Now the \"Log in as\" bar is part of the header, and the header measures its own height and saves it in a CSS variable on the ",[23,634,635],{},"\u003Chtml>"," element:",[61,638,640],{"className":63,"code":639,"language":65,"meta":66,"style":66},"\u003Cscript setup>\nimport { useElementSize } from '@vueuse\u002Fcore'\n\nconst header = ref(null)\nconst { height } = useElementSize(header, undefined, { box: 'border-box' })\n\n\u002F\u002F Header height for the elements that sit under it\nwatch(height, (value) => {\n  document.documentElement.style.setProperty('--panel-header-height', `${value || 72}px`)\n}, { immediate: true })\n\u003C\u002Fscript>\n",[23,641,642,652,666,670,689,720,724,729,747,776,786],{"__ignoreMap":66},[70,643,644,646,648,650],{"class":72,"line":73},[70,645,77],{"class":76},[70,647,81],{"class":80},[70,649,85],{"class":84},[70,651,88],{"class":76},[70,653,654,657,660,663],{"class":72,"line":91},[70,655,656],{"class":101},"import",[70,658,659],{"class":76}," { useElementSize } ",[70,661,662],{"class":101},"from",[70,664,665],{"class":166}," '@vueuse\u002Fcore'\n",[70,667,668],{"class":72,"line":98},[70,669,138],{"emptyLinePlaceholder":137},[70,671,672,674,677,679,681,683,686],{"class":72,"line":134},[70,673,102],{"class":101},[70,675,676],{"class":105}," header",[70,678,109],{"class":101},[70,680,112],{"class":84},[70,682,529],{"class":76},[70,684,685],{"class":105},"null",[70,687,688],{"class":76},")\n",[70,690,691,693,696,698,701,703,706,709,712,715,718],{"class":72,"line":141},[70,692,102],{"class":101},[70,694,695],{"class":76}," { ",[70,697,239],{"class":105},[70,699,700],{"class":76}," } ",[70,702,292],{"class":101},[70,704,705],{"class":84}," useElementSize",[70,707,708],{"class":76},"(header, ",[70,710,711],{"class":105},"undefined",[70,713,714],{"class":76},", { box: ",[70,716,717],{"class":166},"'border-box'",[70,719,131],{"class":76},[70,721,722],{"class":72,"line":163},[70,723,138],{"emptyLinePlaceholder":137},[70,725,726],{"class":72,"line":196},[70,727,728],{"class":94},"\u002F\u002F Header height for the elements that sit under it\n",[70,730,731,734,737,739,742,744],{"class":72,"line":221},[70,732,733],{"class":84},"watch",[70,735,736],{"class":76},"(height, (",[70,738,182],{"class":532},[70,740,741],{"class":76},") ",[70,743,157],{"class":101},[70,745,746],{"class":76}," {\n",[70,748,749,752,755,757,760,762,764,766,769,772,774],{"class":72,"line":246},[70,750,751],{"class":76},"  document.documentElement.style.",[70,753,754],{"class":84},"setProperty",[70,756,529],{"class":76},[70,758,759],{"class":166},"'--panel-header-height'",[70,761,26],{"class":76},[70,763,173],{"class":166},[70,765,182],{"class":76},[70,767,768],{"class":101}," ||",[70,770,771],{"class":105}," 72",[70,773,190],{"class":166},[70,775,688],{"class":76},[70,777,778,781,784],{"class":72,"line":252},[70,779,780],{"class":76},"}, { immediate: ",[70,782,783],{"class":105},"true",[70,785,131],{"class":76},[70,787,788,790,792],{"class":72,"line":262},[70,789,255],{"class":76},[70,791,81],{"class":80},[70,793,88],{"class":76},[11,795,796,797,800],{},"Every element under the header now has the same class, ",[23,798,799],{},"top-(--panel-header-height)",". It doesn't need to know if the \"Log in as\" bar is visible or how tall the header is on a given screen.",[11,802,803],{},[37,804],{"alt":805,"height":806,"src":807,"width":808},"The panel header – before the change on top (75 px), after at the bottom (72 px)",122,"\u002Fblog\u002F2026\u002Fnaglowki-agrestapp\u002Fpanel-header.webp",1440,[11,810,811],{},[37,812],{"alt":813,"height":814,"src":815,"width":358},"The panel on a phone with the sidebar open – before the change on the left, after on the right: only the logo mark and a close button",560,"\u002Fblog\u002F2026\u002Fnaglowki-agrestapp\u002Fpanel-phone.webp",[15,817,819],{"id":818},"scale-values-instead-of-pixels-in-brackets","Scale values instead of pixels in brackets",[11,821,822],{},"Tailwind v4 accepts any multiple of 0.25rem, so most bracket values could become normal classes:",[824,825,826,839],"table",{},[827,828,829],"thead",{},[830,831,832,836],"tr",{},[833,834,835],"th",{},"Before",[833,837,838],{},"After",[840,841,842,856,869,881],"tbody",{},[830,843,844,850],{},[845,846,847],"td",{},[23,848,849],{},"h-[75px]",[845,851,852,855],{},[23,853,854],{},"h-18"," (72 px)",[830,857,858,863],{},[845,859,860],{},[23,861,862],{},"w-[239px]",[845,864,865,868],{},[23,866,867],{},"w-60"," (240 px)",[830,870,871,876],{},[845,872,873],{},[23,874,875],{},"py-[22px]",[845,877,878],{},[23,879,880],{},"py-5.5",[830,882,883,888],{},[845,884,885],{},[23,886,887],{},"dark:bg-[#161e30]",[845,889,890],{},"a colour from the project palette",[11,892,893,894,897],{},"In the website bar one gap needed a fluid value. Between the logo, the menu and the buttons there is a ",[23,895,896],{},"clamp()",", from 16 px at 1024 px width to 84 px at 1440 px. Thanks to this, the whole menu fits from 1024 px, without extra breakpoints.",[15,899,901],{"id":900},"what-i-got-from-it","What I got from it",[474,903,904,907,910,913,916],{},[477,905,906],{},"One menu list for desktop and phone.",[477,908,909],{},"One size rule (36 \u002F 44 px) instead of several random ones.",[477,911,912],{},"One icon colour at rest and a solid icon for the active page.",[477,914,915],{},"One variable with the header height instead of the numbers 75 and 125 in several files.",[477,917,918],{},"A menu that works with a mouse, touch and the keyboard.",[11,920,921],{},"The website dropdown has the same spacing as the account menu in the panel. Both parts now look like one system, even though they are separate apps.",[923,924,925],"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":66,"searchDepth":91,"depth":91,"links":927},[928,929,930,931,932,933],{"id":17,"depth":91,"text":18},{"id":361,"depth":91,"text":362},{"id":468,"depth":91,"text":469},{"id":617,"depth":91,"text":618},{"id":818,"depth":91,"text":819},{"id":900,"depth":91,"text":901},"2026-10-17","How I rewrote the AgrestApp website and panel headers in one day – one sliding menu, sizes that depend on how you use the page, and the header height in a CSS variable instead of numbers written in several files.",false,"md",null,"case-study",{},"\u002Fog\u002Fblog\u002Fnaglowki-agrestapp-en.jpg","\u002Fblog\u002F2026\u002Fnaglowki-agrestapp","\u002Fprojects\u002F2026\u002Fagrestapp",{"title":5,"description":935},"blog\u002F2026\u002Fnaglowki-agrestapp",[947,65,948],"tailwind","accessibility","2abktkD9t8jtopMmT4E6-POZoZMYZHOEHuAvLqSV-58",{"id":951,"title":952,"agency":953,"body":954,"compare":938,"cover":1017,"date":1022,"description":1023,"draft":936,"extension":937,"facts":1024,"featured":936,"gallery":1038,"galleryLead":1059,"highlights":1060,"image":1076,"links":1077,"meta":1079,"navigation":137,"note":1080,"ogImage":1081,"path":943,"seo":1082,"stack":1083,"stem":1087,"tags":1088,"__hash__":1089},"projects_en\u002Fprojects\u002F2026\u002Fagrestapp\u002Findex.md","AgrestApp","Pesi",{"type":8,"value":955,"toc":1014},[956,959,963],[11,957,958],{},"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.",[15,960,962],{"id":961},"what-i-did","What I did",[474,964,965,972,978,984,990,996,1002,1008],{},[477,966,967,971],{},[968,969,970],"strong",{},"Panel header"," – a new header with the \"Log in as\" bar, an account menu and a shared dropdown component",[477,973,974,977],{},[968,975,976],{},"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",[477,979,980,983],{},[968,981,982],{},"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",[477,985,986,989],{},[968,987,988],{},"Analytics"," – the main chart, the date range picker with a compare mode, top suppliers, the savings summary and product profitability",[477,991,992,995],{},[968,993,994],{},"Supplier offer management"," – extending an existing screen: the individual price dialog, adding items and moving between fields with Enter",[477,997,998,1001],{},[968,999,1000],{},"Range selection"," – Shift-click in 9 lists in the panel (raw materials, products, prices, notifications and more)",[477,1003,1004,1007],{},[968,1005,1006],{},"Shop"," (now closed) – a basket with each supplier's minimum order, buying without an account, order history and complaints",[477,1009,1010,1013],{},[968,1011,1012],{},"Migration to Tailwind CSS v4"," for the whole project",{"title":66,"searchDepth":91,"depth":91,"links":1015},[1016],{"id":961,"depth":91,"text":962},{"desktop":1018,"phone":1019,"width":808,"height":1020,"alt":1021},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Fhome-top.webp","\u002Fprojects\u002F2026\u002Fagrestapp\u002Fhome-phone-top.webp",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.",[1025,1029,1032,1035],{"value":1026,"suffix":1027,"text":1028},"12"," mo.","in the project – since October 2025",{"value":1030,"text":1031},"69","icons as Vue components in the new UI set",{"value":1033,"text":1034},"4","industry pages – bakeries, confectioneries, ice cream shops and HoReCa – built from the same sections",{"value":1036,"text":1037},"296","classes in templates left unchanged when moving to Tailwind v4",[1039,1043,1047,1051,1055],{"src":1040,"width":808,"height":1020,"alt":1041,"caption":1042},"\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":1044,"full":137,"alt":1045,"caption":1046},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Fhome-full.webp","AgrestApp home page","Home page – from the first screen to the newsletter",{"src":1048,"full":137,"alt":1049,"caption":1050},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Fabout-system-full.webp","The \"About the system\" page","\"About the system\" – sections describing the modules",{"src":1052,"full":137,"alt":1053,"caption":1054},"\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":1056,"full":137,"alt":1057,"caption":1058},"\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.",[1061,1064,1067,1070,1073],{"title":1062,"text":1063},"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":1065,"text":1066},"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":1068,"text":1069},"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":1071,"text":1072},"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":1074,"text":1075},"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":1078},"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":952,"description":1023},[1084,1085,1086],"Vue 3","Inertia","Tailwind CSS v4","projects\u002F2026\u002Fagrestapp\u002Findex",[65],"b4XUsEjdYl75DhF_L-GLaNcD-DFiu_gZ517SGy9jGjM",1792358719678]