Salla Storefront Snippets Flow
Integrate with Salla storefront events by performing the actions. Device Mode
snippets are injected with the Salla Partners MCP salla_snippets tool; Cloud Mode runs
in an App Function. Follow the steps in order — complete each gate before moving on.
Tools
| Tool |
Action |
What it does |
salla_snippets |
list / parameters / create / update / delete |
Manage the app's storefront snippets |
Prerequisite: the Salla Partners MCP server must be connected, and you need the
app's app_id. Cloud Mode runs as an App Function — authoring and deployment →
salla-app-functions.
Step 0 — Discover
Ask before starting:
- Which storefront event do you want to handle?
(e.g.
cart::item.added, cart::updated, product::price.updated — Twilight events
are ::-namespaced; confirm names in the catalogue in
references/device-mode.md)
- What should happen when the event fires?
(track analytics, sync data, trigger automation, personalize content)
Use the answers to determine the right mode in Step 1.
Step 0.5 — Detect legacy content (Device Mode only)
Before scaffolding, inspect whatever snippet content you were handed (pasted, exported
from the Portal, or read back from an existing snippet):
- Does it contain HTML tags (
<script, <style, <div, <link, <iframe, …)?
- Does it contain a
{{namespace.key}} token (Twig-style double-brace)?
If either is true, this is legacy content from the old server-side template pipeline —
stop here. Hand off to salla-snippets-migration
to convert it to pure JS first, then resume at Step 2 below with the converted output. Do
not attempt to hand-patch HTML/Twig content into something that merely looks like it
might parse — the conversion has real rules (parameter model, salla.onReady timing) that
skill owns.
Gate: content contains no HTML tags and no bare {{...}} tokens before proceeding to
Step 2.
Step 1 — Choose Integration Mode
| Mode |
Where it runs |
Best for |
| Device Mode |
Browser (tracker.js + Twilight SDK) |
Analytics, personalization, marketing attribution |
| Cloud Mode |
Server (App Functions) |
Automation, data sync, reliable backend delivery |
Decision rule:
- Needs real-time browser data or marketing pixels → Device Mode
- Needs guaranteed delivery, backend logic, or API calls → Cloud Mode
If still unclear, ask: "Should this run in the browser or on your server?"
Gate: "Confirmed the mode. Proceeding to scaffold."
Step 2 — Scaffold the Implementation
Device Mode
The snippet body runs in the storefront browser via the Twilight SDK. Write the listener,
then inject it as a storefront snippet with the tool:
Write the snippet body as plain JavaScript — a snippet is a pure-JS file Salla serves
from the CDN, loaded into every storefront page via <script src>. Use salla,
salla.onReady, salla.event, and salla.config.get(...) directly. (Full rules — no
Twig/HTML wrapper, bootstrap timing, deploy guard → references/device-mode.md.)
A snippet does two distinct jobs — keep them separate:
Listen to storefront events (::-namespaced; payload in e.data). Register
listeners at module top level so init-time events aren't missed:
salla.event.on("cart::item.added", (e) => {
var productId = e.data.product_id; // payload in e.data
});
Event catalogue and payload shapes → references/device-mode.md.
Read the app's settings with salla.config.get("app.<key>") — the only way to read
a merchant's App Settings from a storefront snippet, and only settings marked
public: true are visible there. Gate store-state reads on salla.onReady:
salla.onReady(function () {
var rewardsOn = salla.config.get("app.rewards_enabled") || false;
var pointValue = salla.config.get("app.point_value_halalah") || 0;
});
Settings are how the merchant configured the app; events are what the shopper is doing.
Define the keys and which are public in
salla-app-settings. Store/session config
(user.id, user.email — the shopper, store.username, whole store/user
objects) and the defensive-read patterns → Store context & language in
references/device-mode.md. customer.* and
store.domain are forbidden — deprecated/removed, never use them (full rule in
references/device-mode.md).
(Optional) Check available template variables: salla_snippets action=parameters,
app_id.
Inject it: salla_snippets action=create, app_id, name (required), place
("before" — the only accepted value), tag ("body" only — snippets render before
</body>), content (your pure JS). Dedup first: call salla_snippets action=list and
update/delete any existing snippet for this app before creating — stacked duplicates
double-render the UI and double-fire events. Read back with salla_snippets action=list /
get: it returns the snippet metadata with the CDN url (the .js file) and
path. update revalidates the full snippet — resend name, place, tag, and
content together (it is not a partial patch). action=update echoes a confirmation
({ snippet: { id, name, updated } }); use action=list for the live CDN url.
Manage snippets only through the salla_snippets MCP tool — it owns field mapping
and validation (it maps content to the underlying field). Every snippet operation goes
through one of its actions.
Device Mode setup, full event catalogue, payload shapes →
references/device-mode.md
Validate on every create / update (closed loop)
A successful salla_snippets create/update is the START of validation, not the end — a
200 only confirms the file deployed, not that it runs. Treat every create/update as
the trigger for one loop, repeated until clean:
- Parse-as-JS check (do this before
create/update) — confirm the content parses
as valid JavaScript, the same check the Portal editor runs before it lets you save: e.g.
node --check snippet.js, or a new Function(code) in a try/catch. Parse it, don't
pattern-match — <script> tags, HTML, and Twig ({{ … }} / {% … %}) all fail to parse
as JS and are caught for free, because the snippet is served as a .js file. Fix any
syntax error before saving; the MCP no longer guards content shape, so the body parsing
cleanly is the author's responsibility. → references/device-mode.md.
- Forbidden-parameter check — grep the content for
salla.config.get("customer /
salla.config.get('customer and salla.config.get("store.domain /
salla.config.get('store.domain. Reject on any match, no exceptions — customer.*
and store.domain are deprecated/removed, with no mechanical substitute (full catalog
→ Store context & language in references/device-mode.md).
user.* is a different concept (the shopper), not a 1:1 rename target — check what
data the snippet actually needs against that catalog before rewriting; if the exact data
isn't available under user.*/store.* as documented there, it is not available
client-side, don't invent a path.
- Config-key check — for every
salla.config.get("app.<key>") the snippet reads,
confirm <key> is a defined setting marked public: true in the app's settings. A key
that's missing or not public reads undefined on the storefront. The settings define
the contract → cross-check salla-app-settings.
- Browser test — run the DevTools-console recipe below (load marker, no errors, expected
e.data and config values).
- Fix → re-
update via the tool → re-validate until all four pass.
Test the snippet in the browser
A snippet runs in the shopper's browser, so a 200 from salla_snippets only confirms it
deployed — prove it runs in a real browser via the DevTools Console.
Open the storefront. Install the app on a demo store first (→
salla-live-testing), then open that store's url and
navigate to the page where the snippet runs. Drive it with a headless browser
(Playwright/Puppeteer) if available; otherwise guide the user step by step to open the
page and the DevTools Console (and Network tab).
Add debug logging. Instrument the snippet so execution and data are visible — a load
marker, the settings you read, and each event payload:
(function () {
console.log("[myapp] snippet loaded");
salla.event.on("cart::item.added", function (e) {
console.log("[myapp] cart::item.added", e.data); // real payload shape
});
salla.onReady(function () {
console.log(
"[myapp] rewards_on",
salla.config.get("app.rewards_enabled"),
);
});
})();
Trigger and verify. Perform the behavior (e.g. add a product to cart), then confirm
in the console: the load marker logged, no red errors, the handler logged the
expected e.data, and salla.config.get("app.<key>") values are what you expect.
Diagnose from the console:
| You see |
It means |
Do |
| Nothing logs |
Snippet not on this store / page not reloaded / SDK not on page |
Confirm it's deployed to THIS store, reload, and run on a page where salla.onReady fires |
salla.config.get("app.<key>") is undefined |
Setting isn't public or the key is wrong |
Mark it public: true (salla-app-settings) / fix the key |
| A handler never fires |
The event name is wrong |
Check the :: catalogue in references/device-mode.md |
Before publish: remove or guard the debug console.logs, and keep secrets/PII out of
logs (the file is served to every shopper).
Twilight JS SDK (for app snippets)
The Twilight theme engine auto-injects the Twilight Storefront JS SDK (window.salla)
on every storefront page (the body:end hook). Your snippet runs in that same page, so it
can call the same runtime API — auth, cart, wishlist, product, order, rating, currency,
loyalty, comment, profile, booking, salla.api.component.*, salla.config, salla.event,
salla.storage, salla.notify, salla.lang, salla.helpers, metadata.
Method catalogue (signatures, per-module doc links, app-snippet-vs-theme boundary, the
salla.init() rule) → references/twilight-js-sdk.md.
Events (the :: catalogue, the product::fetch.succeeded trap, price encodings) →
references/device-mode.md.
Glue: this skill = the shopper's browser (customer-side actions/events via
snippets). For a server reaction to the same activity, the hookable rule applies — a
server event with an App Function trigger → App Function
(salla-app-functions, server-side V8 isolate,
preferred); else → webhook (salla-webhooks). Native
visible UI → salla-storefront-ui.
Storefront UI compliance (when the snippet renders visible UI)
When a snippet draws on the page, build the UI from Salla's native UI Components
(Twilight <salla-*> web components) driven by the Storefront JS SDK — not hand-rolled
HTML. Native components inherit the theme's tokens, RTL, and locale for free, so they read
as part of the store rather than a standalone SaaS badge.
- Render with
<salla-*> components. Insert the documented tag and set its attributes
/properties — e.g. <salla-button>, <salla-modal>, <salla-rating-stars>,
<salla-quantity-input>, <salla-products-slider>. Confirm the exact tag and props in
the UI Components catalogue (component families below). Themes register these components
on every storefront page; if a component is missing on a target store, load the loader at
runtime from the CDN (@salla.sa/twilight-components ESM loader) before using it.
- Wire behaviour through the SDK — read state with
salla.config.get(...), react with
salla.event.on(...), call salla.cart.* / salla.product.* etc. (method catalogue →
references/twilight-js-sdk.md).
- For any custom markup you still write, inherit Twilight CSS variables
(
--color-primary, --color-text, --font-main, spacing/radius), use Salla Icons
(sicon-* classes), match surrounding spacing/density, and honor dir/lang (Arabic/RTL
first). Hardcoded fonts/colors/borders/shadows are fallbacks only.
- Verify live — open an installed demo store (
salla_apps action=demo_stores →
url) and screenshot the product page. UI that "runs" in code is not proof it looks
right.
salla-storefront-ui owns the "use native components + native look-and-feel" rule (and the
live-verification gate) — follow it for full guidance:
salla-storefront-ui.
UI Component families (all <salla-*>; full catalogue in the docs):
| Family |
Examples |
| Product |
salla-product-card, salla-products-slider, salla-add-product-button |
| Shopping / cart |
salla-quantity-input, salla-quick-buy, salla-cart-summary |
| User / auth |
salla-login-modal, salla-userprofile, salla-verify |
| Forms / input |
salla-tel-input, salla-datetime-picker, salla-file-upload |
| Elements / layout |
salla-button, salla-modal, salla-rating-stars, salla-tabs |
Docs: UI Components Overview https://docs.salla.dev/422688m0.md · Usage
https://docs.salla.dev/422689m0.md · Customization https://docs.salla.dev/422690m0.md ·
Storefront JS SDK https://docs.salla.dev/422610m0.md · theme
https://docs.salla.dev/421877m0.md · CSS variables https://docs.salla.dev/421945m0.md ·
Salla Icons https://docs.salla.dev/422550m0.md · single product page
https://docs.salla.dev/422561m0.md.
Cloud Mode
Cloud Mode is an App Function — write the handler with the storefront event as its
trigger and follow the salla-app-functions skill end-to-end (template, Resp API,
typed contexts, deploy). Don't duplicate its template here.
Gate: "Test the event: trigger it from a demo store and confirm the handler fires
correctly."
Red Flags
| Tempting thought |
Why it's wrong |
| "I'll just strip the HTML tags and keep going, it's mostly JS already" |
Legacy content has real conversion rules (parameter model, salla.onReady timing) — hand-patching skips them and ships broken/undefined reads. Route to salla-snippets-migration. |
"customer.email reads fine in my test, I'll ship it" |
customer.* is forbidden regardless of whether a call happens to resolve — it's deprecated/removed, not a style choice. Use user.* (the shopper). |
"It's just one legacy {{store.id}} token, I'll leave it and fix the rest" |
The content must parse as valid JS to deploy at all — {{ }} ships as literal text and breaks the script (or the salla_snippets save itself). Convert every token before saving. |
Resources
1---2name: salla-snippets3description: Use when behavior must run in the shopper's browser on the Salla storefront — JS snippets injected via the salla_snippets tool, reacting to storefront e-commerce events (cart, product view, checkout, search). Rule: storefront/browser behavior → snippet (Device Mode); server-side handling of the same events → App Function (salla-app-functions, Cloud Mode). Snippets are pure-JS files served from the CDN, placed before `</body>`; covers create/update/delete, the `salla.config.get("app.*")` settings bridge, and the storefront event catalogue.4---56# Salla Storefront Snippets Flow78Integrate with Salla storefront events by **performing the actions**. Device Mode9snippets are injected with the Salla Partners MCP `salla_snippets` tool; Cloud Mode runs10in an App Function. Follow the steps in order — complete each gate before moving on.1112## Tools1314| Tool | Action | What it does |15| ---------------- | ------------------------------------------------------ | ------------------------------------ |16| `salla_snippets` | `list` / `parameters` / `create` / `update` / `delete` | Manage the app's storefront snippets |1718> **Prerequisite:** the Salla Partners MCP server must be connected, and you need the19> app's `app_id`. Cloud Mode runs as an App Function — authoring and deployment →20> **salla-app-functions**.2122---2324## Step 0 — Discover2526Ask before starting:27281. **Which storefront event do you want to handle?**29 (e.g. `cart::item.added`, `cart::updated`, `product::price.updated` — Twilight events30 are `::`-namespaced; confirm names in the catalogue in31 [`references/device-mode.md`](references/device-mode.md))322. **What should happen when the event fires?**33 (track analytics, sync data, trigger automation, personalize content)3435Use the answers to determine the right mode in Step 1.3637---3839## Step 0.5 — Detect legacy content (Device Mode only)4041Before scaffolding, inspect whatever snippet content you were handed (pasted, exported42from the Portal, or read back from an existing snippet):4344- **Does it contain HTML tags** (`<script`, `<style`, `<div`, `<link`, `<iframe`, …)?45- **Does it contain a `{{namespace.key}}` token** (Twig-style double-brace)?4647If **either is true**, this is legacy content from the old server-side template pipeline —48**stop here**. Hand off to **[salla-snippets-migration](../salla-snippets-migration/SKILL.md)**49to convert it to pure JS first, then resume at Step 2 below with the converted output. Do50not attempt to hand-patch HTML/Twig content into something that merely _looks_ like it51might parse — the conversion has real rules (parameter model, `salla.onReady` timing) that52skill owns.5354**Gate:** content contains no HTML tags and no bare `{{...}}` tokens before proceeding to55Step 2.5657---5859## Step 1 — Choose Integration Mode6061| Mode | Where it runs | Best for |62| --------------- | ------------------------------------- | ------------------------------------------------- |63| **Device Mode** | Browser (`tracker.js` + Twilight SDK) | Analytics, personalization, marketing attribution |64| **Cloud Mode** | Server (App Functions) | Automation, data sync, reliable backend delivery |6566Decision rule:6768- Needs real-time browser data or marketing pixels → **Device Mode**69- Needs guaranteed delivery, backend logic, or API calls → **Cloud Mode**7071If still unclear, ask: _"Should this run in the browser or on your server?"_7273**Gate:** "Confirmed the mode. Proceeding to scaffold."7475---7677## Step 2 — Scaffold the Implementation7879### Device Mode8081The snippet body runs in the storefront browser via the Twilight SDK. Write the listener,82then **inject it as a storefront snippet** with the tool:83841. Write the snippet body as plain JavaScript — a snippet is a pure-JS file Salla serves85 from the CDN, loaded into every storefront page via `<script src>`. Use `salla`,86 `salla.onReady`, `salla.event`, and `salla.config.get(...)` directly. (Full rules — no87 Twig/HTML wrapper, bootstrap timing, deploy guard → [`references/device-mode.md`](references/device-mode.md).)8889 A snippet does two distinct jobs — keep them separate:9091 - **Listen to storefront events** (`::`-namespaced; payload in `e.data`). Register92 listeners at module top level so init-time events aren't missed:9394 ```js95 salla.event.on("cart::item.added", (e) => {96 var productId = e.data.product_id; // payload in e.data97 });98 ```99100 Event catalogue and payload shapes → [`references/device-mode.md`](references/device-mode.md).101102 - **Read the app's settings** with `salla.config.get("app.<key>")` — the only way to read103 a merchant's App Settings from a storefront snippet, and only settings marked104 `public: true` are visible there. Gate store-state reads on `salla.onReady`:105106 ```js107 salla.onReady(function () {108 var rewardsOn = salla.config.get("app.rewards_enabled") || false;109 var pointValue = salla.config.get("app.point_value_halalah") || 0;110 });111 ```112113 Settings are how the merchant configured the app; events are what the shopper is doing.114 Define the keys and which are `public` in115 [salla-app-settings](../salla-app-settings/SKILL.md). Store/session config116 (`user.id`, `user.email` — the shopper, `store.username`, whole `store`/`user`117 objects) and the defensive-read patterns → _Store context & language_ in118 [`references/device-mode.md`](references/device-mode.md). **`customer.*` and119 `store.domain` are forbidden — deprecated/removed, never use them** (full rule in120 `references/device-mode.md`).1211222. (Optional) Check available template variables: `salla_snippets action=parameters`,123 `app_id`.1243. Inject it: `salla_snippets action=create`, `app_id`, `name` (required), `place`125 (`"before"` — the only accepted value), `tag` (`"body"` only — snippets render **before126 `</body>`**), `content` (your pure JS). **Dedup first:** call `salla_snippets action=list` and127 `update`/`delete` any existing snippet for this app before creating — stacked duplicates128 double-render the UI and double-fire events. Read back with `salla_snippets action=list` /129 `get`: it returns the snippet **metadata** with the CDN **`url`** (the `.js` file) and130 `path`. `update` revalidates the **full** snippet — resend `name`, `place`, `tag`, and131 `content` together (it is not a partial patch). `action=update` echoes a confirmation132 (`{ snippet: { id, name, updated } }`); use `action=list` for the live CDN `url`.133134 > **Manage snippets only through the `salla_snippets` MCP tool** — it owns field mapping135 > and validation (it maps `content` to the underlying field). Every snippet operation goes136 > through one of its actions.137138Device Mode setup, full event catalogue, payload shapes →139[`references/device-mode.md`](references/device-mode.md)140141#### Validate on every create / update (closed loop)142143**A successful `salla_snippets create`/`update` is the START of validation, not the end** — a144200 only confirms the file deployed, not that it runs. Treat **every `create`/`update`** as145the trigger for one loop, repeated until clean:1461471. **Parse-as-JS check (do this before `create`/`update`)** — confirm the `content` parses148 as valid JavaScript, the same check the Portal editor runs before it lets you save: e.g.149 `node --check snippet.js`, or a `new Function(code)` in a try/catch. Parse it, don't150 pattern-match — `<script>` tags, HTML, and Twig (`{{ … }}` / `{% … %}`) all fail to parse151 as JS and are caught for free, because the snippet is served as a `.js` file. Fix any152 syntax error before saving; the MCP no longer guards content shape, so the body parsing153 cleanly is the author's responsibility. → [`references/device-mode.md`](references/device-mode.md).1542. **Forbidden-parameter check** — grep the content for `salla.config.get("customer` /155 `salla.config.get('customer` and `salla.config.get("store.domain` /156 `salla.config.get('store.domain`. **Reject on any match, no exceptions** — `customer.*`157 and `store.domain` are deprecated/removed, with no mechanical substitute (full catalog158 → _Store context & language_ in [`references/device-mode.md`](references/device-mode.md)).159 `user.*` is a **different concept** (the shopper), not a 1:1 rename target — check what160 data the snippet actually needs against that catalog before rewriting; if the exact data161 isn't available under `user.*`/`store.*` as documented there, it is not available162 client-side, don't invent a path.1633. **Config-key check** — for **every** `salla.config.get("app.<key>")` the snippet reads,164 confirm `<key>` is a defined setting marked `public: true` in the app's settings. A key165 that's missing or not `public` reads `undefined` on the storefront. The settings define166 the contract → cross-check [salla-app-settings](../salla-app-settings/SKILL.md).1674. **Browser test** — run the DevTools-console recipe below (load marker, no errors, expected168 `e.data` and config values).1695. **Fix → re-`update` via the tool → re-validate** until all four pass.170171#### Test the snippet in the browser172173A snippet runs in the shopper's browser, so a 200 from `salla_snippets` only confirms it174deployed — prove it **runs** in a real browser via the DevTools Console.1751761. **Open the storefront.** Install the app on a demo store first (→177 [salla-live-testing](../salla-live-testing/SKILL.md)), then open that store's `url` and178 navigate to the page where the snippet runs. Drive it with a headless browser179 (Playwright/Puppeteer) if available; otherwise guide the user step by step to open the180 page and the **DevTools Console** (and Network tab).1812. **Add debug logging.** Instrument the snippet so execution and data are visible — a load182 marker, the settings you read, and each event payload:183184 ```js185 (function () {186 console.log("[myapp] snippet loaded");187 salla.event.on("cart::item.added", function (e) {188 console.log("[myapp] cart::item.added", e.data); // real payload shape189 });190 salla.onReady(function () {191 console.log(192 "[myapp] rewards_on",193 salla.config.get("app.rewards_enabled"),194 );195 });196 })();197 ```1981993. **Trigger and verify.** Perform the behavior (e.g. add a product to cart), then confirm200 in the console: the load marker logged, **no red errors**, the handler logged the201 expected `e.data`, and `salla.config.get("app.<key>")` values are what you expect.2024. **Diagnose from the console:**203204 | You see | It means | Do |205 | ---------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |206 | Nothing logs | Snippet not on this store / page not reloaded / SDK not on page | Confirm it's deployed to THIS store, reload, and run on a page where `salla.onReady` fires |207 | `salla.config.get("app.<key>")` is `undefined` | Setting isn't `public` or the key is wrong | Mark it `public: true` ([salla-app-settings](../salla-app-settings/SKILL.md)) / fix the key |208 | A handler never fires | The event name is wrong | Check the `::` catalogue in [`references/device-mode.md`](references/device-mode.md) |2092105. **Before publish:** remove or guard the debug `console.log`s, and keep secrets/PII out of211 logs (the file is served to every shopper).212213#### Twilight JS SDK (for app snippets)214215The Twilight theme engine **auto-injects** the Twilight Storefront JS SDK (`window.salla`)216on every storefront page (the `body:end` hook). Your snippet runs in that same page, so it217can call the same runtime API — auth, cart, wishlist, product, order, rating, currency,218loyalty, comment, profile, booking, `salla.api.component.*`, `salla.config`, `salla.event`,219`salla.storage`, `salla.notify`, `salla.lang`, `salla.helpers`, metadata.220221**Method catalogue (signatures, per-module doc links, app-snippet-vs-theme boundary, the222`salla.init()` rule) → [`references/twilight-js-sdk.md`](references/twilight-js-sdk.md).**223Events (the `::` catalogue, the `product::fetch.succeeded` trap, price encodings) →224[`references/device-mode.md`](references/device-mode.md).225226**Glue:** this skill = the **shopper's browser** (customer-side actions/events via227snippets). For a **server reaction** to the same activity, the hookable rule applies — a228server event with an App Function trigger → **App Function**229([salla-app-functions](../salla-app-functions/SKILL.md), server-side V8 isolate,230preferred); else → **webhook** ([salla-webhooks](../salla-webhooks/SKILL.md)). Native231visible UI → [salla-storefront-ui](../salla-storefront-ui/SKILL.md).232233#### Storefront UI compliance (when the snippet renders visible UI)234235When a snippet **draws on the page**, build the UI from Salla's native **UI Components**236(Twilight `<salla-*>` web components) driven by the Storefront JS SDK — not hand-rolled237HTML. Native components inherit the theme's tokens, RTL, and locale for free, so they read238as part of the store rather than a standalone SaaS badge.239240- **Render with `<salla-*>` components.** Insert the documented tag and set its attributes241 /properties — e.g. `<salla-button>`, `<salla-modal>`, `<salla-rating-stars>`,242 `<salla-quantity-input>`, `<salla-products-slider>`. Confirm the exact tag and props in243 the UI Components catalogue (component families below). Themes register these components244 on every storefront page; if a component is missing on a target store, load the loader at245 runtime from the CDN (`@salla.sa/twilight-components` ESM loader) before using it.246- **Wire behaviour through the SDK** — read state with `salla.config.get(...)`, react with247 `salla.event.on(...)`, call `salla.cart.*` / `salla.product.*` etc. (method catalogue →248 [`references/twilight-js-sdk.md`](references/twilight-js-sdk.md)).249- **For any custom markup you still write**, inherit Twilight CSS variables250 (`--color-primary`, `--color-text`, `--font-main`, spacing/radius), use Salla Icons251 (`sicon-*` classes), match surrounding spacing/density, and honor `dir`/`lang` (Arabic/RTL252 first). Hardcoded fonts/colors/borders/shadows are fallbacks only.253- **Verify live** — open an **installed demo store** (`salla_apps action=demo_stores` →254 `url`) and screenshot the product page. UI that "runs" in code is not proof it looks255 right.256257`salla-storefront-ui` owns the "use native components + native look-and-feel" rule (and the258live-verification gate) — follow it for full guidance:259[salla-storefront-ui](../salla-storefront-ui/SKILL.md).260261**UI Component families** (all `<salla-*>`; full catalogue in the docs):262263| Family | Examples |264| ----------------- | ------------------------------------------------------------------------- |265| Product | `salla-product-card`, `salla-products-slider`, `salla-add-product-button` |266| Shopping / cart | `salla-quantity-input`, `salla-quick-buy`, `salla-cart-summary` |267| User / auth | `salla-login-modal`, `salla-userprofile`, `salla-verify` |268| Forms / input | `salla-tel-input`, `salla-datetime-picker`, `salla-file-upload` |269| Elements / layout | `salla-button`, `salla-modal`, `salla-rating-stars`, `salla-tabs` |270271Docs: UI Components Overview https://docs.salla.dev/422688m0.md · Usage272https://docs.salla.dev/422689m0.md · Customization https://docs.salla.dev/422690m0.md ·273Storefront JS SDK https://docs.salla.dev/422610m0.md · theme274https://docs.salla.dev/421877m0.md · CSS variables https://docs.salla.dev/421945m0.md ·275Salla Icons https://docs.salla.dev/422550m0.md · single product page276https://docs.salla.dev/422561m0.md.277278### Cloud Mode279280Cloud Mode **is** an App Function — write the handler with the storefront event as its281trigger and follow the **salla-app-functions** skill end-to-end (template, `Resp` API,282typed contexts, deploy). Don't duplicate its template here.283284**Gate:** "Test the event: trigger it from a demo store and confirm the handler fires285correctly."286287---288289## Red Flags290291| Tempting thought | Why it's wrong |292| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |293| "I'll just strip the HTML tags and keep going, it's mostly JS already" | Legacy content has real conversion rules (parameter model, `salla.onReady` timing) — hand-patching skips them and ships broken/undefined reads. Route to `salla-snippets-migration`. |294| "`customer.email` reads fine in my test, I'll ship it" | `customer.*` is forbidden regardless of whether a call happens to resolve — it's deprecated/removed, not a style choice. Use `user.*` (the shopper). |295| "It's just one legacy `{{store.id}}` token, I'll leave it and fix the rest" | The content must parse as valid JS to deploy at all — `{{ }}` ships as literal text and breaks the script (or the `salla_snippets` save itself). Convert every token before saving. |296297---298299## Resources300301| Topic | Link |302| ----------------------- | ----------------------------------- |303| Device Mode Usage | https://docs.salla.dev/1724504m0.md |304| Cloud Mode Usage | https://docs.salla.dev/1724667m0.md |305| App Functions Overview | https://docs.salla.dev/1726814m0.md |306| App Functions Events | https://docs.salla.dev/1726818m0.md |307| App Snippets Overview | https://docs.salla.dev/2220706m0.md |308| HTML→JS Migration Guide | https://docs.salla.dev/2247590m0.md |