--- phase: 02-product-actions-fixes plan: 01 type: execute wave: 1 depends_on: ["01-01"] files_modified: - themes/ayon/templates/catalog/product.tpl - themes/ayon/assets/js/custom.js - themes/ayon/assets/css/custom.scss autonomous: false delegation: off --- ## Goal Port konfiguratora "piece" (wybór fragmentu tapety + odbicie lustrzane) ze starego layoutu do nowego (IP `== '89.69.31.86'`), z zachowaniem pełnej zgodności DOM kontraktu dla serializacji do koszyka (hidden inputs `is_crop`, `crop_pos_x/y`, `crop_width/height`, `piece_bg_top/left` w formie `#add-to-cart-or-refresh`). ## Purpose Użytkownik w nowym layoucie nie widzi i nie może ustawić wycinka tapety ani odbicia lustrzanego. W starym layoucie ten flow wysyłał wymiary fragmentu + pozycję + `is_crop` + mirror do koszyka. Bez portu — zamówienia z nowego layoutu nie mają informacji o kropie. Funkcja jest kluczowa dla produktu (sklep „na wymiar"). ## Output Działający piece configurator w nowym layoucie: draggable `#piece` na zdjęciu produktu + kontrolki w bloku „Rozmiar i dostosowanie" + button `#button-mirror-reflection`. Hidden inputs w formie `#add-to-cart-or-refresh` aktualizują się przy interakcji. Stary layout nietknięty. ## Project Context @.paul/PROJECT.md @.paul/ROADMAP.md @.paul/STATE.md ## Prior Work @.paul/phases/01-product-variants-fix/01-01-SUMMARY.md Phase 01 wprowadziło form `
` w gałęzi nowego layoutu (ale owija tylko variants) oraz pattern `prestashop.emit('updatedProduct', resp)` po własnym AJAX refresh — piece musi umieć się re-inicjalizować po zmianie wariantu (bo `.product_image_wrapper` jest `.html()`-replace'owany). ## Source Files — stary layout (wzór implementacji) @themes/ayon/templates/catalog/product.tpl (gałąź `!= '89.69.31.86'`, ok. linie 179-244 hidden inputs + `.product-block-piece` + `#button-mirror-reflection`, ok. linie 522-523 `.piece-left-positon` / `.piece-top-positon`) @themes/ayon/assets/js/custom.js (ok. linie 100-330 — `dragElement`, `checkedHandler`, change-handlery `#piece-width` / `#piece-height`, mirror toggle, update `#piece_bg_top/left` + `#product_crop_pos_x/y`) @themes/ayon/assets/css/custom.scss (reguły `.product-block-piece`, `#piece`, `#button-mirror-reflection`, `.piece-size-controls`, `.piece-size-values`) ## Source Files — nowy layout (cel modyfikacji) @themes/ayon/templates/catalog/product.tpl (gałąź `== '89.69.31.86'` — blok `product_variants` z ``, blok `product_size` z pustym `
`, `
`) ## AC-1: Markup piece + mirror w nowym layoucie ```gherkin Given strona produktu jest renderowana dla IP 89.69.31.86 (nowy layout) When DOM się wczytuje Then istnieją: `#piece` jako child `.product_image_wrapper` (initially hidden), `#button-mirror-reflection`, `.product-block-piece` (z `#checkbox-piece`, `#piece-width`, `#piece-height`, `#piece-size-view`) w `.product-size-data .product-box--data`, `.piece-left-positon` i `.piece-top-positon` jako elementy pomocnicze, hidden inputs (`is_crop`, `crop_pos_x`, `crop_pos_y`, `crop_width`, `crop_height`, `piece_bg_top`, `piece_bg_left`) jako dzieci formy `#add-to-cart-or-refresh` ``` ## AC-2: Piece interaktywny — drag, checkbox, resize przez inputy, mirror ```gherkin Given nowy layout jest załadowany When użytkownik klika `#checkbox-piece` Then `#piece` staje się widoczny (fadeIn), inputy `#piece-width` i `#piece-height` stają się edytowalne, `$('#product_is_crop').val()` === '1' When użytkownik przeciąga `#piece` po obrazie Then pozycja `#piece` się aktualizuje, `#product_crop_pos_x`, `#product_crop_pos_y`, `#piece_bg_left`, `#piece_bg_top` mają nowe wartości, `.piece-left-positon` i `.piece-top-positon` mają pozycję piksel When użytkownik zmienia `#piece-width` / `#piece-height` Then wymiar `#piece` się aktualizuje, `#product_crop_width/height` też, `#piece-size-view` pokazuje `WxH` When użytkownik klika `#button-mirror-reflection` Then `#piece` i `.product-images img.thumb` dostają klasę `.mirrored` (toggle) ``` ## AC-3: Form serialization — wartości trafiają do formy ```gherkin Given nowy layout, checkbox aktywny, piece przesunięty i zresize'owany When wykonamy `$('#add-to-cart-or-refresh').serialize()` w konsoli Then string zawiera: `is_crop=1`, `crop_pos_x=`, `crop_pos_y=`, `crop_width=`, `crop_height=`, `piece_bg_top=`, `piece_bg_left=` (obok istniejących `token`, `id_product`, `id_customization`, `group[...]=...` z Phase 01) ``` ## AC-4: Re-init po zmianie wariantu (AJAX refresh) ```gherkin Given piece jest włączony i ustawiony, użytkownik zmienia wariant kolorystyczny (click kafelka) When Phase 01 AJAX refresh wykona `$('.product_image_wrapper').html(resp.product_cover_thumbnails)` + emit `updatedProduct` Then `#piece` jest ponownie wstawiany do `.product_image_wrapper`, `dragElement` jest re-bound, stan (checkbox aktywny, wymiary, pozycja, mirror) jest zachowany lub resetowany do spójnego defaultu — jedno z, udokumentowane ``` ## AC-5: Zero regresji w starym layoucie ```gherkin Given użytkownik NIE jest na IP 89.69.31.86 (stary layout) When strona produktu się ładuje Then stary `.product-block-piece`, `#piece`, `#button-mirror-reflection` działają identycznie jak przed Phase 02 (drag, resize, mirror, hidden inputs serialize) ``` Task 1: Markup piece/mirror + hidden inputs w gałęzi nowego layoutu product.tpl themes/ayon/templates/catalog/product.tpl W gałęzi `{if $smarty.server.REMOTE_ADDR == '89.69.31.86'}`: (a) Dodać 7 hidden inputs JAKO DZIECI formy `` (obok variants grid, wewnątrz form): - `` - `` - `` - `` - `` - `` - `` (b) W `
` (gałąź nowego layoutu) dodać overlay: - `` jako rodzeństwo lub dziecko `.product-cover-thumbnails` - `#piece` musi mieć `background-image` ustawiony inline z URL okładki produktu (to co stary layout robi — odszukaj w starym markup'ie dokładny sposób; zachowaj tę samą zmienną Smarty) (c) W bloku `{block name='product_size'}` (nowa gałąź), w pustym `
` w `.product-box.product-size-data`, dodać wrapper `.product-size-data--new` i umieścić: - `
` z `
`, `
` (zawiera `Wybierz rozmiar`), ``, `` - `

