# Kordit — guide dla Claude Code (i ludzi)

Ten plik to instrukcja obsługi motywu **Kordit**. Opisuje architekturę, jak
dodawać bloki, jak robić powielane (ACF-repeater-like) bloki, jak korzystać
z Chrome DevTools MCP do iteracyjnego developmentu, i wymienia gotcha,
które już zostały znalezione (żeby ich nie powtórzyć).

**Target audience:** Claude Code podpięte pod ten projekt + developer który
forkuje kordit na nowy projekt klienta.

---

## 1. Czym jest ten motyw

Kordit to **neutralny FSE boilerplate**, nie gotowy motyw dla jednego klienta.
Model pracy:

1. Motyw jest hostowany w `github.com/kordit/kordit` (branch `main`).
2. `panel.soeasy.it/instalator` pobiera zip z GitHub main dla każdej nowej
   instalacji klienta — motyw jest forkowany per projekt, z reguły klient
   dostaje własne repo (np. `repo62.mydevil.net/git/priv/kordit/<klient>`).
3. W pojedynczym forku klienta zmiany są LOKALNE do niego:
   `theme.json` palette/typografia, patterny, dodatkowe bloki.
4. Motyw jest czysto FSE — nie ma `header.php` / `footer.php` PHP — wszystko
   przez `templates/` i `parts/` HTML + block comments.

**Konsekwencja dla developera:** cały "branding" musi być w `theme.json`
i patternach, nie hard-coded w CSS ani w blokach. Bloki mają być generyczne
i działać dla dowolnej palety/fontów.

---

## 2. Struktura katalogów

```
kordit/
├── blocks/              # Atomy — pojedyncze bloki (opinia, slide, card itd.)
│   ├── opinia/
│   │   ├── block.json
│   │   ├── index.js     # edit + save (React)
│   │   ├── editor.scss  # editor-only (placeholders, widgets)
│   │   └── style.scss   # frontend + editor (collider-proof)
│   └── slide/           # dumb wrapper — any block can live inside
├── sections/            # Pełnoekranowe sekcje — kontenery (slider, cards-grid)
│   └── slider/
│       ├── block.json
│       ├── index.js     # edit (InnerBlocks + toolbar z + DODAJ ITEM)
│       ├── render.php   # server-side render (frontend Swiper HTML)
│       ├── editor.scss
│       └── style.scss
├── patterns/            # Auto-ładowane PHP patterny (WP 6.0+ convention)
│   ├── hero.php
│   ├── hero-slider.php
│   ├── hero-slider-columns.php
│   └── opinie.php
├── templates/           # FSE templates (index, page, 404, front-page)
├── parts/               # FSE template parts (header, footer)
├── styles/              # Shared frontend stylesheets (theme.scss, nav/)
│   ├── theme.scss
│   └── navigation/
├── inc/                 # PHP modules (no logic in functions.php)
│   ├── setup.php        # theme supports, WP core overrides
│   ├── enqueue.php
│   ├── filters.php
│   ├── shortcodes.php
│   ├── blocks/
│   │   └── loader.php   # register_block_type() calls for everything
│   ├── navigation/
│   ├── options/
│   └── libraries/       # Vendored JS/CSS (Swiper etc.)
├── build/               # Webpack output — compiled index.js per block
├── theme.json           # Single source of truth for palette/typography
├── style.css            # Only the theme header — all real CSS in /styles
├── functions.php        # Pure router — just requires /inc modules
├── package.json         # webpack + sass scripts
└── webpack.config.js    # scans sections/ + blocks/ for index.js entries
```

**Zasada:** `functions.php` to tylko router. Cała logika PHP jest w `inc/`
w modułach, każdy jako pojedyncza odpowiedzialność.

---

## 3. Rejestracja nowego bloku

Dwa rodzaje: **`blocks/<name>`** (atomowy blok jak opinia, karta, button
wrapper) albo **`sections/<name>`** (sekcja-kontener jak slider, cards-grid,
full-width hero). Różnica tylko semantyczna — rejestracja identyczna.

### Krok po kroku

1. **Stwórz katalog** — `blocks/my-block/` albo `sections/my-section/`.

2. **Dodaj `block.json`** z minimalnymi polami:

   ```json
   {
       "$schema": "https://schemas.wp.org/trunk/block.json",
       "apiVersion": 3,
       "name": "kordit/my-block",
       "version": "1.0.0",
       "title": "My Block",
       "category": "kordit-blocks",
       "icon": "format-aside",
       "description": "Opis.",
       "textdomain": "kordit",
       "editorScript": "file:../../build/blocks/my-block/index.js",
       "editorStyle": "file:../../build/blocks/my-block/index.css",
       "style": "file:../../build/blocks/my-block/style-index.css",
       "attributes": {},
       "supports": {
           "html": false,
           "className": true
       }
   }
   ```

   Kategorie kordit: `kordit-blocks` (atomy), `kordit-sections` (sekcje).
   Zarejestrowane w `inc/blocks/loader.php` przez filtr `block_categories_all`.

   Jeżeli blok jest **server-side rendered** (ma dynamic HTML, carousel
   init itd.), dodaj `"render": "file:./render.php"`. Wtedy odpowiadasz
   też za plik `render.php` który dostaje `$block`, `$attributes` i
   `$content`, i emituje frontend HTML. **Uwaga — server-rendered bloki
   mają gotcha w edytorze, patrz sekcja 6.**

