---
name: salesys-vyer
description: >-
  Build, test, and deploy custom SaleSys views ("vyer" / view presets / custom views / vymallar) —
  standalone HTML panels that mount inside app.salesys.se navigation or settings. Two authoring
  paradigms: the NATIVE way (handlebars + built-in components like <Field>, <SecondaryButton>,
  <SettingsList>, <Transition>, Modal, and the window.form.* API) for forms/config/CRUD panels, and
  the RAW-JS way (cookie token + direct fetch + document-level delegation) for complex apps/dashboards/games.
  Covers every native component and window.form method, the handlebars helper set, the
  views-v1/view-presets-v1/presetState/rights/entity-groups/notify APIs, the reference-object-as-database
  pattern, and the repo's build-harness + deploy-preset workflow. Use whenever asked to make/edit/deploy
  a "vy", "view", "custom view", "view preset", "vymall", "inställningsvy", or a SaleSys admin panel/dashboard/screen.
---

# SaleSys egna vyer (custom views)

A **vy** is a standalone HTML fragment that SaleSys renders as a panel inside `app.salesys.se` —
reachable from the top navigation or from Inställningar. No framework, no build step: one HTML file
with an inline `<style>` and `<script>`. It talks to the SaleSys REST API as the logged-in user.

Two layers:
- **View preset** (`view-presets-v1`, admin) = the reusable template (`html` + optional `settingsHtml`, icon, rights).
- **Org view** (`views-v1`) = a per-organisation instance pointing at a preset via `presetId`, deciding
  navigation placement + team/role visibility. URL: `https://app.salesys.se/views/<viewId>`.

## ⚠️ First decision: which paradigm?

There are **two ways to author a view**. Pick before writing a line — they don't mix well.

| | **Native (handlebars + components)** | **Raw JS** |
|---|---|---|
| Best for | Forms, settings/config panels, master-detail CRUD, anything that looks like the rest of SaleSys | Dashboards, games, custom canvas/layout, heavy realtime, xlsx export, anything the components can't express |
| Uses `{{ }}` | **Yes** — this is the templating language. `{{custom.x}}`, `{{#each}}`, `{{#if}}` | **Never** — you build the DOM yourself; `deploy-preset.py` refuses any file containing `{{` |
| Components | `<Field>`, `<SecondaryButton>`, `<SettingsList>`, `<Transition>`, `<CheckboxField>`, `<Tooltip>`, Modal | plain HTML + your own CSS, matching native chrome via CSS variables |
| State | `window.form.custom` / `setState`, `window.form.settings` / `setSettings`, `window.form.addHook` | your own JS variables + a poll loop; persist in the user `reference` object |
| Data access | `window.form.resources.*` (preloaded) + `fetch` with `window.getAccessToken()` | `fetch` with token read from the `s2_utoken` cookie |
| Edited | in the browser (admin.salesys.se) or via API | authored in the `vyer/` repo, deployed with `deploy-preset.py` |
| Reference example | `examples/webhooks.view.html`, `examples/stangtider.view.html` (both bundled here) | any dir under `vyer/` (`tavlingar2/`, `qa/`, `tidsrapportering/`) |

**Rule of thumb:** if it's a form or a settings screen, go **native** — you get validation, theming, and
the SaleSys look for free. If it's an interactive app that fights the component model, go **raw JS**.
The `{{`-refusal in `deploy-preset.py` exists *because the repo standardised on raw JS* — it is a repo
choice, not a platform limit. The platform happily renders handlebars (Webhooks, Stängtider, Rapportverktyg all do).

## Read next (progressive disclosure)

- **`reference/native-components.md`** — the full catalog: every component + attribute, the complete
  `window.form.*` API, the modal system, the handlebars helper set, `resources`, partials. **Read this
  before building a native view.** The bundled `examples/webhooks.view.html` is the masterclass — a
  full CRUD panel (list → modal editor → preview → nested actions) built entirely from native components;
  `examples/stangtider.view.html` is a second complete worked example (opening-hours rules + call routing).
- **`reference/apis-and-deploy.md`** — REST endpoints, `presetState`, custom `rights`, the
  `reference`-object-as-database pattern, in-app notifications, files, entity groups, and the
  build-harness → deploy-preset workflow with the raw-JS gotchas. **Read this before deploying anything
  or for any data/API question.**
- In the SaleSys repo: `vyer/README.md`, `vyer/deploy-preset.py`, `vyer/presets.json`, `vyer/PROJECT-LOG.md`,
  and any existing view dir are the live source of truth — always cross-check them.

## Workflow (either paradigm)

1. **Scaffold** `vyer/<name>/<name>.html` — copy the closest existing view (native → an example here; raw JS → a `vyer/` dir).
2. **Test locally** before prod. Raw-JS views: `python3 vyer/build-harness.py` → serve `/tmp/ksv-<name>-test.html`
   and **retest after `document.body.innerHTML = document.body.innerHTML`** (the app re-renders the view DOM
   after init). Native views: preview in admin.salesys.se ("Förhandsgranska"); `window.form.preview` is `true` there.
3. **Deploy the preset** (admin token in `/tmp/ksv-token`): `python3 vyer/deploy-preset.py <name>`
   (first run POSTs + records the id in `presets.json`; later runs PUT). Register the view in `deploy-preset.py`'s
   `VIEWS` dict (and `build-harness.py` if raw JS). Preset HTML changes propagate to every org view instantly.
4. **The first org view must be created via API** — a preset's settings only surface in the UI once one org
   view exists: `POST /users/views-v1 { presetId, name, iconName, iconColor, locations, teamIds: null }`
   (`null` = everyone, `[]` = admin-only). Thereafter edit HTML in the browser.

## Rules that apply to BOTH paradigms

- **No in-view title/heading** — the app already renders the view's name as a large title top-left
  (part of the nav chrome). Do NOT repeat it with your own `<h1>`/header inside the view — it shows
  the name twice. Start straight with content. Native views can add a subtitle via
  `window.form.setSubtitle("…")` and a title-bar action via `window.form.setButton(...)`; a detail/sub
  heading *inside* a card is fine, just not a top-level view title. (For a full-bleed compact layout you
  may instead hide the app title with CSS, e.g. `.ribbon_title__3ukce{display:none}` — the QA view does this.)
- **Never set page width** — the SaleSys app container controls the view's width and outer padding.
  A view fills the container fluidly; it must NOT impose its own page width, `max-width`, fixed pixel
  width, `margin: 0 auto` centering, or outer horizontal margins on its root. Lay out *within* the given
  width with fluid/responsive CSS (%, `fr`, flex/grid, `minmax`); constrain only inner elements when needed,
  never the whole view. Don't assume a viewport width either — the container narrows on smaller screens and
  when side widgets are open.
- **No emojis** — FontAwesome 5.10.2 (global) or inline SVG. `✓`/`×` typographic marks are fine. (User rule.)
- **Theme via CSS variables with fallbacks** — `var(--color-background-2, #fff)`, `--color-text-2/3`,
  `--color-primary`, `--color-background-1/3`, `--color-element-hover`, `--border-radius-button` (8).
- **No native `<select>` in raw-JS forms** — use icon-card grids / chip rows / preview cards + a side info-panel.
  (Native views use `<Field select-type="dropdown|switches">` instead, which is fine.)
- **Icons**: FontAwesome 5 free — https://fontawesome.com/v5/search?ic=free-collection

Raw-JS-only rules (event delegation on `document`, `window.__<ns>Cleanup`, the mandatory contrast probe
in the poll loop, cookie-token auth) live in `reference/apis-and-deploy.md`.
