# Odzew: instrukcja dla agenta AI (design system → motyw lub formularz)

> **English summary for AI coding agents.** Odzew is an embeddable form service (forms are rendered by a small
> widget on the customer's site). Your job: read the design system of the project you are working in (Tailwind
> config, CSS variables, shadcn theme, MUI/Chakra/Mantine theme, design tokens, fonts) and produce **one of**:
> (A) `odzew-theme.json`: a visual theme validated by `https://api.odzew.hoxu.pl/schema/odzew-theme.json`;
> (B) `odzew-form.json`: a complete form (fields + look) validated by `https://api.odzew.hoxu.pl/schema/odzew-form.json`;
> (C) a local integration: pass the same theme to the widget in code (`theme` prop / attribute, CSS custom
> properties `--odzew-*`, or `postMessage` to the iframe) so the design system stays in this repository.
> Rules that matter most: colors only as hex / `rgb()` / `hsl()` / `oklch()` / `oklab()` with plain numbers (no
> `var()`, `url()`, `calc()`, named colors, `deg` units); `design.accent` must be `#rrggbb`; fonts may be loaded only
> from `https://fonts.googleapis.com/`; never output `notify` or `webhookUrl`; field ids match `^[a-zA-Z0-9_-]+$`.
> Validate the file against the JSON Schema, then tell the user to paste it in the Odzew panel (**Import JSON**).
> The rest of this document is in Polish; every rule, table and example below applies as written.

Ten dokument czyta agent AI (Claude Code, Cursor, Copilot, Codex…) pracujący w repozytorium klienta Odzew.
Adres serwera Odzew: `https://api.odzew.hoxu.pl`. Wszystko, czego potrzebujesz, jest tutaj i w dwóch schematach JSON:

| Plik                     | Adres                                    | Do czego                                |
| ------------------------ | ---------------------------------------- | --------------------------------------- |
| ta instrukcja            | `https://api.odzew.hoxu.pl/agent-instructions.md`     | proces, mapowanie tokenów, przykłady    |
| schemat motywu           | `https://api.odzew.hoxu.pl/schema/odzew-theme.json`   | walidacja `odzew-theme.json`            |
| schemat formularza       | `https://api.odzew.hoxu.pl/schema/odzew-form.json`    | walidacja `odzew-form.json`             |

## 1. Ustal, co masz zrobić

Zapytaj użytkownika tylko wtedy, gdy nie wynika to z polecenia:

1. **Motyw** (`odzew-theme.json`): „dopasuj wygląd formularzy Odzew do naszej strony”. Najczęstszy przypadek.
2. **Cały formularz** (`odzew-form.json`): „zrób formularz kontaktowy / wyceny / zapisu na…”. Pola + wygląd.
3. **Integracja lokalna**: „podepnij formularz `frm_…` w naszym kodzie i niech korzysta z naszego design systemu”.
   Wtedy motyw zostaje w repozytorium klienta (props `theme`, zmienne CSS), a panel Odzew nie musi go znać.

Możesz łączyć: wygeneruj `odzew-theme.json` **i** pokaż, jak użyć go lokalnie.

## 2. Odkryj design system projektu

Nie zgaduj kolorów. Znajdź źródło prawdy i zapisz, skąd wziąłeś każdą wartość (pokażesz to użytkownikowi).
Przeszukaj repozytorium w tej kolejności i zatrzymaj się na pierwszym kompletnym źródle:

1. **Tokeny designu**: `tokens.json`, `design-tokens.*`, `*.tokens.json` (W3C DTCG: `$value`, `$type`), Style
   Dictionary (`style-dictionary.config.*`, `tokens/**`), Figma Tokens / Tokens Studio (`tokens/$themes.json`).
2. **Tailwind v4**: blok `@theme { --color-*: …; --font-*: …; --radius-*: … }` w głównym CSS (często
   `app/globals.css`, `src/index.css`, `src/styles/*.css`). Szukaj: `rg -n "@theme" --glob "*.css"`.
3. **shadcn/ui**: `components.json` wskazuje plik CSS; tam `:root { --background; --foreground; --primary;
   --primary-foreground; --muted-foreground; --border; --input; --ring; --destructive; --radius }` i `.dark { … }`.
   Starsze wersje mają same liczby HSL (`--primary: 222.2 47.4% 11.2%`), nowsze `oklch(…)`.
4. **Tailwind v3**: `tailwind.config.{js,ts,cjs,mjs}` → `theme.extend.colors` (`primary`, `brand`, `accent`),
   `fontFamily.sans`, `borderRadius`. Kolory mogą wskazywać na zmienne CSS (`hsl(var(--primary))`): wtedy
   rozwiąż zmienną w pliku CSS.
5. **Zwykłe zmienne CSS**: `:root { --brand…; --color-…; --font-… }` w `*.css`, `*.scss` (`$primary`), `*.less`.
6. **Biblioteki komponentów**: MUI (`createTheme({ palette: { primary: { main } }, shape: { borderRadius },
   typography: { fontFamily } })`), Chakra (`extendTheme` / `createSystem`, `colors.brand.500`, `radii`, `fonts`),
   Mantine (`createTheme({ primaryColor, colors, radius, fontFamily })`), Bootstrap (`$primary`, `$border-radius`,
   `$font-family-sans-serif`), Vuetify (`themes.light.colors`), DaisyUI (`daisyui.themes`).
7. **Fonty**: `next/font/google` (`Inter({ subsets… })` → rodzina `Inter`), `next/font/local` (font lokalny: patrz
   niżej), `@fontsource/<rodzina>` w `package.json`, `<link href="https://fonts.googleapis.com/css2?family=…">`
   w `index.html` / `_document.tsx` / `layout.tsx`, `@font-face` w CSS.