3. **Stwórz `index.js`** z `edit` + `save`:

   ```jsx
   import { registerBlockType } from '@wordpress/blocks';
   import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
   import './editor.scss';
   import './style.scss';

   registerBlockType('kordit/my-block', {
       edit: () => {
           const blockProps = useBlockProps({ className: 'my-block' });
           return <div {...blockProps}>Hello</div>;
       },
       save: () => {
           const blockProps = useBlockProps.save({ className: 'my-block' });
           return <div {...blockProps}>Hello</div>;
       },
   });
   ```

   **Ważne:** jeżeli `edit` nadaje `className`, `save` MUSI robić
   to samo — inaczej block validation zwróci "block invalid" przy
   reload'owaniu strony.

4. **Zarejestruj blok w `inc/blocks/loader.php`**:

   ```php
   register_block_type(__DIR__ . '/../../blocks/my-block');
   ```

   Wszystkie kordit bloki są tam, grupowane: najpierw sections, potem blocks.

5. **Dodaj wpis do `webpack.config.js`** — nie jest potrzebny. Webpack
   skanuje `sections/**/index.js` i `blocks/**/index.js` automatycznie
   i generuje entry per plik.

6. **Build:**

   ```bash
   npm run build
   ```

   Skompilowane pliki trafiają do `build/blocks/my-block/index.js`,
   `build/blocks/my-block/index.css`, `build/blocks/my-block/style-index.css`.

7. **Test:** otwórz edytor dowolnej strony, "+" inserter → kategoria
   "Kordit - Bloki" → twój blok powinien być na liście. (Clear pattern
   cache jeżeli nowe patterny: `wp --path=... transient delete --all`.)

### Styling — gdzie co leci

| Plik | Kontekst | Co tam |
|---|---|---|
| `blocks/x/style.scss` | **Frontend + editor** | Wygląd bloku — ten sam na froncie i w panelu. Powinien być główny plik. |
| `blocks/x/editor.scss` | **Tylko editor** | Placeholdery, widgets, UI toolbary. Nie wizualny wygląd bloku — to leci do style.scss. |
| `theme.json` | **Global** | Paleta, fontSizes, spacing, global element styles (h1, button, link). Nie kopiuj tego do CSS. |

**Rule:** jeżeli zmiana stylu ma się pojawić i w panelu i na froncie, idzie
do `style.scss`. Jeżeli tylko w panelu (np. `:empty::before` placeholder), do
`editor.scss`.

---

## 4. Patterny

Patterny to gotowe kompozycje bloków z predefiniowaną treścią, wklejane
do edytora przez inserter → zakładka "Wzorce".

### Jak dodać

1. Stwórz `patterns/my-pattern.php` z headerem:

   ```php
   <?php
   /**
    * Title: My Pattern
    * Slug: kordit/my-pattern
    * Categories: featured, header
    * Keywords: hero, intro, banner
    * Description: Krótki opis wyświetlany w insert panelu.
    * Viewport Width: 1400
    * Block Types: core/group
    */
   ?>
   <!-- wp:group {"align":"full"} -->
   <div class="wp-block-group alignfull">
       <!-- wp:heading -->
       <h2 class="wp-block-heading">Hello</h2>
       <!-- /wp:heading -->
   </div>
   <!-- /wp:group -->
   ```

2. **Done.** WP 6.0+ auto-ładuje patterny z `patterns/` bez żadnej
   dodatkowej rejestracji. Nie trzeba `register_block_pattern`.

### Pattern cache gotcha

WP cache'uje listę patternów per-theme w site transient
`_site_transient_wp_theme_files_patterns-<hash>`. Gdy dodajesz nowy pattern
lub edytujesz istniejący, WP może **nie zauważyć zmiany** dopóki cache
nie wygaśnie.

**Clear cache:**

```bash
wp --path=<wp-root> transient delete --all
# lub ręcznie w DB:
wp --path=<wp-root> db query "DELETE FROM wp_options WHERE option_name LIKE '%wp_theme_files_patterns%';"
```

Po usunięciu, przeładuj edytor. W Claude Code workflow: `cd` do wp-root,
wykonaj powyższe, potem reload edit page w Chrome DevTools MCP.

### Gdzie ustawić domyślne atrybuty sekcji

Na block comment w pattern PHP:

```
<!-- wp:kordit/section-slider {"slidesPerViewDesktop":1,"loop":true,"effect":"fade","align":"full"} -->
```

