# Pierwszy szablon krok po kroku

## Co zbudujesz

Zbudujesz szablon **Launch Theme**: sekcję powitalną, dodawane teksty, przyciski i karty z mediami, stały podpis, wspólny nagłówek i stopkę oraz stronę kontaktową. Użytkownik zmieni tekst, kolor, zaokrąglenie i odstępy w kreatorze. Paczka zawiera słowniki PL/EN i responsywny CSS. Przykład jest kompletny, sprawdzany kompilatorem GrowSite i dostępny jako [paczka ZIP](/downloads/launch-theme.zip).

## 1. Przygotuj środowisko

Potrzebujesz dostępu do repozytorium GrowSite, Node.js zgodnego z projektem, zainstalowanych zależności npm oraz działającego API z bazą danych do końcowego sprawdzenia w panelu. Instalacja paczki jest obecnie zadaniem osoby utrzymującej projekt, nie publicznym uploadem klienta.

Z katalogu głównego repozytorium:

```bash
npm ci
```

Rozpakuj pobrany ZIP poza katalogiem zainstalowanych szablonów. W repozytorium identyczny przykład znajdziesz w `dev-site/examples/launch-theme/`. Możesz go skopiować do własnego katalogu roboczego. Nie kopiuj go bezpośrednio do katalogu zainstalowanych szablonów, jeśli chcesz sprawdzić import.

