# Alus Builder — AI Generation Reference (non-MCP / copy-paste)
You are generating a page for **Alus Builder** (the Alusio visual website builder). The user will
**copy your JSON and Import it** into the builder. Follow this exactly.
## OUTPUT CONTRACT — read first
Output **one JSON code block** — the minimal page schema:
```json
{
"elements": [ /* AlusElement[] — the page tree */ ]
}
```
- `elements` is the page tree (array of elements). This shape alone is enough — the Import dialog accepts it directly.
- You do **NOT** need any envelope. The `{ "source": "alusioCopiedElements", "version", "exportedAt", … }` wrapper from the builder's Export is **optional** metadata, never required — do not hand-author it.
- A single bare element node `{ "id", "type", "props", … }` and a bare array `[ {…} ]` are also accepted, but prefer `{ "elements": [...] }`.
- Do **NOT** build a global header/footer — the site adds them automatically around every page.
- Every page MUST be **mobile-responsive**: add `tablet`/`mobile` overrides — stack multi-column grids to 1 column and reduce tall section padding on phones. Standardize spacing/radius/shadow/color/font-size through `var(--theme-*)` tokens, never hardcoded values. See the Mobile & responsive + theme-token sections below.
- Output ONLY the JSON block (no commentary) unless the user asks you to explain.
> There are no tools to call here — **everything you need is in this document**: the rules, the full
> element type list, the template-type gating, and worked examples.
---
# Alus Builder — AI Generation Guide (Entry Point)
> **Read this first.** This is the thin orientation layer for generating valid `BuilderSchema`
> JSON. It does NOT repeat the full block spec — it points you at the right resource, lists the
> mistakes that actually break output, and gives you a decision tree. The detail lives in the
> resources indexed at the bottom; pull them on demand instead of guessing.
---
## 0. The 30-second contract
1. Output shape is always `{ "elements": AlusElement[] }`. Nothing else at the root.
2. Every element needs a unique `id` (short random string, e.g. `"el-a1b2"`) and a `type`.
3. Only `section` / `container` / `block` carry arbitrary `children`. `iconList` carries
`iconListItem` children only; `accordion` carries `accordionItem` children only. Nothing else
may have `children`.
4. `props` is required (can be `{}`). `style` and `selectorConfig` are optional — omit when unused.
5. **The save tool is the validator.** `pages_set_builder_schema` / `kits_set_builder_schema` /
`themes_set_template` validate server-side AND run the contrast lint in the same call, returning a
fix-list on error. Submit the schema **as an object** directly — do NOT run a separate
`validate_schema` pass first (it just doubles the payload). If the save returns errors, fix and
resubmit. Use `validate_schema` for a schema you are NOT yet saving — e.g. one you will paste into
the builder's Import Schema box; it runs the identical gate (accepts the object directly — no need
to stringify).
6. Colors/fonts/radius come from the site **Theme** via CSS vars. Use `var(--theme-*, #fallback)` —
never invent token names, always include a hex fallback.
---
## 1. Decision tree — what element do I reach for?
**Need a layout box?**
- Full-bleed band (one per logical page section) → `section` (`htmlTag: "section"`).
- Centered content wrapper inside a section → `container`. **It is ALREADY boxed + centered** at the
theme's container width (`--theme-container-width`, ~1100px) via CSS — do NOT set
`maxWidth: "1100px"` or `marginLeft/Right: "auto"` for the default. Only set `maxWidth` to make it
**narrower** (e.g. `"720px"` for a reading column) or wider; margins stay auto automatically.
- A flex/grid child cell (column, card, row item) → `block`.
- Columns side-by-side → `container`/`block` with `style.layout.display: "grid"` +
`gridTemplateColumns: "1fr 1fr"`. **Never** reach for legacy `twoColLayout`/`tabs`/`col1` — removed.
**Need text?**
- Big title → `heading` (`level` h1–h6). Inline emphasis only → `mode: "rich"` + `richText`.
- Paragraph with inline formatting / multiple blocks → `richText` (`body` = HTML).
- Plain paragraph, no toolbar needed → `basicText`.
**Need it to pull live content (product/post/page/course)?**
- → Use a **Dynamic element** (`productTitle`, `postContent`, …) OR a primitive + entity tag
(`heading` + `{product_title}`). See §3. These only resolve inside the matching **template type**
or a `queryLoop` on that data source — elsewhere they render placeholders.
**Need a repeating grid of items (recent products, blog list)?**
- → ONE template `block`/card with a **root-level `queryLoop`** (sibling of `props`, NOT inside it), inside a grid `container`. The block IS
the repeated template (repeat-self model). Descendants read the current row via entity
tags. Don't hand-emit N copies. See §4.
**Need a carousel?** → `slider` with each child a slide (or one `queryLoop` child for dynamic).
**Need a checklist / feature list?** → `iconList` > `iconListItem[]` (NOT generic blocks + icons).
**Need an FAQ?** → `accordion` > `accordionItem[]`.
When unsure which element exists, call `list_blocks` / `get_block_schema` — don't invent a type.
---
## 2. Anti-patterns — the mistakes that actually fail generation
| ❌ Don't | ✅ Do | Why |
|---|---|---|
| `children` on `heading`/`image`/`button`/etc. | Keep leaf elements childless | Validator rejects it |
| `iconListItem`/`accordionItem` at root or under `section` | Only inside `iconList`/`accordion` | Child-only; validator rejects |
| Invent a type like `"hero"`, `"card"`, `"column"` | Compose from `section`/`container`/`block` | Only registry types render |
| Literal `gap: "24px"` | `gap: "var(--theme-space-lg)"` | Literals freeze layout off the theme scale |
| `maxWidth: "1100px"` + `margin auto` on a `container` | Nothing — leave it; only set `maxWidth` to go narrower/wider | `container` is already centered at the theme width; the redundant value overrides the theme token and sticks as a default in the inspector |
| Light text with no background color set | Set `style.background.color` on the band, or dark text | The classic invisible-text bug (contrast lint) |
| Invent `--theme-brand-primary` etc. | Use names from `get_theme_tokens`, always with hex fallback | Unknown vars resolve to nothing |
| `display: "grid"` expecting 3 columns by default | Set `gridTemplateColumns` explicitly | Empty grid = single `auto` column |
| `layoutMode: "simple"` on a horizontal row / grow-flex item | `layoutMode: "advanced"` | Wrong mode hides Inspector controls |
| Dynamic element (`addToCartButton`) on a `home`/`single_page` template | Only on its compatible surface (see `get_template_guide`) | Renders placeholder otherwise |
| `pageTitle` / `pageContent` inside a **Page** | `heading` / `richText` with the copy written in | Rejected on save. They bind to the page entity and exist only on the `single_page` TEMPLATE; the Page builder hides them from the palette, so the merchant could never edit what you emitted |
| Recreating the global header/footer inside a page/template schema | Leave them out — the site renders the theme header/footer automatically | Duplicated, misaligned chrome |
| Raw HTML in a page's text `content` to fake a layout | Use `pages_set_builder_schema` (builder mode) — never the text field | The text field is not a design surface; it won't render as a designed page |
| Empty `children: []` / placeholder-less output | Fill with real placeholder content | Empty sections look broken |
| Per-page colors/fonts in `style` everywhere | Lean on theme tokens; only override intentionally | Keeps the theme cascade working |
**`layoutMode` rule of thumb:** emit `"advanced"` for any horizontal row, any grow/shrink flex item,
or any grid beyond plain columns. Emit `"simple"` only for a plain column stack or a plain
columns-grid. When in doubt → `"advanced"`.
---
## 3. Dynamic content — tags vs dedicated elements
Both are valid and coexist:
- **Dedicated Dynamic elements** (`productTitle`, `productPrice`, `postFeaturedImage`, …) — richer
Inspector controls, friendlier. Prefer these when a dedicated element exists for the field.
- **Primitive + entity tag** — `heading` with `text: "{product_title}"`, `richText` with
`{post_content}`. Handy for inline text and inside query loops.
Tags resolve against: (a) the ambient entity on a matching **template type**, or (b) the current
row inside a `queryLoop` on that data source. Outside both → placeholder text.
- Iteration metadata inside a loop: `{{loop.index}}`, `{{loop.first}}`, `{{loop.count}}`.
- The legacy data-source-agnostic `{{item.*}}` set is **removed** — do not emit it.
- Price tags (`{product_price}`, `{product_sale_price}`, `{product_original_price}`) are
**pre-formatted** in the site currency — do NOT add a `|currency` filter.
Call `get_dynamic_tags` (optionally scoped by context: product/post/course/page) for the live list,
and `list_dynamic_filters` for filters (`truncate`, `date`, `default`, `upper`, `image_url`, …).
---
## 4. Query Loop (repeat-self model)
The element holding `queryLoop` is the **template that gets repeated N times**.
🔴 **`queryLoop` sits at the ELEMENT ROOT, next to `props` / `style` / `children` — never
inside `props`.** Zod strips unknown keys, so `props.queryLoop` is accepted with a green
save and then silently dropped: the loop never runs and the block renders exactly once.
Nothing warns you. (Measured 2026-09-03; this guide and `examples/templates/shop-catalog.json`
both had it wrong, so every schema generated from them lost its loops.)
Shape: `{ enabled, dataSource, limit, orderBy, categoryId? }` — `enabled` MUST be `true`,
`dataSource` is one of `posts|products|courses|bundles|pages`, `orderBy` one of
`date_desc|date_asc|title_asc|title_desc|price_asc|price_desc`. There is no `source` key.
Typical:
```
container[display:grid, gridTemplateColumns:"repeat(3,1fr)"]
└─ block{ queryLoop: { enabled:true, dataSource:"products", limit:6, orderBy:"date_desc" } } ← repeated once per product
├─ productImage / image+{product_image}
├─ heading + {product_title}
└─ productPrice / {product_price}
```
Do not emit one block per item by hand. One template block + `queryLoop` → renderer expands it.
For a dynamic carousel: a `slider` with ONE `queryLoop` child. For masonry: a grid container with
`style.layout.masonry: true` wrapping the loop block. Call `get_data_contexts` for available sources.
---
## 5. Template type gating — what belongs where
Every page is rendered through a **template type**. Dynamic elements are gated to compatible types;
some elements are forbidden in some types. Before generating any template, call
**`get_template_kit(type)`** — ONE call returns the guide (purpose, required/recommended/forbidden,
layout, notes) + every relevant block schema + suggested patterns + critical rules. Don't fan out
into many `get_block_schema` calls.
Quick map (full detail via `get_template_guide`):
| Template type | Renders | Key dynamic elements allowed |
|---|---|---|
| `home` | Landing / homepage | Recent-items loop subset (title/image/price/excerpt) |
| `tenantPage` | **A tenant Page** (`pages_set_builder_schema`) — the ordinary case | none: write the copy straight into `heading` / `richText` |
| `single_page` | The theme TEMPLATE wrapped around every page | page* (title/content/featured image) |
| `single_post` | One blog post | post* (title/content/featured/excerpt/meta) |
| `post_archive` / `post_category` / `search` | Post lists | post* in a loop |
| `single_product` | One product | full product* set incl. addToCart, variants, quantity |
| `shop_catalog` / `product_category` | Product lists | product* loop subset |
| `single_bundle` | A product bundle | product* + checkout helpers |
| `single_course` | One course | course* + curriculum |
| `course_archive` | Course list | course* loop subset |
| `checkout` | Checkout flow | checkout* family (items/form/shipping/payment/summary) |
| `header` / `footer` | Site chrome fragments | logo, menu, searchBar, socialIcons; site_* shortcodes |
Interactive single-entity elements (`addToCartButton`, `productVariants`, `productQuantity`,
`productDescription`, `postContent`) need a single ambient entity → only on `single_*`, never in a
recent-items grid on `home`.
---
## 6. Mobile, responsive & theme tokens (REQUIRED)
### Mobile & responsive — do not skip
Every page is viewed on phones first. `style` is keyed by breakpoint and cascades:
**`desktop` is the base; `tablet` and `mobile` are partial overrides.** Breakpoints:
**tablet ≤1024px**, **mobile ≤767px**. You only repeat the keys that change.
Checklist for every page you generate:
- **Stack multi-column grids.** Any `gridTemplateColumns` with >1 column MUST add a `mobile`
override to `"repeat(1, 1fr)"`, and usually a `tablet` step too (e.g. 4→2→1, 3→2→1, 2→1).
- **Reduce big section padding on mobile.** Section `paddingTop`/`paddingBottom` of
`var(--theme-space-section)` (96) is too tall on phones — drop to `var(--theme-space-xl)` (32)
or `-lg` (24) at `style.mobile.layout`.
- **Let type auto-scale.** Set `style.desktop.typography.fontSize` and leave `tablet`/`mobile`
**empty** — it auto-scales 0.9× (tablet) / 0.85× (tablet→mobile) / 0.75× (desktop→mobile).
Only set a `mobile` fontSize for a non-proportional size. `rem`/`em` desktop values cascade
as-is (follow browser zoom, no auto-scale).
- Switch a flex `row` to `direction: "column"` on mobile when items get cramped.
```json
{
"type": "container",
"style": {
"desktop": { "layout": { "display": "grid", "gridTemplateColumns": "repeat(3, 1fr)", "gap": "var(--theme-space-xl)", "paddingTop": "var(--theme-space-section)" } },
"tablet": { "layout": { "gridTemplateColumns": "repeat(2, 1fr)" } },
"mobile": { "layout": { "gridTemplateColumns": "repeat(1, 1fr)", "paddingTop": "var(--theme-space-xl)" } }
}
}
```
### Standardize with theme tokens — don't hardcode
Pull every standardizable value from a theme token — `var(--token, #fallback)`. Always include a
fallback; never invent token names (unknown names silently fall back). Call `get_theme_tokens` for
the live values.
| Dimension (CSS) | Token family |
|---|---|
| `gap` / `rowGap` / `columnGap`, `padding*` | `--theme-space-xxs…-section` |
| `borderRadius` | `--theme-radius-button` / `-card` / `-pill` / `-sm` / `-md` |
| `boxShadow` / elevation | `--theme-shadow-xs` / `-sm` / `-md` / `-lg` |
| `fontSize` | `--theme-text-display-xl/lg/md` · `-title-lg/md` · `-body-md/sm` · `-caption` |
| `lineHeight` | `--theme-leading-tight` / `-normal` / `-relaxed` |
| `color` / `backgroundColor` / `borderColor` | `--theme-ink` / `-body` / `-muted` / `-primary` / `-on-primary` / `-canvas` / `-surface-card` / `-surface-dark` / `-on-dark` / `-hairline` |
| `fontFamily` | `--theme-heading-font` / `--theme-body-font` |
| container `maxWidth` | `--theme-container-width` |
- **Spacing scale:** `--theme-space-xxs`(4) `-xs`(8) `-sm`(12) `-md`(16) `-lg`(24) `-xl`(32)
`-xxl`(48) `-section`(96). Default body gaps → `var(--theme-space-lg)`. Literal px only for
intentionally out-of-scale values (`0px`, `2px` hairline).
- A card should pull `borderRadius: var(--theme-radius-card)` + `boxShadow: var(--theme-shadow-sm)`
+ `backgroundColor: var(--theme-surface-card, #fff)` rather than ad-hoc values.
---
## 7. Generation workflow (recommended order)
1. Identify the **surface** from the request. Building one concrete page a merchant asked for →
`tenantPage`. Building a reusable theme slot → its template type (`home` for the site landing,
`single_page` for the wrapper rendered around every page, etc.).
2. `get_template_kit(type)` — one call for guide + schemas + patterns + rules.
3. Look at a **full-page example** for the closest topic: `list_examples` → `get_example(name)`.
4. Assemble: `section` → `container` → content, top to bottom. Fill placeholders with real copy.
5. Use tokens for spacing/color; set background on dark bands.
6. **Submit directly** via the save tool (`pages_set_builder_schema` / `kits_set_builder_schema` /
`themes_set_template`) — pass the schema as an object. It validates + lints in one call.
7. If it returns errors or contrast warnings, fix and resubmit. Done when it saves clean.
(Only use `validate_schema` to preview something you are not yet saving — it runs the same gate.)
**Field caps that bite** (the Zod gate, not style advice): `style.*.background.color` and
`border.color` are capped at **50 characters** — `var(--theme-x, #hex)` fits, a long
`color-mix(in srgb, var(--theme-x, #hex) 15%, transparent)` does NOT (use `rgba()` or drop the
fallback). `boxShadow` 200, `gradientCss` 2000, `border.radius` 100, typography fields 100.
`layout.flexGrow` / `flexShrink` / `order` are **numbers**, not strings. `heading` requires `text`
even when `mode: "rich"` (keep it as the plain-text fallback next to `richText`).
### Editing an existing page: `set` vs `patch`
- **Creating a page, or replacing its whole layout** → `pages_set_builder_schema` (full schema).
- **Tweaking a page that is already builder-type** (recolor a section, add one FAQ item,
remove a block, reorder) → `pages_patch_builder_schema`. Send only the delta by element
`id` instead of regenerating the entire JSON — far less to write, much faster.
1. `pages_get` to read current element `id`s and the layout `version`.
2. Send `ops` (applied in order): `update {id, set}` (deep-merge; `null` deletes a key),
`replace {id, element}`, `insert {element, parentId?, index?|before?|after?}`,
`remove {id}`, `move {id, parentId?, …}`. A later op may reference an id an earlier op created.
3. Pass `baseVersion` = the `version` from step 1 so a concurrent edit can't be clobbered.
The patch validates the FULL resulting tree (same gates as `set`) and saves nothing if anything fails.
---
## 8. Resource index (call these, don't memorize)
| Need | Resource |
|---|---|
| Full narrative spec | `builder-spec.md` (knowledge file) |
| Machine spec (props/defaults/categories) | `builder-spec.json` + `get_block_schema(type)` |
| All types | `list_blocks` (optional `category`) |
| One element's controls | `get_element_controls(type)` / `get_control_options` |
| Per-template rules | `get_template_guide(type)` |
| One-shot template bundle | `get_template_kit(type)` |
| Section UI patterns | `list_patterns` → `get_pattern(name)` |
| **Full-page & per-template examples** | `list_examples` → `get_example(name)` |
| Theme tokens | `get_theme_tokens` |
| Dynamic tags / filters | `get_dynamic_tags`, `list_dynamic_filters` |
| Loop data sources | `get_data_contexts` |
| Nesting rules | `get_nesting_rules` |
| Typography slots / responsive | `get_typography_slots`, `get_responsive_typography_rules` |
| Header config | `get_header_settings` |
| Save (validates + lints) | `pages_set_builder_schema` / `kits_set_builder_schema` / `themes_set_template` |
| Incremental edit by id | `pages_patch_builder_schema` (update/replace/insert/remove/move; pass `baseVersion`) |
| Preview-only validate | `validate_schema` (same gate as save; accepts object or string) |
> Maintenance note: this guide is intentionally thin so it doesn't drift. Detailed truth lives in
> `template-rules.ts`, per-element `meta.ts`, and `builder-spec.json`. If a rule here contradicts
> those, those win — fix this file.
## All element types
### layout
- `section` (container) — Struktur/pembagi halaman (flex column)
- `container` (container) — Kontainer 1100px terpusat
- `block` (container) — Flexbox lebar penuh (flex column)
### checkout
- `checkoutItems` — Daftar produk dalam keranjang
- `checkoutShipping` — Pilihan metode pengiriman
- `checkoutPayment` — Pilihan metode pembayaran
- `checkoutMessages` — Tempat semua pesan sistem checkout tampil — wajib ada di tiap template
### dynamic
- `postTitle` — Judul post — terisi otomatis dari konten saat halaman dirender.
- `postContent` — Isi konten post — terisi otomatis dari konten saat halaman dirender.
- `postFeaturedImage` — Gambar utama post — terisi otomatis dari konten.
- `postExcerpt` — Ringkasan singkat post — fall back ke trim konten kalau excerpt kosong.
- `postMeta` — Penulis, tanggal, kategori — komposit dengan separator.
- `productTitle` — Nama produk — terisi otomatis dari konten saat halaman dirender.
- `productPrice` — Harga produk — otomatis tampilkan harga sale jika sedang berlaku.
- `productDescription` — Deskripsi produk — terisi otomatis dari konten.
- `productImage` — Thumbnail produk — single image (untuk gallery, pakai Product Image Gallery).
- `productVariants` — Pemilih varian produk (pills / dropdown / list) — sinkron dengan Add to Cart & Quantity.
- `productPlans` — Pemilih paket untuk produk digital (multiple plans) — sinkron dengan Add to Cart.
- `productQuantity` — Stepper qty (− 1 +) untuk Add to Cart. Otomatis mengikuti stok varian.
- `productCustomerNote` — Input catatan opsional dari customer. Nilainya ikut ke keranjang saat Add to Cart.
- `addToCartButton` — Tombol Add to Cart / Buy Now. Sinkron dengan varian & qty terpilih; di dalam Query Loop jadi tambah-langsung atau tautan ke halaman produk.
- `productStock` — Indikator status stok (Tersedia / Stok Terbatas / Habis). Bisa diatur tampil selalu atau hanya saat stok habis. Otomatis update saat varian dipilih.
- `productSku` — Kode SKU produk — otomatis update saat varian dipilih.
- `productSalePercent` — Badge diskon — "−25%" atau "Hemat Rp 50.000". Otomatis tersembunyi jika tidak sedang sale.
- `productSalePrice` — Harga diskon saja — otomatis tersembunyi jika produk tidak sedang sale.
- `productRegularPrice` — Harga asli (sebelum diskon) — bisa dicoret otomatis saat produk sedang sale.
- `productCountdown` — Countdown sampai jadwal sale produk berakhir. Otomatis tersembunyi jika produk tidak punya sale aktif.
- `productShare` — Tombol share produk ke WhatsApp, Facebook, Twitter/X, Telegram, atau salin link.
- `productWholesaleTable` — Tabel harga grosir per kuantitas. Otomatis update sesuai varian terpilih.
- `pageTitle` — Judul halaman — terisi otomatis dari konten saat halaman dirender.
- `pageContent` — Isi konten halaman — terisi otomatis dari konten saat halaman dirender.
- `pageFeaturedImage` — Gambar utama halaman — terisi otomatis dari konten.
- `courseTitle` — Nama course — terisi otomatis saat halaman dirender.
- `courseDescription` — Deskripsi course — terisi otomatis dari konten.
- `courseImage` — Thumbnail course — terisi otomatis dari konten.
- `coursePrice` — Harga kursus dari produk tertautnya — sadar diskon, gratis, dan belum tersedia.
- `courseCurriculum` — Daftar modul & materi kursus langsung dari database, lengkap dengan status terkunci.
- `courseEnroll` — Tombol aksi kursus — menyesuaikan otomatis: mulai, lanjut, daftar gratis, beli, atau login.
### basic
- `heading` — Judul teks (H1 - H6)
- `basicText` — Paragraf teks polos tanpa toolbar
- `richText` — Konten teks dengan heading
- `video` — Embed video YouTube, Vimeo, atau Bunny.net
- `image` — Gambar (URL atau dari Media Library)
- `button` — Tombol dengan link
- `textLink` — Teks dengan hyperlink
- `icon` — Tampilkan satu icon dengan opsi link, frame, atau background
- `divider` — Garis pemisah antar konten
- `spacer` — Ruang kosong vertikal
### general
- `slider` (container) — Carousel dengan panah & dot — tiap anak jadi slide
- `logo` — Logo situs — auto dari Settings, atau image/text kustom
- `accordion` (container) — Konten expandable dengan pertanyaan & jawaban
- `breadcrumb` — Trail navigasi: Home > Section > Page (auto JSON-LD)
- `counter` — Angka statistik dengan animasi count-up saat masuk viewport
- `iconList` (container) — Daftar item (Item List) dengan icon, title, deskripsi & highlight badge
- `countdownTimer` — Hitung mundur menuju tanggal & waktu tertentu
- `alertBanner` — Kotak notifikasi atau pengumuman (info/success/warning/error)
- `mapEmbed` — Embed Google Maps untuk lokasi toko
- `searchBar` — Form pencarian — inline, overlay, atau icon expand.
- `menu` (container) — Header all-in-one: logo, items dengan dropdown/mega, plus tombol Login/Register/Account/Cart
- `htmlEmbed` — Embed kode HTML kustom atau dari AI
- `cart` — Icon keranjang + panel cart slide-in (off-canvas kanan)
- `miniCheckout` — Checkout compact 1 produk/bundle di landing page
- `accordionItem` (container) — Satu panel di dalam Accordion — punya judul + konten nested (drop zone)
- `iconListItem` — Satu item di dalam Icon List — punya icon, title, deskripsi & style lengkap
- `brand` — Logo / nama brand di dalam Navigation
- `menuItem` (container) — Satu item navigasi di dalam Navigation (bisa submenu / mega)
- `customMega` (container) — Dropdown mega berisi element bebas, di dalam sebuah Menu Item
- `menuAction` — Satu action header (login / cart / search / dll) di dalam Navigation
### ecommerce
- `productImageGallery` — Galeri foto produk (Featured + Gallery)
- `cartItems` — Daftar item keranjang dengan ubah jumlah & hapus
- `cartSummary` — Subtotal keranjang + tombol lanjut ke checkout
### forms
- `nameField` — Input nama lengkap
- `emailField` — Input email address
- `phoneField` — Nomor telepon atau WhatsApp
- `textareaField` — Input teks panjang
- `selectField` — Pilihan dari daftar opsi
- `checkboxField` — Centang persetujuan
- `customHtml` — Raw HTML block
- `submitButton` — Tombol submit formulir
## Element reference — props for every type
Use these to build with the FULL element set (not just section/container/block/heading). Each entry lists the element's `props`. **Layout/visual CSS** (display, gap, padding, color, background, border, …) does NOT go here — it lives in `style.{bp}.layout.*` / `style.{bp}.typography.*` etc. (see the guide above). Dynamic elements (productTitle, postContent, …) only resolve in their compatible template type or a query loop.
### layout
#### `section` · can have children
Struktur/pembagi halaman (flex column)
Props:
- `htmlTag` — select "div"|"section"|"article"|"aside"|"nav"|"header"|"footer"|"main"|"a"|"figure"|"address"|"ul"|"ol"|"li" — HTML Tag — default: "section"
- `layoutMode` — radio "simple"|"advanced" — default: "simple"
#### `container` · can have children
Kontainer 1100px terpusat
Props:
- `htmlTag` — select "div"|"section"|"article"|"aside"|"nav"|"header"|"footer"|"main"|"a"|"figure"|"address"|"ul"|"ol"|"li" — HTML Tag — default: "div"
- `layoutMode` — radio "simple"|"advanced" — default: "simple"
#### `block` · can have children
Flexbox lebar penuh (flex column)
Props:
- `htmlTag` — select "div"|"section"|"article"|"aside"|"nav"|"header"|"footer"|"main"|"a"|"figure"|"address"|"ul"|"ol"|"li" — HTML Tag — default: "div"
- `layoutMode` — radio "simple"|"advanced" — default: "simple"
### checkout
#### `checkoutItems`
Daftar produk dalam keranjang
Props:
- `showTitle` — checkbox — Tampilkan Judul
- `title` — optional string (default: 'Pesanan Kamu')
- `titleTag` — select "h1"|"h2"|"h3"|"h4"|"h5"|"h6" — Tag HTML — default: "h3"
- `showImage` — boolean (default: true)
- `showQuantity` — boolean (default: true)
#### `checkoutShipping`
Pilihan metode pengiriman
Props:
- `showTitle` — checkbox — Tampilkan Judul
- `title` — optional string (default: 'Metode Pengiriman')
- `titleTag` — select "h1"|"h2"|"h3"|"h4"|"h5"|"h6" — Tag HTML — default: "h3"
#### `checkoutPayment`
Pilihan metode pembayaran
Props:
- `showTitle` — checkbox — Tampilkan Judul
- `title` — optional string (default: 'Metode Pembayaran')
- `titleTag` — select "h1"|"h2"|"h3"|"h4"|"h5"|"h6" — Tag HTML — default: "h3"
#### `checkoutMessages`
Tempat semua pesan sistem checkout tampil — wajib ada di tiap template
Props:
- `showIcon` — optional boolean (default: true)
### dynamic
#### `postTitle`
Judul post — terisi otomatis dari konten saat halaman dirender.
Props:
- `level` — 'h1'|'h2'|'h3'|'h4'|'h5'|'h6' (default: 'h1')
- `alignment` — 'left'|'center'|'right' (default: 'left')
- `linkToPost` — boolean (default: false)
#### `postContent`
Isi konten post — terisi otomatis dari konten saat halaman dirender.
Props:
- `plainText` — boolean — strip HTML (default: false)
#### `postFeaturedImage`
Gambar utama post — terisi otomatis dari konten.
Props:
- `aspectRatio` — 'auto'|'16:9'|'4:3'|'1:1'|'3:2'|'21:9'
- `fit` — 'cover'|'contain'
- `preset` — 'cardMedium'|'cardWide'|'hero'|'thumbnail'
- `linkToPost` — boolean
- `fallbackBehavior` — select "hide"|"placeholder"|"image" — Jika gambar kosong — default: "hide"
- `fallbackImageUrl` — image — Gambar fallback
#### `postExcerpt`
Ringkasan singkat post — fall back ke trim konten kalau excerpt kosong.
Props:
- `maxLength` — number — 0 disables truncation
- `fallbackToContent` — boolean
- `ellipsis` — string
#### `postMeta`
Penulis, tanggal, kategori — komposit dengan separator.
Props:
- `showAuthor` — boolean
- `showDate` — boolean
- `showCategory` — boolean
- `dateFormat` — 'long'|'short'|'relative'
- `separator` — ' • '|' | '|' / '|' — '
- `alignment` — 'left'|'center'|'right'
#### `productTitle`
Nama produk — terisi otomatis dari konten saat halaman dirender.
Props:
- `level` — 'h1'-'h6'
- `alignment` — 'left'|'center'|'right'
- `linkToProduct` — boolean
#### `productPrice`
Harga produk — otomatis tampilkan harga sale jika sedang berlaku.
Props:
- `showOriginalOnSale` — boolean
- `saleColorScheme` — 'primary'|'destructive'|'inherit'
- `alignment` — 'left'|'center'|'right'
- `size` — 'sm'|'md'|'lg'|'xl'
- `outOfStockLabel` — string (shown in place of price when the selected variant is sold out)
- `variantPriceDisplay` — 'auto'|'base' (auto = min–max range before selection; base = always base price)
- `showShippingDiscountBadge` — checkbox — Tampilkan badge Potongan Ongkir
#### `productDescription`
Deskripsi produk — terisi otomatis dari konten.
Props:
- `plainText` — boolean
#### `productImage`
Thumbnail produk — single image (untuk gallery, pakai Product Image Gallery).
Props:
- `aspectRatio` — 'auto'|'1:1'|'4:3'|'3:2'|'16:9'
- `fit` — 'cover'|'contain'
- `preset` — 'productMain'|'cardMedium'|'cardWide'|'thumbnail'|'hero'
- `linkToProduct` — boolean
#### `productVariants`
Pemilih varian produk (pills / dropdown / list) — sinkron dengan Add to Cart & Quantity.
Props:
- `style` — 'pills'|'dropdown'|'list'|'grouped'
- `label` — string (empty hides)
- `showPrice` — boolean
- `showStockState` — boolean
- `alignment` — DEPRECATED legacy — for positioning set style.layout.alignSelf ('Align Self': flex-start|center|flex-end|stretch). Pre-migration data only; do NOT seed on new elements.
#### `productPlans`
Pemilih paket untuk produk digital (multiple plans) — sinkron dengan Add to Cart.
Props:
- `style` — 'cards'|'list'
- `label` — string (empty hides)
- `alignment` — DEPRECATED legacy — for positioning set style.layout.alignSelf ('Align Self': flex-start|center|flex-end|stretch). Pre-migration data only; do NOT seed on new elements.
#### `productQuantity`
Stepper qty (− 1 +) untuk Add to Cart. Otomatis mengikuti stok varian.
Props:
- `label` — string
- `minQuantity` — number
- `maxQuantity` — number (0 = no cap, defer to stock)
- `size` — 'sm'|'md'|'lg'
- `alignment` — DEPRECATED legacy — for positioning set style.layout.alignSelf ('Align Self': flex-start|center|flex-end|stretch). Pre-migration data only; do NOT seed on new elements.
#### `productCustomerNote`
Input catatan opsional dari customer. Nilainya ikut ke keranjang saat Add to Cart.
Props:
- `label` — string
- `placeholder` — string
- `maxLength` — number (1-1000)
- `rows` — number (textarea rows)
#### `addToCartButton`
Tombol Add to Cart / Buy Now. Sinkron dengan varian & qty terpilih; di dalam Query Loop jadi tambah-langsung atau tautan ke halaman produk.
Props:
- `label` — string
- `mode` — 'cart'|'buy-now'
- `showIcon` — boolean
- `iconName` — Lucide icon name (default 'ShoppingCart')
- `size` — 'sm'|'md'|'lg'
- `variant` — 'primary'|'secondary'|'outline'
- `outOfStockLabel` — string
- `needsSelectionLabel` — string
- `needsPlanLabel` — string (digital multi-plan)
- `subscribeLabel` — string (subscription)
- `contactLabel` — string (service/booking)
- `preorderLabel` — string - label saat produk dalam mode pre-order (tombol TETAP aktif)
- `comingSoonLabel` — string - label sebelum jendela pre-order rilis dibuka
- `soldOutLabel` — string - label saat kuota pre-order event habis
- `chooseOptionsLabel` — string - label saat tombol ada DI DALAM kartu Query Loop dan produknya butuh pilihan (varian / paket digital / PWYW / langganan / service). Dalam keadaan itu tombol berubah jadi TAUTAN ke halaman produk, bukan tombol mati (default: 'Pilih Opsi').
- `width` — DEPRECATED legacy — for sizing set style.layout.width ('fit-content' for Auto, '100%' for Full, or any length for Custom). Defaults to Full. Pre-migration data only; do NOT seed on new elements.
- `alignment` — DEPRECATED legacy — for positioning set style.layout.alignSelf ('Align Self': flex-start|center|flex-end|stretch). Pre-migration data only; do NOT seed on new elements.
#### `productStock`
Indikator status stok (Tersedia / Stok Terbatas / Habis). Bisa diatur tampil selalu atau hanya saat stok habis. Otomatis update saat varian dipilih.
Props:
- `displayMode` — 'always'|'lowAndOut'|'outOfStockOnly' - kapan badge tampil. always = selalu; lowAndOut = hanya saat stok menipis/habis; outOfStockOnly = hanya saat habis. Keadaan pre-order yang memblokir pembelian (comingSoon/soldOut) tetap tampil di semua mode.
- `inStockLabel` — string
- `outOfStockLabel` — string
- `lowStockLabel` — string
- `lowStockThreshold` — number (0 = disabled)
- `showCount` — boolean
- `variant` — 'plain'|'badge'|'dot'
- `size` — 'sm'|'md'|'lg'
- `preorderLabel` — string - label saat produk dalam mode pre-order
- `comingSoonLabel` — string - label sebelum jendela pre-order rilis dibuka
- `soldOutLabel` — string - label saat kuota pre-order event habis
- `showPreorderEta` — boolean - sambung estimasi kirim ke label saat pre-order
- `alignment` — DEPRECATED legacy — for positioning set style.layout.alignSelf ('Align Self': flex-start|center|flex-end|stretch). Pre-migration data only; do NOT seed on new elements.
#### `productSku`
Kode SKU produk — otomatis update saat varian dipilih.
Props:
- `prefix` — string (e.g. 'SKU:')
- `hideIfEmpty` — boolean
- `size` — 'sm'|'md'|'lg'
- `alignment` — DEPRECATED legacy — for positioning set style.layout.alignSelf ('Align Self': flex-start|center|flex-end|stretch). Pre-migration data only; do NOT seed on new elements.
#### `productSalePercent`
Badge diskon — "−25%" atau "Hemat Rp 50.000". Otomatis tersembunyi jika tidak sedang sale.
Props:
- `showAs` — 'percent'|'amount'|'both'
- `variant` — 'badge'|'pill'|'plain'
- `amountPrefix` — string
- `hideWhenNoSale` — boolean
- `size` — 'sm'|'md'|'lg'
- `alignment` — DEPRECATED legacy — for positioning set style.layout.alignSelf ('Align Self': flex-start|center|flex-end|stretch). Pre-migration data only; do NOT seed on new elements.
#### `productSalePrice`
Harga diskon saja — otomatis tersembunyi jika produk tidak sedang sale.
Props:
- `alignment` — 'left'|'center'|'right'
- `size` — 'sm'|'md'|'lg'|'xl'
- `hideWhenNoSale` — boolean
- `outOfStockLabel` — string (shown in place of price when the selected variant is sold out)
- `variantPriceDisplay` — 'auto'|'base' (auto = min–max range before selection; base = always base price)
#### `productRegularPrice`
Harga asli (sebelum diskon) — bisa dicoret otomatis saat produk sedang sale.
Props:
- `alignment` — 'left'|'center'|'right'
- `size` — 'sm'|'md'|'lg'|'xl'
- `strikethrough` — 'onSale'|'always'|'never'
- `hideWhenNoSale` — boolean
- `variantPriceDisplay` — 'auto'|'base' (auto = min–max range before selection; base = always base price)
#### `productCountdown`
Countdown sampai jadwal sale produk berakhir. Otomatis tersembunyi jika produk tidak punya sale aktif.
Props:
- `label` — string
- `variant` — 'compact'|'boxes'|'inline'
- `showDays` — boolean
- `onExpired` — 'hide'|'showText'
- `expiredText` — string
- `labelDays` — string
- `labelHours` — string
- `labelMinutes` — string
- `labelSeconds` — string
- `alignment` — DEPRECATED legacy — for positioning set style.layout.alignSelf ('Align Self': flex-start|center|flex-end|stretch). Pre-migration data only; do NOT seed on new elements.
#### `productShare`
Tombol share produk ke WhatsApp, Facebook, Twitter/X, Telegram, atau salin link.
Props:
- `label` — string
- `platforms` — Array<'whatsapp'|'facebook'|'twitter'|'copy'|'telegram'|'linkedin'>
- `variant` — 'icons'|'labeled'|'compact'
- `iconSize` — number (px)
- `shareText` — string template with {name} {url}
- `alignment` — DEPRECATED legacy — for positioning set style.layout.alignSelf ('Align Self': flex-start|center|flex-end|stretch). Pre-migration data only; do NOT seed on new elements.
#### `productWholesaleTable`
Tabel harga grosir per kuantitas. Otomatis update sesuai varian terpilih.
Props:
- `title` — string
- `layout` — 'table'|'cards'|'inline'
- `showSavings` — boolean
- `qtyFormat` — 'min+'|'min'
- `qtySuffix` — string (e.g. 'pcs')
- `hideWhenNoTiers` — boolean
- `alignment` — DEPRECATED legacy — for positioning set style.layout.alignSelf ('Align Self': flex-start|center|flex-end|stretch). Pre-migration data only; do NOT seed on new elements.
#### `pageTitle`
Judul halaman — terisi otomatis dari konten saat halaman dirender.
Props:
- `level` — 'h1'-'h6'
- `alignment` — 'left'|'center'|'right'
#### `pageContent`
Isi konten halaman — terisi otomatis dari konten saat halaman dirender.
Props:
- `plainText` — boolean
#### `pageFeaturedImage`
Gambar utama halaman — terisi otomatis dari konten.
Props:
- `aspectRatio` — 'auto'|'16:9'|'4:3'|'1:1'|'3:2'|'21:9'
- `fit` — 'cover'|'contain'
- `preset` — 'cardMedium'|'cardWide'|'hero'|'thumbnail'
- `fallbackBehavior` — select "hide"|"placeholder"|"image" — Jika gambar kosong — default: "hide"
- `fallbackImageUrl` — image — Gambar fallback
#### `courseTitle`
Nama course — terisi otomatis saat halaman dirender.
Props:
- `level` — 'h1'-'h6'
- `alignment` — 'left'|'center'|'right'
#### `courseDescription`
Deskripsi course — terisi otomatis dari konten.
Props:
- `plainText` — boolean
#### `courseImage`
Thumbnail course — terisi otomatis dari konten.
Props:
- `aspectRatio` — 'auto'|'16:9'|'4:3'|'1:1'|'3:2'
- `fit` — 'cover'|'contain'
- `preset` — 'cardMedium'|'cardWide'|'hero'|'thumbnail'
#### `coursePrice`
Harga kursus dari produk tertautnya — sadar diskon, gratis, dan belum tersedia.
Props:
- `freeLabel` — string — shown for public/free_enrollment courses
- `unavailableLabel` — string — shown when accessType is 'paid' but no active linked product sells it
- `showOriginalOnSale` — boolean — strikethrough original price next to the sale price
- `showDiscountBadge` — boolean — '-40%' pill
- `hideWhenEnrolled` — boolean — hide entirely once the viewer is enrolled
- `size` — 'sm'|'md'|'lg'|'xl'
#### `courseCurriculum`
Daftar modul & materi kursus langsung dari database, lengkap dengan status terkunci.
Props:
- `defaultExpanded` — 'all'|'first'|'none' — which sections start open
- `showLessonType` — boolean — per-lesson icon (video/text/file/quiz)
- `showDuration` — boolean — lesson duration mm:ss, hidden when absent
- `showLockState` — boolean — lock icon + muted styling on inaccessible lessons
- `showPreviewBadge` — boolean — 'Preview' pill on free-preview lessons
- `showLessonCount` — boolean — '{n} materi' next to the section title
#### `courseEnroll`
Tombol aksi kursus — menyesuaikan otomatis: mulai, lanjut, daftar gratis, beli, atau login.
Props:
- `startLabel` — string — public course
- `continueLabel` — string — already enrolled
- `enrollLabel` — string — free enrollment
- `buyLabel` — string — paid, logged in
- `loginLabel` — string — paid, logged out
- `unavailableLabel` — string — paid but no active linked product
- `pendingLabel` — string — shown while enrolment/checkout is in flight
- `size` — 'sm'|'md'|'lg'
- `fullWidth` — boolean
### basic
#### `heading`
Judul teks (H1 - H6)
Props:
- `text` — string — heading text (used when mode='plain')
- `level` — 'h1'|'h2'|'h3'|'h4'|'h5'|'h6' (default: 'h2')
- `mode` — 'plain'|'rich' (default: 'plain'). Rich enables inline HTML formatting per word.
- `alignment` — DEPRECATED legacy text-align — for positioning set style.layout.alignSelf ('Align Self': flex-start|center|flex-end|stretch). Pre-migration data only; do NOT seed on new headings.
- `link` — optional URL string
- `richText` — optional inline-only HTML string (span/strong/em/u/s/a + style color). Used when mode='rich'. No block tags.
#### `basicText`
Paragraf teks polos tanpa toolbar
Props:
- `text` — string — plain text (newlines render as )
- `tag` — 'div'|'p'|'span'|'figcaption'|'address'|'figure' (default: 'p')
- `link` — optional URL string — wraps text content as
- `wordsLimit` — optional integer — trim text to N words
- `readMore` — optional string — appended after trimmed text when wordsLimit is set
#### `richText`
Konten teks dengan heading
Props:
- `body` — string — HTML content
#### `video`
Embed video YouTube, Vimeo, atau Bunny.net
Props:
- `url` — string — YouTube URL
- `aspectRatio` — '16/9'|'4/3'|'1/1' (default: '16/9')
- `title` — optional string
#### `image`
Gambar (URL atau dari Media Library)
Props:
- `src` — string — image URL or shortcode
- `shape` — customRenderer — Bentuk
- `aspectRatio` — optional 'auto'|'1:1'|'4:3'|'3:2'|'16:9' (default: 'auto') — crops image to a fixed ratio
- `fit` — optional 'cover'|'contain' (default: 'cover') — object-fit when aspectRatio is not 'auto'
- `linkType` — select "none"|"url"|"whatsapp" — Tipe Link — default: "none"
- `htmlTag` — select "none"|"figure"|"div"|"custom" — HTML Tag — default: "none"
- `metaTrackingEnabled` — checkbox — Aktifkan Meta Pixel — default: false
- `tiktokTrackingEnabled` — checkbox — Aktifkan TikTok Pixel — default: false
- `alt` — optional string — alt text
- `link` — optional URL string
- `mediaId` — optional string — media ID from R2
- `alignment` — optional 'left'|'center'|'right'
#### `button`
Tombol dengan link
Props:
- `text` — string — button label
- `link` — string — href URL (default: '#'). Used when linkType='url'.
- `linkType` — 'url' (default) | 'whatsapp' | 'checkout'. 'checkout' makes the button add the selected product to cart and go to /checkout.
- `whatsappNumber` — string — international format, no '+' (e.g. '6281234567890'). Used when linkType='whatsapp'.
- `whatsappMessage` — optional string — prefilled WhatsApp message. Used when linkType='whatsapp'.
- `metaTrackingEnabled` — checkbox — Aktifkan Meta Pixel — default: false
- `tiktokTrackingEnabled` — checkbox — Aktifkan TikTok Pixel — default: false
- `productId` — string — product UUID to buy. REQUIRED when linkType='checkout'. Picker also caches productSlug/productName/productPrice/productType.
- `alignment` — DEPRECATED legacy — for positioning set style.layout.alignSelf ('Align Self': flex-start|center|flex-end|stretch). Pre-migration data only; do NOT seed on new buttons.
- `width` — DEPRECATED legacy — for sizing set style.layout.width ('fit-content' for Auto, '100%' for Full Width, or any length like '300px'/'20rem' for Custom). Pre-migration data only; do NOT seed on new buttons.
- `icon` — optional IconValue { library: 'lucide'|'fa-solid'|..., icon: string } — leave unset for no icon
- `iconPosition` — 'left'|'right' (default: 'right') — side of the text the icon sits on
- `iconGap` — optional unit string e.g. '8px' — spacing between icon and text
- `iconSize` — optional unit string e.g. '20px' (default 1em, follows button font-size)
- `metaEvent` — optional string — Meta Pixel standard event e.g. 'Lead', 'Purchase' (linkType='url'/'whatsapp' only; checkout mode auto-fires AddToCart)
- `tiktokEvent` — optional string — TikTok Pixel event (engagement mode only)
#### `textLink`
Teks dengan hyperlink
Props:
- `text` — string
- `link` — string — href URL
- `alignment` — DEPRECATED legacy — for positioning set style.layout.alignSelf ('Align Self': flex-start|center|flex-end|stretch). Pre-migration data only; do NOT seed on new links.
#### `icon`
Tampilkan satu icon dengan opsi link, frame, atau background
Props:
- `icon` — IconValue object — see icon_value_format above
- `view` — 'default'|'stacked'|'framed'
- `shape` — 'circle'|'square'|'rounded'
- `link` — optional URL
- `linkTarget` — '_self'|'_blank'
- `primaryColor` — view-dependent → default: icon color • stacked: fill bg (icon glyph auto-contrast white) • framed: border + icon color. Empty = theme primary.
- `secondaryColor` — framed bg only — IGNORED in default & stacked views. For a stacked dot just set primaryColor (e.g. '#2563eb'); do NOT set primaryColor='#ffffff' + secondaryColor='#2563eb' (renders invisible: white-on-white).
- `size` — number (px)
- `rotate` — number (degrees)
- `borderWidth` — number (px, framed view)
- `padding` — number (px)
- `alignment` — DEPRECATED legacy — for positioning set style.layout.alignSelf ('Align Self': flex-start|center|flex-end|stretch). Pre-migration data only; do NOT seed on new icons.
- `borderRadius` — number (px)
#### `divider`
Garis pemisah antar konten
Props:
- `height` — number — line thickness in px (default: 1)
- `width` — string — CSS width e.g. '100%' (default: '100%')
- `style` — 'solid'|'dashed'|'dotted'|'double' (default: 'solid')
- `direction` — 'horizontal'|'vertical' (default: 'horizontal')
- `alignment` — 'left'|'center'|'right' (default: 'center')
- `color` — CSS color (default: '#e5e5e5')
- `contentType` — customRenderer — Konten Tengah
- `space` — number — vertical spacing in px (default: 16)
- `icon` — optional Lucide icon name in center
#### `spacer`
Ruang kosong vertikal
Props:
- `height` — number — height in px on desktop (default: 48)
- `mobileHeight` — optional number — height on mobile
### general
#### `slider` · can have children
Carousel dengan panah & dot — tiap anak jadi slide
Props:
- `htmlTag` — string — (undocumented)
- `layoutMode` — string — (undocumented)
- `perPage` — number — Slides / view (Desktop) — default: 4
- `perPageTablet` — number — Slides / view (Tablet) — default: 2
- `perPageMobile` — number — Slides / view (Mobile) — default: 1
- `gap` — unit — Gap — default: "24px"
- `arrows` — checkbox — Tampilkan panah — default: true
- `arrowsPosition` — radio "inside"|"outside" — Posisi — default: "inside"
- `pagination` — checkbox — Tampilkan dot — default: true
- `sliderType` — select "slide"|"loop"|"fade" — Tipe — default: "slide"
- `autoplay` — checkbox — Autoplay — default: false
- `interval` — number — Interval (ms) — default: 3000
- `speed` — number — Kecepatan transisi (ms) — default: 400
- `drag` — checkbox — Drag mouse/sentuh — default: true
- `pauseOnHover` — checkbox — Pause saat hover — default: true
- `arrowState` — radio "normal"|"hover" — default: "normal"
- `arrowSize` — number — Ukuran tombol (px) — default: 40
- `arrowIconSize` — number — Ukuran ikon (px) — default: 16
- `arrowRadius` — unit — Radius (0 = persegi) — default: "9999px"
- `arrowShadow` — select "none"|"soft"|"medium" — Shadow — default: "soft"
- `arrowBg` — color — Background — default: "#ffffff"
- `arrowIconColor` — color — Warna ikon — default: "#111827"
- `arrowBorderWidth` — number — Border (px) — default: 1
- `arrowBorderColor` — color — Warna border — default: "rgba(0,0,0,0.08)"
- `arrowBgHover` — color — Background
- `arrowIconColorHover` — color — Warna ikon
- `arrowBorderColorHover` — color — Warna border
- `dotState` — radio "normal"|"active" — default: "normal"
- `dotSize` — number — Ukuran (px) — default: 8
- `dotGap` — number — Jarak antar dot (px) — default: 8
- `dotRadius` — unit — Radius (0 = persegi) — default: "9999px"
- `dotColor` — color — Warna — default: "rgba(0,0,0,0.2)"
- `dotColorHover` — color — Warna :hover
- `dotActiveColor` — color — Warna aktif — default: "rgba(0,0,0,0.7)"
- `dotActiveScale` — number — Skala aktif — default: 1.2
#### `logo`
Logo situs — auto dari Settings, atau image/text kustom
Props:
- `sourceMode` — optional 'auto'|'image'|'text' (default: 'auto')
- `linkType` — optional 'home'|'url'|'none' (default: 'home')
- `loading` — optional 'eager'|'lazy' (default: 'eager')
- `src` — optional string — image URL when sourceMode='image'
- `mediaId` — optional string — media ID from R2
- `alt` — optional string — alt text for the logo image
- `text` — optional string — text logo, or fallback when auto-mode has no tenant logo
- `inverseSrc` — optional URL — inverse logo shown on scrolled headers
- `inverseMediaId` — optional string
- `height` — optional string e.g. '40px' — required for SVG sources
- `width` — optional string e.g. '120px' — required for SVG sources
- `link` — optional URL — used when linkType='url'
#### `accordion` · children: accordionItem only
Konten expandable dengan pertanyaan & jawaban
Props:
- `titleTag` — 'div'|'h1'..'h6' (default: 'h3') — HTML tag for each panel title, shared
- `icon` — IconValue — toggle icon shown COLLAPSED (shared across items)
- `iconExpanded` — IconValue — toggle icon shown EXPANDED (shared)
- `iconPosition` — 'left'|'right' (default: 'right')
- `independentToggle` — boolean — allow multiple panels open at once (default: false)
- `transition` — number — open/close transition in ms (default: 200)
- `faqSchema` — boolean — emit FAQPage JSON-LD (default: false)
- `iconRotate` — number — degrees to rotate the icon when expanded (optional)
#### `breadcrumb`
Trail navigasi: Home > Section > Page (auto JSON-LD)
Props:
- `dataSource` — select "auto"|"manual" — Sumber Data
- `items` — BreadcrumbItem[] — each: { id, label, url } (1-10 items)
- `separator` — 'slash'|'arrow'|'chevron'|'dot' (default: 'chevron')
- `showHomeIcon` — boolean — show Home icon on first item (default: true)
- `homeUrl` — string — URL for home link (default: '/')
- `homeLabel` — text — Label Home
- `showBlogArchive` — checkbox — Sertakan langkah arsip Blog
- `showCatalog` — checkbox — Sertakan langkah Katalog
- `showCategory` — checkbox — Sertakan kategori post/product
- `hideCurrentItem` — checkbox — Sembunyikan halaman saat ini
- `color` — string — link color (CSS)
- `activeColor` — string — last item (current page) color
- `separatorColor` — string — separator char color
#### `counter`
Angka statistik dengan animasi count-up saat masuk viewport
Props:
- `countFrom` — number — Mulai Dari
- `countTo` — number — Sampai
- `duration` — number — Durasi Animasi (ms)
- `prefix` — optional string e.g. 'Rp'
- `suffix` — optional string e.g. '+'
- `thousandSeparator` — checkbox — Tampilkan separator ribuan
- `separatorText` — text — Karakter Separator
- `count` — number
- `label` — string
- `alignment` — 'left'|'center'|'right' (default: 'center')
#### `iconList` · children: iconListItem only
Daftar item (Item List) dengan icon, title, deskripsi & highlight badge
#### `countdownTimer`
Hitung mundur menuju tanggal & waktu tertentu
Props:
- `mode` — 'fixed'|'evergreen' (default: 'fixed'). 'fixed' counts down to targetDate; 'evergreen' gives each visitor their own countdown of evergreenSeconds, resumed across refreshes via localStorage
- `targetDate` — string — datetime-local format e.g. '2026-12-31T23:59' (mode='fixed')
- `evergreenSeconds` — number — per-visitor countdown length in seconds (default: 1200 = 20 minutes, mode='evergreen')
- `evergreenOnEnd` — 'restart'|'expire' (default: 'restart') — loop into a new cycle, or stay on expiredText (mode='evergreen')
- `showDays` — boolean (default: true)
- `showHours` — boolean (default: true)
- `showMinutes` — boolean (default: true)
- `showSeconds` — boolean (default: true)
- `expiredText` — string (default: 'Penawaran telah berakhir')
- `layout` — 'boxes'|'minimal' (default: 'boxes')
- `alignment` — 'left'|'center'|'right' (default: 'center')
- `showLabels` — boolean (default: true)
- `labelPosition` — 'above'|'below' (default: 'below')
- `labelPlacement` — select "outside"|"inside" — Penempatan Label
- `separator` — select "none"|"dash"|"colon"|"slash" — Separator
- `boxBg` — CSS color (default: '#f3f4f6')
- `boxBorderColor` — CSS color (default: '#e5e7eb')
- `boxBorderRadius` — CSS string (default: '8px')
#### `alertBanner`
Kotak notifikasi atau pengumuman (info/success/warning/error)
Props:
- `title` — optional string
- `message` — string — main message
- `variant` — 'info'|'success'|'warning'|'error' (default: 'info')
- `showIcon` — boolean (default: true)
- `isDismissible` — boolean (default: false)
- `alignment` — 'left'|'center'|'right' (default: 'left')
#### `mapEmbed`
Embed Google Maps untuk lokasi toko
Props:
- `embedUrl` — string — location keyword/address, coords, any Google Maps URL, embed URL,