8. **Brak systemu**: weź kolory z istniejących przycisków i formularzy (komponent `Button`, `Input`), a w
   ostateczności z logo. Powiedz użytkownikowi, że to przybliżenie.

Ustal też: język strony (`<html lang>`, teksty w komponentach), czy strona ma tryb ciemny (`.dark`,
`[data-theme=dark]`, `prefers-color-scheme`), styl pól (wypełnione tło, obramowanie, podkreślenie) i przycisków
(zaokrąglenie, pełna szerokość).

## 3. Mapowanie na tokeny Odzew

Wygląd formularza to warstwy (każda kolejna nadpisuje poprzednią):

```text
domyślne wartości widżetu
  → preset `design.theme` (clean | clay | line | night)
    → tokeny motywu projektu (ustawienia projektu w panelu; wspólne dla formularzy jednej strony)
      → design zapisany w Odzew (pola `design.*` i `design.tokens`, np. z Twojego pliku)
        → lokalny `theme` w kodzie strony (props / atrybut / postMessage)
          → zmienne CSS `--odzew-*` strony (tylko tryb inline, dziedziczą się przez Shadow DOM)
```

Motyw całej strony (wiele formularzy) najlepiej zapisać raz jako motyw projektu: tokeny wpisane w ustawieniach
projektu (sekcja „Motyw projektu”, te same klucze co `design.tokens`) działają pod każdym formularzem projektu;
formularz nadpisuje tylko to, co ma inne.

### 3.1. Pola podstawowe `design`

| Pole       | Wartości                                   | Jak wybrać                                                                                 |
| ---------- | ------------------------------------------ | ------------------------------------------------------------------------------------------ |
| `theme`    | `clean`, `clay`, `line`, `night`           | `clean`: pola z jasnym tłem bez ramki; `clay`: białe pola z ramką (najbliżej shadcn, MUI outlined, Bootstrap); `line`: tylko dolna linia (minimalistyczne, edytorialne strony); `night`: ciemny |
| `accent`   | `#rrggbb` (dokładnie 6 cyfr hex)           | kolor główny (primary / brand). Jeśli źródło jest w HSL/OKLCH, przelicz na hex tutaj, a dokładną wartość daj w `tokens.colorAccent` |
| `radius`   | liczba całkowita 0–24 (px)                 | `--radius` z shadcn (`0.5rem` = 8), `borderRadius` z MUI/Tailwind                          |
| `font`     | `grotesk`, `display`, `serif`, `system`    | preset kroju; gdy podajesz `tokens.fontFamily`, ustaw `system` (preset nie będzie pobierany) |
| `labels`   | `top`, `floating`, `hidden`                | `top` domyślnie; `floating`, jeśli strona ma pływające etykiety (Material); `hidden` tylko przy 1–2 oczywistych polach |
| `density`  | `comfy`, `compact`                         | `compact` dla gęstych interfejsów (panele, aplikacje)                                     |
| `btnWidth` | `full`, `auto`                             | `full` w wąskich kolumnach i na mobile; `auto`, jeśli przyciski strony mają szerokość treści |

### 3.2. Tokeny `design.tokens` (wszystkie opcjonalne)

Podawaj tylko te, które znasz ze źródła. Pominięty token = wartość presetu.

| Token             | Zmienna CSS (lokalnie)        | Co zmienia                                              | Format i zakres                          | Typowe źródło                         |
| ----------------- | ----------------------------- | ------------------------------------------------------- | ---------------------------------------- | ------------------------------------- |
| `colorBackground` | `--odzew-color-background`    | tło karty formularza                                    | kolor                                    | `--background`, `--card`, `bg-white`  |
| `colorSurface`    | `--odzew-color-surface`       | tło pól (input, select, textarea)                       | kolor                                    | `--input` (tło), `--background`       |
| `colorText`       | `--odzew-color-text`          | tekst, etykiety, nagłówek                               | kolor                                    | `--foreground`, `text.primary`        |
| `colorMuted`      | `--odzew-color-muted`         | opis, podpowiedzi, placeholdery                         | kolor                                    | `--muted-foreground`, `text.secondary`|
| `colorBorder`     | `--odzew-color-border`        | obramowanie pól                                         | kolor                                    | `--border`, `--input`                 |
| `colorAccent`     | `--odzew-color-accent`        | przycisk, fokus, zaznaczenia, gwiazdka wymaganych       | kolor                                    | `--primary`, `brand.500`              |
| `colorAccentText` | `--odzew-color-accent-text`   | tekst na przycisku                                      | kolor                                    | `--primary-foreground`                |
| `colorDanger`     | `--odzew-color-danger`        | komunikaty błędów, ramka błędnego pola                  | kolor                                    | `--destructive`, `error.main`         |
| `colorSuccess`    | `--odzew-color-success`       | ikona na ekranie „wysłano” (domyślnie = akcent)         | kolor                                    | `--success`, `green.600`              |
| `fontFamily`      | `--odzew-font-family`         | krój całego formularza                                  | rodziny CSS po przecinku, max 200 znaków | `--font-sans`, `fontFamily.sans`      |
| `fontStylesheet`  | (brak, to arkusz)             | arkusz fontów ładowany przez widżet                     | `https://fonts.googleapis.com/…`         | link Google Fonts, `next/font/google` |
| `fontSize`        | `--odzew-font-size`           | bazowy rozmiar tekstu (pola mają zawsze 16 px: iOS)     | liczba 12–22 (px)                        | `font-size` body                      |
| `radius`          | `--odzew-radius`              | zaokrąglenie pól (karta: radius + 6 px)                 | liczba całkowita 0–32 (px)               | `--radius`, `rounded-md`              |
| `buttonRadius`    | `--odzew-button-radius`       | zaokrąglenie przycisku (999 = pigułka)                  | liczba całkowita 0–999 (px)              | komponent `Button`                    |
| `borderWidth`     | `--odzew-border-width`        | grubość obramowania pól                                 | liczba całkowita 0–4 (px)                | `border`, `border-2`                  |
| `gap`             | `--odzew-gap`                 | odstęp pionowy między polami                            | liczba całkowita 4–48 (px)               | `space-y-*`, `gap-*` formularzy       |
| `padding`         | `--odzew-padding`             | wewnętrzny margines karty                               | liczba całkowita 0–64 (px)               | `p-6` kart (= 24)                     |
| `shadow`          | `--odzew-shadow`              | cień karty                                              | `none`, `sm`, `md`, `lg`                 | `shadow-sm`, `elevation`              |