Wszystko co jest w `block.json attributes` można tak pre-fillować.

---

## 5. Powielane bloki (ACF-repeater-style)

To najbardziej charakterystyczny wzorzec w Kordit. Przykład z boku:
**`kordit/section-slider`** — kontener dla dowolnej liczby dzieci, z
toolbarem "Dodaj item" pod spodem który klonuje ostatnie dziecko.

### Architektura

- **Parent block** (`kordit/section-slider`) jest kontenerem — nie ma
  własnej treści, tylko `InnerBlocks` + toolbar button pod spodem.
- **Child block** (`kordit/slide`, `kordit/opinia`, dowolny inny) jest
  wrapperem nad realną zawartością. Może wrapować `core/paragraph`,
  `core/columns`, cokolwiek.
- **Pattern** dostarcza initial set dzieci — np. opinie-carousel ma
  3 `kordit/opinia` pre-fillowane treścią, hero-slider-columns ma 2
  `kordit/slide` z dwukolumnowym layoutem.
- **Toolbar "+" button** klonuje **ostatnie** istniejące dziecko. Dzięki
  temu struktura nowego slajdu jest identyczna z ostatnim — user edytuje
  tylko teksty, nie musi rebuild'ować layoutu.

### Minimalna implementacja parenta

Patrz `sections/slider/index.js` jako pełen przykład. Szkielet:

```jsx
import { registerBlockType, createBlock } from '@wordpress/blocks';
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
import { useSelect, useDispatch } from '@wordpress/data';
import { useEffect, useRef, useState } from '@wordpress/element';

registerBlockType('kordit/my-repeater', {
    edit: ({ clientId }) => {
        const { replaceInnerBlocks, setBlockEditingMode } = useDispatch('core/block-editor');

        // Auto-detect first child type (so "+" in hero-slider adds slides,
        // and "+" in opinie-carousel adds opinia).
        const childBlockName = useSelect((select) => {
            const children = select('core/block-editor').getBlock(clientId)?.innerBlocks || [];
            return children[0]?.name || 'kordit/slide';
        }, [clientId]);

        // REACTIVE — track child clientIds as a stable string so effects
        // re-fire when children change.
        const childClientIdsKey = useSelect((select) => {
            return (select('core/block-editor').getBlock(clientId)?.innerBlocks || [])
                .map((b) => b.clientId)
                .join(',');
        }, [clientId]);

        // GOTCHA: server-rendered blocks are auto-put in "disabled" editing
        // mode by WP. That hides their children from list view and blocks
        // the custom appender. Force "default" on every render + every
        // time children change.
        useEffect(() => {
            if (!setBlockEditingMode) return;
            setBlockEditingMode(clientId, 'default');
            childClientIdsKey.split(',').filter(Boolean).forEach((id) =>
                setBlockEditingMode(id, 'default')
            );
        }, [clientId, childClientIdsKey, setBlockEditingMode]);

        // GOTCHA: insertBlock() respects canInsertBlockType() which caches
        // restrictions and often returns false for custom kordit/* blocks
        // inside server-rendered parents. replaceInnerBlocks bypasses that
        // check — use it as the only reliable way to grow the list.
        const addNewItem = () => {
            const existing = wp.data.select('core/block-editor')
                .getBlock(clientId)?.innerBlocks || [];
            const cloned = existing.map((b) => wp.blocks.cloneBlock(b));
            // Clone LAST child (not createBlock) so layout structure is
            // preserved — user just edits text.
            const last = existing[existing.length - 1];
            const newBlock = last
                ? wp.blocks.cloneBlock(last)
                : createBlock(childBlockName);
            replaceInnerBlocks(clientId, [...cloned, newBlock], false);
        };

        const blockProps = useBlockProps({ className: 'my-repeater' });

        return (
            <section {...blockProps}>
                <div className="my-repeater__items">
                    <InnerBlocks
                        template={[[childBlockName]]}
                        renderAppender={false}
                    />
                </div>
                <div className="my-repeater__toolbar">
                    <button type="button" onClick={addNewItem}>
                        + Dodaj item
                    </button>
                </div>
            </section>
        );
    },
    save: () => {
        const blockProps = useBlockProps.save({ className: 'my-repeater' });
        return (
            <div {...blockProps}>
                <InnerBlocks.Content />
            </div>
        );
    },
});
```

**Kluczowe punkty** (wszystkie wyciągnięte z bugów które już raz zostały
znalezione — patrz sekcja 6):

1. `renderAppender={false}` — nie używamy WP-owego appendera, mamy własny
   toolbar button pod spodem, poza flex-scroll wrapperem slidesów.
2. `cloneBlock(lastChild)` — nie `createBlock(name)`, bo to daje pustą
   skorupę bez layoutu.
3. `replaceInnerBlocks(cloned + new)` — nie `insertBlock`, bo insertBlock
   sprawdza `canInsertBlockType` które jest cachowane i może odmawiać.
