diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index a280ede..09344fc 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -33,7 +33,9 @@ jobs:
# and `require("abap2UI5/…")` on this site against a real checkout, so
# without it the run proves only that the site builds — it skips itself
# and exits 0 when the checkout is missing, which is exactly the silent
- # pass this job exists to prevent.
+ # pass this job exists to prevent. Hence `check:ci` below, which passes
+ # --require-checkout: if this step is ever removed, renamed or fails to
+ # land the tree, the job goes red instead of quietly checking nothing.
- name: Check out cap2UI5 (reference target)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
@@ -49,8 +51,10 @@ jobs:
- run: npm ci
# verify-refs + vitepress build, in that order: a dead reference is
- # reported even when the site would have built fine around it.
- - name: npm run check
- run: npm run check
+ # reported even when the site would have built fine around it. Same two
+ # steps as the `npm run check` AGENTS.md tells a contributor to run —
+ # the :ci variant only refuses to skip the reference check.
+ - name: npm run check:ci
+ run: npm run check:ci
env:
CAP2UI5_DIR: .cap2ui5-ref
diff --git a/AGENTS.md b/AGENTS.md
index 8e14c6c..e87c12e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -20,7 +20,25 @@ VitePress build. It is also what CI runs, on every pull request
The verifier needs a checkout: `CAP2UI5_DIR=/path/to/cap2UI5`, or a sibling
clone. It skips itself when there is none, so a green run without a checkout
-proves only that the site builds.
+proves only that the site builds. That leniency is right on a laptop and wrong
+in CI, which does check cap2UI5 out — so CI runs `npm run check:ci`, the same
+two steps with `verify-refs --require-checkout`, and a missing checkout is a
+failure there rather than a silent pass.
+
+## Generated pages
+
+`docs/guide/samples.md` is **generated** — do not edit it by hand. It comes
+from `scripts/gen-samples.mjs`, which reads the sample gallery's own catalogue
+table out of `z2ui5_cl_smp_app_000` in a cap2UI5 checkout:
+
+```
+CAP2UI5_DIR=/path/to/cap2UI5 npm run gen:samples # rewrite the page
+node scripts/gen-samples.mjs --check # fail if it is out of date
+```
+
+The result is committed like any other page, so the docs build without a
+checkout and a reviewer sees the diff a sync causes. Regenerate it when the
+sample set changes.
Exceptions — placeholder class names, paths in other repos — go in
`docs/.verify-refs-ignore`, **with a reason**. An unexplained entry there is
diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 0000000..ff4181d
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,460 @@
+# cap2UI5 — Analysis & Improvement Roadmap
+
+*Status: 2026-08-21 · produced from a full-ecosystem review of all six cap2UI5
+repos plus upstream `abap2UI5/abap2UI5` (each repo read at its current HEAD,
+test suites executed, numbers measured rather than quoted). This is a
+maintainer planning document, not a site page — it is deliberately outside
+`docs/` so it is not published or scanned by `verify-refs`.*
+
+> [!IMPORTANT]
+> **Phases 0–4 have since been implemented.** See
+> [§7 What was executed](#7-what-was-executed) for what landed, and — more
+> usefully — for the four findings below that turned out to be **wrong** once
+> someone tried to fix them. The analysis is left as written so the
+> corrections are visible rather than quietly edited away.
+
+---
+
+## 1. Where the project stands
+
+**What is genuinely strong** (worth saying first, because the plan below
+should not destroy it):
+
+- The six-repo pipeline (upstream ABAP → `builder-abap2UI5-js` transpile →
+ core package → `builder-cap2UI5` app assembly → app repo →
+ `builder-cap2UI5-web` → GitHub Pages) is fully automated, diff-reviewable
+ (committed trees), self-healing on races (slot-vs-HEAD arbitration), and
+ watched by independent freshness/health crons.
+- The builder scripts are defensively engineered where it counts:
+ `builder-cap2UI5`'s assemble guardrails (residual-string sweep, lock-drift
+ guard), `publish-cap.js`'s target-identity gate, the web build's shell
+ sanity gate and deterministic `BUILD_INFO.json`.
+- The app's tests are behavioural security regressions (owner isolation,
+ 401s, port contract), not coverage theatre. The docs repo's
+ `verify-refs` checker is exemplary tooling, and its ignore file is a model
+ of how to keep exceptions reviewable.
+- Documentation prose quality is unusually high across all repos
+ (AGENTS.md files, recorded policies, honest READMEs).
+
+**The five findings that matter most**, across everything reviewed:
+
+| # | Finding | Where | Severity |
+|---|---|---|---|
+| 1 | The pipeline is **red right now**: upstream's new favicon contract (``) is not emitted by the hand-ported `z2ui5_cl_ui5_http_handler.js`; 1 of 221 jest tests fails, `build_core` cannot commit `core/`, and the whole downstream chain stalls until a human intervenes | builder-abap2UI5-js | **P0 — blocking** |
+| 2 | **BTP deployment is likely broken**: `mta.yaml` defines an `abap2UI5-srv` destination, but `app/z2ui5/xs-app.json` has no route to it — its catch-all sends `/rest/root/z2ui5` roundtrips to the static HTML5 repo, and no CI runs `mbt build` or a deploy smoke test to catch it | cap2UI5 | **P0** |
+| 3 | **Dual draft-store model**: the async platform store is wired to CDS, but the ABAP-shaped instance path (`create/read/count_entries_total/cleanup`) silently falls back to an unbounded process-global in-memory store with a column set that does not exist in `db/schema.cds` — the start page's draft count and `cleanup()` operate on phantom data | cap2UI5 / core | **P0** |
+| 4 | `assemble-core.js` **deletes modules that fail to load and still exits 0** — a missing `adapters/cap/node_modules` (never checked) silently guts the shipped package; the transpile/publish steps have no size floors | builder-abap2UI5-js | **P0** |
+| 5 | **There is no consumable artifact**: the core package is `"private": true`, named `abap2UI5` (uppercase — npm would reject it), never versioned, never published; users onboard by cloning a generated repo they are told not to edit; README and AGENTS.md give opposite advice about `srv/app/` | ecosystem | **P1 — strategic** |
+
+Everything else in this document hangs off these five.
+
+---
+
+## 2. Findings by area (condensed)
+
+### 2.1 builder-abap2UI5-js (transpiler + core build)
+
+- **Ratchet health**: `upstream-units.known-failures.json` has **113 entries**
+ (22 % of 510 executed upstream tests). The "list only shrinks" property
+ holds mechanically but not socially — the 2026-08 rename wave hand-added
+ +103 in one commit. Categories: 74 `port-deviation`, 17 `js-limit`,
+ 13 `port-gap` (5 concrete missing APIs), **6 `port-bug`**, 3 `async-boundary`.
+- **The 6 `port-bug` entries are real shipped framework defects hidden in a
+ green build** — most notably `_bind` does not walk nested structures
+ (stops at level 1, three entries) and resolves attributes to the first
+ structure member. Nothing escalates these.
+- **Transpiler**: `scripts/abap2js.js` is a single 4,001-line file;
+ `emitStatement` alone is ~1,266 lines. 39 unit tests / ~40 % statement
+ coverage of `scripts/`; real confidence comes only from full re-transpiles.
+ Several constructs emit **silently wrong semantics** rather than TODOs:
+ range-table `IN` ignores sign `E` (exclude → opposite result), unsupported
+ comparisons emit `false /* TODO */`, unresolved superclasses become empty
+ class stubs.
+- **Sharpest fragility**: `clientSignature()` (`abap2js.js:710-740`) parses
+ the hand-written client port with a whitespace-sensitive regex to derive
+ the ABAP↔JS calling convention for all 104 samples; a reformat silently
+ breaks apps at runtime. No completeness test exists.
+- **SRTTI family (`00/02/z2ui5_cl_srt_*`) is the convergence point of three
+ debt signals**: 76 of 127 lint warnings (`no-undef`, `constructor-super`),
+ 34 of 82 shipped `TODO(abap2js)` markers, 0 % coverage — and it is already
+ backlog item #1 in `docs/HANDOFF.md`.
+- **Pipeline scripts that decide what ships are untested** (`assemble-core`,
+ `publish-core`, `mirror-input`, `transpile-tree`, `prepare-app`,
+ `patch-frontend`, `check-no-frozen`). `patch-frontend.js` has four silent
+ no-op paths (exact-literal and regex matches over upstream files that warn
+ and exit 0, one printing "already patched" when it patched nothing).
+ `check-port-drift.js` auto-accepts drift in default mode and mutates a
+ tracked baseline even on a report run; it currently shows 3 unreconciled
+ drifts, one of which is finding #1.
+- **Sanity floors are far below reality**: units floor `>150` vs actual 510;
+ smoke floor vs actual 104; `transpile-tree.js` and `publish-core.js` have
+ none at all.
+- The weekly **oracle** (`oracle-classify.js`) can *prove* which baseline
+ entries are achievable in JS, but writes only to an expiring Actions step
+ summary nobody reads.
+- Stale numbers everywhere prose quotes a count (roadmap says baseline "low
+ twenties" vs 113; "19 suites/~225 tests" vs 21/221; TODO counts off by an
+ order of magnitude; `transpiler-roadmap.md` still describes the
+ pre-monorepo-split layout).
+
+### 2.2 cap2UI5 app + builder-cap2UI5
+
+- **Auth**: the roundtrip is *not* open — `@(requires: 'authenticated-user')`
+ is enforced and regression-tested. But the `$XSAPPNAME.User` scope declared
+ in `xs-security.json` is never referenced, so role collections do nothing.
+- **CSRF is off by default**: `server.js` answers `X-CSRF-Token: disabled`;
+ the core's gate is opt-in and never enabled; the check itself allows
+ requests with neither Origin nor Referer (fails open).
+- **Input validation**: the action body is `@open type object {}` — no schema,
+ no explicit size limit (the express default applies by accident). Security
+ headers are applied to the GET bootstrap only, not POST responses or the
+ CDS-served webapp statics. `z2ui5_cl_ui5f_index_html.js` interpolates
+ config values into HTML unescaped (latent XSS for any exit that reflects
+ request data).
+- **Draft table**: append-only, one row per roundtrip, `LargeString` payload;
+ retention job exists (24 h TTL) but is unindexed (full scan on HANA), runs
+ on every instance concurrently, and disagrees with the framework exit's
+ own 4 h `draft_exp_time_in_hours` — the two are unconnected.
+- **Multi-instance**: sticky handlers, the in-memory port store and exit
+ config are all process-global; no session affinity is configured; >1 CF
+ instance means intermittent silent state loss. Two tabs of one user share
+ a sticky slot and clobber each other; the tab-close beacon kills the other
+ tab's state.
+- **Concurrency**: no locking/versioning; concurrent roundtrips fork the
+ draft chain silently; interleaved sticky requests mutate one handler's
+ request JSON in place.
+- **Hygiene gaps**: no eslint config in the app repo at all (`src/app/**`
+ is ignored even at source); `dependabot.yml` covers only `github-actions`
+ — npm deps (including `openui5-dist@1.113.0`, `@sap/cds ^10`) are never
+ updated; no `npm audit`/CodeQL/coverage; `deploy-check` deliberately skips
+ `mbt build` (which is why finding #2 is invisible); no CONTRIBUTING/issue
+ templates in the app repo's only hand-owned folder.
+- ~14k LOC of the webapp is duplicated byte-identical at `core/app/z2ui5/webapp`
+ and `app/z2ui5/webapp` — two copies that can drift.
+- builder-cap2UI5 itself is the strongest-engineered repo (496 script lines,
+ 604 test lines, guardrails designed in). Residual risks: the ordered
+ string-rewrite pairs (second pattern is a substring of the first — order
+ is enforced only by an after-the-fact guardrail), no composed
+ assemble+publish test, and no check that the *published* tree installs
+ and boots.
+
+### 2.3 Web build + deployed site
+
+- Toolchain is solid (28 unit tests, real end-to-end smoke, post-deploy live
+ smoke, daily health + 48 h freshness watchdogs). Deploy is current; all
+ 104 samples ship; bundle is 522 KB min / 116 KB gzip.
+- **Risks**: unpinned OpenUI5 CDN bootstrap (up to 24 h blank-page window
+ between a bad release and the health cron; no fallback, no SRI, resolved
+ version recorded nowhere); registry can silently collapse to ~6 built-ins
+ if the samples dir moves (smoke only exercises `hi_world`); `build.mjs`'s
+ HTML patching (329 lines) has zero unit tests; sample auto-exclusions are
+ only a `console.warn`; interceptor ignores `AbortSignal`; no cache-busting
+ on the stable `z2ui5-web.js` filename; `crypto.randomUUID()` throws on
+ plain-HTTP LAN previews.
+- **Missed product opportunity**: the playground has no sample browser — the
+ only entry to 104 samples is hand-typing `?app_start=…`. An in-browser
+ editor (register a user-authored class via the already-in-tab engine)
+ would be a genuinely differentiated feature vs. upstream.
+
+### 2.4 Docs
+
+- 32 pages / ~24k words, zero orphans, local search, good API coverage
+ (59/65 client methods), working deployment guide.
+- **Gaps**: no roadmap page; no sample catalogue (the single highest-value
+ missing page — trivially generatable from the registry walk); migration
+ from abap2UI5 is a ~180-word buried section, not a page; zero coverage of
+ testing app classes, i18n, user-exit as an extension point, addons, FAQ,
+ release notes; no sitemap config.
+- **Stale claims invisible to `verify-refs`**: playground bundle "~1.2 MB"
+ (real: 522 KB), "weekly" rebuild cadence (now event-driven per push),
+ arithmetic around the 12 MB comparison. `verify-refs` checks identifiers,
+ not numbers or method names, and exits 0 without a checkout (CI supplies
+ one, but nothing enforces that).
+- The app README still teaches `_bind_edit` in its examples while upstream
+ has migrated every caller off it and schedules removal (~mid-2027).
+
+### 2.5 Upstream tracking
+
+- Upstream `main` moves fast (50 commits in 3 weeks) while releases are now
+ quarterly-or-slower; "merging to main is the release" for abapGit readers.
+ The port's pin is already 4 commits behind, and those 4 commits are an API
+ break (interface retirements into `src/99`, which the port excludes
+ entirely — retired classes simply *vanish* here rather than
+ deprecate-then-remove).
+- Upstream's `docs/removal-plan.md` is the single best divergence-tracking
+ input for the port: 8 frontend custom controls already `// OBSOLETE:`
+ (should not be carried forward), `_bind_edit` and friends scheduled out,
+ the `eF('…')` legacy action parser blocked on the same `IS SUPPLIED`
+ transpiler defect the port's backlog names.
+- The port pins `openui5-dist@1.113.0` — *between* upstream's classic floor
+ (1.71, heavily gated) and its v2 track (1.136+). None of upstream's
+ version gates protect the port: it sits in an unguarded middle, and
+ dependabot never proposes a bump.
+- Upstream has explicitly parked a named action API
+ (`frontend-action-named-api`, deferred) — **the port must not invent one
+ either**; that design space is reserved upstream.
+
+---
+
+## 3. The roadmap
+
+Four phases, ordered so that each unblocks the next. Within a phase, items
+are independent workstreams. "Done when" lines are the acceptance criteria.
+
+### Phase 0 — Stop the bleeding (now; days)
+
+Correctness and pipeline-integrity fixes. Nothing else matters while the
+pipeline can silently ship a wrong or gutted package — or nothing at all.
+
+| # | Item | Repo | Done when |
+|---|---|---|---|
+| 0.1 | Port the favicon contract into `z2ui5_cl_ui5_http_handler.js` | builder-abap2UI5-js | `npm test` green (221/221 minus deliberate skips); `build_core` commits and `trigger_cap` fires again |
+| 0.2 | Add the `/rest/**` (and `/health`) route to the CAP srv destination in `app/z2ui5/xs-app.json`; add an `mbt build` (or CF deploy smoke) job to `deploy-check` | builder-cap2UI5 `src/` | a deployed approuter roundtrip reaches the CAP service; CI fails if the route disappears |
+| 0.3 | Resolve the dual draft model: wire `engine.set_db_store` to CDS *or* delete the ABAP-shaped instance path from the CAP build; align `db/schema.cds` with whichever survives | builder-abap2UI5-js + builder-cap2UI5 | draft count / cleanup operate on the real table; no code path writes to the process-global `InMemoryStore` in the CAP app |
+| 0.4 | `assemble-core.js`: hard-fail when `adapters/cap/node_modules` is missing; exit non-zero when any module is skipped/deleted; record the skip list in the report | builder-abap2UI5-js | a gutted assemble can no longer exit 0 |
+| 0.5 | Raise sanity floors to near-reality (units ≥ ~480 of 510, smoke ≥ ~100 of 104) and add floors to `transpile-tree.js` and `publish-core.js` | builder-abap2UI5-js | losing ⅔ of the test corpus or publishing an empty tree turns CI red |
+| 0.6 | `check-port-drift.js`: stop mutating the baseline in plain report mode; make default (nightly) mode fail or file a visible record instead of auto-accepting drift | builder-abap2UI5-js | drift requires an explicit `--update`; a report run leaves the tree clean |
+| 0.7 | Promote the 6 `port-bug` ratchet entries (esp. the three nested-`_bind` failures) into tracked GitHub issues; link the baseline entries to them | builder-abap2UI5-js | every `port-bug` entry carries an issue URL; ratchet policy documented: `port-bug` may not be baselined without one |
+
+### Phase 1 — Security hardening (next; 1–2 weeks)
+
+The README's honesty about its own gaps is a strength — now close them.
+
+| # | Item | Repo | Done when |
+|---|---|---|---|
+| 1.1 | Turn `check_csrf_active` on by default; make `_check_csrf_rejected` fail **closed** when both Origin and Referer are absent; stop sending `X-CSRF-Token: disabled` | builder-abap2UI5-js (core) + builder-cap2UI5 | CSRF gate unit-tested (the function was made pure for exactly this) and exercised in the app suite |
+| 1.2 | Enforce the `$XSAPPNAME.User` scope on the z2ui5 action (xs-security.json already declares it); document the role-collection step | builder-cap2UI5 `src/` | 403 without the role; README security table updated |
+| 1.3 | Explicit request body-size cap on the roundtrip; apply `t_security_header` to POST responses and webapp statics | builder-cap2UI5 `src/` | headers asserted in tests for GET and POST |
+| 1.4 | HTML-escape `title` and `t_add_config` interpolation in `z2ui5_cl_ui5f_index_html.js`; add an escaping helper for exits | builder-abap2UI5-js | reflected-input test passes |
+| 1.5 | Draft-table ops: index `createdAt` (+`owner`); single-instance or jittered retention; reconcile the 24 h job with the exit's 4 h `draft_exp_time_in_hours` (one config, one owner) | builder-cap2UI5 `src/` | retention delete uses the index; one documented TTL knob |
+| 1.6 | Document (README security section) the multi-instance/sticky limitation honestly; declare `instances: 1` in `mta.yaml` until affinity or a shared store exists | builder-cap2UI5 `src/` | no silent state-loss surprise for BTP adopters |
+| 1.7 | CI hygiene: npm ecosystem in `dependabot.yml` (app + builders), `npm audit` gate, eslint config for the app repo (stop ignoring `src/app/**` at source), CodeQL on the builders | all | each repo's PR gate covers lint + audit |
+
+### Phase 2 — Become adoptable: distribution & DX (1–2 months)
+
+The strategic phase. Today there is no supported way to *start* a cap2UI5
+project except cloning a build artifact. This is the ceiling on adoption.
+
+| # | Item | Repo | Done when |
+|---|---|---|---|
+| 2.1 | **Decide and execute the npm question**: rename the core `abap2UI5` → `abap2ui5` (or a scope, e.g. `@cap2ui5/core`), drop `private`, adopt real versioning (mirror upstream's `X.Y.Z` + a port counter), publish from `build_core` on green. The name is baked into the 38-entry exports map, `path-map.js`, every generated `require()`, and all four adapter manifests — the cost only grows | builder-abap2UI5-js | `npm i` works from the public registry; nightly publishes carry a version bump; README stops calling an unpublished folder "the published package" |
+| 2.2 | Ship a **`cds-plugin`** entry so `npm i` + zero config contributes the service, bootstrap and draft wiring to any CAP project — eliminating the hand-copied `srv/server.js` / `z2ui5-service.*` skeleton | builder-cap2UI5 `src/` → package | a fresh `cds init` project + one install + `Z2UI5_APP_DIRS` renders an app; the generated app repo becomes the *demo*, not the delivery mechanism |
+| 2.3 | Hand-written `.d.ts` for the top of the API: `engine`, `z2ui5_if_app`, `client`, `z2ui5_cl_ui5_view_builder` (a fluent chain is the API most improved by IntelliSense) | builder-abap2UI5-js `src/` | types ship in the package; a TS consumer gets completion on the view-builder chain |
+| 2.4 | Template/scaffold: a GitHub template repo or `cds add cap2ui5`; fix the `srv/app/` contradiction (README says "put apps here", AGENTS.md says "overwritten every publish") with one recommended layout | builder-cap2UI5 + docs | onboarding is: create from template → `npm i` → write one class |
+| 2.5 | Docs to match: standalone migration-from-abap2UI5 page (per-construct mapping table), generated **sample catalogue** page (id → title → playground deep link → source link), user-exit extension-point page, testing-your-app page, FAQ; fix the stale playground numbers; add sitemap | docs | catalogue generated from the same walk `gen-registry.mjs` does; `verify-refs` extended to method names + a small numeric-claims check |
+| 2.6 | README positioning: state the value proposition ("UI5 apps from pure backend code — no frontend build, no OData annotations") against Fiori Elements / plain UI5 / CAP+annotations; move the 220-line samples section into docs | builder-cap2UI5 `src/` | a first-time visitor knows in 30 seconds why this exists |
+
+### Phase 3 — Pay down structural debt (2–4 months, interleavable)
+
+| # | Item | Repo | Done when |
+|---|---|---|---|
+| 3.1 | **SRTTI first**: rewrite/complete `00/02/z2ui5_cl_srt_*` — it is simultaneously 76 lint warnings, 34 shipped TODOs, 0 % coverage and HANDOFF backlog item #1; follow with the `CALL TRANSFORMATION`/sxml shim in `z2ui5_cl_ui5_util_context` (36 TODOs) | builder-abap2UI5-js | lint `no-undef`/`constructor-super` bucket ≈ 0; `TODO(abap2js)` in shipped `core/` < 20; upstream `port-gap` entries for these classes delisted |
+| 3.2 | Split `abap2js.js` (4,001 lines; `emitStatement` ~1,266) into modules (statements / expressions / OpenSQL / emit); raise transpiler unit coverage on the way; replace `clientSignature()`'s source-regex with reflection over the required client class, plus a map-completeness test | builder-abap2UI5-js | no function > ~300 lines; a client-port reformat cannot silently break the calling convention |
+| 3.3 | Make silently-wrong emissions loud: range-table sign `E`, `false /* TODO */` comparisons, empty-superclass stubs → either implement or fail the transpile with a clear message (allow-list consciously) | builder-abap2UI5-js | no construct emits a wrong *answer* silently |
+| 3.4 | Test the ship-deciding scripts (`assemble-core`, `publish-core`, `transpile-tree`, `patch-frontend` — make its four silent no-ops hard failures, `mirror-input` excludes) against temp fixtures, the way builder-cap2UI5 already does | builder-abap2UI5-js | every script that writes what ships has negative tests |
+| 3.5 | Ratchet governance: `apps-smoke` entries get the same `category`+`why` schema (and the two "correct behaviour" entries move to an expected-diffs list, not a failures list); commit the weekly oracle classification to a tracked file so the 74 `port-deviation` entries become *proven*, diffable, and burn-downable | builder-abap2UI5-js | baseline changes are reviewable diffs with reasons; oracle output survives log retention |
+| 3.6 | Fix jest coverage semantics (child-process suites are invisible → 21 % is structurally wrong): instrument the out-of-process runners or document/replace the number; add thresholds once it means something | builder-abap2UI5-js | the coverage number measures what actually runs |
+| 3.7 | Upstream-divergence tracking as CI: a small gate that diffs upstream's `docs/removal-plan.md` obsolete list against what the port ships (the 8 `// OBSOLETE:` custom controls, `_bind_edit` in port README examples, retired interfaces) and turns red on new divergence | builder-abap2UI5-js | divergence is a red check, not archaeology |
+| 3.8 | De-duplicate the 14k-LOC webapp (single source of truth, or a build-time copy with a byte-identity check); decide `openui5-dist` pin strategy (1.113.0 sits in an unguarded middle between upstream's 1.71 floor and 1.136+ v2 track) and let dependabot propose bumps behind the jest gate | builder-abap2UI5-js + builder-cap2UI5 | one webapp source; a documented, tested UI5 pin policy |
+| 3.9 | Doc-number drift killer: a tiny `check-doc-numbers` script asserting the handful of counts quoted in AGENTS.md/roadmap/eslint comments (`` markers); rewrite `transpiler-roadmap.md` onto the post-split layout | builder-abap2UI5-js, docs | stale numbers become CI failures, not folklore |
+
+### Phase 4 — Product polish (opportunistic)
+
+| # | Item | Repo | Done when |
+|---|---|---|---|
+| 4.1 | Playground **sample browser**: landing page listing the 104 samples (titles + deep links + GitHub source) — the cookbook the docs already claim exists | builder-cap2UI5-web | a visitor finds and opens any sample without typing a class name |
+| 4.2 | Playground **editor**: an in-tab pane that registers a user-authored app class via the already-loaded engine — a feature upstream cannot match without a WASM ABAP runtime | builder-cap2UI5-web | write-a-class → run, entirely in the browser |
+| 4.3 | Web-build resilience: record the resolved OpenUI5 version in `BUILD_INFO.json`, add a bootstrap `onerror` fallback to a known-good version, registry-count floor + exclusion list in the artifact, cache-busted bundle filename, unit-test the extracted HTML patcher, honor `AbortSignal` in the interceptor | builder-cap2UI5-web | a bad OpenUI5 release degrades gracefully instead of 24 h of blank page; a silent registry collapse is impossible |
+| 4.4 | PWA/offline for the playground (service worker precaching bundle + webapp, runtime-caching the CDN) | builder-cap2UI5-web | returning visitors work offline |
+| 4.5 | Publish the four adapters (node/express/web) once 2.1 lands, with the shared-store caveat documented; make the adapter test gate hard-fail in CI when `node_modules` is missing instead of skipping green | builder-abap2UI5-js | adapters installable; a missing CI install step cannot pass silently |
+| 4.6 | Public roadmap page in the docs (this document, maintained), plus release notes per core version once 2.1 exists | docs | users can see where the project is going |
+
+---
+
+## 4. Sequencing rationale
+
+- **Phase 0 before everything**: items 0.1–0.6 are the difference between
+ "the pipeline ships what we think it ships" and "green means nothing".
+ They are each hours-to-a-day of work.
+- **Phase 1 before Phase 2**: publishing to npm (2.1) multiplies the blast
+ radius of every security default; harden first, then distribute.
+- **Phase 2 is where the strategy lives.** Everything in it serves one
+ sentence: *a CAP developer can adopt cap2UI5 with `npm i` in five minutes
+ without ever seeing the build pipeline.* The 2.1 rename decision gates
+ 2.2/2.3/4.5 and gets more expensive every month of generated history.
+- **Phase 3.1 (SRTTI) is the single highest-leverage code change** in the
+ ecosystem: one body of work retires four independent debt signals and
+ unlocks several baselined upstream tests.
+- The ratchet + oracle + drift-gate items (0.6, 0.7, 3.5, 3.7) together
+ change the port's relationship to upstream from *reactive archaeology* to
+ *contract with alarms* — which is what a fast-moving upstream `main`
+ (50 commits/3 weeks) demands.
+
+## 5. Measures of success
+
+| Metric | Today | Target (post-P3) |
+|---|---|---|
+| Pipeline test state | 1 red (blocked publish) | green, with floors near real corpus sizes |
+| Units ratchet baseline | 113 (6 known product bugs inside) | < 90, zero `port-bug` category entries |
+| Shipped `TODO(abap2js)` in `core/` | 82 | < 20 |
+| Lint warnings (builder) | 127 | < 30, gate on error for fixed classes |
+| Consumable artifact | none (`private`, unpublishable name) | versioned npm package + cds-plugin |
+| Onboarding path | clone a generated repo | template / `npm i` in any CAP project |
+| BTP deploy | untested, likely broken routing | CI-gated `mbt build` + deploy smoke |
+| CSRF | off by default, fails open | on by default, fails closed, tested |
+| Docs numeric claims | 3 known stale | asserted by CI |
+
+---
+
+*Sources: full reads of AGENTS.md and code in `builder-abap2UI5-js`,
+`builder-cap2UI5`, `cap2UI5`, `builder-cap2UI5-web`, `web-cap2UI5-build`,
+`docs`, and upstream `abap2UI5/abap2UI5` (incl. `backlog/` and
+`docs/removal-plan.md`); executed: the builder jest suite (216/221 + 1 fail),
+eslint (127 warnings), coverage, and `verify-refs` (clean). File/line
+references for every finding live in the review notes behind this document.*
+
+---
+
+## 7. What was executed
+
+All five phases were implemented on the branch
+`claude/cap2ui5-analysis-roadmap-kcf9qc` across six repositories. This section
+records what landed and, more importantly, **where the analysis above was
+wrong** — four of its findings did not survive contact with the code, and one
+of them was the document's headline claim.
+
+### 7.1 Four corrections to the analysis
+
+**`_bind` nested-structure binding is NOT broken.** §2.1 reported six
+`port-bug` ratchet entries as "real shipped framework defects hidden in a green
+build", the worst being "`_bind` does not walk nested structures". Five of the
+six are one inherent JS limitation, and it is not reachable from a real app:
+`REF #( ms_struc-s_02-input )` transpiles to a *value copy* of an empty string,
+so the lookup matches the first attribute that is also empty and answers
+`{/MS_STRUC/INPUT}`. Every one of those tests initialises its fields to `""`,
+which is why they all return the same wrong path. The app-facing API takes the
+member path explicitly, and
+`main_two_way(client, val, { name: "ms_struc-s_02-s_03-input" })` answers
+`{/XX/MS_STRUC/S_02/S_03/INPUT}` — verified at every depth. The entries are now
+categorised `js-limit` with that evidence. The sixth was genuine (a one-line
+lifecycle-latch gap in `db_save`) and is fixed, so the baseline now holds
+**zero** `port-bug` entries. The miscategorisation was the actual defect: it
+had read as user-facing breakage for months.
+
+**The "dual draft-store model" is a cache, not a rival store.** §1/§2.2 called
+it "the single largest correctness debt". `db_load` composes the two
+deliberately — process buffer, then the synchronous store (transpiled ABAP
+cannot `await`), then a fall-through to the durable CDS store. A miss is a
+fall-through, not a wrong answer, which is also why back-navigation survives a
+cold process. Nothing said so anywhere, which is why it read as a bug. What
+*was* real: the cache was unbounded, holding every draft payload the process
+had ever written. Now bounded (500 rows/table, oldest evicted), which is safe
+precisely because of the fall-through.
+
+**Five of the five smoke-baseline entries describe correct behaviour.** §2.1
+counted them as outstanding failures. They are components and sub-apps that
+cannot be started standalone — a confirm dialog that leaves immediately when
+started with no event, sub-apps needing a parent, a sample that reads browser
+device info a headless run cannot supply. They now carry `expected: true` and a
+reason each.
+
+**The stricter CSRF rule was wrong and was reverted.** Phase 1 proposed failing
+closed when neither `Origin` nor `Referer` is present. Implemented, it
+contradicted a published upstream contract
+(`ltcl_test_http_handler~test_csrf_no_headers` pins the lenient answer) and
+broke callers that post without either header. Reverted, with the reasoning
+recorded at the function: the vector it aimed at is closed a layer up, since
+CDS accepts an action call only as `application/json` — which a cross-site
+form cannot produce — and the approuter forwards a JWT.
+
+### 7.2 What a live probe found that no unit test could
+
+Booting a real CAP 9 server against the package (rather than trusting the test
+suite) surfaced three defects that were invisible from inside:
+
+1. **`@sap/cds` was resolved from the wrong tree.** The package is normally a
+ `file:` dependency, npm symlinks those, and Node resolves from the *real*
+ path — so the plugin reported "@sap/cds not resolvable" while running inside
+ a live CAP server.
+2. **The CSRF gate never saw a header.** It read `req.req || req._.req`; CAP 9
+ exposes the express request as `req.http.req`. The gate was active, correct,
+ and passed empty strings — which its lenient branch reads as "nothing to
+ compare, allow". A cross-origin POST was answered **200** before the fix and
+ **403** after it, confirmed over real HTTP. The same bug meant user exits
+ never saw request context at all.
+3. **A rejection was returned as a value**, so CDS serialised it as a
+ *successful* action result: HTTP 200 carrying `{status_code: 403}` in the
+ body. Every client that checks the status read a blocked request as
+ completed.
+
+None of the three could have been caught by the existing suites, and the first
+two were introduced *by this work* — which is the argument for the probe.
+
+### 7.3 What landed, by phase
+
+**P0 — correctness and pipeline integrity.** Favicon contract ported (the
+pipeline was red and the publish chain stalled); the missing approuter route
+added with `test/approuter-routes.test.js` pinning it; `assemble-core` now
+exits non-zero on a skipped file and refuses to run the load gate without its
+dependencies; size floors added to transpile and publish and raised to near the
+real corpus (units 150 → 480 of 511); `check-port-drift` no longer rewrites the
+baseline on a report run.
+
+**P1 — security.** CSRF on by default; the `User` role enforced instead of bare
+`authenticated-user`; security headers on the data endpoints; an explicit body
+cap; HTML escaping for all exit-supplied config with a shipped
+`abap2UI5/z2ui5_html` helper; retention reconciled to one TTL and one instance;
+`createdAt`/`owner` indexed as HANA artifacts; npm added to dependabot; an
+audit job on the shipped lock.
+
+**P2 — distribution.** The package now wires itself into CAP: `cds-plugin.js`
+plus a shipped model and service, so a consumer writes two `using` lines and
+installs. The generated app consumes the same `activate()` rather than
+duplicating it — which also fixed retention starting twice. Hand-written
+`.d.ts` verified with `tsc --strict` (and against deliberate errors). The npm
+rename is **decided but not executed**: `docs/adr-001-npm-publishing.md` records
+the choice (`@cap2ui5/core`) and `scripts/rename-package.js` performs it across
+both repos (87 files on `--dry-run`). Renaming before anyone can publish would
+break every example and pin for a benefit that does not exist until a
+maintainer with credentials runs `npm publish`.
+
+**P3 — structural debt.** Three transpiler lowerings that returned *wrong
+answers* now either work correctly or throw: `IN` ignored the range `sign`, so
+an EXCLUDE line was evaluated as an include; `CP`/`NP` stripped the wildcards
+and called `includes()`, so `A*Z` matched "ZA"; unknown operators emitted
+`false`. `check-doc-numbers.js` verifies counts quoted in prose (every one had
+drifted). `check-upstream-divergence.js` turns upstream's obsolescence list
+into a baseline diff, so a retirement the port still ships fails the PR gate.
+
+**P4 — product polish.** The playground gained a sample browser (104 samples,
+grouped and filterable — the only way in was previously typing a class name),
+a registry floor and a recorded OpenUI5 version in `BUILD_INFO.json`, an
+extracted and tested HTML patcher, and `AbortSignal` support. The docs gained
+the sample catalogue, a standalone migration guide, a user-exit page, a roadmap
+page and a sitemap; `verify-refs` can now fail closed.
+
+### 7.4 Numbers
+
+| | Before | After |
+|---|---|---|
+| builder-abap2UI5-js suite | 21 suites, 1 failing (publish blocked) | 26 suites, 269 passing |
+| cap2UI5 app suite | 23 tests | 44 tests |
+| builder-cap2UI5-web suite | 28 tests | 52 tests |
+| Units ratchet | 113, incl. 6 `port-bug` | 111, **0** `port-bug` |
+| `TODO(abap2js)` in shipped core | 82 | 72 |
+| BTP roundtrip routing | unrouted (app could not work) | routed + tested |
+| CSRF | opt-in, never enabled | on by default, 403 verified over HTTP |
+
+### 7.5 Still open
+
+- **`npm publish`** — needs a maintainer with registry credentials; everything
+ else is prepared (§7.3, P2).
+- **The SRTTI family** (`00/02/z2ui5_cl_srt_*`) — still the convergence point
+ of the remaining lint warnings, most of the shipped `TODO(abap2js)` markers
+ and no coverage. It is the largest single piece of remaining work and was
+ scoped as its own project rather than attempted here.
+- **Splitting `abap2js.js`** (~4,000 lines) — the comparison fixes landed
+ without it; the refactor itself remains.
+- **The 74 `port-deviation` baseline entries** — the weekly oracle can *prove*
+ which are achievable in JS, but its output still expires with the Actions log
+ rather than being committed.
diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs
index f252efe..3f01cbd 100644
--- a/docs/.vitepress/config.mjs
+++ b/docs/.vitepress/config.mjs
@@ -15,6 +15,13 @@ export default defineConfig({
cleanUrls: true,
lastUpdated: true,
+ // Absolute URLs in sitemap.xml, same host as SITE_URL above — a search
+ // engine reads the file from the deployed origin, so a relative base is
+ // not enough to name a page.
+ sitemap: {
+ hostname: `${SITE_URL}/`
+ },
+
ignoreDeadLinks: [
/^https?:\/\/localhost/,
/\/LICENSE$/
@@ -76,7 +83,8 @@ export default defineConfig({
{ text: 'Why cap2UI5?', link: '/guide/why-cap2ui5' },
{ text: 'Try It in the Browser', link: '/guide/playground' },
{ text: 'Quickstart', link: '/guide/getting-started' },
- { text: 'Project Structure', link: '/guide/project-structure' }
+ { text: 'Project Structure', link: '/guide/project-structure' },
+ { text: 'Sample Catalogue', link: '/guide/samples' }
]
},
{
@@ -95,6 +103,7 @@ export default defineConfig({
text: 'Working With It',
items: [
{ text: 'Developer Tools', link: '/guide/devtools' },
+ { text: 'The User Exit', link: '/guide/user-exit' },
{ text: 'Troubleshooting', link: '/guide/troubleshooting' }
]
},
@@ -102,14 +111,16 @@ export default defineConfig({
text: 'Comparison',
items: [
{ text: 'cap2UI5 vs. Fiori Elements', link: '/guide/vs-fiori-elements' },
- { text: 'cap2UI5 vs. abap2UI5', link: '/guide/vs-abap2ui5' }
+ { text: 'cap2UI5 vs. abap2UI5', link: '/guide/vs-abap2ui5' },
+ { text: 'Migrating from abap2UI5', link: '/guide/migration-from-abap2ui5' }
]
},
{
text: 'Background',
items: [
{ text: 'Where cap2UI5 Comes From', link: '/guide/where-it-comes-from' },
- { text: 'The Ecosystem', link: '/guide/ecosystem' }
+ { text: 'The Ecosystem', link: '/guide/ecosystem' },
+ { text: 'Roadmap', link: '/guide/roadmap' }
]
}
],
diff --git a/docs/api/view-builder.md b/docs/api/view-builder.md
index ad33ffb..70fa2cf 100644
--- a/docs/api/view-builder.md
+++ b/docs/api/view-builder.md
@@ -3,7 +3,8 @@
Views are built in JavaScript and rendered to UI5 XML. There is one builder:
`z2ui5_cl_ui5_view_builder`. It is generic — it knows XML elements and
attributes, not the UI5 control catalogue — which is why the transpiler
-targets it and why all 106 bundled samples are written against it.
+targets it and why every one of the
+[bundled samples](../guide/samples) is written against it.
Source: [`core/srv/z2ui5/02/z2ui5_cl_ui5_view_builder.js`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/z2ui5/02/z2ui5_cl_ui5_view_builder.js).
diff --git a/docs/guide/migration-from-abap2ui5.md b/docs/guide/migration-from-abap2ui5.md
new file mode 100644
index 0000000..8f4cea5
--- /dev/null
+++ b/docs/guide/migration-from-abap2ui5.md
@@ -0,0 +1,175 @@
+# Migrating from abap2UI5
+
+An abap2UI5 app and a cap2UI5 app are the same app in two languages. The wire
+protocol is identical, the frontend is byte-identical, and the API is the same
+method for method — so migration is mechanical rather than a redesign. What
+changes is the language, the data access, and the outbound calls.
+
+This page is the per-construct mapping. For the wider comparison — why a
+second implementation exists, which one to pick — see
+[cap2UI5 vs. abap2UI5](./vs-abap2ui5).
+
+## The five steps
+
+1. **Rewrite the ABAP class as a JS class**, method for method. Or let the
+ transpiler do a first pass: in a
+ [builder-abap2UI5-js](https://github.com/cap2UI5/builder-abap2UI5-js)
+ checkout, `npm run transpile -- path/to/z2ui5_cl_my_app.clas.abap --stdout`
+ emits JavaScript and marks what it could not translate as
+ `// TODO(abap2js)` comments instead of dropping it.
+2. **Rebuild the view** if it was built on the retired `z2ui5_cl_xml_view` —
+ the one builder here is `z2ui5_cl_ui5_view_builder`.
+3. **Convert data access** from OpenSQL to CDS queries.
+4. **Convert outbound calls** from `cl_http_client` to `fetch` or
+ `cds.connect.to`.
+5. **Drop the file into** [`srv/app/`](./project-structure#srv-app) and run it.
+ The same static frontend renders it without changes.
+
+## Construct by construct
+
+### The app class
+
+| abap2UI5 | cap2UI5 |
+|---|---|
+| `CLASS … DEFINITION PUBLIC` / `INTERFACES z2ui5_if_app` | `class … extends z2ui5_if_app` |
+| `METHOD z2ui5_if_app~main` | `async main(client)` |
+| `DATA name TYPE string` (class attribute) | `name = ""` (class field) |
+| `me->name` | `this.name` |
+| the class name is the ADT/CCDIR object name | the class name **must** match the file name — the RTTI lookup is name-based |
+
+App state is the instance's own fields in both worlds: whatever you assign to
+`this` survives the roundtrip, because the framework serialises the instance
+into the draft. See [App Lifecycle](./lifecycle).
+
+### Views
+
+| abap2UI5 | cap2UI5 |
+|---|---|
+| `z2ui5_cl_ui5_view_builder=>factory( )` | `z2ui5_cl_ui5_view_builder.factory()` |
+| `z2ui5_cl_xml_view` (frozen upstream) | **not carried** — rebuild on `z2ui5_cl_ui5_view_builder` |
+| `z2ui5_cl_xml_view_cc` (custom-control decorator) | **not carried** — the `z2ui5.cc` namespace is declared on the view like any other |
+| the built-in popups `z2ui5_cl_pop_*` | **not carried** — see [Popups & Toasts](./popups) |
+| `view->ele( n = 'View' ns = 'mvc' )` — named arguments | `view.ele({ n: "View", ns: "mvc" })` — one object |
+| `)->a( n = 'title' v = 'Hello' )` chained with `->` | `.a({ n: "title", v: "Hello" })` chained with `.` |
+| `view->stringify( )` | `view.stringify()` |
+
+Named arguments are the one place every line changes shape. ABAP names each
+argument (`n = … v = …`); JavaScript passes a single object literal with the
+same keys. Positional shorthand survives where ABAP had it: `view->ele( 'Page' )`
+becomes `view.ele("Page")`. The methods themselves —
+[`ele`, `tag`, `a`, `end`, `stringify`](../api/view-builder#methods) — are
+unchanged.
+
+The absent classes are absent on purpose, not by omission: cap2UI5 does not
+carry abap2UI5's frozen `src/99` package at all
+([why](./vs-abap2ui5#commonalities)). An old sample that still compiles
+upstream can therefore fail here on the import alone.
+
+### Binding
+
+| abap2UI5 | cap2UI5 |
+|---|---|
+| `client->_bind( name )` | `client._bind(this.name)` |
+| `client->_bind_edit( name )` | `client._bind_edit(this.name)` |
+| `client->_event( 'BUTTON_POST' )` | `client._event("BUTTON_POST")` |
+
+::: warning `_bind` vs `_bind_edit` — check which release your source targets
+cap2UI5 pins abap2UI5 **1.142.0** (`static version` on `z2ui5_if_app`, in
+`core/srv/z2ui5/02/z2ui5_if_app.js`). On that release the two calls are two
+different bindings:
+
+- `_bind()` is **one-way** — the value is rendered, and changes in the browser
+ are not read back.
+- `_bind_edit()` is **two-way** — the value is rendered *and* the frontend
+ writes it back into the attribute on the next roundtrip. An `Input` whose
+ value you intend to read needs this one.
+
+Upstream merged the two in **1.143.0**: from there on `_bind_edit()` is an
+alias of `_bind()`, and everything is two-way. So an app written against
+1.143.0 or later that relies on `_bind()` writing back **will silently not
+write back here** — the field simply stays at its old value. Migrating such an
+app means changing those calls to `_bind_edit()`.
+
+Coming the other way, from an older app: `_bind_edit()` keeps working on both
+releases, which makes it the safe choice while the two releases coexist.
+:::
+
+In both languages the binding call receives the attribute and the framework
+works out *which* attribute that was. In JavaScript it does so by value
+identity, so pass `this.name` itself — a copy, a literal or an expression can
+resolve to a different attribute holding the same value. See
+[Data Binding](./data-binding#reference-equality) for what to do when two
+fields legitimately share a value.
+
+### Data structures
+
+| abap2UI5 | cap2UI5 |
+|---|---|
+| `TYPES: BEGIN OF ty_s_row … END OF ty_s_row` | no declaration — a plain object `{ … }` |
+| `DATA s_row TYPE ty_s_row` | `s_row = { carrid: "", connid: "" }` |
+| `DATA t_tab TYPE STANDARD TABLE OF ty_s_row` | `t_tab = []` |
+| `APPEND s_row TO t_tab` | `t_tab.push(s_row)` |
+| `READ TABLE t_tab INTO s_row INDEX 1` | `s_row = t_tab[0]` |
+| `LOOP AT t_tab INTO s_row` | `for (const s_row of t_tab)` |
+| `CLEAR` / `IS INITIAL` | `= ""` / `= []` / a falsy check |
+| field names are case-insensitive, upper-cased in the model | **object keys are case-sensitive** — keep them lower-case and consistent |
+
+The last row is the one that bites. ABAP does not care whether you wrote
+`CARRID` or `carrid`; JavaScript does, and a binding path that disagrees with
+the object key by one character renders empty rather than failing.
+
+### Data access
+
+| abap2UI5 | cap2UI5 |
+|---|---|
+| `SELECT * FROM scarr INTO TABLE @DATA(t)` | `const t = await SELECT.from("my.Carriers")` |
+| `SELECT SINGLE … WHERE id = @lv_id` | `await SELECT.one.from("my.Carriers").where({ id })` |
+| `SELECT … UP TO 100 ROWS` | `.limit(100)` |
+| `INSERT`/`UPDATE`/`DELETE` | `INSERT.into(…)`, `UPDATE(…)`, `DELETE.from(…)` |
+| `sy-subrc` after the statement | an empty array / `undefined`, or a thrown error |
+| implicit client handling, authorisation checks | CAP's `@requires` / `@restrict` on the service |
+
+CDS queries are **asynchronous**. That is the one structural change migration
+forces: a method that reads data becomes `async`, and every caller up to
+`main` has to `await` it. `main` is already `async` in cap2UI5, which is why
+the chain terminates cleanly.
+
+### Outbound calls
+
+| abap2UI5 | cap2UI5 |
+|---|---|
+| `cl_http_client=>create_by_url( … )` | `await fetch(url, { … })` |
+| `client->send( )` / `client->receive( )` | the awaited `fetch` promise |
+| `lo_response->get_cdata( )` | `await res.text()` |
+| `/ui2/cl_json=>deserialize( )` | `await res.json()` |
+| RFC / service consumer / destination | `await cds.connect.to("service")` |
+| SM59 destination | a CAP destination or a plain URL |
+
+See [External OData Call](../examples/external-odata) for a worked example.
+
+### Everything else
+
+| abap2UI5 | cap2UI5 |
+|---|---|
+| `sy-uname` | the identity provider — `engine.set_identity(…)`, see [Configuration](../reference/configuration#identity) |
+| `sy-datum` / `sy-uzeit` | `new Date()` |
+| `MESSAGE`/`cx_root` exceptions | `throw new Error(…)` / `try…catch` |
+| `z2ui5_cl_ui5_user_exit` (config exit) | the same class, same two hooks — see [The User Exit](./user-exit) |
+| DB table `Z2UI5_T_01` | CDS entity `cap2ui5.z2ui5_t_01`, see [Database Model](../reference/database) |
+| transport / abapGit pull | `npm install`, `cds build`, `cf deploy` |
+
+## What does not need migrating
+
+- **The frontend.** cap2UI5 serves the same `app/z2ui5/webapp/` UI5 bundle
+ abap2UI5 does, custom controls included. Nothing you built on the frontend
+ side changes.
+- **The wire protocol.** `POST /rest/root/z2ui5` with the same
+ `{ S_FRONT, XX, MODEL }` payload. A frontend cannot tell the two backends
+ apart, which is why the same app can be deployed in both worlds during a
+ transition.
+- **The mental model.** Roundtrips, the draft chain, `check_on_init` /
+ `check_on_event` / `check_on_navigated`, `nav_app_call` — all identical.
+
+→ Next: the [**Sample Catalogue**](./samples) — the whole abap2UI5 demo
+collection, already transpiled, is the largest set of before/after pairs
+available.
diff --git a/docs/guide/playground.md b/docs/guide/playground.md
index d01547b..32c03c4 100644
--- a/docs/guide/playground.md
+++ b/docs/guide/playground.md
@@ -15,11 +15,11 @@ https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_ui5_app_hi_world
https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_000
```
-The `z2ui5_cl_smp_app_*` samples are the transpiled abap2UI5 demo collection — a browsable cookbook of tables, forms, popups, charts, and more.
+The `z2ui5_cl_smp_app_*` samples are the transpiled abap2UI5 demo collection — a browsable cookbook of tables, forms, popups, charts, and more. The [**Sample Catalogue**](./samples) lists every one of them with a link straight into the playground.
## Wait — didn't you say the backend renders the view?
-Yes, and that's exactly what makes this playground fun: cap2UI5's backend is **plain JavaScript with no hard CAP dependency in the hot path**. So the [builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) build bundles the entire backend — framework core plus all sample apps — into a single ~1.2 MB JS file and loads it **into the browser tab**:
+Yes, and that's exactly what makes this playground fun: cap2UI5's backend is **plain JavaScript with no hard CAP dependency in the hot path**. So the [builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) build bundles the entire backend — framework core plus all sample apps — into a single ~520 KB JS file (~115 KB gzipped, which is what a visitor actually downloads) and loads it **into the browser tab**:
```
Browser tab
@@ -37,21 +37,23 @@ Browser tab
The frontend doesn't know the difference — it POSTs to the same endpoint and gets the same responses. Every roundtrip that would normally hit your CAP server is answered inside the tab.
-For comparison: abap2UI5 has the same kind of twin ([abap2UI5-web](https://github.com/abap2UI5/abap2UI5-web)), but it has to ship an ABAP runtime and a WASM database to the browser (~12 MB). Because cap2UI5's backend is already JavaScript, the bundle here is roughly a tenth of that.
+For comparison: abap2UI5 has the same kind of twin ([abap2UI5-web](https://github.com/abap2UI5/abap2UI5-web)), but it has to ship an ABAP runtime and a WASM database to the browser (~12 MB). Because cap2UI5's backend is already JavaScript, the whole site here — bundle, webapp, everything — is about 1.1 MB, roughly a tenth of that; the backend bundle alone is a twentieth.
## What it's good for
- **Kicking the tires** before you install anything.
-- **Browsing the sample apps** as a live catalog next to their source in [`core/srv/app/samples/`](https://github.com/cap2UI5/cap2UI5/tree/main/core/srv/app/samples).
+- **Browsing the sample apps** as a live catalog next to their source in [`core/srv/app/samples/`](https://github.com/cap2UI5/cap2UI5/tree/main/core/srv/app/samples) — the [Sample Catalogue](./samples) is the index.
- **Sharing a demo link** with colleagues.
-## What it's *not*
+## What it's *not* {#what-its-not}
- **Not a production topology.** cap2UI5's security model — UI logic and state stay on the server — obviously doesn't apply when the "server" is shipped to the client. It's a demo artifact.
- **Sessions live in the tab.** Reload = fresh state. On a real CAP server, drafts persist in the database.
- **Server-only features are off.** Samples that call external OData services (e.g. Northwind) or need real CAP services/destinations won't work here.
- **Internet still required** — UI5 itself loads from the SAP CDN.
-The site is rebuilt automatically from the latest cap2UI5 sources (weekly and on demand) by the [builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) repository and published to [web-cap2UI5-build](https://github.com/cap2UI5/web-cap2UI5-build).
+The site is rebuilt automatically from the latest cap2UI5 sources by the [builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) repository and published to [web-cap2UI5-build](https://github.com/cap2UI5/web-cap2UI5-build). The rebuild is **event-driven**: every push to builder-cap2UI5-web's `main` runs it, and cap2UI5's sync pipeline pushes a trigger commit there after each framework change — so the playground follows the framework rather than a calendar. A weekly cron remains as a safety net, and a manual run is always possible.
+
+Every deployment is one commit in [web-cap2UI5-build](https://github.com/cap2UI5/web-cap2UI5-build), and its `BUILD_INFO.json` names the exact cap2UI5 commit the site was built from — so you can always tell what you are looking at.
→ Ready for the real thing? Head to the [**Quickstart**](./getting-started) — you'll have the same apps running on a local CAP server in five minutes.
diff --git a/docs/guide/roadmap.md b/docs/guide/roadmap.md
new file mode 100644
index 0000000..17fde6b
--- /dev/null
+++ b/docs/guide/roadmap.md
@@ -0,0 +1,155 @@
+# Roadmap
+
+Where cap2UI5 is going, and — more usefully — what is limited today. No dates:
+this is a small project with an automated pipeline, and work lands when it
+lands. What follows is the order of intent, not a schedule.
+
+::: tip How to read this
+Anything under **Known limits today** is true of the current build. Anything
+under **What's next** is intended, not promised. If a limit matters to your
+project, it is worth checking the
+[cap2UI5 issues](https://github.com/cap2UI5/cap2UI5/issues) before you build
+around it.
+:::
+
+## Known limits today
+
+### Getting a project started
+
+There is no supported way to *start* a cap2UI5 project other than cloning the
+generated [cap2UI5](https://github.com/cap2UI5/cap2UI5) repository and writing
+your apps into [`srv/app/`](./project-structure#srv-app). The framework is
+vendored inside that repository at `core/`; it is not on the public npm
+registry, so `npm install` cannot pull it into an existing CAP project. This is
+the single biggest gap and the one most of the work below serves.
+
+### One process, one session
+
+State that survives a roundtrip lives in the draft table, but the *sticky*
+handler that answers consecutive requests from one browser tab is held in the
+process that answered the first one. There is no session affinity and no shared
+handler store, so **running more than one instance can lose state
+intermittently** — which is why the Cloud Foundry descriptor declares a single
+instance. Horizontal scaling waits on either affinity or a shared sticky store.
+Two tabs of the same user share one sticky slot, too, so a second tab can
+disturb the first.
+
+Concurrency inside one session is not guarded either: two roundtrips answered at
+the same time fork the draft chain silently rather than failing.
+
+### Pinned versions
+
+- **abap2UI5 1.142.0.** The port pins one upstream release; `static version` on
+ `z2ui5_if_app` says which. Upstream material written against a later release
+ can disagree with what runs here — most visibly `_bind` and `_bind_edit`,
+ which are still two different bindings on 1.142.0. See
+ [Migrating from abap2UI5](./migration-from-abap2ui5#binding).
+- **OpenUI5 1.113.0**, served locally. Newer UI5 controls and properties are
+ not available. The pin is deliberately excluded from automated dependency
+ updates: moving it is a compatibility decision about which UI5 releases the
+ framework supports, not a routine bump.
+
+### Not everything upstream ships is here
+
+cap2UI5 deliberately does not carry abap2UI5's frozen legacy package, so
+`z2ui5_cl_xml_view`, its custom-control decorator and the built-in `z2ui5_cl_pop_*`
+popups do not exist here — an old upstream sample can fail on the import alone.
+That is a decision, not a backlog item; the detail is on
+[cap2UI5 vs. abap2UI5](./vs-abap2ui5).
+
+Beyond that, a number of transpiled framework internals still carry
+`TODO(abap2js)` markers where an ABAP construct has no finished JavaScript
+equivalent — mostly in runtime type introspection. The common paths are
+exercised by the whole sample suite on every sync, but the corners are real.
+
+The one limit you are most likely to meet is not a TODO but a language
+difference: because JavaScript has no reference to a field, `_bind` identifies
+*which* attribute you meant by the value you passed, so two attributes holding
+the same value are ambiguous. [Data Binding](./data-binding#reference-equality)
+explains the workaround — pass the path explicitly.
+
+### BTP deployment
+
+The Cloud Foundry deployment descriptor ships and is documented
+([Deployment](../reference/deployment)), but it is exercised by far less
+automation than the local path. Treat a first BTP rollout as something to
+verify end to end rather than assume.
+
+## What's next
+
+### Distribution — making `npm install` work
+
+The intent is that a CAP developer can adopt cap2UI5 without ever seeing the
+build pipeline: install a published package into an existing project, add a
+plugin entry, write one class. That means publishing the core to the public
+registry under a real version, and shipping a CAP plugin that contributes the
+service, the bootstrap and the draft wiring on its own — replacing today's
+hand-copied `srv/server.js` and `srv/z2ui5-service.cds` skeleton. A template or
+scaffold would then be the recommended way to start, and the generated
+repository becomes the demo rather than the delivery mechanism.
+
+### Security defaults
+
+The principle is that the defaults should be safe when nobody configures
+anything, and the work is closing what is left between that and what ships.
+The 2026-08 pass moved several settings onto that footing: the CSRF
+gate is on by default (see [The User Exit](./user-exit#csrf)), security headers
+are applied to the roundtrip and OData responses rather than only to the
+bootstrap page, the roundtrip has an explicit body-size cap, and the
+authorisation scope the app declares is enforced on the endpoint — so a
+deployment now needs the role assigned, not merely defined.
+
+What is *not* solved is the multi-instance state problem above: an honest
+single-instance deployment is the current answer, not a shared store. The
+[Deployment](../reference/deployment) page carries the operational detail as it
+changes.
+
+### Types
+
+Hand-written type definitions for the top of the API — `client`,
+`z2ui5_if_app`, `z2ui5_cl_ui5_view_builder` — so that editors complete the
+builder chain instead of guessing. A fluent chain is the API most improved by
+completion, and it is the one you write most.
+
+### Playground
+
+The [browser playground](./playground) runs the whole stack in a tab. Two things
+would make it a genuinely better front door: a landing page that lists the
+samples instead of requiring a hand-typed class name (the
+[Sample Catalogue](./samples) is the documentation half of that), and an in-tab
+editor that registers a class you wrote yourself — write a class, run it,
+without installing anything. The engine already supports registering a class
+directly, so this is a tooling question rather than a framework one.
+
+### Keeping up with abap2UI5
+
+The port tracks a fast-moving upstream through an automated transpile pipeline.
+The work here is less about features than about alarms: making divergence from
+upstream a visible check rather than something discovered by a user, and
+carrying upstream's own deprecations through instead of having classes quietly
+appear and vanish between releases.
+
+## What is deliberately not planned
+
+- **A named frontend action API.** Upstream has parked that design space; the
+ port will not invent a competing one.
+- **Carrying the frozen legacy classes.** See above — an npm package has no
+ installed base to protect, which is exactly why they were dropped.
+- **Divergence from the wire protocol.** The frontend is shared with abap2UI5
+ byte for byte, and staying protocol-compatible is what makes the same app
+ deployable in both worlds.
+
+## Where the real backlog lives
+
+This page is a summary written for people *using* cap2UI5. The working plans
+live with the code, in each repository's issues and `AGENTS.md`:
+
+| | |
+|---|---|
+| the app, and the framework as delivered | [cap2UI5/cap2UI5](https://github.com/cap2UI5/cap2UI5) |
+| transpiler and core build | [builder-abap2UI5-js](https://github.com/cap2UI5/builder-abap2UI5-js) |
+| app assembly and publishing | [builder-cap2UI5](https://github.com/cap2UI5/builder-cap2UI5) |
+| the browser playground | [builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) |
+| this site | [cap2UI5/docs](https://github.com/cap2UI5/docs) |
+
+[The Ecosystem](./ecosystem) explains how those fit together.
diff --git a/docs/guide/samples.md b/docs/guide/samples.md
new file mode 100644
index 0000000..2dfdc59
--- /dev/null
+++ b/docs/guide/samples.md
@@ -0,0 +1,242 @@
+
+
+
+# Sample Catalogue
+
+The framework ships 104 sample apps in `core/srv/app/samples/` — the abap2UI5 demo collection, transpiled to JavaScript. 97 of them are listed in the built-in gallery, and this page is that gallery's own table: it is generated from `z2ui5_cl_smp_app_000`, the sample browser the framework itself renders, so it cannot drift from what the playground offers.
+
+Every title links into the [browser playground](./playground) — no install, no server. The same apps run on your own CAP server under the same names: `?app_start=`, or `client.nav_app_call()` from an app of your own.
+
+→ Prefer to browse in the app itself? Open [`z2ui5_cl_smp_app_000`](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_000) — the same catalogue with a search field.
+
+Two markers are carried over from the upstream gallery: **(A)** marks a sample that drives the frontend through a client action, **(C)** one that uses a custom control from the `z2ui5.cc` namespace.
+
+## Categories
+
+[Basics](#basics) · [Binding](#binding) · [Browser](#browser) · [Control Behaviour](#control-behaviour) · [CSS](#css) · [Device](#device) · [Event](#event) · [File](#file) · [Focus](#focus) · [Formatter](#formatter) · [Grid Table](#grid-table) · [List](#list) · [Menu](#menu) · [Message](#message) · [Navigation](#navigation) · [Nested View](#nested-view) · [Popover](#popover) · [Popup](#popup) · [Scroll](#scroll) · [Table](#table) · [Templating](#templating) · [Timer](#timer) · [Tree](#tree)
+
+## Basics
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Hello World, the Smallest App](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_493) | [`z2ui5_cl_smp_app_493`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_493.js) | hello world smallest first app minimal start here template |
+| [Data Binding: Input and Button](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_494) | [`z2ui5_cl_smp_app_494`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_494.js) | binding _bind model attribute value input button serialize |
+| [Lifecycle: Init, Event, Navigated](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_495) | [`z2ui5_cl_smp_app_495`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_495.js) | lifecycle roundtrip main dispatcher state serialize check_on_init check_on_event check_on_navigated |
+| [Events, Views and Roundtrips](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_004) | [`z2ui5_cl_smp_app_004`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_004.js) | roundtrip restart second view uncaught error controller basics |
+| [The Developer Tools (Ctrl+F12)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_496) | [`z2ui5_cl_smp_app_496`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_496.js) | developer tools devtools ctrl f12 debug inspect payload previous request response view xml view model source code log error adt export |
+
+## Binding
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Currency Amounts (sap.ui.model.type.Currency)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_067) | [`z2ui5_cl_smp_app_067`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_067.js) | amount decimals leading zeros number format |
+| [Dynamic Table Typed at Runtime (RTTI)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_061) | [`z2ui5_cl_smp_app_061`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_061.js) | generic data reference create data ddic dynamic itab |
+| [Expression Binding, Types and Composite Parts](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_027) | [`z2ui5_cl_smp_app_027`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_027.js) | formatter parts conditional regexp visible enabled syntax |
+| [Model setSizeLimit for Large Tables (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_071) | [`z2ui5_cl_smp_app_071`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_071.js) | combobox jsonmodel size limit large itab 100 entries |
+| [Single Table Cell (tab_index)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_144) | [`z2ui5_cl_smp_app_144`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_144.js) | cell input internal table row field level |
+| [Structure Fields and INCLUDEs](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_166) | [`z2ui5_cl_smp_app_166`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_166.js) | structure component include flat form level |
+| [Types for Integer, Decimal, Date and Time](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_047) | [`z2ui5_cl_smp_app_047`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_047.js) | type conversion sum amount number field |
+
+## Browser
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Copy to Clipboard (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_325) | [`z2ui5_cl_smp_app_325`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_325.js) | clipboard paste copy text area |
+| [Local and Session Storage (A,C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_327) | [`z2ui5_cl_smp_app_327`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_327.js) | localstorage sessionstorage persist store_data offline |
+| [Logout from the Client (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_361) | [`z2ui5_cl_smp_app_361`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_361.js) | logoff signout icf session end fiori launchpad |
+| [Open a URL in a New Tab (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_073) | [`z2ui5_cl_smp_app_073`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_073.js) | url window open_new_tab link target |
+| [Open Mail, Phone and SMS Links (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_316) | [`z2ui5_cl_smp_app_316`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_316.js) | mailto tel sms urlhelper redirect native link |
+| [Reload the Page (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_492) | [`z2ui5_cl_smp_app_492`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_492.js) | reload refresh restart location_reload url |
+| [Set the Tab Favicon (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_491) | [`z2ui5_cl_smp_app_491`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_491.js) | favicon icon tab image data uri |
+| [Set the Tab Title (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_125) | [`z2ui5_cl_smp_app_125`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_125.js) | document.title tab caption headline set_title |
+| [Soft Keyboard Mode on Mobile (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_352) | [`z2ui5_cl_smp_app_352`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_352.js) | mobile numeric keypad keyboard_set_mode phone input |
+
+## Control Behaviour
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Expand a Panel by ID (setExpanded) (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_448) | [`z2ui5_cl_smp_app_448`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_448.js) | panel collapse expand setexpanded control_by_id whitelisted |
+| [MultiInput with Tokens (C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_078) | [`z2ui5_cl_smp_app_078`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_078.js) | multiinput token tokens suggestion custom control |
+| [Open the PDF Viewer by ID (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_449) | [`z2ui5_cl_smp_app_449`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_449.js) | pdfviewer pdf document viewer popup control_by_id whitelisted |
+| [Switch NavContainer Page by ID (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_088) | [`z2ui5_cl_smp_app_088`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_088.js) | navcontainer icontabbar icontabheader page switch control_by_id whitelisted |
+| [Wizard with Steps (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_202) | [`z2ui5_cl_smp_app_202`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_202.js) | wizard step branching discardprogress setnextstep control_by_id |
+
+## CSS
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Color Table Cells from the Backend](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_305) | [`z2ui5_cl_smp_app_305`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_305.js) | color background conditional formatting style data attribute |
+| [FlexBox Layouts with Custom Classes](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_255) | [`z2ui5_cl_smp_app_255`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_255.js) | flexbox layout responsive navigation tile panel |
+| [Ship Your Own CSS with the View](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_050) | [`z2ui5_cl_smp_app_050`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_050.js) | style stylesheet inline html class own design |
+
+## Device
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Camera, Take Photos (C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_306) | [`z2ui5_cl_smp_app_306`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_306.js) | camera photo picture webcam capture facing mode |
+| [Device Model: Phone, Tablet, Desktop (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_445) | [`z2ui5_cl_smp_app_445`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_445.js) | sap.ui.device responsive orientation resize media model |
+| [Frontend Info: UI5 Version, Theme, OS, Browser](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_122) | [`z2ui5_cl_smp_app_122`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_122.js) | client info ui5 version theme os user agent device |
+| [Geolocation from the Browser (C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_120) | [`z2ui5_cl_smp_app_120`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_120.js) | gps position latitude longitude altitude location |
+
+## Event
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Control Objects in t_arg (FacetFilter)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_197) | [`z2ui5_cl_smp_app_197`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_197.js) | facetfilter filter object marshalling selected items |
+| [Extra Arguments with t_arg](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_167) | [`z2ui5_cl_smp_app_167`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_167.js) | argument parameter payload event data fixed value |
+| [Keyboard Shortcuts, Ctrl+S (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_471) | [`z2ui5_cl_smp_app_471`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_471.js) | shortcut hotkey ctrl key combination keyboard_shortcut |
+| [Link with preventDefault (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_472) | [`z2ui5_cl_smp_app_472`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_472.js) | link href default action check_prevent_default |
+
+## File
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Download to the Browser (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_186) | [`z2ui5_cl_smp_app_186`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_186.js) | export save base64 attachment xstring document |
+| [Upload to the Backend (C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_074) | [`z2ui5_cl_smp_app_074`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_074.js) | fileuploader base64 attachment import picture document |
+
+## Focus
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Focus a Table Cell by Column and Row (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_421) | [`z2ui5_cl_smp_app_421`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_421.js) | table cell column row aggregation set_focus |
+| [Jump to the Next Input on Enter (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_189) | [`z2ui5_cl_smp_app_189`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_189.js) | cursor enter tab next field form set_focus |
+| [Set Focus and Select Text in an Input (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_133) | [`z2ui5_cl_smp_app_133`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_133.js) | cursor set_focus selection position textfield |
+
+## Formatter
+
+| Sample | Class | Topics |
+|---|---|---|
+| [ABAP Date and Time Strings (DATS/TIMS)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_450) | [`z2ui5_cl_smp_app_450`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_450.js) | dats tims conversion initial date 00000000 sy-datum |
+| [Date Object for the DatePicker](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_457) | [`z2ui5_cl_smp_app_457`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_457.js) | datepicker datevalue javascript date object iso |
+| [Date Objects for the PlanningCalendar](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_456) | [`z2ui5_cl_smp_app_456`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_456.js) | planningcalendar appointment javascript date object iso |
+| [Inline Icons in a Text](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_466) | [`z2ui5_cl_smp_app_466`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_466.js) | icon glyph placeholder text status expandinlineicons |
+| [When Not to Use One: Compute in ABAP](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_453) | [`z2ui5_cl_smp_app_453`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_453.js) | no formatter computed backend thin frontend prepare |
+
+## Grid Table
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Events on Cell Level](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_160) | [`z2ui5_cl_smp_app_160`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_160.js) | cell enter row index event grid alv |
+| [Full Example with sap.ui.table](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_070) | [`z2ui5_cl_smp_app_070`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_070.js) | grid alv dynamicpage column row action currency search sort filter |
+| [Keep Column Filters on Refresh (C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_143) | [`z2ui5_cl_smp_app_143`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_143.js) | column filter reset refresh uitableext grid alv |
+
+## List
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Filter and Sort the Binding from ABAP (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_454) | [`z2ui5_cl_smp_app_454`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_454.js) | binding_call getbinding sorter filter follow_up_action |
+| [Live Filter on the Client, No Roundtrip (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_455) | [`z2ui5_cl_smp_app_455`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_455.js) | binding_call live search client side no roundtrip filter |
+| [StandardListItem, Highlight and Events](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_048) | [`z2ui5_cl_smp_app_048`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_048.js) | sap.m.list standardlistitem highlight infostate press selection |
+
+## Menu
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Full Path of the Selected Item (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_473) | [`z2ui5_cl_smp_app_473`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_473.js) | menuitem nested submenu textpath controller path |
+| [Menu Button with core:require](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_163) | [`z2ui5_cl_smp_app_163`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_163.js) | menubutton menuitem popover messagetoast require module |
+
+## Message
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Message Model and MessageManager (C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_467) | [`z2ui5_cl_smp_app_467`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_467.js) | messagemanager validation target field state central model |
+| [MessageBox from SY, BAPIRET2 or Exception](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_008) | [`z2ui5_cl_smp_app_008`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_008.js) | t100 message class number exception cx_root error abend |
+| [MessageBox, Types and Custom Actions](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_382) | [`z2ui5_cl_smp_app_382`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_382.js) | confirm warning error success information dialog action |
+| [MessagePopover URL Policy (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_474) | [`z2ui5_cl_smp_app_474`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_474.js) | url policy link security validator relative allow deny |
+| [MessageToast, Text and Duration](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_381) | [`z2ui5_cl_smp_app_381`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_381.js) | toast notification duration position animation |
+| [MessageView and MessagePopover (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_452) | [`z2ui5_cl_smp_app_452`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_452.js) | messagepopover messageitem dialog grouped message list |
+
+## Navigation
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Call and Leave Apps (nav_app_call)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_024) | [`z2ui5_cl_smp_app_024`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_024.js) | nav_app_call nav_app_leave sub app stack call back |
+| [Data Loss Protection on Leaving (A,C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_279) | [`z2ui5_cl_smp_app_279`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_279.js) | dirty unsaved changes leave confirmation warning |
+| [Return Data and Events to the Caller](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_488) | [`z2ui5_cl_smp_app_488`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_488.js) | r_data result get_app_prev return event payload |
+| [Uncaught Error and Error Popup](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_464) | [`z2ui5_cl_smp_app_464`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_464.js) | exception dump error handling debugtool restart retry |
+
+## Nested View
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Basic Example (nest_view_display)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_065) | [`z2ui5_cl_smp_app_065`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_065.js) | nest_view_display rerender model refresh sub view |
+| [Embed Another App's View](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_104) | [`z2ui5_cl_smp_app_104`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_104.js) | sub app class embed instantiate another app rtti |
+| [Master-Detail with FlexibleColumnLayout](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_097) | [`z2ui5_cl_smp_app_097`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_097.js) | fcl master detail list report two column split |
+| [Three Columns with FlexibleColumnLayout](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_098) | [`z2ui5_cl_smp_app_098`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_098.js) | fcl three column detail detail deep navigation |
+
+## Popover
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Basic Example with Placement](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_026) | [`z2ui5_cl_smp_app_026`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_026.js) | placement anchor button confirm cancel popover_display |
+| [Open from a Table Row](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_052) | [`z2ui5_cl_smp_app_052`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_052.js) | list report dynamicpage row link details table |
+| [Open Together with the View Build](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_490) | [`z2ui5_cl_smp_app_490`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_490.js) | initial render one roundtrip anchor button |
+| [QuickView Contact Card](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_109) | [`z2ui5_cl_smp_app_109`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_109.js) | quickview contact card links grouped fields |
+| [Select from a List](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_081) | [`z2ui5_cl_smp_app_081`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_081.js) | list selection placement anchor |
+| [Toggle by ID (toggleBy) (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_465) | [`z2ui5_cl_smp_app_465`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_465.js) | toggleby open close control_by_id whitelisted |
+
+## Popup
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Dialog inside a Dialog](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_161) | [`z2ui5_cl_smp_app_161`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_161.js) | nested stack popup in popup second dialog |
+| [Element Binding to the Selected Row (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_470) | [`z2ui5_cl_smp_app_470`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_470.js) | element binding relative path aggregation dialog row |
+| [Navigate between Dialogs (NavContainer) (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_170) | [`z2ui5_cl_smp_app_170`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_170.js) | navcontainer dialog pages back forward |
+| [Value Help: Suggestions and F4 Dialog](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_009) | [`z2ui5_cl_smp_app_009`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_009.js) | f4 search help suggestion input dialog select |
+| [Ways to Open a Dialog (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_012) | [`z2ui5_cl_smp_app_012`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_012.js) | dialog sub app destroy rerender background view |
+
+## Scroll
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Scroll a Control into View (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_363) | [`z2ui5_cl_smp_app_363`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_363.js) | scroll_into_view control id validation jump |
+| [Scroll to a Pixel Position (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_362) | [`z2ui5_cl_smp_app_362`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_362.js) | position pixel scroll_to restore refresh toolbar |
+
+## Table
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Drag and Drop Rows (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_459) | [`z2ui5_cl_smp_app_459`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_459.js) | dnd dragdropinfo reorder rows move |
+| [Editable Cells, Add and Delete Rows](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_011) | [`z2ui5_cl_smp_app_011`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_011.js) | edit input add row delete multiselect toolbar |
+| [Filter Rows in the Backend](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_045) | [`z2ui5_cl_smp_app_045`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_045.js) | filter server side form growing where |
+| [Large Table with Growing and ScrollContainer](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_006) | [`z2ui5_cl_smp_app_006`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_006.js) | growing 10000 rows sticky toolbar sort performance |
+| [Live Search with Parallel Requests](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_059) | [`z2ui5_cl_smp_app_059`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_059.js) | live search parallel requests busy queue typing |
+| [Search in the Backend (SearchField)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_053) | [`z2ui5_cl_smp_app_053`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_053.js) | search go enter server side where |
+| [Selection Modes: Single and Multi Select](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_019) | [`z2ui5_cl_smp_app_019`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_019.js) | selectionmode none single multi segmentedbutton checkbox |
+
+## Templating
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Build Columns Dynamically (template:repeat)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_173) | [`z2ui5_cl_smp_app_173`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_173.js) | template repeat runtime generated columns if then else |
+| [Dynamic Content in a Nested View](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_176) | [`z2ui5_cl_smp_app_176`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_176.js) | template repeat runtime generated nested nest_view_display |
+
+## Timer
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Progress Indicator during a Backend Call (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_064) | [`z2ui5_cl_smp_app_064`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_064.js) | progressindicator busy wait long running backend |
+| [Refresh the View Every n Seconds (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_028) | [`z2ui5_cl_smp_app_028`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_028.js) | interval polling auto refresh follow_up_action seconds |
+
+## Tree
+
+| Sample | Class | Topics |
+|---|---|---|
+| [Drag and Drop Nodes (A,C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_461) | [`z2ui5_cl_smp_app_461`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_461.js) | dnd move node hierarchy binding context |
+| [Editable Nodes with CustomTreeItem (C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_463) | [`z2ui5_cl_smp_app_463`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_463.js) | customtreeitem rename input binding write back |
+| [Inside a Dialog (C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_462) | [`z2ui5_cl_smp_app_462`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_462.js) | popup expand state hierarchy nodes |
+| [Nested ABAP Table in a sap.m.Tree](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_460) | [`z2ui5_cl_smp_app_460`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_460.js) | hierarchy nodes nested json items |
+
+## Not in the gallery
+
+6 further classes ship in the same folder without a catalogue entry — they are the sub-apps the listed samples call (dialogs, detail views, nested apps), not entries in their own right. They run the same way if you name one directly:
+
+`z2ui5_cl_smp_app_020`, `z2ui5_cl_smp_app_025`, `z2ui5_cl_smp_app_105`, `z2ui5_cl_smp_app_112`, `z2ui5_cl_smp_app_469`, `z2ui5_cl_smp_app_489`
+
+## Where they come from
+
+The samples are machine-transpiled from the ABAP originals in [abap2UI5-samples](https://github.com/abap2UI5/abap2UI5-samples) by the [builder-abap2UI5-js](https://github.com/cap2UI5/builder-abap2UI5-js) pipeline, and land in `core/srv/app/samples/` on every sync. That folder is pipeline-owned: edits to it are overwritten on the next publish. Copy a sample into [`srv/app/`](./project-structure#srv-app) before you change it.
+
+Some samples need a real server: those that call external OData services or CAP destinations will not work in the browser playground. See [what the playground is not](./playground#what-its-not).
+
+→ Ready to write your own? Start with [Hello World](../examples/hello-world), or read the [App Lifecycle](./lifecycle).
diff --git a/docs/guide/user-exit.md b/docs/guide/user-exit.md
new file mode 100644
index 0000000..a546c8a
--- /dev/null
+++ b/docs/guide/user-exit.md
@@ -0,0 +1,222 @@
+# The User Exit
+
+Everything the framework renders or sends — the tab title, the theme, the tab
+icon, the UI5 bootstrap URL, the Content-Security-Policy, the response headers,
+how long drafts live, whether the CSRF gate is armed — comes from one config
+object. The **user exit** is the one supported way to change it.
+
+It is a single class implementing `z2ui5_if_exit`, and it is the framework's
+only extension point of this kind: there is no config file, no environment
+variable for these values, and nothing to register. You write the class, the
+framework finds it.
+
+## The smallest exit
+
+```js
+// srv/app/my_exit.js
+class my_exit {
+
+ set_config_http_get(s_context, s_config) {
+ s_config.title = "Order Management";
+ s_config.theme = "sap_horizon_dark";
+ return s_config;
+ }
+
+ set_config_http_post(s_context, s_config) {
+ return s_config;
+ }
+}
+
+module.exports = my_exit;
+```
+
+That is the whole contract. Both methods must exist, but either may simply hand
+back what it was given.
+
+::: warning `z2ui5_if_exit` is a contract, not a base class
+Unlike `z2ui5_if_app`, it is **not** something to `extend` — it is a plain
+object holding the method names, and `class my_exit extends z2ui5_if_exit`
+throws at load time. The framework matches an exit by those method names alone.
+If you want the check written down, assert it explicitly:
+
+```js
+const z2ui5_if_exit = require("abap2UI5/z2ui5_if_exit");
+z2ui5_if_exit.check_implements(my_exit.prototype); // throws if one is missing
+```
+:::
+
+## How the framework finds it
+
+`z2ui5_cl_ui5_user_exit` is the framework's own implementation of the same
+interface. On the first request it scans the registered app directories for
+**any other** class implementing `z2ui5_if_exit`, instantiates the one it
+finds, and keeps it for the life of the process.
+
+- The scan uses the same app-directory search as app classes, so put the exit
+ where your apps are: [`srv/app/`](./project-structure#srv-app), or any
+ directory named in `Z2UI5_APP_DIRS` / registered via
+ `require("abap2UI5/register-apps")`.
+- The file name must match the class name, as it must for app classes.
+- **If more than one exit exists, exactly one wins** — the first by class name,
+ sorted. That is deliberate rather than a race: the chosen exit controls the
+ CSP and every security header, so which one wins must not depend on the order
+ the file system happens to return. Ship one exit.
+- If none exists, the framework defaults apply unchanged.
+
+The framework runs its own defaults **first** and calls your exit **after**, so
+`s_config` arrives fully populated. You override what you care about and leave
+the rest alone. ABAP's `CHANGING` semantics map to either style in JavaScript:
+mutate `s_config` in place, return it, or both — the framework accepts all
+three.
+
+## The request context
+
+The first argument describes the request being answered:
+
+| Field | What it is |
+|---|---|
+| `path` | the request path |
+| `method` | the HTTP method |
+| `t_params` | the URL query parameters, as `[{ n, v }]` |
+| `app_start` | the `?app_start=` value, lifted out of `t_params` for convenience |
+| `session_id` | the session key the framework isolates state by |
+| `tenant` | the tenant, when the platform supplies one |
+| `body` | the raw request body |
+
+So the config can depend on the request — a different theme per app, for
+instance:
+
+```js
+set_config_http_get(s_context, s_config) {
+ if (s_context.app_start === "z2ui5_cl_smp_app_000") {
+ s_config.theme = "sap_horizon";
+ }
+ return s_config;
+}
+```
+
+The context is **per request**, not process-global: two roundtrips in flight at
+the same time each see their own, so an exit cannot read the other request's
+parameters.
+
+## `set_config_http_get` — the bootstrap page
+
+This hook builds the HTML page that boots UI5. It runs once per full page
+load, not once per roundtrip.
+
+| Field | Default | Effect |
+|---|---|---|
+| `title` | `abap2UI5` | the browser tab title |
+| `theme` | `sap_horizon` | any UI5 theme id |
+| `favicon` | an inline SVG data URI | the tab icon; set your own, or clear it and the page emits no icon link at all (the browser then falls back to `/favicon.ico`) |
+| `src` | `/resources/sap-ui-core.js` | the UI5 bootstrap — the locally served runtime by default, which is what keeps the stack working offline |
+| `content_security_policy` | a full `` tag | see below |
+| `t_security_header` | 7 headers | `[{ n, v }]`, applied to the bootstrap response |
+| `t_add_config` | — | extra `data-sap-ui-*` bootstrap attributes, as `[{ n, v }]` |
+
+::: tip Escaping
+Values are escaped for the context they land in — text, attribute or URI — so a
+title with an apostrophe or an ampersand renders as written. Attribute *names*
+in `t_add_config` are restricted rather than escaped: a name outside
+`[A-Za-z_][A-Za-z0-9_:.-]*` is dropped.
+:::
+
+### Content-Security-Policy
+
+The default policy allows `'unsafe-eval'` for one specific reason: the OpenUI5
+1.71 `ui5loader` evaluates module source as a string, and without it that
+bootstrap fails with a CSP `EvalError`. Modern UI5 does not need it. If you pin
+a modern release, tighten the policy:
+
+```js
+set_config_http_get(s_context, s_config) {
+ s_config.content_security_policy = s_config.content_security_policy
+ .replace(" 'unsafe-eval'", "");
+ return s_config;
+}
+```
+
+The policy also allow-lists the public UI5 CDNs plus jsDelivr and cdnjs, and
+permits `data:` — which is what makes the default inline-SVG favicon work. If
+you serve everything locally, those hosts can go too.
+
+### Security headers
+
+The seven defaults are `cache-control: no-cache, no-store, must-revalidate`,
+`Pragma: no-cache`, `Expires: 0`, `X-Content-Type-Options: nosniff`,
+`X-Frame-Options: SAMEORIGIN`, `Referrer-Policy: strict-origin-when-cross-origin`
+and a `Permissions-Policy` allowing geolocation, microphone and camera for the
+same origin only. Replace or extend the array to add your own — HSTS, for
+instance, on a deployment that terminates TLS itself.
+
+## `set_config_http_post` — the roundtrip
+
+This hook runs on the roundtrip endpoint, the POST that every user interaction
+goes through.
+
+| Field | Default | Effect |
+|---|---|---|
+| `draft_exp_time_in_hours` | `4` | how long a draft stays resumable |
+| `check_csrf_active` | `true` | the cross-origin POST gate |
+
+### Draft expiry
+
+```js
+set_config_http_post(s_context, s_config) {
+ s_config.draft_exp_time_in_hours = 24;
+ return s_config;
+}
+```
+
+A value of zero or less is ignored and the default restored — an expiry of
+"never" is not something a typo should be able to configure.
+
+::: tip One knob, not two
+The CAP app's retention job deletes draft rows on the same clock: it asks this
+exit for `draft_exp_time_in_hours` and follows it, so raising the expiry here
+also keeps the rows around longer. `Z2UI5_DRAFT_TTL_HOURS` overrides both when
+it is set — see [Configuration](../reference/configuration).
+:::
+
+### CSRF
+
+The gate compares the `Origin` header — falling back to `Referer` — against the
+app's own `Host`, and rejects a mismatch with `403 Forbidden`. The frontend
+posts from the same origin, so first-party traffic is unaffected.
+
+Two properties are worth knowing before you rely on it:
+
+- It is **on by default** since the 2026-08 security pass. It used to be
+ opt-in, and nothing opted in. If you are looking at a core vendored before
+ that change, `set_config_http_post` in
+ `core/srv/z2ui5/01/04/z2ui5_cl_ui5_user_exit.js` tells you which one you
+ have.
+- It is **lenient when there is nothing to compare**: a request carrying
+ neither `Origin` nor `Referer` is allowed through, so that proxies which
+ strip those headers do not lock users out. It is a defence against
+ cross-origin form posts, not a complete CSRF defence, and it does not replace
+ authentication.
+
+Turn it off — in front of your own gateway, say — the same way you turn
+anything else off:
+
+```js
+set_config_http_post(s_context, s_config) {
+ s_config.check_csrf_active = false;
+ return s_config;
+}
+```
+
+## What the exit is not for
+
+- **Not app configuration.** It is called for every request, for every app. App
+ state belongs on the app instance; see [App Lifecycle](./lifecycle).
+- **Not a request filter.** It shapes config, it does not accept or reject
+ requests. Authentication and authorisation belong on the CAP service
+ (`@requires`, `@restrict`) — see [Deployment](../reference/deployment).
+- **Not a place for per-user secrets.** The instance is created once and shared
+ by every request in the process; only the context argument is per request.
+
+→ Related: [Configuration](../reference/configuration) for environment
+variables, identity and the draft store; [Architecture](../reference/architecture)
+for where `z2ui5_cl_ui5_user_exit` sits in the stack.
diff --git a/docs/guide/vs-abap2ui5.md b/docs/guide/vs-abap2ui5.md
index a422986..3598326 100644
--- a/docs/guide/vs-abap2ui5.md
+++ b/docs/guide/vs-abap2ui5.md
@@ -151,14 +151,8 @@ A jest suite gates every sync — only a green build is committed.
## Migration ABAP → CAP
-Because the wire format and API are compatible, migrating an existing abap2UI5 app to cap2UI5 is mechanical:
+Because the wire format and API are compatible, migrating an existing abap2UI5 app to cap2UI5 is mechanical: rewrite the class method for method, convert data access from OpenSQL to CDS queries and outbound calls from `cl_http_client` to `fetch`, drop the file into `srv/app/`, run it. The same static frontend renders both without changes.
-1. Rewrite the ABAP class as a JS class — method for method, with the two caveats above: a view built on the retired `z2ui5_cl_xml_view` has to be rebuilt on `z2ui5_cl_ui5_view_builder`, and two-way binding is `_bind_edit` on the release cap2UI5 pins. Or let the transpiler do a first pass: `npm run transpile -- path/to/z2ui5_cl_my_app.clas.abap --stdout` in a [builder-abap2UI5-js](https://github.com/cap2UI5/builder-abap2UI5-js) checkout emits JavaScript, marking unsupported statements as `// TODO(abap2js)` comments instead of dropping them
-2. Convert data access from OpenSQL to CDS queries
-3. Convert external calls from `cl_http_client` to `fetch`/`cds.connect.to`
-4. Drop the file into `srv/app/` (or a [registered app folder](./project-structure#srv-app))
-5. Run it — done.
+→ [**Migrating from abap2UI5**](./migration-from-abap2ui5) is the full walkthrough, with a per-construct mapping table for classes, views, binding, structures, data access and outbound calls.
-The same static frontend renders both without changes.
-
-→ Have a look at the [**examples**](../examples/hello-world) or jump straight to the [**API reference**](../api/client).
+→ Or have a look at the [**examples**](../examples/hello-world) and jump straight to the [**API reference**](../api/client).
diff --git a/docs/index.md b/docs/index.md
index f538829..f623cbf 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -38,4 +38,6 @@ features:
- title: A live sample for every feature
details: The transpiled abap2UI5 demo collection ships in core/srv/app/samples/ — and runs zero-install in the browser playground on GitHub Pages.
icon: 🎨
+ link: /guide/samples
+ linkText: Browse the sample catalogue
---
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md
index 41b1bf1..e9539e7 100644
--- a/docs/reference/configuration.md
+++ b/docs/reference/configuration.md
@@ -9,16 +9,26 @@ framework renders or sends.
| Variable | Default | Effect |
|---|---|---|
| `Z2UI5_APP_DIRS` | — | extra directories to search for app classes, separated by the platform path separator |
-| `Z2UI5_DRAFT_TTL_HOURS` | `24` | how long draft rows are kept; `0` disables the retention job entirely |
+| `Z2UI5_DRAFT_TTL_HOURS` | the exit's `draft_exp_time_in_hours` | how long draft rows are kept; `0` disables the retention job entirely |
+| `Z2UI5_DRAFT_RETENTION_INSTANCE` | `0` | which Cloud Foundry instance runs the retention loop; `*` for all of them |
| `PORT` | `4004` (CAP) | the port the server listens on |
```bash
Z2UI5_APP_DIRS=/srv/my-apps:/srv/more-apps npx cds-serve
-Z2UI5_DRAFT_TTL_HOURS=4 npx cds watch
+Z2UI5_DRAFT_TTL_HOURS=24 npx cds watch
```
-An unparseable `Z2UI5_DRAFT_TTL_HOURS` falls back to 24 hours rather than
-disabling cleanup — a typo must not silently turn retention off.
+Retention has **one** clock, not two. Unset, `Z2UI5_DRAFT_TTL_HOURS` follows
+the framework's own draft expiry — `draft_exp_time_in_hours` from the user exit,
+4 hours by default — so rows are not deleted while the framework still considers
+the session live. Set, it overrides both. An unparseable value falls back to the
+framework expiry rather than disabling cleanup: a typo must not silently turn
+retention off.
+
+Outside Cloud Foundry every process runs the retention loop, which is the right
+answer for a single server. On CF only the instance whose `CF_INSTANCE_INDEX`
+matches `Z2UI5_DRAFT_RETENTION_INSTANCE` does — the same hourly `DELETE` run by
+N instances is the same work done N times.
## Registering app directories in code
@@ -47,28 +57,40 @@ by scanning for an implementation — you do not register it anywhere. Its two
methods are called after the framework defaults, so you receive a fully
populated config object and change only what you care about.
-```js
-const z2ui5_if_exit = require("abap2UI5/z2ui5_if_exit");
+This section is the summary; [**The User Exit**](../guide/user-exit) is the
+full treatment — discovery, the request context, and every field.
-class my_exit extends z2ui5_if_exit {
+```js
+// srv/app/my_exit.js
+class my_exit {
set_config_http_get(s_context, s_config) {
s_config.title = "My Application";
s_config.theme = "sap_horizon_dark";
return s_config;
}
+
+ set_config_http_post(s_context, s_config) {
+ return s_config;
+ }
}
module.exports = my_exit;
```
+`z2ui5_if_exit` is a contract, not a base class: an exit is any class carrying
+both methods, and `extends z2ui5_if_exit` throws. Both must be present, even
+when one only hands back what it was given.
+
### What `set_config_http_get` controls
| Field | Default | Notes |
|---|---|---|
| `title` | `abap2UI5` | the browser tab title |
| `theme` | `sap_horizon` | any UI5 theme id |
+| `favicon` | an inline SVG data URI | the tab icon; clear it and the page emits no icon link at all |
| `src` | `/resources/sap-ui-core.js` | the UI5 bootstrap — the local runtime by default, so the stack works offline |
| `content_security_policy` | a full `` tag | see below |
| `t_security_header` | 7 headers | `[{n, v}]`, applied to the bootstrap response |
+| `t_add_config` | — | extra `data-sap-ui-*` bootstrap attributes, as `[{n, v}]` |
The default security headers are `cache-control: no-cache, no-store,
must-revalidate`, `Pragma: no-cache`, `Expires: 0`,
@@ -96,12 +118,15 @@ serve everything locally you can strip those hosts too.
### `set_config_http_post`
-The same idea for the roundtrip response — the hook to add response headers
-or to switch on the CSRF gate, which is **off by default**:
+The same idea for the roundtrip. Two fields: `draft_exp_time_in_hours` (default
+`4`) and `check_csrf_active`, the cross-origin POST gate, which is **on by
+default** — turn it off only when something in front of the app already covers
+it:
```js
set_config_http_post(s_context, s_config) {
- s_config.check_csrf_active = true;
+ s_config.draft_exp_time_in_hours = 24;
+ s_config.check_csrf_active = false;
return s_config;
}
```
diff --git a/docs/reference/database.md b/docs/reference/database.md
index 6f7b825..7de1e09 100644
--- a/docs/reference/database.md
+++ b/docs/reference/database.md
@@ -106,23 +106,26 @@ The `managed` aspect requires no code patch in cap2UI5 — the engine ignores th
### Option 1: the retention job that already ships
-You do not have to write this one — the app ships it as `srv/draft-retention.js`,
-started from `srv/server.js` on the `served` event. It deletes rows past their
-TTL once at startup and hourly after that, on an `unref`'d timer so it never
-holds the process open.
+You do not have to write this one — it ships inside the framework package
+(`core/srv/cap/retention.js`) and is started by its `cds-plugin`, so it is
+running in any project that installs cap2UI5, not only in this repository. It
+deletes rows past their TTL once at startup and hourly after that, on an
+`unref`'d timer so it never holds the process open.
Configure it with one environment variable:
| Variable | Default | Meaning |
|---|---|---|
-| `Z2UI5_DRAFT_TTL_HOURS` | `24` | how long a draft row is kept; `0` disables cleanup entirely |
+| `Z2UI5_DRAFT_TTL_HOURS` | the framework's own `draft_exp_time_in_hours` (`4`) | how long a draft row is kept; `0` disables cleanup entirely |
+| `Z2UI5_DRAFT_RETENTION_INSTANCE` | `0` | which Cloud Foundry instance runs the cleanup loop; `*` lets every instance run it |
```bash
-Z2UI5_DRAFT_TTL_HOURS=4 npx cds watch # shorter retention
+Z2UI5_DRAFT_TTL_HOURS=2 npx cds watch # shorter retention
Z2UI5_DRAFT_TTL_HOURS=0 npx cds watch # keep everything (debugging)
```
-Anything unparseable falls back to 24 hours rather than disabling cleanup —
+Anything unparseable falls back to the framework's own TTL rather than
+disabling cleanup —
a typo in the variable must not silently turn retention off.
### Option 2: DB-side job
diff --git a/package.json b/package.json
index 015aeda..8702b21 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,9 @@
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs",
"verify": "node scripts/verify-refs.mjs",
- "check": "npm run verify && npm run docs:build"
+ "check": "npm run verify && npm run docs:build",
+ "check:ci": "node scripts/verify-refs.mjs --require-checkout && npm run docs:build",
+ "gen:samples": "node scripts/gen-samples.mjs"
},
"devDependencies": {
"vitepress": "^1.6.3"
diff --git a/scripts/gen-samples.mjs b/scripts/gen-samples.mjs
new file mode 100644
index 0000000..e274702
--- /dev/null
+++ b/scripts/gen-samples.mjs
@@ -0,0 +1,222 @@
+#!/usr/bin/env node
+/**
+ * gen-samples — generate docs/guide/samples.md from the shipped sample gallery.
+ *
+ * WHY THIS EXISTS
+ * ---------------
+ * cap2UI5 ships over a hundred sample apps, and until this page existed the
+ * only way into them was to already know a class name and hand-type it into
+ * `?app_start=`. A hand-written index would have been a second copy of a list
+ * that changes on every sync — stale within a week, and invisible to
+ * verify-refs until a class actually disappeared.
+ *
+ * So the page is generated from the gallery itself. `z2ui5_cl_smp_app_000` is
+ * the sample browser that ships with the framework; its `get_catalog()` holds
+ * one `{ group, header, sub, keywords, path, app }` entry per sample. That
+ * table is the same source the in-app gallery renders from, which makes it the
+ * only list that cannot disagree with what the playground actually offers.
+ *
+ * Usage:
+ * CAP2UI5_DIR=/path/to/cap2UI5 node scripts/gen-samples.mjs
+ * node scripts/gen-samples.mjs # defaults to ../cap2UI5
+ * node scripts/gen-samples.mjs --check # fail if the page is out of date
+ *
+ * The result is committed like any other page: the docs must build without a
+ * cap2UI5 checkout, and a reviewer should see the diff a sync causes.
+ */
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
+const APP = path.resolve(process.env.CAP2UI5_DIR || path.join(ROOT, "..", "cap2UI5"));
+const SAMPLES = path.join(APP, "core", "srv", "app", "samples");
+const GALLERY = "z2ui5_cl_smp_app_000";
+const OUT = path.join(ROOT, "docs", "guide", "samples.md");
+const CHECK = process.argv.includes("--check");
+
+const PLAYGROUND = "https://cap2ui5.github.io/web-cap2UI5-build/";
+const SOURCE = "https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples";
+
+if (!fs.existsSync(path.join(SAMPLES, `${GALLERY}.js`))) {
+ console.error(`gen-samples: no sample gallery at ${SAMPLES}/${GALLERY}.js`);
+ console.error(` set CAP2UI5_DIR or clone cap2UI5/cap2UI5 next to this repo.`);
+ process.exit(1);
+}
+
+// The catalogue is one long array literal of object literals, all fields in a
+// fixed order because the file is machine-generated by the transpiler. Parsing
+// it with a regex is fine for exactly that reason — and if the shape ever
+// changes, the count check below turns the surprise into a failure.
+const ENTRY_RE =
+ /\{ group: `([^`]*)`, header: `([^`]*)`, sub: `([^`]*)`, keywords: `([^`]*)`, path: `([^`]*)`, app: `([^`]*)` \}/g;
+
+const src = fs.readFileSync(path.join(SAMPLES, `${GALLERY}.js`), "utf8");
+const entries = [...src.matchAll(ENTRY_RE)].map((m) => ({
+ group: m[1],
+ header: m[2],
+ sub: m[3],
+ keywords: m[4],
+ app: m[6],
+}));
+
+if (entries.length < 50) {
+ console.error(`gen-samples: only ${entries.length} catalogue entries parsed from ${GALLERY}.js`);
+ console.error(` the gallery table's shape probably changed — fix ENTRY_RE.`);
+ process.exit(1);
+}
+
+const files = new Set(
+ fs
+ .readdirSync(SAMPLES)
+ .filter((f) => f.endsWith(".js"))
+ .map((f) => f.slice(0, -3)),
+);
+
+const missing = entries.filter((e) => !files.has(e.app));
+if (missing.length) {
+ console.error(`gen-samples: the gallery lists ${missing.length} class(es) with no file:`);
+ for (const e of missing) console.error(` ${e.app}`);
+ process.exit(1);
+}
+
+// The `group` field separates the upstream sample repositories (samples,
+// controls, stack). Only one of them is carried into the port today, so
+// grouping by it yields a single bucket — the useful axis is `header`, the
+// category the in-app gallery prints above each block. Both are honoured:
+// groups first (in catalogue order), categories inside them.
+//
+// One presentation liberty: the gallery numbers its introductory tiles
+// `Basics I` … `Basics V`, one sample each. Five one-row tables is noise on a
+// page you scroll rather than search, so they collapse into one `Basics`
+// section — in catalogue order, which is the order they are meant to be read.
+const categoryOf = (header) => (/^Basics\b/.test(header) ? `Basics` : header);
+
+const groups = [];
+for (const e of entries) {
+ let g = groups.find((x) => x.name === e.group);
+ if (!g) groups.push((g = { name: e.group, categories: [] }));
+ const name = categoryOf(e.header);
+ let c = g.categories.find((x) => x.name === name);
+ if (!c) g.categories.push((c = { name, rows: [] }));
+ c.rows.push(e);
+}
+
+const md = (s) => s.replace(/\|/g, "\\|");
+const anchor = (s) =>
+ s
+ .toLowerCase()
+ .replace(/[^\w\- ]+/g, "")
+ .replace(/ /g, "-");
+
+const categories = groups.flatMap((g) => g.categories);
+const uncatalogued = [...files].filter((f) => f !== GALLERY && !entries.some((e) => e.app === f)).sort();
+
+const out = [];
+out.push(``);
+out.push(``);
+out.push(``);
+out.push(`# Sample Catalogue`);
+out.push(``);
+out.push(
+ `The framework ships ${files.size} sample apps in \`core/srv/app/samples/\` — the ` +
+ `abap2UI5 demo collection, transpiled to JavaScript. ${entries.length} of them are ` +
+ `listed in the built-in gallery, and this page is that gallery's own table: it is ` +
+ `generated from \`${GALLERY}\`, the sample browser the framework itself renders, so it ` +
+ `cannot drift from what the playground offers.`,
+);
+out.push(``);
+out.push(
+ `Every title links into the [browser playground](./playground) — no install, no server. ` +
+ `The same apps run on your own CAP server under the same names: ` +
+ `\`?app_start=\`, or \`client.nav_app_call()\` from an app of your own.`,
+);
+out.push(``);
+out.push(
+ `→ Prefer to browse in the app itself? Open ` +
+ `[\`${GALLERY}\`](${PLAYGROUND}?app_start=${GALLERY}) — the same catalogue with a search field.`,
+);
+out.push(``);
+out.push(
+ `Two markers are carried over from the upstream gallery: **(A)** marks a sample that ` +
+ `drives the frontend through a client action, **(C)** one that uses a custom control ` +
+ `from the \`z2ui5.cc\` namespace.`,
+);
+out.push(``);
+out.push(`## Categories`);
+out.push(``);
+out.push(
+ categories.map((c) => `[${c.name}](#${anchor(c.name)})`).join(" · "),
+);
+out.push(``);
+
+for (const c of categories) {
+ out.push(`## ${c.name}`);
+ out.push(``);
+ out.push(`| Sample | Class | Topics |`);
+ out.push(`|---|---|---|`);
+ for (const r of c.rows) {
+ out.push(
+ `| [${md(r.sub)}](${PLAYGROUND}?app_start=${r.app}) ` +
+ `| [\`${r.app}\`](${SOURCE}/${r.app}.js) ` +
+ `| ${md(r.keywords)} |`,
+ );
+ }
+ out.push(``);
+}
+
+if (uncatalogued.length) {
+ out.push(`## Not in the gallery`);
+ out.push(``);
+ out.push(
+ `${uncatalogued.length} further classes ship in the same folder without a catalogue ` +
+ `entry — they are the sub-apps the listed samples call (dialogs, detail views, ` +
+ `nested apps), not entries in their own right. They run the same way if you name ` +
+ `one directly:`,
+ );
+ out.push(``);
+ out.push(uncatalogued.map((a) => `\`${a}\``).join(", "));
+ out.push(``);
+}
+
+out.push(`## Where they come from`);
+out.push(``);
+out.push(
+ `The samples are machine-transpiled from the ABAP originals in ` +
+ `[abap2UI5-samples](https://github.com/abap2UI5/abap2UI5-samples) by the ` +
+ `[builder-abap2UI5-js](https://github.com/cap2UI5/builder-abap2UI5-js) pipeline, and ` +
+ `land in \`core/srv/app/samples/\` on every sync. That folder is pipeline-owned: ` +
+ `edits to it are overwritten on the next publish. Copy a sample into ` +
+ `[\`srv/app/\`](./project-structure#srv-app) before you change it.`,
+);
+out.push(``);
+out.push(
+ `Some samples need a real server: those that call external OData services or CAP ` +
+ `destinations will not work in the browser playground. See ` +
+ `[what the playground is not](./playground#what-its-not).`,
+);
+out.push(``);
+out.push(
+ `→ Ready to write your own? Start with [Hello World](../examples/hello-world), ` +
+ `or read the [App Lifecycle](./lifecycle).`,
+);
+out.push(``);
+
+const text = out.join("\n");
+
+if (CHECK) {
+ const current = fs.existsSync(OUT) ? fs.readFileSync(OUT, "utf8") : "";
+ if (current !== text) {
+ console.error(`gen-samples: ${path.relative(ROOT, OUT)} is out of date — run npm run gen:samples`);
+ process.exit(1);
+ }
+ console.log(`gen-samples: OK — ${path.relative(ROOT, OUT)} matches ${APP}`);
+ process.exit(0);
+}
+
+fs.writeFileSync(OUT, text);
+console.log(
+ `gen-samples: wrote ${path.relative(ROOT, OUT)} — ` +
+ `${entries.length} catalogued samples in ${categories.length} categories ` +
+ `(${files.size} files in ${path.relative(APP, SAMPLES)})`,
+);
diff --git a/scripts/verify-refs.mjs b/scripts/verify-refs.mjs
index 2296368..4e64bec 100644
--- a/scripts/verify-refs.mjs
+++ b/scripts/verify-refs.mjs
@@ -37,10 +37,14 @@
* CAP2UI5_DIR=/path/to/cap2UI5 node scripts/verify-refs.mjs
* node scripts/verify-refs.mjs # defaults to ../cap2UI5
* node scripts/verify-refs.mjs --list # print the resolved inventory
+ * node scripts/verify-refs.mjs --require-checkout # missing checkout = error
*
* Exits 1 on the first broken claim, with the file and line to fix. When no
* checkout is available it says so and exits 0 — a missing checkout is a
- * setup gap, not a documentation defect.
+ * setup gap, not a documentation defect. That leniency is right on a laptop
+ * and wrong in CI: a workflow that checks cap2UI5 out and then silently loses
+ * it would go on passing while checking nothing. `--require-checkout` turns
+ * the skip into a failure, and .github/workflows/check.yml passes it.
*/
import fs from "node:fs";
import path from "node:path";
@@ -50,8 +54,15 @@ const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
const DOCS = path.join(ROOT, "docs");
const APP = path.resolve(process.env.CAP2UI5_DIR || path.join(ROOT, "..", "cap2UI5"));
const LIST = process.argv.includes("--list");
+const REQUIRE_CHECKOUT = process.argv.includes("--require-checkout");
if (!fs.existsSync(path.join(APP, "package.json"))) {
+ if (REQUIRE_CHECKOUT) {
+ console.error(`verify-refs: no cap2UI5 checkout at ${APP} — required by --require-checkout.`);
+ console.error(` set CAP2UI5_DIR or clone cap2UI5/cap2UI5 next to this repo.`);
+ console.error(`\nSkipping here would mean the run checked nothing while reporting success.`);
+ process.exit(1);
+ }
console.log(`verify-refs: no cap2UI5 checkout at ${APP} — skipping.`);
console.log(` set CAP2UI5_DIR or clone cap2UI5/cap2UI5 next to this repo.`);
process.exit(0);