W pliku JSON liczby są bez jednostek (`"radius": 12`). W CSS strony (integracja lokalna) zmienne przyjmują zwykłe
wartości CSS z jednostkami (`--odzew-radius: 12px`, `--odzew-shadow: 0 1px 2px rgb(0 0 0 / .1)`).

### 3.3. Kolory: dozwolony format (najczęstsze błędy)

Kolor musi pasować do wzorca ze schematu: `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `transparent` albo funkcja
`rgb|rgba|hsl|hsla|oklch|oklab(…)` zawierająca **tylko** cyfry, kropki, przecinki, spacje, `%`, `/`, `+`, `-`.

| Źródło                                   | Wpisz                                | Nie wpisuj                         |
| ---------------------------------------- | ------------------------------------ | ---------------------------------- |
| shadcn HSL `--primary: 222.2 47.4% 11.2%`| `"hsl(222.2 47.4% 11.2%)"`           | `"222.2 47.4% 11.2%"`              |
| shadcn OKLCH `oklch(0.205 0 0)`          | `"oklch(0.205 0 0)"`                 | `"var(--primary)"`                 |
| `hsl(220deg 90% 50%)`                    | `"hsl(220 90% 50%)"` (bez `deg`)     | `"hsl(220deg 90% 50%)"`            |
| `color-mix(…)`, `calc(…)`, `red`         | policz wynik i wpisz hex             | nazwy kolorów, `color-mix`         |
| Tailwind `blue-600`                      | `"#2563eb"` (wartość z palety)       | `"blue-600"`                       |
| alfa `rgb(0 0 0 / 0.5)`                  | `"rgb(0 0 0 / 0.5)"`                 | `"rgb(0 0 0 / 50%)"` jest też OK   |

`design.accent` zawsze jako `#rrggbb` (np. `oklch(0.205 0 0)` ≈ `#171717`). Sprawdź kontrast: tekst na przycisku
(`colorAccentText`) do akcentu i `colorText` do `colorBackground` powinny mieć co najmniej 4.5:1 (WCAG AA). Jeśli
brak `colorAccentText`, widżet sam wybierze biały albo ciemny tekst (tylko gdy akcent jest w hex).

### 3.4. Fonty

- Google Fonts (także `next/font/google`, `@fontsource/<rodzina>` istniejącej w Google Fonts): podaj
  `fontFamily` (np. `"Inter, ui-sans-serif, system-ui, sans-serif"`) i `fontStylesheet`
  (`https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap`: tylko grubości 400, 500, 600).
  Rodziny ze spacją zapisuj w cudzysłowie: `"\"DM Sans\", system-ui, sans-serif"`.
- Font lokalny / komercyjny (`next/font/local`, własne `@font-face`): **nie** wolno go podać jako arkusza. Podaj
  samo `fontFamily` z dobrym zapasowym krojem systemowym. W trybie inline (formularz w DOM strony) zadziała krój
  strony; w iframe zobaczysz zapasowy. Powiedz o tym użytkownikowi.
- Ustaw `"font": "system"`, gdy podajesz `tokens.fontFamily`.

## 4. Tryb ciemny

Plik motywu opisuje jeden wygląd. Gdy strona ma tryb ciemny:

- wygeneruj **dwa pliki**: `odzew-theme.json` (jasny) i `odzew-theme-dark.json` (`"theme": "night"` + ciemne
  tokeny z `.dark { … }`), a w integracji lokalnej przełączaj `theme` zależnie od trybu strony (przykład w 7.2);
- albo, przy integracji inline, zdefiniuj zmienne `--odzew-*` w CSS dla `.dark odzew-form` (przykład w 7.1).

Panel Odzew przechowuje jeden motyw na formularz: zaimportuj jasny, a ciemny trzymaj w kodzie strony.

## 5. Plik motywu `odzew-theme.json`

Struktura (tylko te klucze; nieznane klucze są odrzucane albo ignorowane):

```json
{
  "$schema": "https://api.odzew.hoxu.pl/schema/odzew-theme.json",
  "kind": "odzew.theme",
  "version": 1,
  "name": "Nazwa motywu (opcjonalnie, max 120 znaków)",
  "design": {
    "theme": "clay",
    "accent": "#2563eb",
    "radius": 8,
    "font": "system",
    "labels": "top",
    "density": "comfy",
    "btnWidth": "full",
    "tokens": { "colorAccent": "#2563eb" }
  }
}
```

Motyw **nie** zawiera treści: tytułu, wstępu, tekstu przycisku, pól ani ustawień. Wszystkie pola `design` są
opcjonalne; podaj to, co wynika z design systemu.

## 6. Cały formularz `odzew-form.json`