4. Reactive `useEffect` z `childClientIdsKey` jako dep — wymusza `default`
   mode na każdym nowym dziecku.

### Jak child ma wyglądać

Minimalny generyczny child — patrz `blocks/slide/index.js`:

```jsx
registerBlockType('kordit/slide', {
    edit: () => {
        const blockProps = useBlockProps({ className: 'kordit-slide' });
        const innerBlocksProps = useInnerBlocksProps(blockProps, {});
        return <div {...innerBlocksProps} />;
    },
    save: () => {
        const blockProps = useBlockProps.save();
        return <div {...blockProps}><InnerBlocks.Content /></div>;
    },
});
```

**Nic więcej.** Żadnych `allowedBlocks`, żadnego `template`, żadnego
`templateLock`. Pattern dyktuje strukturę, child jest dumb container.

**Jeżeli child ma mieć custom header** (np. `kordit/opinia` ma gwiazdki
nad paragrafami z treścią), użyj `useInnerBlocksProps` + destrukturyzuj
`children`, żeby móc wrender'ować coś DODATKOWEGO obok inner blocks:

```jsx
const { children: innerBlocksChildren, ...innerBlocksContainerProps } =
    useInnerBlocksProps(blockProps, { template: TEMPLATE });

return (
    <div {...innerBlocksContainerProps}>
        <div className="my-block__header">★★★★★</div>
        {innerBlocksChildren}
    </div>
);
```

**Dlaczego destrukturyzacja zamiast zwykłego `<InnerBlocks />`:** WP
list view używa `useInnerBlocksProps` ownership żeby znaleźć, który
blok jest "parentem" innerBlocksów. Zwykły `<InnerBlocks />` w środku
custom JSX nie aktywuje tego ownership, przez co list view pokazuje
inner blocks jako bezpośrednie dzieci parenta (flat), a wrapper jest
niewidoczny. Destrukturyzacja + spread naprawia to.

---

## 6. Gotcha — typowe pułapki

Wszystkie znalezione w prawdziwych debugowaniach. Przeczytaj zanim zaczniesz
reprodukować.

### 6.1. Server-rendered bloki trafiają w "disabled" editing mode

**Objaw:** dodanie custom `renderAppender` do InnerBlocks — appender się
nie pojawia. Albo: list view pokazuje inner blocks custom bloku jako flat
leaves pod parentem, bez wrappera.

**Przyczyna:** WP auto-ustawia `"disabled"` editing mode na bloki, które
mają `render.php` (server-side render), i propaguje to na wszystkie ich
dzieci. Bloki w disabled mode są ukrywane z list view.

**Fix:** w `edit` function wymuszaj `"default"` przez reaktywny useEffect:

```jsx
const { setBlockEditingMode } = useDispatch('core/block-editor');
const childClientIdsKey = useSelect((select) =>
    (select('core/block-editor').getBlock(clientId)?.innerBlocks || [])
        .map((b) => b.clientId).join(','),
    [clientId]
);

useEffect(() => {
    setBlockEditingMode(clientId, 'default');
    childClientIdsKey.split(',').filter(Boolean).forEach((id) =>
        setBlockEditingMode(id, 'default')
    );
}, [clientId, childClientIdsKey, setBlockEditingMode]);
```

**Dlaczego reaktywny?** Jak user doda nowe dziecko (przez toolbar "+"),
nowy clientId jest nieznany w momencie mount'u. Reaktywne śledzenie
`childClientIdsKey` jako string zapewnia, że useEffect re-fire'uje za
każdym razem gdy się zmienia lista dzieci.

### 6.2. `insertBlock` vs `replaceInnerBlocks`

**Objaw:** `wp.data.dispatch('core/block-editor').insertBlock(newBlock, index, rootClientId, false)`
— nic się nie dzieje, żaden błąd, ale blok nie jest dodany. Stan
niezmieniony.

**Przyczyna:** insertBlock wewnętrznie sprawdza `canInsertBlockType`
które ma cache per-parent. W pewnych sytuacjach (odnoszących się do
server-rendered blocków + pewnych filterów wewnętrznych Gutenberga)
ten selektor zwraca `false` dla naszych `kordit/*` dzieci nawet gdy
nie ma żadnej restrykcji `parent`/`ancestor`/`allowedBlocks`.

**Fix:** używaj `replaceInnerBlocks(rootClientId, [...clonedExisting, newBlock], false)`.
Ten selektor omija canInsertBlockType i po prostu zapisuje do store'a.

### 6.3. Block list view flattens nieuprawnione children

**Objaw:** list view w edytorze pokazuje child bloki (np. core/paragraph)
bezpośrednio pod section-sliderem, zamiast pod kordit/opinia wrapperem.

**Przyczyna:** dwa możliwe źródła:

1. Parent block ma editing mode `"disabled"` (patrz 6.1).
2. Child block używa `<InnerBlocks />` jako raw komponent, zamiast
   `useInnerBlocksProps` hooka. Gutenberg list view tracking opiera się
   o hook, raw component nie rejestruje ownership.

