[{"data":1,"prerenderedAt":1179},["ShallowReactive",2],{"blog-\u002Fpl\u002Fblog\u002F2026\u002Ftabela-jak-arkusz":3,"blog-project-\u002Fpl\u002Fblog\u002F2026\u002Ftabela-jak-arkusz":1038},{"id":4,"title":5,"art":6,"body":7,"date":1023,"description":1024,"draft":1025,"extension":1026,"image":1027,"kind":1028,"meta":1029,"navigation":108,"ogImage":1030,"path":1031,"project":1032,"seo":1033,"slug":1027,"stem":1034,"tags":1035,"updated":1027,"__hash__":1037},"blog_pl\u002Fpl\u002Fblog\u002F2026\u002Ftabela-jak-arkusz.md","Tabela jak arkusz: Shift + klik i Enter","\u002Fart\u002Fblog\u002Ftabela-jak-arkusz.svg",{"type":8,"value":9,"toc":1018},"minimark",[10,14,19,22,33,42,45,71,74,564,666,675,679,682,689,696,992,995,999,1014],[11,12,13],"p",{},"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,16,18],"h2",{"id":17},"shift-klik-zaznaczanie-zakresu","Shift + klik: zaznaczanie zakresu",[11,20,21],{},"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,23,24,25,29,30,32],{},"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 ",[26,27,28],"code",{},"id"," zaznaczonych wierszy – zależnie od miejsca przekazuję mu albo ",[26,31,28],{},", albo cały obiekt i dopiero potem przygotowuję dane do wysłania.",[11,34,35],{},[36,37],"img",{"alt":38,"height":39,"src":40,"width":41},"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,43,44],{},"Najprostsza wersja ma trzy pułapki:",[46,47,48,59,65],"ol",{},[49,50,51,55,56,58],"li",{},[52,53,54],"strong",{},"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ć ",[26,57,28],{}," wiersza, a jego pozycję szukać dopiero przy kliknięciu.",[49,60,61,64],{},[52,62,63],{},"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.",[49,66,67,70],{},[52,68,69],{},"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,72,73],{},"Uproszczona wersja jako composable, bez tych pułapek:",[75,76,81],"pre",{"className":77,"code":78,"language":79,"meta":80,"style":80},"language-js shiki shiki-themes github-light github-dark","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","js","",[26,82,83,103,110,145,171,178,198,203,224,238,260,279,316,345,350,378,427,436,441,459,480,495,509,515,527,538,544,549,558],{"__ignoreMap":80},[84,85,88,92,96,99],"span",{"class":86,"line":87},"line",1,[84,89,91],{"class":90},"szBVR","import",[84,93,95],{"class":94},"sVt8B"," { ref, toValue } ",[84,97,98],{"class":90},"from",[84,100,102],{"class":101},"sZZnC"," 'vue'\n",[84,104,106],{"class":86,"line":105},2,[84,107,109],{"emptyLinePlaceholder":108},true,"\n",[84,111,113,116,119,123,126,130,133,136,139,142],{"class":86,"line":112},3,[84,114,115],{"class":90},"export",[84,117,118],{"class":90}," function",[84,120,122],{"class":121},"sScJk"," useRangeSelection",[84,124,125],{"class":94},"(",[84,127,129],{"class":128},"siNPP","rows",[84,131,132],{"class":94},", ",[84,134,135],{"class":128},"key",[84,137,138],{"class":90}," =",[84,140,141],{"class":101}," 'id'",[84,143,144],{"class":94},") {\n",[84,146,148,151,155,157,160,162,165,168],{"class":86,"line":147},4,[84,149,150],{"class":90},"  const",[84,152,154],{"class":153},"sj4cs"," selected",[84,156,138],{"class":90},[84,158,159],{"class":121}," ref",[84,161,125],{"class":94},[84,163,164],{"class":90},"new",[84,166,167],{"class":121}," Set",[84,169,170],{"class":94},"())\n",[84,172,174],{"class":86,"line":173},5,[84,175,177],{"class":176},"sqY6-","  \u002F\u002F klucz ostatnio klikniętego wiersza – nie jego numer\n",[84,179,181,183,186,188,190,192,195],{"class":86,"line":180},6,[84,182,150],{"class":90},[84,184,185],{"class":153}," anchor",[84,187,138],{"class":90},[84,189,159],{"class":121},[84,191,125],{"class":94},[84,193,194],{"class":153},"null",[84,196,197],{"class":94},")\n",[84,199,201],{"class":86,"line":200},7,[84,202,109],{"emptyLinePlaceholder":108},[84,204,206,209,212,214,217,219,222],{"class":86,"line":205},8,[84,207,208],{"class":90},"  function",[84,210,211],{"class":121}," toggle",[84,213,125],{"class":94},[84,215,216],{"class":128},"row",[84,218,132],{"class":94},[84,220,221],{"class":128},"event",[84,223,144],{"class":94},[84,225,227,230,233,235],{"class":86,"line":226},9,[84,228,229],{"class":90},"    const",[84,231,232],{"class":153}," id",[84,234,138],{"class":90},[84,236,237],{"class":94}," row[key]\n",[84,239,241,243,246,248,251,254,257],{"class":86,"line":240},10,[84,242,229],{"class":90},[84,244,245],{"class":153}," select",[84,247,138],{"class":90},[84,249,250],{"class":90}," !",[84,252,253],{"class":94},"selected.value.",[84,255,256],{"class":121},"has",[84,258,259],{"class":94},"(id)\n",[84,261,263,265,268,270,273,276],{"class":86,"line":262},11,[84,264,229],{"class":90},[84,266,267],{"class":153}," list",[84,269,138],{"class":90},[84,271,272],{"class":121}," toValue",[84,274,275],{"class":94},"(rows) ",[84,277,278],{"class":176},"\u002F\u002F wiersze w kolejności, którą widać na ekranie\n",[84,280,282,284,287,289,292,295,298,301,304,307,310,313],{"class":86,"line":281},12,[84,283,229],{"class":90},[84,285,286],{"class":153}," from",[84,288,138],{"class":90},[84,290,291],{"class":94}," list.",[84,293,294],{"class":121},"findIndex",[84,296,297],{"class":94},"((",[84,299,300],{"class":128},"r",[84,302,303],{"class":94},") ",[84,305,306],{"class":90},"=>",[84,308,309],{"class":94}," r[key] ",[84,311,312],{"class":90},"===",[84,314,315],{"class":94}," anchor.value)\n",[84,317,319,321,324,326,328,330,332,334,336,338,340,342],{"class":86,"line":318},13,[84,320,229],{"class":90},[84,322,323],{"class":153}," to",[84,325,138],{"class":90},[84,327,291],{"class":94},[84,329,294],{"class":121},[84,331,297],{"class":94},[84,333,300],{"class":128},[84,335,303],{"class":94},[84,337,306],{"class":90},[84,339,309],{"class":94},[84,341,312],{"class":90},[84,343,344],{"class":94}," id)\n",[84,346,348],{"class":86,"line":347},14,[84,349,109],{"emptyLinePlaceholder":108},[84,351,353,355,358,360,363,366,369,372,375],{"class":86,"line":352},15,[84,354,229],{"class":90},[84,356,357],{"class":153}," ids",[84,359,138],{"class":90},[84,361,362],{"class":94}," event?.shiftKey ",[84,364,365],{"class":90},"&&",[84,367,368],{"class":94}," from ",[84,370,371],{"class":90},"!==",[84,373,374],{"class":90}," -",[84,376,377],{"class":153},"1\n",[84,379,381,384,386,389,392,395,398,401,404,407,410,413,416,418,420,422,424],{"class":86,"line":380},16,[84,382,383],{"class":90},"      ?",[84,385,291],{"class":94},[84,387,388],{"class":121},"slice",[84,390,391],{"class":94},"(Math.",[84,393,394],{"class":121},"min",[84,396,397],{"class":94},"(from, to), Math.",[84,399,400],{"class":121},"max",[84,402,403],{"class":94},"(from, to) ",[84,405,406],{"class":90},"+",[84,408,409],{"class":153}," 1",[84,411,412],{"class":94},").",[84,414,415],{"class":121},"map",[84,417,297],{"class":94},[84,419,300],{"class":128},[84,421,303],{"class":94},[84,423,306],{"class":90},[84,425,426],{"class":94}," r[key])\n",[84,428,430,433],{"class":86,"line":429},17,[84,431,432],{"class":90},"      :",[84,434,435],{"class":94}," [id]\n",[84,437,439],{"class":86,"line":438},18,[84,440,109],{"emptyLinePlaceholder":108},[84,442,444,446,449,451,454,456],{"class":86,"line":443},19,[84,445,229],{"class":90},[84,447,448],{"class":153}," next",[84,450,138],{"class":90},[84,452,453],{"class":90}," new",[84,455,167],{"class":121},[84,457,458],{"class":94},"(selected.value)\n",[84,460,462,465,468,471,474,477],{"class":86,"line":461},20,[84,463,464],{"class":90},"    for",[84,466,467],{"class":94}," (",[84,469,470],{"class":90},"const",[84,472,473],{"class":153}," rowId",[84,475,476],{"class":90}," of",[84,478,479],{"class":94}," ids) {\n",[84,481,483,486,489,492],{"class":86,"line":482},21,[84,484,485],{"class":90},"      if",[84,487,488],{"class":94}," (select) next.",[84,490,491],{"class":121},"add",[84,493,494],{"class":94},"(rowId)\n",[84,496,498,501,504,507],{"class":86,"line":497},22,[84,499,500],{"class":90},"      else",[84,502,503],{"class":94}," next.",[84,505,506],{"class":121},"delete",[84,508,494],{"class":94},[84,510,512],{"class":86,"line":511},23,[84,513,514],{"class":94},"    }\n",[84,516,518,521,524],{"class":86,"line":517},24,[84,519,520],{"class":94},"    selected.value ",[84,522,523],{"class":90},"=",[84,525,526],{"class":94}," next\n",[84,528,530,533,535],{"class":86,"line":529},25,[84,531,532],{"class":94},"    anchor.value ",[84,534,523],{"class":90},[84,536,537],{"class":94}," id\n",[84,539,541],{"class":86,"line":540},26,[84,542,543],{"class":94},"  }\n",[84,545,547],{"class":86,"line":546},27,[84,548,109],{"emptyLinePlaceholder":108},[84,550,552,555],{"class":86,"line":551},28,[84,553,554],{"class":90},"  return",[84,556,557],{"class":94}," { selected, toggle }\n",[84,559,561],{"class":86,"line":560},29,[84,562,563],{"class":94},"}\n",[75,565,569],{"className":566,"code":567,"language":568,"meta":80,"style":80},"language-vue shiki shiki-themes github-light github-dark","\u003Cinput\n  type=\"checkbox\"\n  :checked=\"selected.has(row.id)\"\n  @mousedown=\"$event.shiftKey && $event.preventDefault()\"\n  @click=\"toggle(row, $event)\"\n>\n","vue",[26,570,571,580,590,614,642,661],{"__ignoreMap":80},[84,572,573,576],{"class":86,"line":87},[84,574,575],{"class":94},"\u003C",[84,577,579],{"class":578},"s9eBZ","input\n",[84,581,582,585,587],{"class":86,"line":105},[84,583,584],{"class":121},"  type",[84,586,523],{"class":94},[84,588,589],{"class":101},"\"checkbox\"\n",[84,591,592,595,598,600,603,606,608,611],{"class":86,"line":112},[84,593,594],{"class":94},"  :",[84,596,597],{"class":121},"checked",[84,599,523],{"class":94},[84,601,602],{"class":101},"\"",[84,604,605],{"class":94},"selected.",[84,607,256],{"class":121},[84,609,610],{"class":94},"(row.id)",[84,612,613],{"class":101},"\"\n",[84,615,616,619,622,624,626,629,631,634,637,640],{"class":86,"line":147},[84,617,618],{"class":94},"  @",[84,620,621],{"class":121},"mousedown",[84,623,523],{"class":94},[84,625,602],{"class":101},[84,627,628],{"class":94},"$event.shiftKey ",[84,630,365],{"class":90},[84,632,633],{"class":94}," $event.",[84,635,636],{"class":121},"preventDefault",[84,638,639],{"class":94},"()",[84,641,613],{"class":101},[84,643,644,646,649,651,653,656,659],{"class":86,"line":173},[84,645,618],{"class":94},[84,647,648],{"class":121},"click",[84,650,523],{"class":94},[84,652,602],{"class":101},[84,654,655],{"class":121},"toggle",[84,657,658],{"class":94},"(row, $event)",[84,660,613],{"class":101},[84,662,663],{"class":86,"line":180},[84,664,665],{"class":94},">\n",[11,667,668,671,672,674],{},[26,669,670],{},"Set"," sprawdza, czy wiersz jest zaznaczony, bez przeszukiwania całej tablicy. Przy każdej zmianie tworzę nowy ",[26,673,670],{},", żeby Vue na pewno zauważył zmianę.",[15,676,678],{"id":677},"enter-pole-w-wierszu-niżej","Enter: pole w wierszu niżej",[11,680,681],{},"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,683,684],{},[36,685],{"alt":686,"height":39,"src":687,"width":688},"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,690,691,692,695],{},"W przykładzie każde pole dostaje numer wiersza i nazwę kolumny w atrybutach ",[26,693,694],{},"data-*",". Po Enterze funkcja szuka pola z tą samą kolumną i sąsiednim numerem:",[75,697,699],{"className":566,"code":698,"language":568,"meta":80,"style":80},"\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",[26,700,701,713,731,736,746,750,762,793,811,831,836,840,850,859,863,872,876,885,893,903,913,923,933,943,948,974,984],{"__ignoreMap":80},[84,702,703,705,708,711],{"class":86,"line":87},[84,704,575],{"class":94},[84,706,707],{"class":578},"script",[84,709,710],{"class":121}," setup",[84,712,665],{"class":94},[84,714,715,718,721,723,725,727,729],{"class":86,"line":105},[84,716,717],{"class":90},"function",[84,719,720],{"class":121}," moveOnEnter",[84,722,125],{"class":94},[84,724,221],{"class":128},[84,726,132],{"class":94},[84,728,216],{"class":128},[84,730,144],{"class":94},[84,732,733],{"class":86,"line":112},[84,734,735],{"class":176},"  \u002F\u002F Enter w formularzu normalnie go wysyła – tu ma tylko przenieść kursor\n",[84,737,738,741,743],{"class":86,"line":147},[84,739,740],{"class":94},"  event.",[84,742,636],{"class":121},[84,744,745],{"class":94},"()\n",[84,747,748],{"class":86,"line":173},[84,749,109],{"emptyLinePlaceholder":108},[84,751,752,754,757,759],{"class":86,"line":180},[84,753,150],{"class":90},[84,755,756],{"class":153}," column",[84,758,138],{"class":90},[84,760,761],{"class":94}," event.currentTarget.dataset.column\n",[84,763,764,766,768,770,773,775,778,781,783,786,789,791],{"class":86,"line":200},[84,765,150],{"class":90},[84,767,448],{"class":153},[84,769,138],{"class":90},[84,771,772],{"class":94}," row ",[84,774,406],{"class":90},[84,776,777],{"class":94}," (event.shiftKey ",[84,779,780],{"class":90},"?",[84,782,374],{"class":90},[84,784,785],{"class":153},"1",[84,787,788],{"class":90}," :",[84,790,409],{"class":153},[84,792,197],{"class":94},[84,794,795,797,800,802,805,808],{"class":86,"line":205},[84,796,150],{"class":90},[84,798,799],{"class":153}," input",[84,801,138],{"class":90},[84,803,804],{"class":94}," document.",[84,806,807],{"class":121},"querySelector",[84,809,810],{"class":94},"(\n",[84,812,813,816,819,822,825,828],{"class":86,"line":226},[84,814,815],{"class":101},"    `[data-row=\"${",[84,817,818],{"class":94},"next",[84,820,821],{"class":101},"}\"][data-column=\"${",[84,823,824],{"class":94},"column",[84,826,827],{"class":101},"}\"] input`",[84,829,830],{"class":94},",\n",[84,832,833],{"class":86,"line":240},[84,834,835],{"class":94},"  )\n",[84,837,838],{"class":86,"line":262},[84,839,109],{"emptyLinePlaceholder":108},[84,841,842,845,848],{"class":86,"line":281},[84,843,844],{"class":94},"  input?.",[84,846,847],{"class":121},"focus",[84,849,745],{"class":94},[84,851,852,854,857],{"class":86,"line":318},[84,853,844],{"class":94},[84,855,856],{"class":121},"select",[84,858,745],{"class":94},[84,860,861],{"class":86,"line":347},[84,862,563],{"class":94},[84,864,865,868,870],{"class":86,"line":352},[84,866,867],{"class":94},"\u003C\u002F",[84,869,707],{"class":578},[84,871,665],{"class":94},[84,873,874],{"class":86,"line":380},[84,875,109],{"emptyLinePlaceholder":108},[84,877,878,880,883],{"class":86,"line":429},[84,879,575],{"class":94},[84,881,882],{"class":578},"template",[84,884,665],{"class":94},[84,886,887,890],{"class":86,"line":438},[84,888,889],{"class":94},"  \u003C",[84,891,892],{"class":578},"div\n",[84,894,895,898,900],{"class":86,"line":443},[84,896,897],{"class":121},"    v-for",[84,899,523],{"class":94},[84,901,902],{"class":101},"\"(item, index) in items\"\n",[84,904,905,908,910],{"class":86,"line":461},[84,906,907],{"class":121},"    :key",[84,909,523],{"class":94},[84,911,912],{"class":101},"\"item.id\"\n",[84,914,915,918,920],{"class":86,"line":482},[84,916,917],{"class":121},"    :data-row",[84,919,523],{"class":94},[84,921,922],{"class":101},"\"index\"\n",[84,924,925,928,930],{"class":86,"line":497},[84,926,927],{"class":121},"    data-column",[84,929,523],{"class":94},[84,931,932],{"class":101},"\"price\"\n",[84,934,935,938,940],{"class":86,"line":511},[84,936,937],{"class":121},"    @keydown.enter",[84,939,523],{"class":94},[84,941,942],{"class":101},"\"moveOnEnter($event, index)\"\n",[84,944,945],{"class":86,"line":517},[84,946,947],{"class":94},"  >\n",[84,949,950,953,956,959,961,964,967,969,972],{"class":86,"line":529},[84,951,952],{"class":94},"    \u003C",[84,954,955],{"class":578},"input",[84,957,958],{"class":121}," v-model",[84,960,523],{"class":94},[84,962,963],{"class":101},"\"item.price\"",[84,965,966],{"class":121}," inputmode",[84,968,523],{"class":94},[84,970,971],{"class":101},"\"decimal\"",[84,973,665],{"class":94},[84,975,976,979,982],{"class":86,"line":540},[84,977,978],{"class":94},"  \u003C\u002F",[84,980,981],{"class":578},"div",[84,983,665],{"class":94},[84,985,986,988,990],{"class":86,"line":546},[84,987,867],{"class":94},[84,989,882],{"class":578},[84,991,665],{"class":94},[11,993,994],{},"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,996,998],{"id":997},"co-z-tego-zostało","Co z tego zostało",[1000,1001,1002,1005,1008,1011],"ul",{},[49,1003,1004],{},"Shift + klik w 9 listach panelu.",[49,1006,1007],{},"Enter i Shift + Enter w tabeli cen i w oknie ceny indywidualnej – nową cenę wpisuje się bez sięgania po mysz.",[49,1009,1010],{},"Obie rzeczy to mało kodu, a oszczędzają najwięcej klikania tam, gdzie użytkownik przechodzi przez całą listę.",[49,1012,1013],{},"Przy zaznaczaniu zakresu zapamiętuj klucz wiersza, a nie jego numer – wtedy sortowanie i filtry nie psują zakresu.",[1015,1016,1017],"style",{},"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":80,"searchDepth":105,"depth":105,"links":1019},[1020,1021,1022],{"id":17,"depth":105,"text":18},{"id":677,"depth":105,"text":678},{"id":997,"depth":105,"text":998},"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.",false,"md",null,"case-study",{},"\u002Fog\u002Fblog\u002Ftabela-jak-arkusz-pl.jpg","\u002Fpl\u002Fblog\u002F2026\u002Ftabela-jak-arkusz","\u002Fprojects\u002F2026\u002Fagrestapp",{"title":5,"description":1024},"pl\u002Fblog\u002F2026\u002Ftabela-jak-arkusz",[568,1036],"javascript","KhPBlUO9WpeXSqYOG9SEHQ--QvykCVDXEn_lXX3kehI",{"id":1039,"title":1040,"agency":1041,"body":1042,"compare":1027,"cover":1104,"date":1110,"description":1111,"draft":1025,"extension":1026,"facts":1112,"featured":1025,"gallery":1126,"galleryLead":1147,"highlights":1148,"image":1164,"links":1165,"meta":1167,"navigation":108,"note":1168,"ogImage":1169,"path":1170,"seo":1171,"stack":1172,"stem":1176,"tags":1177,"__hash__":1178},"projects_pl\u002Fpl\u002Fprojects\u002F2026\u002Fagrestapp\u002Findex.md","AgrestApp","Pesi",{"type":8,"value":1043,"toc":1101},[1044,1047,1051],[11,1045,1046],{},"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,1048,1050],{"id":1049},"co-zrobiłem","Co zrobiłem",[1000,1052,1053,1059,1065,1071,1077,1083,1089,1095],{},[49,1054,1055,1058],{},[52,1056,1057],{},"Nagłówek panelu"," – nowy nagłówek z paskiem „Zaloguj jako”, menu konta i wspólnym komponentem rozwijanej listy",[49,1060,1061,1064],{},[52,1062,1063],{},"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",[49,1066,1067,1070],{},[52,1068,1069],{},"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",[49,1072,1073,1076],{},[52,1074,1075],{},"Analityka"," – główny wykres, wybór zakresu dat z trybem porównania, najlepsi dostawcy, podsumowanie oszczędności i rentowność produktów",[49,1078,1079,1082],{},[52,1080,1081],{},"Zarządzanie ofertą dostawcy"," – rozbudowa istniejącego ekranu: okno ceny indywidualnej, dodawanie artykułów i przechodzenie Enterem między polami",[49,1084,1085,1088],{},[52,1086,1087],{},"Zaznaczanie zakresu"," – Shift + klik w 9 listach panelu (surowce, produkty, ceny, powiadomienia i inne)",[49,1090,1091,1094],{},[52,1092,1093],{},"Sklep"," (dziś zamknięty) – koszyk z minimum logistycznym każdego dostawcy, zakup bez konta, historia zamówień i reklamacje",[49,1096,1097,1100],{},[52,1098,1099],{},"Migracja na Tailwind CSS v4"," całego projektu",{"title":80,"searchDepth":105,"depth":105,"links":1102},[1103],{"id":1049,"depth":105,"text":1050},{"desktop":1105,"phone":1106,"width":1107,"height":1108,"alt":1109},"\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.",[1113,1117,1120,1123],{"value":1114,"suffix":1115,"text":1116},"12"," mies.","w projekcie – od października 2025",{"value":1118,"text":1119},"69","ikon jako komponenty Vue w nowym zestawie UI",{"value":1121,"text":1122},"4","strony branżowe – piekarnie, cukiernie, lodziarnie i HoReCa – zbudowane z tych samych sekcji",{"value":1124,"text":1125},"296","klas w szablonach bez zmian przy przejściu na Tailwind v4",[1127,1131,1135,1139,1143],{"src":1128,"width":1107,"height":1108,"alt":1129,"caption":1130},"\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":1132,"full":108,"alt":1133,"caption":1134},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Fhome-full.webp","Strona główna AgrestApp","Strona główna – od pierwszego ekranu do newslettera",{"src":1136,"full":108,"alt":1137,"caption":1138},"\u002Fprojects\u002F2026\u002Fagrestapp\u002Fabout-system-full.webp","Strona „O systemie”","„O systemie” – sekcje z opisem modułów",{"src":1140,"full":108,"alt":1141,"caption":1142},"\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":1144,"full":108,"alt":1145,"caption":1146},"\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ć.",[1149,1152,1155,1158,1161],{"title":1150,"text":1151},"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":1153,"text":1154},"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":1156,"text":1157},"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":1159,"text":1160},"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":1162,"text":1163},"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":1166},"https:\u002F\u002Fagrestapp.pl",{},"Pracuję w zespole – opisuję tylko części, przy których front-end jest mój. Panel aplikacji nie jest publiczny.","\u002Fog\u002Fprojects\u002Fagrestapp-pl.jpg","\u002Fpl\u002Fprojects\u002F2026\u002Fagrestapp",{"title":1040,"description":1111},[1173,1174,1175],"Vue 3","Inertia","Tailwind CSS v4","pl\u002Fprojects\u002F2026\u002Fagrestapp\u002Findex",[568],"9K9-hLbbaGH3H17k_QO2mHzbXXsCfmWEW5rHxmSri_M",1792358954483]