Gdy użytkownik opisuje formularz („formularz wyceny remontu z metrażem i terminem”), wygeneruj pełny dokument.

### 6.1. Typy pól

| `type`     | Co to jest                                   | Wartość w zgłoszeniu        | Uwagi                                                                  |
| ---------- | -------------------------------------------- | --------------------------- | ---------------------------------------------------------------------- |
| `text`     | jedna linia tekstu                           | tekst (max 500)             | imię, firma, miasto; `id` typu `name`/`company` włącza autouzupełnianie |
| `email`    | adres e-mail                                 | tekst                       | walidowany format; zwykle `required: true`                             |
| `tel`      | telefon                                      | tekst                       | cyfry, spacje, `+()-./`                                                |
| `textarea` | dłuższa wiadomość                            | tekst (max 5000)            | zawsze `width: "full"`                                                 |
| `select`   | lista rozwijana, jeden wybór                 | tekst (jedna z `options`)   | `options` 2–50; `placeholder` = tekst pustej opcji                     |
| `radio`    | jeden wybór, opcje widoczne                  | tekst (jedna z `options`)   | do 5 opcji; więcej = `select`                                          |
| `checkbox` | wiele wyborów                                | lista tekstów               | `options` 1–50; `required` = co najmniej jedna                         |
| `number`   | liczba (przecinek lub kropka)                | tekst z liczbą              | metraż, budżet, liczba osób                                            |
| `date`     | data                                         | `RRRR-MM-DD`                | termin, data wydarzenia                                                |
| `consent`  | pojedynczy checkbox zgody                    | `true` / `false`            | `label` = pełna treść zgody; RODO: `required: true`                    |
| `heading`  | nagłówek sekcji (nie zbiera danych)          | brak                        | `label` = tytuł sekcji, `help` = opis                                  |
| `url`      | adres strony                                 | tekst `http(s)://…`        | strona firmy, portfolio, link do projektu                              |
| `rating`   | ocena gwiazdkami                             | tekst `"1"`…`"N"`          | `scaleMax` 3–10 (domyślnie 5); ocena obsługi, produktu                 |
| `scale`    | skala NPS 0–10                               | tekst `"0"`…`"10"`         | `minLabel` / `maxLabel` (np. „Wcale”, „Na pewno”)                     |
| `hidden`   | ukryte pole (UTM, kampania)                  | tekst (max 500)             | `source`: `query` (+ `param`, np. `utm_source`), `referrer`, `default` |
| `file`     | załącznik (upload do R2)                     | lista `{ uploadId, name, size, type }` | `accept`, `maxSizeMb` (≤ 10), `maxFiles` (≤ 5)                   |
| `calc`     | wynik obliczenia (tylko do odczytu)          | tekst z liczbą (liczy serwer) | `expression`, `unit`; np. wycena = metraż × stawka                 |

Każde pole: `id`, `type`, `label` (wymagane) oraz opcjonalne `placeholder` (max 160), `help` (max 500),
`required` (domyślnie `false`), `width` (`full` | `half`; dwa `half` obok siebie tworzą wiersz, na wąskim ekranie
się składają), `options` (tylko `select`, `radio`, `checkbox`; każda opcja 1–160 znaków).

### 6.2. Rozbudowane formularze: logika, reguły, kroki

Wszystko poniżej jest opcjonalne; używaj tylko, gdy użytkownik tego potrzebuje (każda funkcja dociąga mały moduł
widżetu).

- **Logika warunkowa**: `showIf` (pokaż pole) i `requireIf` (wymagaj pola) = `{ "all": [warunki] }` i/lub
  `{ "any": [warunki] }`. Warunek: `{ "field": "<id wcześniejszego pola>", "op": "...", "value": ... }`;
  `op`: `eq`, `neq`, `contains` (jedna wartość), `in`, `notIn` (lista), `gt`, `lt` (liczba), `filled`, `empty`
  (bez `value`). Warunek może wskazywać **tylko pole wcześniejsze** (bez cykli). Pole ukryte nie jest walidowane ani
  zapisywane.
- **Reguły** `rules`: `min`/`max` (liczba dla `number`, liczba wyborów dla `checkbox`, przesunięcie w dniach od dziś dla
  `date`: `"min": 0` = nie wcześniej niż dziś), `minLength`/`maxLength` (tekst), `step` (krok liczby), `pattern`
  (pełne dopasowanie, max 200 znaków, **bez** odwołań wstecznych, lookbehind i zagnieżdżonych kwantyfikatorów typu
  `(a+)+`; np. kod pocztowy `[0-9]{2}-[0-9]{3}`).
- **Kroki**: `design.steps` = `[{ "title": "Zakres" }, { "title": "Kontakt" }]`, pole wybiera krok przez `step` (od 0),
  `design.progress`: `bar` (domyślnie), `steps` (tytuły) albo `none`. Dalej waliduje tylko bieżący krok.
- **`calc`**: `expression` z liczbami, `+ - * /`, nawiasami, `round(x)` / `round(x, 2)` i `id` **wcześniejszych** pól
  `number`, `rating` lub `scale` (`id` z myślnikiem w nawiasach klamrowych: `{area-m2}`). Puste pole = 0. Wynik liczy
  też serwer (wartość od klienta jest ignorowana).
- **`file`**: `accept` z listy: `image/*`, `image/jpeg`, `image/png`, `image/webp`, `image/gif`,
  `application/pdf`, `text/plain`, `text/csv`, dokumenty Word/Excel albo rozszerzenia (`.pdf`, `.docx`, `.xlsx`, …);
  domyślnie obrazy i PDF. Bez SVG/HTML/plików wykonywalnych.

### 6.3. Zasady