Odbicie lustrzane

` (d) Dodać rodzeństwem (poza image wrapperem, gdziekolwiek w kontenerze): `` `` — wymagane przez `custom.js`. (e) Użyć DOKŁADNIE tych samych ID co w starym layoucie — dzięki temu handlery w `custom.js` (`jQuery("#piece-width").change(...)`, itd.) bind'ują się do elementów w nowym layoucie identycznie. DUPLIKATY ID ze starym layoutem są OK — tylko jedna gałąź renderuje się na raz (IP-gated). **Avoid:** - Nie zmieniać markup'u starej gałęzi (`!= '89.69.31.86'`) — `DO NOT CHANGE`. - Nie dotykać partial'a `_partials/product-variants.tpl`. - Nie zmieniać nazw `name="..."` hidden inputów — zmiana łamie zapis do koszyka po stronie serwera. Załaduj produkt na nowym layoucie i w DevTools: - `document.getElementById('piece')` zwraca element - `document.getElementById('product_is_crop').value === '0'` na starcie - `document.getElementById('checkbox-piece')` istnieje - `document.getElementById('button-mirror-reflection')` istnieje - `$('#add-to-cart-or-refresh input[name="is_crop"]').length === 1` - Stary layout (przez drugi browser/IP) — bez zmian AC-1 spełnione + częściowo AC-3 (hidden inputs istnieją w formie). Task 2: JS — piece init() + re-init po updatedProduct, zachowanie istniejących handlerów themes/ayon/assets/js/custom.js Ponieważ ID elementów są identyczne, istniejące handlery (binding bezpośredni `jQuery("#piece-width").change(...)`, `jQuery("#checkbox-piece").change(...)`, itd.) ZAPINAJĄ się na starcie niezależnie od layoutu. Kluczowe dwa ryzyka: (1) `dragElement(document.getElementById("piece"))` w `checkedHandler` — to zapina mousedown na `#piece`. Po `.product_image_wrapper.html(resp.product_cover_thumbnails)` w Phase 01 AJAX refresh, `#piece` może być: - zniszczony (bo nie jest częścią `resp.product_cover_thumbnails`) → po AJAX trzeba go re-stworzyć lub przenieść przed replace'em - zachowany (jeśli nie jest w `.product_image_wrapper` w nowym layoucie) → wtedy OK Decyzja: `#piece` NIE JEST dzieckiem kontenera replace'owanego przez AJAX. Umieść go jako sibling do `.product-cover-thumbnails` ale WEWNĄTRZ `.product_image_wrapper`, I przenieś go przed replacem (detach → replace → re-append), ALBO: umieść `#piece` jako overlay nad `.product_image_wrapper` (sibling, absolutnie pozycjonowany) aby AJAX replace nie ruszał go. **Preferowane: overlay sibling** — prostsze i mniej edge case'ów. (2) W custom.js (miejsce po obecnym handlerze `change` wariantu w Phase 01), zarejestruj listener: ```js prestashop.on('updatedProduct', function(event){ // re-ensure #piece position sync po ew. resize wrappera if ($('#product_is_crop').val() === '1') { // re-trigger width/height change handlers to recompute background-position $('#piece-width').trigger('change'); $('#piece-height').trigger('change'); } }); ``` Cel: piece pozostaje zsynchronizowany wizualnie po resize kontenera (różne warianty mogą mieć różne wymiary obrazu okładki). (3) (Opcjonalnie, jeżeli inline background-image dla `#piece` jest ustawiany przez JS zamiast w Smarty) — w handlerze `updatedProduct` odświeżyć `#piece`'s `background-image` na bazie `resp.product_cover` lub nowego `img.thumb`. **Avoid:** - Nie refactoruj istniejących handlerów piece — dokładaj tylko to co potrzebne dla nowego layoutu. - Nie duplikuj `dragElement` — użyj istniejącej funkcji. - Nie dotykaj handlera variant-change z Phase 01 (ok. istniejąca sekcja AJAX refresh). W browserze (nowy layout): 1. `$('#checkbox-piece').click()` → `#piece` widoczny, `#product_is_crop` = '1' 2. Przeciągnij `#piece` myszką → `#product_crop_pos_x` i `#product_crop_pos_y` mają wartości != 0 3. Zmień `#piece-width` na 200, trigger change → `#product_crop_width` = 200, `.piece-width-px` text = '200' 4. Klik `#button-mirror-reflection` → `#piece.mirrored` true 5. Zmień wariant kolorystyczny (click kafelka) → po AJAX refresh `#piece` nadal istnieje, stan zachowany AC-2 i AC-4 spełnione. Task 3: CSS — scoped styles i wizualna warstwa piece dla nowego layoutu themes/ayon/assets/css/custom.scss Reguły dla starego layoutu są już w `custom.scss` (`#piece`, `.product-block-piece`, `#button-mirror-reflection`, itd.). W nowym layoucie kontekst jest inny — `.product_image_wrapper` zamiast `.product-images`, osadzenie w `.product-size-data .product-box--data .product-size-data--new`. Dopisać sekcję (na końcu pliku lub obok dotychczasowego scope Phase 01): ```scss body#product .product-size-data { .product-box--data { padding: 0; } .product-size-data--new { // layout wewnętrzny — piece controls + mirror side-by-side display: flex; align-items: center; gap: 16px; .product-block-piece { /* ... */ } .piece-size-controls, .piece-size-values { /* ... */ } #button-mirror-reflection { cursor: pointer; /* ... */ } } } body#product .product_image_wrapper { position: relative; // anchor dla #piece overlay #piece { position: absolute; background-size: cover; cursor: move; z-index: 10; // display:none default (JS fadeIn) } } ``` **NIE ustawiaj globalnie na `#piece` / `.product-block-piece`** — to zepsuje stary layout. Scope'uj pod `body#product .product-size-data .product-size-data--new` oraz `body#product .product_image_wrapper #piece`. Edytuj TYLKO `custom.scss`. `custom.css` jest auto-generowany przez watcher user'a (feedback memory `.claude/memory/feedback_scss_only.md`). Zadanie nie wymaga pixel-perfect fit — celem jest: piece widoczny, draggable, mieści się na obrazie; controls czytelne w panelu. Vizualny polish (pixel fit) odnotować jako Deferred jeżeli nie ma ref'u Figma. **Avoid:** - Nie usuwaj ani nie modyfikuj istniejących reguł (stary layout). - Nie dodawaj `!important`. Po rekompilacji SCSS (watcher usera) w `custom.css` pojawia się nowy blok `body#product .product-size-data` i `body#product .product_image_wrapper #piece`. Stary layout w DevTools nadal ma działające stare reguły (brak konfliktów w DevTools Elements panel). AC-1 spełnione wizualnie (kontrolki widoczne, piece z position/cursor), AC-5 zachowane. - Markup piece + mirror + hidden inputs w gałęzi nowego layoutu `product.tpl` - JS init + re-init na `updatedProduct` w `custom.js` - Scoped SCSS w `custom.scss` 1. Nowy layout (IP 89.69.31.86): - Przejdź na dowolny produkt z wariantami - W bloku „Rozmiar i dostosowanie" kliknij checkbox „Wymiary tapety" — `#piece` powinien się pokazać nad zdjęciem - Przeciągnij `#piece` myszką — pozycja się zmienia - Zmień wartość `#piece-width` i `#piece-height` — `#piece` się przeskalowuje, `#piece-size-view` pokazuje `WxH` - Kliknij `#button-mirror-reflection` — piece i thumb dostają klasę `.mirrored` - W DevTools: `$('#add-to-cart-or-refresh').serialize()` zawiera `is_crop=1&crop_pos_x=&crop_pos_y=&crop_width=&crop_height=&piece_bg_top=&piece_bg_left=` - Kliknij inny wariant kolorystyczny — po AJAX refresh piece dalej istnieje i działa 2. Stary layout (dowolny inny IP): - Otwórz produkt w zwykłej przeglądarce (IP != 89.69.31.86) - Sprawdź że piece/mirror/resize/drag działają bez regresji - Dodaj do koszyka — wartości crop zapisują się do customization (jak wcześniej) 3. Playwright (opcjonalnie jeżeli dostępny): zautomatyzuj kroki 1 i zaloguj wyniki. Napisz „approved" aby zamknąć APPLY, lub opisz problemy (z dokładnymi krokami odtworzenia) do naprawy. ## DO NOT CHANGE - Gałąź `{if $smarty.server.REMOTE_ADDR != '89.69.31.86'}` w `themes/ayon/templates/catalog/product.tpl` (stary layout, produkcja). - Istniejące handlery piece w `themes/ayon/assets/js/custom.js` (linie ~100-330 stare binding'i — tylko dodawaj nowe listenery/inicjalizacje, nie modyfikuj). - Handler variant-change z Phase 01 w `custom.js` — pattern ustalony, tylko dodaj listener `prestashop.on('updatedProduct', ...)`. - Partial `themes/ayon/templates/catalog/_partials/product-variants.tpl` — shared, nie dotykać. - Wszystkie istniejące reguły SCSS dla starego layoutu. ## SCOPE LIMITS - Nie naprawiamy „Dodaj do koszyka" w tym planie (osobny plan fazy 02). - Nie wypełniamy pustych bloków `.product-protect`, `.product-installation`, `.product-order-sample` (osobny plan fazy 02). - Nie dodajemy resize handles na `#piece` („bonus" z rozmowy z userem) — deferred do ewentualnego Plan 02-02 jako feature add-on. W tym planie: drag + resize przez inputy, jak stary layout. - Nie dodajemy nowych zależności (jquery-ui resizable, interact.js, itp.). - Nie zmieniamy server-side logiki zapisu kropu do koszyka — polegamy na tym że nazwy hidden inputs są identyczne ze starym layoutem. Before declaring plan complete: - [ ] Hidden inputs obecne wewnątrz `#add-to-cart-or-refresh` w nowym layoucie (AC-1, AC-3) - [ ] `#piece`, `.product-block-piece`, `#button-mirror-reflection` widoczne i interaktywne w nowym layoucie (AC-2) - [ ] Drag + resize przez inputy + mirror aktualizują hidden inputs (AC-2, AC-3) - [ ] Po zmianie wariantu kolorystycznego piece nadal działa (AC-4) - [ ] Stary layout bez regresji w pełnym cyklu (drag → resize → mirror → add-to-cart → customization zapisuje się) (AC-5) - [ ] `custom.scss` edytowany, `custom.css` nie tknięty ręcznie - [ ] Brak duplikatów funkcji JS, brak konfliktów CSS - Wszystkie 3 auto taski ukończone + checkpoint human-verify z "approved" - 5/5 AC satysfakcjonowane - Zero regresji na starym layoucie (weryfikowane checkpoint'em) - Dokumentacja decyzji i deviation'ów w SUMMARY After completion, create `.paul/phases/02-product-actions-fixes/02-01-SUMMARY.md`