**Fix:** w każdym wrapping block z custom headerem używaj wzorca z 5.:

```jsx
const { children, ...props } = useInnerBlocksProps(blockProps, { template });
return <div {...props}>{header}{children}</div>;
```

### 6.4. `createBlock(name)` daje pustą skorupę

**Objaw:** klik "+" dodaje blok, ale ten blok jest pusty — nie ma
template'u ani struktury która była w innych dzieciach.

**Przyczyna:** `wp.blocks.createBlock('kordit/slide')` tworzy blok bez
innerBlocks. Template w `<InnerBlocks template={...} />` kick'uje tylko
przy FIRST render pustego blokowania — jeżeli blok został już raz
renderowany (przez addNewItem), template się nie instantiuje.

**Fix:** klonuj ostatnie istniejące dziecko zamiast create:

```jsx
const last = existing[existing.length - 1];
const newBlock = last ? wp.blocks.cloneBlock(last) : createBlock(name);
```

### 6.5. `style.css` header blokuje instalację na stock WP

**Objaw:** panel.soeasy.it instalator odmawia:
`"Wersja WordPressa to 6.9.4, wysłany motyw wymaga co najmniej wersji 7.0"`.

**Przyczyna:** `Requires at least:` w style.css był zbyt wysoki.
Musi być ≤ aktualnego stable WP (6.5 jest bezpieczne, bo theme.json v3
i `wp_theme_json_data_default` są od 6.5+).

**Fix:** trzymaj `Requires at least: 6.5` + `Tested up to: <aktualna stable>`.

### 6.6. `useBlockProps.save()` className musi pasować do `useBlockProps()` edit

**Objaw:** reload strony i block komunikat "This block contains
unexpected or invalid content". User musi kliknąć "Attempt recovery".

**Przyczyna:** `edit` nadaje `className: 'foo-bar'` przez `useBlockProps`,
ale `save` robi `useBlockProps.save()` bez className. Gutenberg porównuje
wygenerowany HTML z zapisanym HTML, widzi różnicę w klasie, wyrzuca
validation error.

**Fix:** oba wywołania muszą dostać ten sam className:

```jsx
edit: () => {
    const blockProps = useBlockProps({ className: 'foo-bar' });
    // ...
}
save: () => {
    const blockProps = useBlockProps.save({ className: 'foo-bar' });
    // ...
}
```

### 6.7. Stary `wp-content/themes/kordit/` blokuje reinstall

**Objaw:** panel.soeasy.it instalator mówi "Znaleziono katalog motywu",
ale potem "Motyw nie istnieje w oczekiwanej lokalizacji" i `wp theme list`
zwraca pusty CSV.

**Przyczyna:** poprzednia nieudana próba zostawiła `wp-content/themes/kordit/`
w częściowym stanie (np. tylko `.git/` bez `style.css`). `wp theme install`
nie nadpisuje istniejącego katalogu i silent fail'uje. Instalator glob'em
znajduje stare coś i idzie dalej, aż weryfikacja padnie na brak `style.css`.

**Fix (już zaaplikowany w `kordit-installer.php`):** przed `wp theme install`
skrypt robi `rm -rf wp-content/themes/kordit` + `rm -rf wp-content/themes/kordit-kordit-*`.

---

## 7. Build workflow

### Komendy

```bash
npm install              # raz, na początku
npm run build            # webpack + sass (full build, raz)
npm run start            # webpack watch (tylko JS/blocks)
npm run watch:styles     # sass watch (tylko theme/navigation CSS)
```

### Co webpack robi

Webpack (via `@wordpress/scripts`) automatycznie skanuje:

- `sections/**/index.js` → `build/sections/<name>/index.js` + `index.css`
- `blocks/**/index.js` → `build/blocks/<name>/index.js` + `index.css`

Każdy `index.js` importuje `editor.scss` (staje się `index.css`) i
`style.scss` (staje się `style-index.css`). `block.json` referuje build
output relatywną ścieżką `file:../../build/...`.

### Co sass robi

`npm run build:styles` kompiluje:

- `styles/theme.scss` → `styles/theme.css` (globalne style frontu)
- `styles/navigation/basic.scss` → `basic.css` (basic nav)
- `styles/navigation/mega.scss` → `mega.css` (mega menu)

Te są enqueue'owane w `inc/enqueue.php` (frontend).

### Kiedy rebuild

**Zawsze po zmianie** w:
- `blocks/**/*.{js,scss}`
- `sections/**/*.{js,scss}`
- `styles/**/*.scss`

PHP changes (patterns, render.php, inc/) **nie wymagają build** — są
wczytywane per-request.

**Workflow w Claude Code:** po każdej zmianie JS/SCSS → `npm run build`
→ reload stronę w Chrome DevTools MCP → verify.

---

## 8. Chrome DevTools MCP — workflow testowania