- `id`: 1–40 znaków `^[a-zA-Z0-9_-]+$`, unikalne, po angielsku i czytelne (`email`, `full_name`, `budget`,
  `consent_privacy`): są kluczami w eksporcie CSV i webhooku.
- Liczba pól 1–50; `label` max 300 znaków; `name` formularza 1–120 znaków.
- Teksty w języku strony (polska strona: polskie etykiety, komunikaty i przycisk).
- Tylko potrzebne pola; wymagaj minimum (zwykle e-mail lub telefon + wiadomość).
- **RODO / GDPR**: gdy formularz zbiera dane osobowe, dodaj na końcu pole `consent` z `required: true` i treścią
  w rodzaju: „Wyrażam zgodę na przetwarzanie moich danych osobowych w celu odpowiedzi na zapytanie. Administratorem
  danych jest [nazwa firmy]. Szczegóły w polityce prywatności.” Weź nazwę firmy ze strony; jeśli jej nie znasz,
  zostaw `[nazwa firmy]` i poproś użytkownika o uzupełnienie. Zgoda marketingowa to osobne, **niewymagane** pole.
- `design` (opcjonalnie): pola z punktu 3 oraz treść: `title` (max 160), `intro` (max 500), `btnText` (1–60),
  `showHead` (`true`/`false`).
- `settings` (opcjonalnie): `successTitle` (max 160), `successText` (max 500), `redirect` (`""` albo adres
  `https://…`), `honeypot` (zostaw `true`), `turnstile` (zostaw `true`), `allowedOrigins` (lista adresów stron,
  które mogą osadzać formularz, np. `["https://firma.pl"]`; pusta lista = lista z ustawień projektu, a gdy i ta
  jest pusta - dowolna strona). Plik nie wskazuje projektu: import trafia do projektu otwartego w panelu.
- `color` (opcjonalnie): kolor etykiety w panelu: `butter`, `mint`, `pink`, `sky`, `lilac`, `peach`.

Przykład wielokrokowego formularza z logiką, oceną i wyceną: punkt 11.4.

### 6.4. Formularz dwujęzyczny (polski + angielski)

Teksty w `fields`, `design` i `settings` są w **języku domyślnym**. Drugi język to nakładka
`settings.i18n.translations.<język>`; widżet pokazuje ją, gdy osadzenie ma `locale="en"`, strona ma
`<html lang="en">` albo przeglądarka woła po angielsku (inaczej język domyślny).

- `settings.i18n`: `defaultLanguage` (`pl` albo `en`), `languages` (np. `["pl", "en"]`), `translations`
  tylko dla języka innego niż domyślny.
- W nakładce: `fields` po `id` pola (`label`, `placeholder`, `help`, `minLabel`, `maxLabel`, `unit`,
  `options` jako `{ "opcja w języku domyślnym": "tłumaczenie" }`), `title`, `intro`, `btnText`, `steps`
  (tytuły w kolejności `design.steps`), `successTitle`, `successText`.
- `id` pól i wartości opcji się nie tłumaczy: trafiają do zgłoszeń, CSV i warunków logiki. Brak tłumaczenia =
  tekst w języku domyślnym.
- Formularz tylko po angielsku: teksty po angielsku i `"i18n": { "defaultLanguage": "en", "languages": ["en"] }`
  (wbudowane komunikaty, np. błędy walidacji, też będą po angielsku).

```json
{
  "$schema": "https://api.odzew.hoxu.pl/schema/odzew-form.json",
  "kind": "odzew.form",
  "version": 1,
  "name": "Kontakt (PL/EN)",
  "fields": [
    { "id": "email", "type": "email", "label": "E-mail", "required": true, "placeholder": "jan@firma.pl" },
    { "id": "topic", "type": "select", "label": "Temat", "options": ["Oferta", "Wsparcie"] },
    { "id": "message", "type": "textarea", "label": "Wiadomość", "required": true }
  ],
  "design": { "title": "Napisz do nas", "btnText": "Wyślij" },
  "settings": {
    "successTitle": "Dzięki, odezwiemy się",
    "i18n": {
      "defaultLanguage": "pl",
      "languages": ["pl", "en"],
      "translations": {
        "en": {
          "fields": {
            "email": { "label": "Email", "placeholder": "john@company.com" },
            "topic": { "label": "Topic", "options": { "Oferta": "Offer", "Wsparcie": "Support" } },
            "message": { "label": "Message" }
          },
          "title": "Get in touch",
          "btnText": "Send",
          "successTitle": "Thanks, we will get back to you"
        }
      }
    }
  }
}
```

## 7. Integracja lokalna (design system zostaje w kodzie klienta)

Widżet: `@odzew/embed` (npm) albo skrypt `https://api.odzew.hoxu.pl/embed.js`. Ten sam obiekt `theme` co `design` w pliku
motywu (bez `$schema`/`kind`). Wybierz sposób pasujący do projektu i zaproponuj gotowy kod.

### 7.1. Zmienne CSS (tryb inline, najprościej przy Tailwind/shadcn)

W trybie `inline` formularz renderuje się w Shadow DOM strony, a zmienne CSS się dziedziczą: zmienne `--odzew-*`
ustawione przez stronę **wygrywają** z motywem z panelu i z `theme`. Możesz wskazać zmienne design systemu:

```css
/* np. app/globals.css; shadcn z kolorami w oklch() */
odzew-form,
[data-odzew] {
  --odzew-color-background: var(--card);
  --odzew-color-surface: var(--background);
  --odzew-color-text: var(--foreground);
  --odzew-color-muted: var(--muted-foreground);
  --odzew-color-border: var(--input);
  --odzew-color-accent: var(--primary);
  --odzew-color-accent-text: var(--primary-foreground);
  --odzew-color-danger: var(--destructive);
  --odzew-font-family: var(--font-sans);
  --odzew-radius: calc(var(--radius) - 2px);
  --odzew-button-radius: var(--radius);
  --odzew-border-width: 1px;
}
```