Przed importem możesz sprawdzić paczkę w [GS Loom Playground](/pl/playground/) — lokalnie, bez API i bazy danych. Uruchom `npm run loom:playground -- dev-site/examples/launch-theme`, a następnie otwórz [lokalny podgląd](http://127.0.0.1:5180/__loom).

## 2. Ustal tożsamość

Otwórz `loom.json`. Dla pierwszej próby pozostaw klucz `launch-theme`. Dla własnego produktu wybierz unikalny klucz, zmień nazwę katalogu i pole `name`. Zachowuj ten sam klucz w kolejnych wydaniach. Wersja początkowa to `1.0.0`.

## 3. Prześledź przepływ danych

`templates/home.json` tworzy instancję sekcji `hero`. Jej definicja w `sections/hero.html` odczytuje `section.settings.title`. Schema deklaruje ten identyfikator, etykietę kontrolki i wartość początkową. Karta działa analogicznie przez `block.settings`.

Zmodyfikuj domyślny tytuł oraz tekst w schema. Zmień `radius.default` w karcie z 16 na 24. W CSS zmień układ lub odstęp między kartami. Nie wpisuj tekstu na sztywno w HTML zamiast odczytu ustawienia — wtedy kontrolka nie będzie nim sterować.

Pełne pliki przykładu znajdują się niżej. Zachowaj ścieżki, rozszerzenia i osobny plik `.schema.json` dla każdej sekcji i bloku.

[Import ZIP w panelu administratora](/pl/release/#import-zip-w-panelu-administratora) wymaga `archive.json` i źródeł w `theme/`. Zobacz przykład manifestu paczki i strukturę archiwum przed przygotowaniem ZIP-a.

## 4. Zaimportuj i zbuduj katalog

Z katalogu głównego repozytorium, podając rzeczywistą ścieżkę do rozpakowanego katalogu:

```bash
npm run loom:import -- /absolute/path/launch-theme
npm run themes:build
npm run themes:check
npm run build:customer
npm run build:renderer
```

W katalogu `api` uruchom:

```bash
php artisan themes:sync --activate
```

Import odrzuca istniejący klucz. Przy kolejnych zmianach użyj procedury [aktualizacji](/pl/release/) i zwiększ wersję. Sam import nie publikuje stron klientów.

## 5. Sprawdź efekt w kreatorze

Utwórz testową witrynę z nowym szablonem. Otwórz sekcję powitalną: wartości kontrolek powinny odpowiadać schema. Zmień tekst, dodaj drugą kartę, przesuń ją, ustaw promień na 0 i wyłącz link. Zmień podpis statyczny — nie powinien mieć opcji usunięcia. Zapisz, odśwież stronę i porównaj z publicznym podglądem. Sprawdź telefon oraz oba języki słowników.

Wartości startowe treści w przykładzie są dwujęzyczne. Słowniki tłumaczą etykiety i stopkę, a nie automatycznie tekst wprowadzony przez użytkownika.

## Pliki kompletnego przykładu

Poniższa lista jest generowana bezpośrednio z paczki do pobrania. Jeśli zmieniasz pliki źródłowe przykładu, przebuduj dokumentację i ponownie uruchom jej testy.

Przepisy na konkretne elementy: [tekst](/pl/text/), [przycisk](/pl/buttons/), [pola](/pl/inputs/), [formularz](/pl/forms/) i [karta z ikoną](/pl/media/).

### assets/theme.css

```css
.launch { font-size: 16px; line-height: 1.65; }
.launch-header, .launch-footer { padding: 24px 32px; }
.launch-header { font-weight: 700; }
.launch-hero h1 { font-size: clamp(32px, 5vw, 64px); line-height: 1.1; max-width: 18ch; margin: 20px 0; }
.launch-eyebrow { text-transform: uppercase; letter-spacing: .12em; }
.launch-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); gap: 20px; margin: 32px 0; }
.launch-card { padding: 24px; border: 1px solid #d7dfd6; }
.launch-card h2 { font-size: 24px; margin-bottom: 12px; }
.launch-link { display: inline-block; padding: 10px 18px; background: #16221b; color: #fff; border-radius: 8px; margin-top: 20px; }
.launch-signature, .launch-footer { font-size: 14px; }
.launch-copy { padding: 24px; }
.launch-copy-body { white-space: pre-line; }
.launch-cta { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; min-height: 44px; align-self: start; }
.launch-cta:hover { filter: brightness(.92); }
.launch-cta:focus-visible, .launch-link:focus-visible { outline: 3px solid currentColor; outline-offset: 4px; }
.launch-card-image { display: block; width: 100%; height: auto; aspect-ratio: 8 / 5; object-fit: cover; border-radius: inherit; margin-bottom: 20px; }
.launch-card-icon { display: inline-flex; margin-bottom: 16px; }
.launch-card-icon svg { width: var(--icon-size, 32px); height: var(--icon-size, 32px); }
.launch-contact { padding: 32px; }
```

### blocks/_signature.html

```html
<p class="launch-signature" data-text-field="[[ block.settings_path ]].text" gs-text="block.settings.text"></p>
```

### blocks/_signature.schema.json

```json
{
  "name": "Signature",
  "settings": [
    {
      "type": "text",
      "id": "text",
      "label": "Text",
      "default": "Zbudowane z pomysłem / Built with care"
    }
  ]
}
```

### blocks/button.html

```html
<template gs-if="block.settings.show_link">
<a class="launch-cta" href="[[ safe_url(block.settings.url) ]]" style="background:[[ block.settings.background ]];color:[[ block.settings.color ]];border:[[ block.settings.border_width ]]px solid [[ block.settings.border_color ]];border-radius:[[ block.settings.radius ]]px;padding:[[ block.settings.padding_y ]]px [[ block.settings.padding_x ]]px;font-size:[[ block.settings.font_size ]]px">
  <span data-text-field="[[ block.settings_path ]].label" gs-text="block.settings.label"></span>
</a>
</template>
```

### blocks/button.schema.json

```json
{
  "name": "Button / Przycisk",
  "settings": [
    {
      "type": "text",
      "id": "label",
      "label": "Label / Tekst",
      "default": "Napisz do nas / Contact us"
    },
    {
      "type": "url",
      "id": "url",
      "label": "URL",
      "default": "mailto:hello@example.com"
    },
    {
      "type": "checkbox",
      "id": "show_link",
      "label": "Visible / Widoczny",
      "default": true
    },
    {
      "type": "color",
      "id": "background",
      "label": "Background / Tło",
      "default": "#16221b"
    },
    {
      "type": "color",
      "id": "color",
      "label": "Text color / Kolor tekstu",
      "default": "#ffffff"
    },
    {
      "type": "color",
      "id": "border_color",
      "label": "Border color / Kolor obramowania",
      "default": "#16221b"
    },
    {
      "type": "range",
      "id": "border_width",
      "label": "Border width / Grubość obramowania",
      "min": 0,
      "max": 8,
      "step": 1,
      "default": 0,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "radius",
      "label": "Radius / Zaokrąglenie",
      "min": 0,
      "max": 48,
      "step": 1,
      "default": 8,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "padding_y",
      "label": "Vertical padding / Wypełnienie pionowe",
      "min": 4,
      "max": 32,
      "step": 1,
      "default": 12,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "padding_x",
      "label": "Horizontal padding / Wypełnienie poziome",
      "min": 8,
      "max": 64,
      "step": 1,
      "default": 20,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "font_size",
      "label": "Text size / Rozmiar tekstu",
      "min": 12,
      "max": 32,
      "step": 1,
      "default": 16,
      "unit": "px"
    }
  ],
  "presets": [
    {
      "name": "Button / Przycisk"
    }
  ]
}
```

### blocks/card.html

```html
<article class="launch-card" data-canvas-card="[[ block.field_path ]].card" style="background:[[ block.settings.background ]];border-radius:[[ block.settings.radius ]]px">
  <h2 data-text-field="[[ block.settings_path ]].title" gs-text="block.settings.title"></h2>
  <p data-text-field="[[ block.settings_path ]].text" gs-text="block.settings.text"></p>
  <template gs-if="block.settings.show_link">
    <template gs-component="link" gs-prop-href="block.settings.url" gs-prop-label="block.settings.label"></template>
  </template>
</article>
```

### blocks/card.schema.json

```json
{
  "name": "t:blocks.card",
  "settings": [
    {
      "type": "text",
      "id": "title",
      "label": "t:settings.title",
      "default": "Karta / Card"
    },
    {
      "type": "textarea",
      "id": "text",
      "label": "t:settings.text",
      "default": "Szczegóły oferty / Offer details"
    },
    {
      "type": "color",
      "id": "background",
      "label": "t:settings.background",
      "default": "#ffffff"
    },
    {
      "type": "range",
      "id": "radius",
      "label": "t:settings.radius",
      "default": 16,
      "min": 0,
      "max": 48,
      "step": 1,
      "unit": "px"
    },
    {
      "type": "checkbox",
      "id": "show_link",
      "label": "t:settings.show_link",
      "default": true
    },
    {
      "type": "text",
      "id": "label",
      "label": "t:settings.label",
      "default": "Kontakt / Contact"
    },
    {
      "type": "url",
      "id": "url",
      "label": "URL",
      "default": "mailto:hello@example.com"
    }
  ],
  "presets": [
    {
      "name": "Card"
    }
  ]
}
```

### blocks/media-card.html

```html
<article class="launch-card" data-canvas-card="[[ block.field_path ]].card" style="background:[[ block.settings.background ]];border:[[ block.settings.border_width ]]px solid [[ block.settings.border_color ]];border-radius:[[ block.settings.radius ]]px">
  <template gs-if="block.settings.icon != &#x27;none&#x27;"><span class="launch-card-icon" style="color:[[ block.settings.icon_color ]];--icon-size:[[ block.settings.icon_size ]]px"><gs-icon name="[[ block.settings.icon ]]" field="[[ block.field_path ]].icon"></gs-icon></span></template>
  <template gs-if="block.settings.image_url != blank"><img class="launch-card-image" src="[[ safe_url(block.settings.image_url) ]]" alt="[[ block.settings.image_alt ]]" loading="lazy" width="640" height="400"></template>
  <h2 data-text-field="[[ block.settings_path ]].title" gs-text="block.settings.title"></h2>
  <p data-text-field="[[ block.settings_path ]].text" gs-text="block.settings.text"></p>
</article>
```

### blocks/media-card.schema.json

```json
{
  "name": "Media card / Karta z obrazem",
  "settings": [
    {
      "type": "text",
      "id": "title",
      "label": "Heading / Nagłówek",
      "default": "Oferta / Offer"
    },
    {
      "type": "textarea",
      "id": "text",
      "label": "Description / Opis",
      "default": "Opisz korzyść. / Describe a benefit."
    },
    {
      "type": "image_picker",
      "id": "image_url",
      "label": "Image URL / Adres obrazu",
      "default": ""
    },
    {
      "type": "text",
      "id": "image_alt",
      "label": "Alternative text / Tekst alternatywny",
      "default": ""
    },
    {
      "type": "select",
      "id": "icon",
      "label": "Icon / Ikona",
      "default": "shield",
      "options": [
        {
          "value": "none",
          "label": "None / Brak"
        },
        {
          "value": "shield",
          "label": "Shield / Tarcza"
        },
        {
          "value": "bolt",
          "label": "Bolt / Błyskawica"
        }
      ]
    },
    {
      "type": "range",
      "id": "icon_size",
      "label": "Icon size / Rozmiar ikony",
      "min": 16,
      "max": 96,
      "step": 1,
      "default": 32,
      "unit": "px"
    },
    {
      "type": "color",
      "id": "icon_color",
      "label": "Icon color / Kolor ikony",
      "default": "#16221b"
    },
    {
      "type": "color",
      "id": "background",
      "label": "Background / Tło",
      "default": "#ffffff"
    },
    {
      "type": "color",
      "id": "border_color",
      "label": "Border color / Kolor obramowania",
      "default": "#d7dfd6"
    },
    {
      "type": "range",
      "id": "border_width",
      "label": "Border width / Grubość obramowania",
      "min": 0,
      "max": 8,
      "step": 1,
      "default": 1,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "radius",
      "label": "Radius / Zaokrąglenie",
      "min": 0,
      "max": 48,
      "step": 1,
      "default": 16,
      "unit": "px"
    }
  ],
  "presets": [
    {
      "name": "Media card / Karta z obrazem"
    }
  ]
}
```

### blocks/text.html

```html
<div class="launch-copy" style="color:[[ block.settings.color ]];text-align:[[ block.settings.alignment ]]">
  <h2 data-text-field="[[ block.settings_path ]].title" style="font-size:[[ block.settings.heading_size ]]px" gs-text="block.settings.title"></h2>
  <p class="launch-copy-body" data-text-field="[[ block.settings_path ]].text" gs-text="block.settings.text"></p>
</div>
```

### blocks/text.schema.json

```json
{
  "name": "Text / Tekst",
  "settings": [
    {
      "type": "text",
      "id": "title",
      "label": "Heading / Nagłówek",
      "default": "Poznaj nas / Meet us"
    },
    {
      "type": "textarea",
      "id": "text",
      "label": "Description / Opis",
      "default": "Opowiedz swoją historię. / Tell your story."
    },
    {
      "type": "color",
      "id": "color",
      "label": "Color / Kolor",
      "default": "#16221b"
    },
    {
      "type": "range",
      "id": "heading_size",
      "label": "Heading size / Rozmiar nagłówka",
      "min": 20,
      "max": 64,
      "step": 1,
      "default": 32,
      "unit": "px"
    },
    {
      "type": "select",
      "id": "alignment",
      "label": "Alignment / Wyrównanie",
      "default": "left",
      "options": [
        {
          "value": "left",
          "label": "Left / Do lewej"
        },
        {
          "value": "center",
          "label": "Center / Do środka"
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "Text / Tekst"
    }
  ]
}
```

### config/settings_data.json

```json
{"current":{"background":"#f4f6ef","foreground":"#16221b","font":"Inter"}}
```

### config/settings_schema.json

```json
[{"name":"Appearance","settings":[{"type":"color","id":"background","label":"Background","default":"#f4f6ef"},{"type":"color","id":"foreground","label":"Text","default":"#16221b"},{"type":"font_picker","id":"font","label":"Font","default":"Inter"}]}]
```

### layout/theme.html

```html
<div class="launch" style="background:[[ settings.background ]];color:[[ settings.foreground ]];font-family:[[ settings.font ]]">
<template gs-group="header"></template>
<template gs-slot="layout"></template>
<template gs-group="footer"></template>
</div>
```

### locales/en.json

```json
{"footer":{"rights":"All rights reserved."}}
```

### locales/en.schema.json

```json
{"sections":{"hero":"Hero section"},"blocks":{"card":"Card"},"settings":{"title":"Heading","text":"Description","spacing":"Inner spacing","background":"Background","radius":"Corner radius","show_link":"Show link","label":"Link label"}}
```

### locales/pl.default.json

```json
{"footer":{"rights":"Wszelkie prawa zastrzeżone."}}
```

### locales/pl.default.schema.json

```json
{"sections":{"hero":"Sekcja powitalna"},"blocks":{"card":"Karta"},"settings":{"title":"Nagłówek","text":"Opis","spacing":"Odstęp wewnętrzny","background":"Tło","radius":"Zaokrąglenie","show_link":"Pokaż link","label":"Tekst linku"}}
```

### loom.json

```json
{
  "key": "launch-theme",
  "name": "Launch Theme",
  "version": "1.0.0",
  "defaultLocale": "pl",
  "catalog": true,
  "engine": "gs-loom",
  "engineVersion": 1
}
```

### sections/article.html

```html
<gs-blog variant="blog-detail-sidebar"></gs-blog>
```

### sections/article.schema.json

```json
{
  "name": "Article / Artykuł",
  "settings": [],
  "presets": [
    {
      "name": "Article / Artykuł"
    }
  ]
}
```

### sections/contact.html

```html
<section class="launch-contact">
  <h1 data-text-field="[[ section.settings_path ]].title" gs-text="section.settings.title"></h1>
  <p data-text-field="[[ section.settings_path ]].text" gs-text="section.settings.text"></p>
<gs-form instance-id="[[ section.settings.form_id ]]"></gs-form>
</section>
```

### sections/contact.schema.json

```json
{
  "name": "Contact / Kontakt",
  "settings": [
    {
      "type": "text",
      "id": "title",
      "label": "Heading / Nagłówek",
      "default": "Porozmawiajmy / Let's talk"
    },
    {
      "type": "textarea",
      "id": "text",
      "label": "Description / Opis",
      "default": "Dodaj formularz w kreatorze. / Add a form in the builder."
    },
    {
      "type": "text",
      "id": "form_id",
      "label": "ID formularza / Form ID",
      "default": ""
    }
  ],
  "presets": [
    {
      "name": "Contact / Kontakt"
    }
  ]
}
```

### sections/footer.html

```html
<footer class="launch-footer">© [[ date(&#x27;now&#x27;, &#x27;%Y&#x27;) ]] [[ site.name ]]. [[ t(&#x27;footer.rights&#x27;) ]]<gs-menu location="footer" label="Footer"></gs-menu></footer>
```

### sections/footer.json

```json
{"type":"footer","sections":{"main":{"type":"footer"}},"order":["main"]}
```

### sections/footer.schema.json

```json
{
  "name": "Footer",
  "settings": [],
  "presets": [
    {
      "name": "Footer"
    }
  ]
}
```

### sections/header.html

```html
<div class="launch-header">[[ site.name ]]</div>
```

### sections/header.json

```json
{"type":"header","sections":{"main":{"type":"header"}},"order":["main"]}
```

### sections/header.schema.json

```json
{
  "name": "Header",
  "settings": [],
  "presets": [
    {
      "name": "Header"
    }
  ]
}
```

### sections/hero.html

```html
<section class="launch-hero" style="padding:[[ section.settings.spacing ]]px">
  <p class="launch-eyebrow" gs-text="site.name"></p>
  <h1 data-text-field="[[ section.settings_path ]].title" gs-text="section.settings.title"></h1>
  <p data-text-field="[[ section.settings_path ]].text" gs-text="section.settings.text"></p>
  <div class="launch-grid"><template gs-slot="blocks"></template></div>
  <template gs-block="_signature" gs-id="signature"></template>
</section>
```

### sections/hero.schema.json

```json
{
  "name": "t:sections.hero",
  "settings": [
    {
      "type": "text",
      "id": "title",
      "label": "t:settings.title",
      "default": "Stwórz coś dobrego / Create something good"
    },
    {
      "type": "textarea",
      "id": "text",
      "label": "t:settings.text",
      "default": "Opisz swoją ofertę. / Introduce your business."
    },
    {
      "type": "range",
      "id": "spacing",
      "label": "t:settings.spacing",
      "min": 16,
      "max": 80,
      "step": 4,
      "default": 32,
      "unit": "px"
    }
  ],
  "blocks": [
    {
      "type": "card"
    },
    {
      "type": "text"
    },
    {
      "type": "button"
    },
    {
      "type": "media-card"
    }
  ],
  "max_blocks": 6,
  "presets": [
    {
      "name": "Hero"
    }
  ]
}
```

### sections/journal.html

```html
<section class="theme-journal">
  <gs-blog variant="blog-grid-sidebar"></gs-blog>
</section>
```

### sections/journal.schema.json

```json
{
  "name": "Blog",
  "settings": [],
  "presets": [
    {
      "name": "Blog"
    }
  ]
}
```

### snippets/link.html

```html
<a class="launch-link" href="[[ safe_url(href) ]]" gs-text="label"></a>
```

### snippets/menu.html

```html
<gs-menu location="[[ location ]]" label="[[ label ]]"></gs-menu>
```

### templates/article.json

```json
{
  "title": "Article",
  "role": "blog-details",
  "sections": {
    "journal": {
      "type": "article"
    }
  },
  "order": [
    "journal"
  ]
}
```

### templates/blog.json

```json
{
  "title": "Blog",
  "role": "blog",
  "sections": {
    "journal": {
      "type": "journal"
    }
  },
  "order": [
    "journal"
  ]
}
```

### templates/contact.json

```json
{"title":"Contact","sections":{"contact":{"type":"contact"}},"order":["contact"]}
```

### templates/home.json

```json
{"title":"Home","sections":{"hero":{"type":"hero","blocks":{"first":{"type":"card","settings":{"title":"Twój pomysł / Your idea"}},"signature":{"type":"_signature","static":true}},"block_order":["first"]}},"order":["hero"]}
```