**Dlaczego:** development FSE bez live feedback jest koszmarem. Ten
motyw jest intensywnie testowany przez Chrome DevTools MCP żeby
iterować szybko.

### Setup

1. Otwórz Chrome, nawiguj do lokalnej instancji (np. `https://theme.kord.it/wp-admin`).
2. Zaloguj się jako admin (kordit-admin / kordit123#).
3. Otwórz `wp-admin/post.php?post=2&action=edit` (Sample Page — idealny test page).
4. W Claude Code — użyj `mcp__plugin_chrome-devtools-mcp_chrome-devtools__*`
   tools do interakcji.

### Typowe operacje

**Reload po rebuildzie:**

```
mcp__plugin_chrome-devtools-mcp_chrome-devtools__navigate_page
  type: "reload"
  ignoreCache: true
```

**Inspect block state programowo** (bez scrollowania/klikania ręcznie):

```js
mcp__plugin_chrome-devtools-mcp_chrome-devtools__evaluate_script
  function: `() => {
    const editor = wp.data.select('core/block-editor');
    const blocks = editor.getBlocks();
    // walk tree, find, inspect...
    return { ... };
  }`
```

**Sprawdź edit-mode bloków:**

```js
() => {
    const editor = wp.data.select('core/block-editor');
    const slider = editor.getBlocks().find(b => b.name === 'kordit/section-slider');
    return {
        mode: editor.getBlockEditingMode(slider.clientId),
        children: slider.innerBlocks.map(c => ({
            name: c.name,
            mode: editor.getBlockEditingMode(c.clientId),
        })),
    };
}
```

**Klik custom button w iframe** (edytor renderuje się w `iframe[name="editor-canvas"]`):

```js
() => {
    const iframe = document.querySelector('iframe[name="editor-canvas"]');
    const doc = iframe.contentDocument;
    const btn = doc.querySelector('.kordit-slider-add-btn');
    btn.click();
}
```

**Screenshot do weryfikacji wizualnej:**

```
mcp__plugin_chrome-devtools-mcp_chrome-devtools__take_screenshot
  fullPage: true
```

**Sprawdzenie list view:**

```js
async () => {
    const btn = document.querySelector('[aria-label="Przegląd dokumentu"]');
    btn.click();
    await new Promise(r => setTimeout(r, 500));
    // Expand everything
    for (let i = 0; i < 8; i++) {
        document.querySelectorAll('.block-editor-list-view__expander').forEach(e => {
            const row = e.closest('.block-editor-list-view-leaf');
            if (row?.getAttribute('data-expanded') === 'false') {
                e.dispatchEvent(new MouseEvent('click', { bubbles: true }));
            }
        });
        await new Promise(r => setTimeout(r, 100));
    }
    const rows = [...document.querySelectorAll('.block-editor-list-view-leaf')];
    return rows.map(r => ({
        label: r.querySelector('.block-editor-list-view-block-contents')?.textContent?.trim(),
        level: r.getAttribute('aria-level'),
        clientId: r.getAttribute('data-block')?.slice(0, 8),
    }));
}
```

### Rule of thumb

Po KAŻDEJ zmianie w JS/SCSS:

1. `npm run build` w shell
2. Reload stronę (ignoreCache: true)
3. Uruchom evaluate_script do sprawdzenia stanu
4. Take screenshot dla weryfikacji wizualnej
5. Jeżeli się wali — `list_console_messages` dla errorów

Ten cykl działa w <10 sekund i pozwala iterować szybko bez wychodzenia
z edytora.

---

## 9. Deployment — jak motyw trafia do klienta

1. **Commit + push na `kordit/kordit` main** (GitHub). To single source.
2. **Panel.soeasy.it/instalator** dla klienta:
   - User wybiera subdomenę (np. `nowyklient.kord.it`).
   - Instalator: `createDirectories` → `downloadWordPress` →
     `extractWordPress` → `createDatabase` → `configureWordPress` →
     `installWordPressCore` → `installTheme`.
   - `installTheme()` → `installThemeFromGitHub()`:
     - `wget` zipball z `api.github.com/repos/kordit/kordit/zipball/main`
     - `wp theme install kordit.zip`
     - `mv wp-content/themes/kordit-kordit-<sha>/ wp-content/themes/kordit/`
     - `wp theme activate kordit`
3. **(Opcjonalnie) Jeżeli `installOnServer` jest true** — instalator
   też zakłada nową instalację na `s62.mydevil.net` przez
   `ssh kordit@panel62.mydevil.net /home/kordit/bin/add-domain <domain>`,
   dostaje credentials dla repo mydevil, i klonuje repo **per-klient**
   zamiast z GitHub. W tym scenariuszu klient dostaje własny fork w
   `repo62.mydevil.net/git/priv/kordit/<klient>` do dalszych zmian.

### Co sprawdzić przy błędach instalacji

Wszystko w `wp-content/themes/crm/inc/installers/kordit-installer.php`
— tam jest pełen skrypt instalacyjny + logi. Rozszerzone logowanie
zostało dodane, pokazuje:

- Rozmiar `kordit.zip` (sanity check czy GitHub zwrócił zip a nie error JSON)
- Output z `wp theme install`
- Listę katalogów w `wp-content/themes/` przed i po
- Final check `is_dir` + `file_exists style.css`

Najczęstsze problemy:

1. **`Requires at least` za wysokie** — fix w `style.css`.
2. **Stale `wp-content/themes/kordit/`** z poprzedniej próby — instalator
   sam je teraz czyści.
3. **GitHub token wygasły** — w `kordit-installer.php::getConfig()` jest
   `github_token`, musi być valid.
4. **Rate limit GitHub** — podczas testów w pętli, token limituje.

---

## 10. Komendy na szybko

```bash
# Rebuild all
npm run build

# Watch JS/blocks
npm run start

# Watch styles
npm run watch:styles

# Commit + push
git add . && git commit -m "…" && git push origin main

# Clear WP pattern cache (na lokalnym theme.kord.it)
cd /Users/kordiansasiela/localhost/theme.kord.it/public_html && \
  wp --path=. transient delete --all

# Clear pattern cache ręcznie przez DB
wp --path=. db query "DELETE FROM wp_options WHERE option_name LIKE '%wp_theme_files_patterns%';"

# Zainspiektuj stan bloku w Chrome DevTools MCP — patrz sekcja 8
```

---

## 11. Linki

- GitHub repo: `git@github.com:kordit/kordit.git`
- Lokalny dev site: `https://theme.kord.it/` (Apache vhost w `/Users/kordiansasiela/localhost/`)
- Panel instalatora: `https://panel.soeasy.it/instalator`
- Instalator code: `/Users/kordiansasiela/localhost/panel.soeasy.it/public_html/wp-content/themes/crm/inc/installers/kordit-installer.php`

---

## 12. Reuse-first — co JUŻ JEST w motywie

**Zanim napiszesz nową klasę CSS lub nowy pattern, sprawdź czy to nie istnieje.** Cały system jest atomic — masz gotowe atomy, molekuły, custom bloki i 9 patternów. Jeśli wymyślasz coś nowego, robisz źle.

### Inwentarz (stan na dziś)

**Atomy SCSS — `styles/atoms/`:**
- `_card.scss` → `.kordit-card` (biała karta z bg+border+radius+shadow+padding)
- `_eyebrow.scss` → `.kordit-eyebrow` (small caps gold label)
- `_lede.scss` → `.kordit-lede` (lead paragraph)
- `_pill.scss` → `.kordit-pill` (badge inline)
- `_icon-chip.scss` → `.kordit-icon-chip` (badge + icon combo)
- `_photo-frame.scss`, `_photo-pin.scss` (image wrappers)
- `_accent.scss`, `_accent-line.scss` (color/border utilities)

**Molekuły SCSS — `styles/molecules/`:**
- `_section-header.scss` → `.kordit-section-header` (eyebrow + h2 + lede stack)
- `_cta-card.scss` → `.kordit-cta-card`
- `_trust-row.scss` → `.kordit-trust-row`
- `_trust-strip.scss` → `.kordit-trust-strip` (animated)

**Custom bloki — `blocks/`:**
- `card` (atom dla gridu, supports spacing.padding + color)
- `flip-card` (efekt przewracania na hover)
- `opinia` (gwiazdki + treść, child sections-grid)
- `faq` + `faq-item` (kontener `<details>` z toolbarem "+ dodaj")
- `product-grid`, `product-peek`, `selected-posts` (dynamic query)
- `slide` (dumb wrapper dla section-slider)

**Sekcje — `sections/`:**
- `basic` (kordit/section-basic — container wrapper, używane wszędzie)
- `hero`, `full-width`, `cards-grid`, `slider`

**Patterny PHP — `patterns/`:**
- `hero-import` (hero z video bg + USP)
- `kontakt`, `kontakt-faq`
- `konsultacja-call` (bezpłatna ocena z mapą)
- `proces` (workflow 12 tygodni)
- `uslugi` (Import + OEM scenariusze)
- `porownanie`, `tabela-porownawcza` (4 sposoby na zakup)
- `footer-product-cats` (dynamic footer)

**Block style variations — `inc/blocks/styles.php`:**
- `core/button`: `primary` / `accent` / `red` / `ghost` / `outline` / `catalog` / `arrow`
- `core/paragraph`: `lede`

### Czerwone flagi — kiedy psujesz motyw

| Robisz | Dlaczego źle | Co zamiast |
|---|---|---|
| Piszesz `padding: 12px` w SCSS (np. `_uslugi.scss:172`) | Hardcoded poza systemem theme.json | `var(--space-sm)` lub `style.spacing.padding` w block JSON |
| Piszesz `font-size: 0.875rem` w SCSS | To samo — bypass presets | `fontSize:"text-small"` w block comment |
| Piszesz `color: #0a1f3c` w SCSS | Color outside palette | `textColor:"primary"` w block comment |
| Tworzysz `kordit-X__card` z własną implementacją bg/border/radius | Duplikujesz `.kordit-card` z atomu | Użyj `.kordit-card` + ewentualny `kordit-X__card` jako **dodatkowa** klasa tylko dla **layoutu** (nie dla wyglądu karty) |
| Inline `style="min-height:0px"` w HTML | Block attrs powinny być w JSON | `style.dimensions.minHeight` w block JSON |
| Każda sekcja ma własny header z różnymi tagami | Niespójność wizualna między podstronami | ZAWSZE `.kordit-section-header` z molekuły |
| FAQ pominie H2 a reszta stron go ma | Złamana konwencja | Każda strona ma identyczną strukturę: eyebrow + H1/H2 + lede |
| `!important padding: 0` (8x w motywie) | Override hack zamiast naprawy | Znajdź źródło problemu (która reguła globalna nakłada padding) i zfixuj tam |

### Procedura dla każdej nowej sekcji

1. **Czy istnieje pattern który robi coś podobnego?** → Otwórz `patterns/`, znajdź najbliższy, **skopiuj jego markup** do post_content i zmień treść. Nie pisz od zera.
2. **Czy istnieje atom/molekuła która dostarcza wyglądu?** → Sprawdź listę wyżej, użyj.
3. **Czy potrzebujesz layout-specific SCSS?** → Tylko wtedy dodaj **layout-only** reguły (grid template, flex direction, max-width) w `styles/patterns/_<nazwa>.scss`. **Wygląd elementów (bg, color, padding, font) zawsze z atomów + theme.json.**
4. **Audyt po sobie:**
   ```bash
   grep -n "padding:\s*[0-9]\|font-size:\s*[0-9]\|color:\s*#" styles/patterns/_<nazwa>.scss
   ```
   Powinno być 0 wyników w nowo dodanym pliku.

### Workflow CRM → strony

CRM (owncrm-sitemap MCP) ma standaryzowane typy bloków. Dla każdego typu jest gotowy pattern w motywie:

| CRM block type | Pattern motywu (kopiuj markup) |
|---|---|
| `hero_minimal` / `hero_split` | `patterns/hero-import.php` |
| `pricing_simple` | `patterns/tabela-porownawcza.php` lub `patterns/uslugi.php` |
| `faq_accordion` | `patterns/kontakt-faq.php` (lub block `faq` + `faq-item`) |
| `cta_consultation` / `cta_contact_form` | `patterns/konsultacja-call.php` lub `patterns/kontakt.php` |
| `process_steps` / `timeline` | `patterns/proces.php` |
| `usp_*` (4 wartości / 4 liczby) | Sekcja w `patterns/uslugi.php` (cards z `.kordit-card`) |
| Comparison table | `patterns/porownanie.php` / `tabela-porownawcza.php` |

**Procedura krok-po-kroku:**

1. **Pobierz content z CRM:**
   ```
   mcp__owncrm-sitemap__get_page(lead_id=<XX>, page_slug=/<slug>)
   ```
2. **Dla każdego CRM bloku** → znajdź pattern w tabeli wyżej → **skopiuj jego block markup** (sam HTML między `<!-- wp:... -->` znacznikami) do `/tmp/<slug>-content.html`.
3. **Wymień tylko treść** (heading text, paragraph text, ikony) na tę z CRM. Nie ruszaj struktury, klas CSS ani layoutu.
4. **Sprawdź template strony** — czy używa `page-clean.html` (przezroczysty, bez wrapping containera):
   ```bash
   wp --path=. post meta get <PAGE_ID> _wp_page_template
   # Jeśli puste lub != page-clean:
   wp --path=. post meta update <PAGE_ID> _wp_page_template page-clean
   ```
5. **Push do bazy:**
   ```bash
   wp --path=. post update <PAGE_ID> /tmp/<slug>-content.html
   ```
6. **Reload strony**, sprawdź czy:
   - Padding sekcji jest spójny (nie skacze między podstronami)
   - Headery są wyrównane (left, eyebrow + H2 + lede ten sam patten)
   - Karty wyglądają jak na innych podstronach
   - Console nie ma `Block validation failed` (znaczy że block JSON jest poprawny)

**Jeśli któraś sekcja CRM nie ma odpowiednika w patternach** — to JEDYNY moment kiedy myślisz o nowym patternie. Wtedy:
- Zacznij od kopii najbliższego patternu (`cp patterns/X.php patterns/Y.php`)
- Zmień tylko layout/treść
- **Nie dodawaj nowego SCSS** — używaj istniejących atomów + theme.json
- Jeśli brakuje atomu (np. nowy element wizualny) — wyjdź z patternu, **dodaj atom do `styles/atoms/`**, potem wróć do patternu. Atom musi być reusable, nie pattern-specific.