(Starszy shadcn z liczbami HSL: `--odzew-color-accent: hsl(var(--primary));`.) Tutaj `var()` jest dozwolone, bo to
CSS strony, nie plik importu. Tryb ciemny działa sam, jeśli zmienne strony zmieniają się w `.dark`. Użycie:

```html
<script src="https://api.odzew.hoxu.pl/embed.js" async></script>
<odzew-form form-id="frm_TWOJE_ID" mode="inline"></odzew-form>
<!-- albo: <div data-odzew="frm_TWOJE_ID" data-mode="inline"></div> -->
```

W trybie `iframe` (domyślnym) CSS strony nie przechodzi do formularza: użyj 7.2–7.4.

### 7.2. React / Next.js: props `theme`

```tsx
'use client';
import { OdzewForm, type OdzewTheme } from '@odzew/embed/react';

const odzewTheme: OdzewTheme = {
  theme: 'clay',
  accent: '#2563eb',
  tokens: { colorAccent: '#2563eb', fontFamily: 'Inter, system-ui, sans-serif', radius: 10 },
};

export function ContactForm({ dark }: { dark?: boolean }) {
  return (
    <OdzewForm
      formId="frm_TWOJE_ID"
      baseUrl="https://api.odzew.hoxu.pl"
      theme={dark ? { ...odzewTheme, theme: 'night' } : odzewTheme}
      text={{ buttonText: 'Wyślij zapytanie' }}
      defaultValues={{ email: '' }}
    />
  );
}
```

Inne props: `mode` (`iframe` | `inline`), `text` (`title`, `intro`, `buttonText`, `successTitle`, `successText`),
`defaultValues` (po `id` pola), `hideHeader`, `locale` (`pl` | `en`), `onSubmitted`, `onError`, `onReady`,
`onResize`, a w trybie inline `onChange` i `onValidate`. Zmiana `theme`/`text`/`defaultValues` nie montuje formularza
od nowa. Komponent jest generyczny: `<OdzewForm<{ email: string }> …>` typuje `defaultValues` i `onChange`.
Najlepiej zbuduj obiekt `theme` z tych samych stałych/tokenów, z których korzysta strona (import z pliku tokenów).

### 7.3. Web component / skrypt: atrybut `theme` (JSON)

```html
<script src="https://api.odzew.hoxu.pl/embed.js" async></script>
<odzew-form
  form-id="frm_TWOJE_ID"
  theme='{"theme":"clay","accent":"#2563eb","tokens":{"colorAccent":"#2563eb","radius":10}}'
  text='{"buttonText":"Wyślij"}'
  hide-header
></odzew-form>
<!-- skrypt bez web components: data-theme, data-text, data-default-values, data-hide-header -->
<div data-odzew="frm_TWOJE_ID" data-theme='{"theme":"line"}'></div>
```

Z JS: `document.querySelector('odzew-form').theme = {...}` (to samo dla `.text`, `.defaultValues`, `.hideHeader`).
Zły JSON w atrybucie = ostrzeżenie w konsoli i wygląd z panelu.

### 7.4. Własny iframe: postMessage

Gdy strona osadza `<iframe src="https://api.odzew.hoxu.pl/f/frm_TWOJE_ID">` bez biblioteki, wyślij motyw po komunikacie `ready`:

```js
const ODZEW = 'https://api.odzew.hoxu.pl';
const frame = document.querySelector('iframe[src^="https://api.odzew.hoxu.pl/f/"]');
const theme = { theme: 'clay', tokens: { colorAccent: '#2563eb', radius: 10 } };

window.addEventListener('message', (event) => {
  if (event.origin !== new URL(ODZEW).origin || event.source !== frame.contentWindow) return;
  const msg = event.data;

  if (msg?.source === 'odzew' && msg.type === 'ready') {
    frame.contentWindow.postMessage(
      { source: 'odzew-host', type: 'configure', formId: msg.formId, theme, text: { buttonText: 'Wyślij' } },
      new URL(ODZEW).origin,
    );
  }
  if (msg?.source === 'odzew' && msg.type === 'resize') frame.style.height = `${msg.height}px`;
});
```

Iframe przyjmuje `configure` tylko od strony, która go osadza, i stosuje wyłącznie wygląd, teksty i wartości
początkowe (`values`). Pól, ustawień ani adresów nie da się w ten sposób zmienić.

## 8. Walidacja (obowiązkowo przed oddaniem pliku)

Gdy masz dostęp do terminala:

```bash
curl -fsSL https://api.odzew.hoxu.pl/schema/odzew-theme.json -o /tmp/odzew-theme.schema.json
npx -y -p ajv-cli@5 -p ajv-formats@3 ajv validate --spec=draft2020 -c ajv-formats \
  -s /tmp/odzew-theme.schema.json -d odzew-theme.json
# formularz: odzew-form.json + https://api.odzew.hoxu.pl/schema/odzew-form.json
```

Bez terminala sprawdź ręcznie:

- [ ] `kind` to `odzew.theme` albo `odzew.form`, `version` to liczba `1`, `$schema` wskazuje właściwy schemat;
- [ ] `design.accent` to `#rrggbb`; wszystkie kolory pasują do formatu z 3.3 (zero `var(`, `url(`, `deg`, nazw);
- [ ] liczby w zakresach z tabel, całkowite tam, gdzie trzeba; `shadow` z listy;
- [ ] `fontStylesheet` zaczyna się od `https://fonts.googleapis.com/`;
- [ ] formularz: unikalne `id` zgodne z wzorcem, `options` przy `select`/`radio`/`checkbox`, zgoda RODO;
- [ ] brak kluczy spoza schematu (szczególnie `notify`, `webhookUrl`, `id`/`status` formularza).

## 9. Bezpieczeństwo (zasady bezwzględne)

- Nigdy nie umieszczaj `settings.notify` ani `settings.webhookUrl`: adresy powiadomień i webhooków ustawia
  człowiek w panelu. Import i tak je odrzuca.
- W kolorach i fontach żadnych `url()`, `var()`, `expression`, `@import`, średników ani nawiasów klamrowych.
- Fonty tylko z `https://fonts.googleapis.com/`. Żadnych skryptów, obrazów ani innych zewnętrznych adresów.
- `redirect` tylko `https://` i tylko na stronę klienta.
- Nie wysyłaj kodu ani danych użytkownika nigdzie; pliki zapisz w repozytorium (np. w katalogu głównym albo
  `odzew/`) i pokaż użytkownikowi.

## 10. Przekaż wynik użytkownikowi

Napisz krótko:

1. skąd wziąłeś wartości (plik i zmienna dla każdego koloru i fontu), co przybliżyłeś;
2. gdzie zapisałeś plik i że przeszedł walidację;
3. co dalej: **„Otwórz panel Odzew → formularz → Import JSON → wklej albo wgraj plik `odzew-theme.json`”** (motyw
   zmienia tylko wygląd wybranego formularza; `odzew-form.json` tworzy nowy formularz);
4. opcjonalnie: gotowy kod integracji lokalnej z punktu 7 dopasowany do frameworka projektu.

## 11. Przykłady

### 11.1. Motyw shadcn/ui (neutral, OKLCH, Inter)

Źródło: `app/globals.css` (`:root` z shadcn), `app/layout.tsx` (`Inter` z `next/font/google`).

```json
{
  "$schema": "https://api.odzew.hoxu.pl/schema/odzew-theme.json",
  "kind": "odzew.theme",
  "version": 1,
  "name": "shadcn neutral",
  "design": {
    "theme": "clay",
    "accent": "#171717",
    "radius": 8,
    "font": "system",
    "labels": "top",
    "density": "comfy",
    "btnWidth": "full",
    "tokens": {
      "colorBackground": "oklch(1 0 0)",
      "colorSurface": "oklch(1 0 0)",
      "colorText": "oklch(0.145 0 0)",
      "colorMuted": "oklch(0.556 0 0)",
      "colorBorder": "oklch(0.922 0 0)",
      "colorAccent": "oklch(0.205 0 0)",
      "colorAccentText": "oklch(0.985 0 0)",
      "colorDanger": "oklch(0.577 0.245 27.325)",
      "fontFamily": "Inter, ui-sans-serif, system-ui, sans-serif",
      "fontStylesheet": "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap",
      "radius": 8,
      "buttonRadius": 10,
      "borderWidth": 1,
      "gap": 16,
      "shadow": "none"
    }
  }
}
```

### 11.2. Motyw marki z własnym fontem (Tailwind v4, zielony akcent, DM Sans, pigułki)

Źródło: `src/styles/app.css` (`@theme { --color-brand-600: #15803d; --font-sans: "DM Sans", … }`).

```json
{
  "$schema": "https://api.odzew.hoxu.pl/schema/odzew-theme.json",
  "kind": "odzew.theme",
  "version": 1,
  "name": "Zielarnia - jasny",
  "design": {
    "theme": "clean",
    "accent": "#15803d",
    "radius": 12,
    "font": "system",
    "labels": "floating",
    "density": "comfy",
    "btnWidth": "auto",
    "tokens": {
      "colorBackground": "#fbfaf5",
      "colorSurface": "#f1efe6",
      "colorText": "#1c2a1f",
      "colorMuted": "#5f6b61",
      "colorAccent": "#15803d",
      "colorAccentText": "#ffffff",
      "colorSuccess": "#15803d",
      "fontFamily": "\"DM Sans\", ui-sans-serif, system-ui, sans-serif",
      "fontStylesheet": "https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600&display=swap",
      "fontSize": 16,
      "radius": 12,
      "buttonRadius": 999,
      "padding": 32,
      "shadow": "sm"
    }
  }
}
```

Wariant ciemny (`odzew-theme-dark.json`): `"theme": "night"`, `colorBackground` `#0f1a12`, `colorSurface`
`#1a2a1e`, `colorText` `#e8efe9`, `colorMuted` `#9fb0a3`, `colorAccent` `#4ade80`, `colorAccentText` `#052e12`.

### 11.3. Cały formularz (zapytanie o wycenę, polska strona)

```json
{
  "$schema": "https://api.odzew.hoxu.pl/schema/odzew-form.json",
  "kind": "odzew.form",
  "version": 1,
  "name": "Wycena remontu",
  "color": "mint",
  "fields": [
    { "id": "full_name", "type": "text", "label": "Imię i nazwisko", "required": true, "width": "half" },
    { "id": "phone", "type": "tel", "label": "Telefon", "placeholder": "+48 600 000 000", "width": "half" },
    { "id": "email", "type": "email", "label": "E-mail", "required": true },
    { "id": "scope_heading", "type": "heading", "label": "Zakres prac", "help": "Im więcej szczegółów, tym dokładniejsza wycena." },
    {
      "id": "scope",
      "type": "checkbox",
      "label": "Co mamy zrobić?",
      "required": true,
      "options": ["Malowanie", "Podłogi", "Łazienka", "Kuchnia", "Elektryka"]
    },
    { "id": "area", "type": "number", "label": "Metraż (m²)", "width": "half" },
    { "id": "start_date", "type": "date", "label": "Preferowany termin rozpoczęcia", "width": "half" },
    {
      "id": "budget",
      "type": "select",
      "label": "Budżet",
      "placeholder": "Wybierz przedział",
      "options": ["do 10 000 zł", "10 000–30 000 zł", "30 000–60 000 zł", "powyżej 60 000 zł"]
    },
    { "id": "details", "type": "textarea", "label": "Opis", "placeholder": "Opisz mieszkanie i oczekiwania" },
    {
      "id": "consent_privacy",
      "type": "consent",
      "label": "Wyrażam zgodę na przetwarzanie moich danych osobowych w celu przygotowania wyceny. Administratorem danych jest [nazwa firmy]. Szczegóły w polityce prywatności.",
      "required": true
    },
    {
      "id": "consent_marketing",
      "type": "consent",
      "label": "Chcę otrzymywać informacje o promocjach (opcjonalnie)."
    }
  ],
  "design": {
    "theme": "clay",
    "accent": "#2563eb",
    "radius": 8,
    "title": "Bezpłatna wycena remontu",
    "intro": "Odpowiadamy w ciągu jednego dnia roboczego.",
    "btnText": "Poproś o wycenę",
    "tokens": { "colorAccent": "#2563eb", "buttonRadius": 8 }
  },
  "settings": {
    "successTitle": "Dziękujemy! Wycena jest w drodze",
    "successText": "Skontaktujemy się telefonicznie albo mailowo w ciągu 24 godzin.",
    "honeypot": true,
    "turnstile": true,
    "allowedOrigins": ["https://firma.pl"]
  }
}
```

### 11.4. Formularz wielokrokowy z logiką, oceną i wyceną (rozbudowany)

```json
{
  "$schema": "https://api.odzew.hoxu.pl/schema/odzew-form.json",
  "kind": "odzew.form",
  "version": 1,
  "name": "Wycena sprzątania",
  "color": "sky",
  "fields": [
    { "id": "client_type", "type": "radio", "label": "Dla kogo?", "required": true, "options": ["Firma", "Osoba prywatna"] },
    {
      "id": "company",
      "type": "text",
      "label": "Nazwa firmy",
      "required": true,
      "showIf": { "all": [{ "field": "client_type", "op": "eq", "value": "Firma" }] }
    },
    {
      "id": "nip",
      "type": "text",
      "label": "NIP",
      "help": "Potrzebny do faktury.",
      "showIf": { "all": [{ "field": "client_type", "op": "eq", "value": "Firma" }] },
      "requireIf": { "all": [{ "field": "company", "op": "filled" }] },
      "rules": { "pattern": "[0-9]{10}" }
    },
    { "id": "area", "type": "number", "label": "Powierzchnia (m²)", "required": true, "width": "half", "step": 1, "rules": { "min": 10, "max": 5000 } },
    { "id": "visits", "type": "number", "label": "Wizyt w miesiącu", "required": true, "width": "half", "step": 1, "rules": { "min": 1, "max": 30, "step": 1 } },
    { "id": "estimate", "type": "calc", "label": "Szacunkowa cena miesięczna", "expression": "round(area * visits * 0.9)", "unit": "zł", "step": 1 },
    { "id": "start_date", "type": "date", "label": "Od kiedy?", "step": 1, "rules": { "min": 1 } },
    { "id": "email", "type": "email", "label": "E-mail", "required": true, "step": 2 },
    { "id": "previous", "type": "rating", "label": "Jak oceniasz obecną firmę sprzątającą?", "scaleMax": 5, "step": 2 },
    {
      "id": "why_change",
      "type": "textarea",
      "label": "Co chcesz poprawić?",
      "step": 2,
      "showIf": { "any": [{ "field": "previous", "op": "lt", "value": 3 }] }
    },
    { "id": "utm_source", "type": "hidden", "label": "Źródło", "source": "query", "param": "utm_source" },
    {
      "id": "consent_privacy",
      "type": "consent",
      "label": "Wyrażam zgodę na przetwarzanie moich danych osobowych w celu przygotowania wyceny. Administratorem danych jest [nazwa firmy].",
      "required": true,
      "step": 2
    }
  ],
  "design": {
    "theme": "clean",
    "title": "Wycena sprzątania biura",
    "btnText": "Wyślij zapytanie",
    "steps": [{ "title": "Kim jesteś" }, { "title": "Zakres" }, { "title": "Kontakt" }],
    "progress": "steps"
  },
  "settings": { "successTitle": "Dziękujemy! Odezwiemy się z wyceną.", "honeypot": true, "turnstile": true }
}
```

## 12. Prompt do wklejenia (dla użytkownika)

Użytkownik może dać swojemu agentowi:

```text
Pobierz instrukcje z https://api.odzew.hoxu.pl/agent-instructions.md i postępuj według nich.
Na podstawie design systemu tego projektu (kolory, fonty, zaokrąglenia, tryb ciemny) przygotuj plik
odzew-theme.json dla formularzy Odzew, zwaliduj go schematem https://api.odzew.hoxu.pl/schema/odzew-theme.json
i napisz, skąd wziąłeś każdą wartość. Jeśli projekt ma tryb ciemny, przygotuj też odzew-theme-dark.json
i pokaż, jak przełączać motyw w naszym kodzie (formularz frm_TWOJE_ID).
```

Wariant „cały formularz”: zamień drugie zdanie na: „Przygotuj odzew-form.json: [opis formularza], w języku naszej
strony, z wyglądem dopasowanym do naszego design systemu i zgodą RODO.”
