diff --git a/AGENTS.md b/AGENTS.md index 9c142fa..98e8c86 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -541,14 +541,18 @@ share a tool name inside one server. ### Vendored code — `lib/vendor/`, `scripts/vendor-adaptive-cards.mjs` -`lib/vendor/adaptive-cards/{render,mapping,submit}.mjs` are COPIES of -abap2UI5/protocol `renderers/adaptive-cards/`, at the commit +`lib/vendor/adaptive-cards/{render,mapping,submit}.mjs`, `common/{view,request}.mjs` +and `profiles/portable-v1.json` are COPIES of abap2UI5/protocol +`renderers/adaptive-cards/`, `renderers/common/` (the halves the protocol's +renderers share) and the portable profile the view half reads, at the commit `lib/vendor/adaptive-cards/source.json` records, with the sha256 of each. Rules: never edit a copy - change it upstream and re-vendor (`node scripts/vendor-adaptive-cards.mjs /path/to/protocol --ref `); the only -transformation is that the renderer's imports of the protocol repository's -copies of OUR agent modules point back at `lib/` (a copy of a copy would be a -second version of `viewxml`/`snapshot` in one process). `test/vendor.test.mjs` +transformation is that relative paths point at the vendored folder and the +renderer's imports of the protocol repository's copies of OUR agent modules +point back at `lib/` (a copy of a copy would be a second version of +`viewxml`/`snapshot` in one process); a path to anything else upstream fails +the vendoring (extend `MODULES` / `DATA`). `test/vendor.test.mjs` fails offline on a hash mismatch, a missing header, an unrecorded file or an import into the protocol tree, and - with a protocol checkout that has the commit (`PROTOCOL_HOME` or `../protocol`) - when the copies differ from what diff --git a/CHANGELOG.md b/CHANGELOG.md index 68b10d3..016d232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ## Unreleased +- **Evicting an app session leaves a newer one with the same draft id + reachable.** Sessions started from one screen share its id (the + playground's Pilot starts one per change of the reader's typing), and the + eviction of the oldest made the newest an "unknown session". +- **Adaptive Cards: app text is never Markdown.** The renderer is + re-vendored from abap2UI5/protocol (with its shared `renderers/common/` + halves and the portable profile): a model value or a toast like + `[verify your account](https://...)` was a live link in Teams and Copilot; + a text with Markdown syntax is a `TextRun` now. Its `htmlToText` (a + message box's details) is linear. - **System mode: the app tools against a real SAP system.** With `A2UI5_MCP_SYSTEM_URL` (the launch URL, `{class}` as a query parameter), `A2UI5_MCP_SYSTEM_USER` and `A2UI5_MCP_SYSTEM_PASSWORD` - or diff --git a/lib/appclient.mjs b/lib/appclient.mjs index 2e7eacb..28dbd3b 100644 --- a/lib/appclient.mjs +++ b/lib/appclient.mjs @@ -330,7 +330,11 @@ export function createAppClient({ while (sessions.length > maxSessions) { const old = sessions.shift(); old.evicted = true; // an act still in flight must not bring it back unlisted (adopt) - for (const id of old.ids) byId.delete(id); + /* only the ids that still name IT: sessions started from one screen + * share its draft id (the playground's Pilot starts one from its + * mirror on every change of the reader's typing), and evicting the + * oldest took the newest's id with it - "unknown session" */ + for (const id of old.ids) if (byId.get(id) === old) byId.delete(id); } } diff --git a/lib/vendor/adaptive-cards/common/request.mjs b/lib/vendor/adaptive-cards/common/request.mjs new file mode 100644 index 0000000..90826a6 --- /dev/null +++ b/lib/vendor/adaptive-cards/common/request.mjs @@ -0,0 +1,107 @@ +/* + * VENDORED - do not edit. abap2UI5/protocol renderers/common/request.mjs + * at commit 29aad60639a5ff3451d592a474cad8af082a2c55, + * copied by scripts/vendor-adaptive-cards.mjs; the only change is that the + * relative paths point at the vendored folder, and the imports of the agent + * modules at lib/ (the originals the protocol repository vendors) instead of + * its copies. `node scripts/vendor-adaptive-cards.mjs + * --check` fails when this copy drifts from that commit. Change it upstream, + * then re-vendor. + */ +/* + * The way back, shared by the portable renderers of this repository: an + * action payload -> the next protocol request (spec/request.md). Pure, no + * I/O: + * + * submitToRequest(state, payload, { pending }) -> + * { kind: "event", request, model, edits } a roundtrip to send + * { kind: "client", action, model, edits } a frontend-only wire (.eF) + * { kind: "box", action, model, edits } a message box button + * + * A payload is the action data of common/view.mjs `wireData` ({ event, args, + * refs, slot } / { client } / { box }) merged with input values keyed by + * their binding path - what an Adaptive Cards host submits (every input of + * the card) and what the terminal renderer submits (nothing: its edits are + * in the model already and travel as `pending`). Every input whose value + * differs from the model the screen was rendered from is an edit: it is + * written into a copy of the slot's model (`model`) and travels as the model + * delta the UI5 frontend builds (the vendored buildDelta: a scalar or + * structure whole, table cells as `__delta` rows). Values may arrive as + * strings (that is what a card submits) and are read back in the type the + * model holds there. + */ +import { buildDelta } from "../../../appclient.mjs"; +import { getAt, setAt } from "../../../snapshot.mjs"; +import { modelKeyOf } from "./view.mjs"; + +const clone = (v) => (v === undefined ? undefined : JSON.parse(JSON.stringify(v))); + +/** A submitted input value in the type the model holds at that path. */ +export function coerce(value, current) { + if (typeof current === "number") { + const n = Number(value); + return value === "" || value === null || Number.isNaN(n) ? value : n; + } + if (typeof current === "boolean") return value === true || value === "true"; + if (Array.isArray(current)) return String(value ?? "").split(",").map((x) => x.trim()).filter(Boolean); + if (value === null || value === undefined) return ""; + return typeof value === "boolean" || typeof value === "number" ? value : String(value); +} + +const same = (a, b) => JSON.stringify(a ?? "") === JSON.stringify(b ?? ""); + +/** The input values of a payload: the keys that are model paths (`/X`, + * `/T/1/C`; a duplicate id carries `#`) - only those of `ids` when given + * (the inputs of the action's slot: a card submits all of its inputs). */ +export function inputsOf(payload, ids) { + return Object.entries(payload || {}) + .filter(([k]) => k.startsWith("/") && (!ids || ids.includes(k))) + .map(([k, v]) => [k.replace(/#\d+$/, ""), v]); +} + +/** + * state the folded response state the screen was rendered from + * payload { ...action.data, "": value, ... } - data.slot names + * the slot of the action (absent: MAIN) + * ids the input ids of that slot on screen (default: every path key) + * pending paths of the slot's model edited earlier and not sent yet + */ +export function submitToRequest(state, payload, { pending = [], ids } = {}) { + const slot = payload.slot || "MAIN"; + const key = modelKeyOf(slot); + const model = clone((state.models[key] && state.models[key].data) || {}); + const edits = []; + for (const [path, raw] of inputsOf(payload, ids)) { + const current = getAt(model, path); + const value = coerce(raw, current); + if (same(value, current)) continue; + setAt(model, path, value); + if (!edits.includes(path)) edits.push(path); + } + const base = { slot, model, edits }; + if (Array.isArray(payload.client)) return { kind: "client", action: payload.client, ...base }; + if (payload.box !== undefined) return { kind: "box", action: String(payload.box), ...base }; + if (!payload.event) throw new Error("the payload names no event - not an action of this renderer"); + const args = Array.isArray(payload.args) ? payload.args.slice() : []; + // an argument read from the model reads it after this submit's own edits + (payload.refs || []).forEach((p, i) => { + if (p) args[i] = getAt(model, p) ?? null; + }); + return { kind: "event", request: eventRequest(state, payload.event, args, buildDelta([...new Set([...pending, ...edits])], model)), ...base }; +} + +/** An event request continuing the state's draft id (spec/request.md#event-requests). */ +export function eventRequest(state, event, args = [], delta = {}) { + const front = { ID: state.id, EVENT: event }; + if (args.length) front.T_EVENT_ARG = args; + const request = { S_FRONT: front }; + if (delta && Object.keys(delta).length) request.MODEL = delta; + return request; +} + +/** The app-start request (spec/request.md#app-start-shaped-requests). */ +export function startRequest({ origin, pathname, search }) { + const front = { ORIGIN: origin, PATHNAME: pathname }; + if (search) front.SEARCH = search; + return { S_FRONT: front }; +} diff --git a/lib/vendor/adaptive-cards/common/view.mjs b/lib/vendor/adaptive-cards/common/view.mjs new file mode 100644 index 0000000..e554deb --- /dev/null +++ b/lib/vendor/adaptive-cards/common/view.mjs @@ -0,0 +1,287 @@ +/* + * VENDORED - do not edit. abap2UI5/protocol renderers/common/view.mjs + * at commit 29aad60639a5ff3451d592a474cad8af082a2c55, + * copied by scripts/vendor-adaptive-cards.mjs; the only change is that the + * relative paths point at the vendored folder, and the imports of the agent + * modules at lib/ (the originals the protocol repository vendors) instead of + * its copies. `node scripts/vendor-adaptive-cards.mjs + * --check` fails when this copy drifts from that commit. Change it upstream, + * then re-vendor. + */ +/* + * What every portable renderer of this repository does with a view before it + * draws anything - shared by the Adaptive Cards renderer + * (../adaptive-cards/) and the terminal renderer (../terminal/). Pure + * functions over the folded response state of the vendored mcp-server + * snapshot module (`applyResponse`: the views in their slots, the models); + * the view XML itself is read by the vendored viewxml module (namespaces, + * bindings, event wires, expression bindings without eval). + * + * values resolve, boundPath, absolutePath - bindings against a model + * nodes children, aggregation, contentChildren, rowsOf - the control + * tree with its aggregations and list bindings + * events wireData - an event wire as the action data both renderers + * submit ({ event, args, refs, slot } / { client: [...] }) + * slots parseSlot, liveSlots, modelKeyOf + * + * A render context `ctx` is { model, base, slot, report(node, reason), + * with(base) } - the model of the slot, the binding context of the current + * row and the sink for what could not be rendered. + */ +import fs from "node:fs"; +import { fileURLToPath } from "node:url"; +import { parseViewXml, parseBinding, parseWire, evalExpression, isAggregation, controlName } from "../../../viewxml.mjs"; +import { getAt } from "../../../snapshot.mjs"; + +export { isAggregation, controlName, getAt }; + +export const PROFILE = JSON.parse(fs.readFileSync(fileURLToPath(new URL("../profiles/portable-v1.json", import.meta.url)), "utf8")); + +/** The reserved leave event (spec/navigation.md#the-reserved-leave-event). */ +export const LEAVE_EVENT = "___ZZZ_NAL"; + +const MODEL_OWNING = { MAIN: "MAIN", NEST: "MAIN", NEST2: "MAIN", POPUP: "POPUP", POPOVER: "POPOVER" }; +/** The model a slot's bindings read: MAIN, NEST and NEST2 share one. */ +export const modelKeyOf = (slot) => MODEL_OWNING[slot] || "MAIN"; + +/** + * The slots whose inputs and actions are live. A message box and a popup are + * modal: only they are. A popover is not (UI5 closes it on a press outside): + * the popover and MAIN are live, a press in MAIN closes the popover first. + */ +export function liveSlots(state, messages = []) { + const slots = (state && state.slots) || {}; + if (messages.some((m) => m.kind === "box")) return ["BOX"]; + if (slots.POPUP) return ["POPUP"]; + if (slots.POPOVER) return ["POPOVER", "MAIN"]; + return ["MAIN"]; +} + +/** Elements that carry no UI (profiles/portable.md section 2). */ +export const TOLERATED = new Set(Object.entries(PROFILE.controls).filter(([, c]) => c.tolerated).map(([n]) => n)); + +/* + * The aggregation the children of a control go to when they are written + * without an aggregation element (profiles/portable.md section 2) - the + * profile's `defaultAggregation`, plus the two controls whose items views + * write bare though the census records no default; "content" for the rest. + */ +export const DEFAULT_AGGREGATION = { + ...Object.fromEntries(Object.entries(PROFILE.controls).filter(([, c]) => c.defaultAggregation).map(([n, c]) => [n, c.defaultAggregation])), + "sap.m.IconTabBar": "items", + "sap.m.SegmentedButton": "items", +}; + +// ------------------------------------------------------------ values ---- + +const join = (base, rel) => `${base.replace(/\/$/, "")}/${rel.replace(/^\//, "")}`; + +/** The absolute model path of a relative or absolute binding path. */ +export function absolutePath(path, base) { + if (path.startsWith("/")) return path; + return join(base || "", path); +} + +/** A model reference of an expression or a composite binding. */ +function refValue(ref, ctx) { + const r = String(ref).trim(); + if (/^[A-Za-z_][\w.-]*>/.test(r)) return undefined; // a named model (device>): not ours + return getAt(ctx.model, absolutePath(r, ctx.base)); +} + +/** A property value, with its bindings resolved against the model. */ +export function resolve(value, ctx) { + const b = parseBinding(value); + switch (b.kind) { + case "literal": + return b.value; + case "path": + return b.model ? undefined : getAt(ctx.model, absolutePath(b.path, ctx.base)); + case "expression": + return evalExpression(b.expression, (r) => refValue(r, ctx)); + default: + return b.parts.map((p) => { + if (p.text !== undefined) return p.text; + if (p.path !== undefined && !p.model) { + const v = getAt(ctx.model, absolutePath(p.path, ctx.base)); + return v === undefined || v === null ? "" : String(v); + } + return ""; + }).join(""); + } +} + +/** The model path a two-way property is bound to, or null. */ +export function boundPath(value, ctx) { + if (value === undefined) return null; + const b = parseBinding(value); + if (b.kind !== "path" || b.model) return null; + return absolutePath(b.path, ctx.base); +} + +export const text = (v) => (v === undefined || v === null ? "" : String(v)); +export const truthy = (v) => v === true || v === "true" || v === "X"; +export const falsy = (v) => v === false || v === "false" || v === "" || v === null; + +// ------------------------------------------------------------- nodes ---- + +const defaultAggregationOf = (node) => DEFAULT_AGGREGATION[controlName(node)] || "content"; + +/** The control children of a node: its own, and those of its aggregation + * elements (all of them, or the ones named). */ +export function children(node, names) { + const out = []; + for (const c of node.children) { + if (isAggregation(c)) { + if (!names || names.includes(c.local)) out.push(...c.children.filter((x) => !isAggregation(x))); + } else if (!names || names.includes(defaultAggregationOf(node))) { + out.push(c); + } + } + return out; +} + +/** The children of one named aggregation (the default aggregation also + * takes the children written without its element). */ +export function aggregation(node, name) { + return children(node, [name]); +} + +const SKIP_AGGREGATIONS = new Set(["layoutData", "customData", "dependents", "tooltip"]); + +/** Every control child except the non-visual aggregations. */ +export function contentChildren(node, except = []) { + const out = []; + for (const c of node.children) { + if (isAggregation(c)) { + if (SKIP_AGGREGATIONS.has(c.local) || except.includes(c.local)) continue; + out.push(...c.children.filter((x) => !isAggregation(x))); + } else { + out.push(c); + } + } + return out; +} + +/** The rows of a list binding: [{ base, node }] per row, or the static + * children as rows with the current base. */ +export function rowsOf(node, aggName, ctx) { + const binding = node.attrs[aggName]; + const items = aggregation(node, aggName); + if (binding !== undefined) { + const p = boundPath(binding, ctx) || listPath(binding, ctx); + const template = items[0]; + const data = p ? getAt(ctx.model, p) : undefined; + if (!template || !Array.isArray(data)) return []; + return data.map((_, i) => ({ base: `${p}/${i}`, node: template, index: i })); + } + return items.map((n, i) => ({ base: ctx.base, node: n, index: i })); +} + +function listPath(binding, ctx) { + const b = parseBinding(binding); + if (b.kind === "composite" && b.parts.length === 1 && b.parts[0].path !== undefined && !b.parts[0].model) return absolutePath(b.parts[0].path, ctx.base); + return null; +} + +// ------------------------------------------------------------ events ---- + +/** + * An event wire of a control as action data, or null: { event, args, refs, + * slot } for a backend event - args resolved now, `refs` naming the model + * path of an argument read from the model so the request step can read it + * again after the edits that travel with it; { client: [action, args...] } + * for a frontend-only wire (`.eF`). `params` are the event parameters a + * renderer can provide (`${$parameters>/value}`, profiles/portable.md + * section 5); an argument nobody can evaluate is reported and sent as null. + */ +export function wireData(node, eventName, ctx, params) { + const raw = node.attrs[eventName]; + if (raw === undefined) return null; + const w = parseWire(raw); + if (!w) { + ctx.report(node, `event ${eventName}: "${String(raw).slice(0, 60)}" is no abap2UI5 wire`); + return null; + } + const args = []; + const refs = []; + for (const [i, a] of w.args.entries()) { + if (a.static) { + args.push(a.value); + continue; + } + if (a.kind === "model" || a.kind === "row") { + const p = absolutePath(a.path, ctx.base); + args.push(getAt(ctx.model, p) ?? null); + if (a.kind === "model") refs[i] = p; + continue; + } + if (a.kind === "source") { + args.push(resolve(node.attrs[a.prop], ctx) ?? null); + continue; + } + if (a.kind === "parameters" && params && Object.prototype.hasOwnProperty.call(params, a.path)) { + args.push(params[a.path] ?? null); + continue; + } + ctx.report(node, `event ${eventName}: argument ${a.describe} needs the UI5 runtime - sent as null`); + args.push(null); + } + const data = w.fn === "eF" ? { client: [w.action, ...args] } : { event: w.event }; + if (w.fn !== "eF" && args.length) data.args = args; + if (refs.some(Boolean)) data.refs = Array.from(refs, (r) => r || null); + // the slot of the action, when it is not MAIN: its inputs and model are the slot's + if (modelKeyOf(ctx.slot) !== "MAIN") data.slot = ctx.slot; + return data; +} + +// ------------------------------------------------------------- slots ---- + +/** The control nodes of a slot's document: the document element + * (mvc:View / core:FragmentDefinition) carries no UI. */ +export function parseSlot(xml) { + const root = parseViewXml(xml); + const doc = root.children.find((c) => !isAggregation(c)) || root; + return /^(View|FragmentDefinition)$/.test(doc.local) ? doc.children : [doc]; +} + +/* tried every "<" to the end of the text - quadratic, + // 100k characters of "<" held the renderer for seconds + return dropBlocks(String(html || "")) + .replace(/<(br|\/p|\/li|\/div|\/h[1-6])\b[^<>]*>/gi, "\n") + .replace(/]*>/gi, "- ") + .replace(/<[^<>]*>/g, "") + .replace(/ /g, " ") + .replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/&/g, "&") + .split("\n").map((l) => l.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n"); +} diff --git a/lib/vendor/adaptive-cards/mapping.mjs b/lib/vendor/adaptive-cards/mapping.mjs index f9e4353..6e2bba4 100644 --- a/lib/vendor/adaptive-cards/mapping.mjs +++ b/lib/vendor/adaptive-cards/mapping.mjs @@ -1,19 +1,21 @@ /* * VENDORED - do not edit. abap2UI5/protocol renderers/adaptive-cards/mapping.mjs - * at commit 604d26769a9b7dcaa1f985f1297ff9c0a59b012e, + * at commit 29aad60639a5ff3451d592a474cad8af082a2c55, * copied by scripts/vendor-adaptive-cards.mjs; the only change is that the - * imports of the agent modules point at lib/ (the originals the protocol - * repository vendors) instead of its copies. `node scripts/vendor-adaptive-cards.mjs + * relative paths point at the vendored folder, and the imports of the agent + * modules at lib/ (the originals the protocol repository vendors) instead of + * its copies. `node scripts/vendor-adaptive-cards.mjs * --check` fails when this copy drifts from that commit. Change it upstream, * then re-vendor. */ /* * The mapping of portable profile v1 (profiles/portable-v1.json) onto * Adaptive Cards 1.5 - one entry per control: the card element(s) it - * becomes (`card`), what is approximated or left out (`note`), its default - * aggregation, and the render function. README.md's mapping table is - * generated from this table (node scripts/render-adaptive-cards.mjs); a - * control of the profile without an entry fails test/adaptive-cards.test.mjs. + * becomes (`card`), what is approximated or left out (`note`) and the + * render function (the default aggregations are ../common/view.mjs's). + * README.md's mapping table is generated from this table (node + * scripts/render-adaptive-cards.mjs); a control of the profile without an + * entry fails test/adaptive-cards.test.mjs. */ import { resolve, boundPath, text, truthy, falsy, children, aggregation, contentChildren, rowsOf, @@ -21,7 +23,7 @@ import { } from "./render.mjs"; /** Elements that carry no UI (profiles/portable.md section 2). */ -export const TOLERATED = new Set(["sap.ui.core.CustomData", "sap.m.FlexItemData", "sap.ui.layout.GridData", "sap.m.OverflowToolbarLayoutData"]); +export { TOLERATED } from "./common/view.mjs"; const tb = (t, extra = {}) => ({ type: "TextBlock", text: t, wrap: true, ...extra }); const heading = (t, size = "Medium") => tb(t, { size, weight: "Bolder", style: "heading" }); @@ -244,7 +246,7 @@ function datePicker(node, ctx) { const INPUT_STYLE = { Password: "password", Email: "email", Tel: "tel", Url: "url" }; /* - * The table: control -> { card, note, defaultAggregation, render }. + * The table: control -> { card, note, render }. * `card` and `note` are what README.md shows. */ export const CONTROLS = { @@ -252,7 +254,7 @@ export const CONTROLS = { "sap.m.Page": { card: "Container (flattened): title as heading TextBlock, nav button as Action.Submit", note: "`showNavButton` + `navButtonPress` -> an Action.Submit \"Back\" that raises the wired event (the reserved `___ZZZ_NAL` for `_event_nav_app_leave`); header/footer bars render in place", - defaultAggregation: "content", + render(node, ctx) { const out = []; const custom = aggregation(node, "customHeader"); @@ -267,39 +269,39 @@ export const CONTROLS = { return out; }, }, - "sap.m.Shell": { card: "(none) - its app renders in place", note: "", defaultAggregation: "app", render: (node, ctx) => renderList(contentChildren(node), ctx) }, - "sap.m.VBox": { card: "Container", note: "flexbox alignment ignored", defaultAggregation: "items", render: (node, ctx) => container(renderList(contentChildren(node), ctx)) }, + "sap.m.Shell": { card: "(none) - its app renders in place", note: "", render: (node, ctx) => renderList(contentChildren(node), ctx) }, + "sap.m.VBox": { card: "Container", note: "flexbox alignment ignored", render: (node, ctx) => container(renderList(contentChildren(node), ctx)) }, "sap.ui.layout.form.SimpleForm": { card: "Container: each Label becomes the `label` of the input after it", note: "the grid layout properties are ignored; a Label not followed by an input is a bold TextBlock", - defaultAggregation: "content", + render: (node, ctx) => container([ ...(node.attrs.title !== undefined ? [heading(str(node, "title", ctx))] : renderList(aggregation(node, "title"), ctx)), ...renderList(aggregation(node, "content"), ctx), ]), }, - "sap.m.HBox": { card: "ColumnSet (auto-width columns), or one ActionSet when every child is an action", note: "", defaultAggregation: "items", render: (node, ctx) => row(contentChildren(node), ctx) }, + "sap.m.HBox": { card: "ColumnSet (auto-width columns), or one ActionSet when every child is an action", note: "", render: (node, ctx) => row(contentChildren(node), ctx) }, "sap.m.Panel": { card: "Container (style emphasis), headerText as bold TextBlock", note: "`expandable`/`expanded` ignored - the content is always shown; `expand` not raised", - defaultAggregation: "content", + render(node, ctx) { const head = node.attrs.headerText !== undefined ? [tb(str(node, "headerText", ctx), { weight: "Bolder" })] : []; return container([...head, ...renderList(aggregation(node, "headerToolbar"), ctx), ...renderList(aggregation(node, "content"), ctx)], { style: "emphasis" }); }, }, - "sap.ui.layout.Grid": { card: "Container", note: "spans ignored - children stack", defaultAggregation: "content", render: (node, ctx) => container(renderList(contentChildren(node), ctx)) }, + "sap.ui.layout.Grid": { card: "Container", note: "spans ignored - children stack", render: (node, ctx) => container(renderList(contentChildren(node), ctx)) }, "sap.m.FlexBox": { card: "Container (direction Column) or ColumnSet (Row, the default)", note: "alignment, gaps and wrap ignored", - defaultAggregation: "items", + render: (node, ctx) => (/^Column/.test(str(node, "direction", ctx)) ? container(renderList(contentChildren(node), ctx)) : row(contentChildren(node), ctx)), }, - "sap.m.ScrollContainer": { card: "Container", note: "scrolling is the host's", defaultAggregation: "content", render: (node, ctx) => container(renderList(contentChildren(node), ctx)) }, + "sap.m.ScrollContainer": { card: "Container", note: "scrolling is the host's", render: (node, ctx) => container(renderList(contentChildren(node), ctx)) }, "sap.m.IconTabFilter": { card: "Container with the tab text as heading", note: "", - defaultAggregation: "content", + render(node, ctx) { const title = str(node, "text", ctx) || str(node, "key", ctx); const count = str(node, "count", ctx); @@ -309,27 +311,27 @@ export const CONTROLS = { "sap.m.IconTabBar": { card: "Container: every tab stacked, each under its heading", note: "`selectedKey` ignored - all tabs are shown; `select` not raised", - defaultAggregation: "items", + render(node, ctx) { if (node.attrs.select !== undefined && ctx.interactive) ctx.report(node, "event select is not raised by a card - every tab is shown"); return container([...renderList(aggregation(node, "items"), ctx), ...renderList(aggregation(node, "content"), ctx)]); }, }, - "sap.ui.layout.VerticalLayout": { card: "Container", note: "", defaultAggregation: "content", render: (node, ctx) => container(renderList(contentChildren(node), ctx)) }, + "sap.ui.layout.VerticalLayout": { card: "Container", note: "", render: (node, ctx) => container(renderList(contentChildren(node), ctx)) }, "sap.ui.core.Title": { card: "TextBlock (heading)", note: "", render: (node, ctx) => [heading(str(node, "text", ctx))] }, - "sap.ui.layout.HorizontalLayout": { card: "ColumnSet / ActionSet (as HBox)", note: "", defaultAggregation: "content", render: (node, ctx) => row(contentChildren(node), ctx) }, + "sap.ui.layout.HorizontalLayout": { card: "ColumnSet / ActionSet (as HBox)", note: "", render: (node, ctx) => row(contentChildren(node), ctx) }, // ------------------------------------------------------ toolbars & bars - "sap.m.OverflowToolbar": { card: "ActionSet (all buttons) or ColumnSet", note: "no overflow menu - everything is shown", defaultAggregation: "content", render: (node, ctx) => row(contentChildren(node), ctx) }, + "sap.m.OverflowToolbar": { card: "ActionSet (all buttons) or ColumnSet", note: "no overflow menu - everything is shown", render: (node, ctx) => row(contentChildren(node), ctx) }, "sap.m.ToolbarSpacer": { card: "(nothing)", note: "", render: () => [] }, - "sap.m.Toolbar": { card: "ActionSet (all buttons) or ColumnSet", note: "", defaultAggregation: "content", render: (node, ctx) => row(contentChildren(node), ctx) }, + "sap.m.Toolbar": { card: "ActionSet (all buttons) or ColumnSet", note: "", render: (node, ctx) => row(contentChildren(node), ctx) }, "sap.m.Bar": { card: "ActionSet or ColumnSet of contentLeft, contentMiddle, contentRight", note: "", render: (node, ctx) => row(children(node, ["contentLeft", "contentMiddle", "contentRight"]), ctx) }, "sap.m.OverflowToolbarButton": { card: "Action.Submit", note: "as Button", render: button }, // -------------------------------------------------------------- display "sap.m.Text": { card: "TextBlock (wrap)", note: "`maxLines` -> maxLines", render: (node, ctx) => [tb(str(node, "text", ctx), node.attrs.maxLines !== undefined ? { maxLines: Number(attr(node, "maxLines", ctx)) || undefined } : {})] }, "sap.m.Label": { card: "TextBlock (bolder) - or the `label` of the input that follows", note: "", render: (node, ctx) => [tb(str(node, "text", ctx), { weight: "Bolder" })] }, - "sap.m.Title": { card: "TextBlock (heading, bolder, medium)", note: "", defaultAggregation: "content", render: (node, ctx) => [heading(str(node, "text", ctx))] }, + "sap.m.Title": { card: "TextBlock (heading, bolder, medium)", note: "", render: (node, ctx) => [heading(str(node, "text", ctx))] }, "sap.m.ObjectStatus": { card: "TextBlock \"title: text\", colored by `state`", note: "`active` + `press` -> Action.Submit", @@ -412,9 +414,9 @@ export const CONTROLS = { "sap.ui.core.Item": { card: "a choice (`key` -> value, `text` -> title) of its Select/ComboBox", note: "", render: (node, ctx) => [tb(str(node, "text", ctx))] }, "sap.m.CheckBox": { card: "Input.Toggle (`text` -> title, value \"true\"/\"false\")", note: "`select` not raised", render(node, ctx) { unraised(node, ctx, ["select"]); return toggle(node, ctx, "selected", str(node, "text", ctx)); } }, "sap.m.Switch": { card: "Input.Toggle (`state`)", note: "`change` not raised; title = `customTextOn` or \"On\"", render(node, ctx) { unraised(node, ctx, ["change"]); return toggle(node, ctx, "state", str(node, "customTextOn", ctx)); } }, - "sap.m.SegmentedButton": { card: "Input.ChoiceSet (style expanded)", note: "`selectionChange` not raised", defaultAggregation: "items", render(node, ctx) { unraised(node, ctx, ["selectionChange"]); return choiceSet(node, ctx, { prop: "selectedKey", style: "expanded" }); } }, + "sap.m.SegmentedButton": { card: "Input.ChoiceSet (style expanded)", note: "`selectionChange` not raised", render(node, ctx) { unraised(node, ctx, ["selectionChange"]); return choiceSet(node, ctx, { prop: "selectedKey", style: "expanded" }); } }, "sap.m.SegmentedButtonItem": { card: "a choice of its SegmentedButton", note: "", render: (node, ctx) => [tb(str(node, "text", ctx))] }, - "sap.m.Select": { card: "Input.ChoiceSet (style compact), `selectedKey`, choices from the items (list binding or static)", note: "`change` not raised", defaultAggregation: "items", render(node, ctx) { unraised(node, ctx, ["change", "liveChange"]); return choiceSet(node, ctx, { prop: "selectedKey", style: "compact" }); } }, + "sap.m.Select": { card: "Input.ChoiceSet (style compact), `selectedKey`, choices from the items (list binding or static)", note: "`change` not raised", render(node, ctx) { unraised(node, ctx, ["change", "liveChange"]); return choiceSet(node, ctx, { prop: "selectedKey", style: "compact" }); } }, "sap.m.DatePicker": { card: "Input.Date when the value is ISO (yyyy-MM-dd), else Input.Text", note: "Input.Date speaks only yyyy-MM-dd; other `valueFormat`s stay text; `change` not raised", render: datePicker }, "sap.m.SearchField": { card: "Input.Text, `search` -> inlineAction", @@ -429,11 +431,11 @@ export const CONTROLS = { }); }, }, - "sap.m.ComboBox": { card: "Input.ChoiceSet (style filtered)", note: "`change` not raised", defaultAggregation: "items", render(node, ctx) { unraised(node, ctx, ["change"]); return choiceSet(node, ctx, { prop: node.attrs.selectedKey !== undefined ? "selectedKey" : "value", style: "filtered" }); } }, + "sap.m.ComboBox": { card: "Input.ChoiceSet (style filtered)", note: "`change` not raised", render(node, ctx) { unraised(node, ctx, ["change"]); return choiceSet(node, ctx, { prop: node.attrs.selectedKey !== undefined ? "selectedKey" : "value", style: "filtered" }); } }, "sap.m.MultiInput": { card: "Input.Text with the token texts", note: "tokens are shown, not edited as tokens - approximated", - defaultAggregation: "suggestionItems", + render(node, ctx) { const tokens = rowsOf(node, "tokens", ctx).map(({ base, node: t }) => str(t, "text", ctx.with(base)) || str(t, "key", ctx.with(base))); if (tokens.length) ctx.report(node, "tokens shown as text"); @@ -442,7 +444,7 @@ export const CONTROLS = { }, }, "sap.m.Token": { card: "a text of its MultiInput", note: "", render: (node, ctx) => [tb(str(node, "text", ctx))] }, - "sap.m.MultiComboBox": { card: "Input.ChoiceSet (isMultiSelect), `selectedKeys` joined by commas", note: "`selectionChange`/`selectionFinish` not raised", defaultAggregation: "items", render(node, ctx) { unraised(node, ctx, ["selectionChange", "selectionFinish"]); return choiceSet(node, ctx, { prop: "selectedKeys", style: "compact", multi: true }); } }, + "sap.m.MultiComboBox": { card: "Input.ChoiceSet (isMultiSelect), `selectedKeys` joined by commas", note: "`selectionChange`/`selectionFinish` not raised", render(node, ctx) { unraised(node, ctx, ["selectionChange", "selectionFinish"]); return choiceSet(node, ctx, { prop: "selectedKeys", style: "compact", multi: true }); } }, "sap.m.StepInput": { card: "Input.Number (`min`, `max`)", note: "`step` ignored; `change` not raised", render(node, ctx) { unraised(node, ctx, ["change"]); return field(node, ctx, "value", (id, v) => { const el = { type: "Input.Number", id, value: Number.isNaN(Number(v)) || v === "" || v === null || v === undefined ? undefined : Number(v) }; for (const k of ["min", "max"]) if (node.attrs[k] !== undefined) el[k] = Number(attr(node, k, ctx)); return el; }); } }, "sap.ui.core.ListItem": { card: "a choice of its ComboBox/Select", note: "`additionalText` dropped", render: (node, ctx) => [tb(str(node, "text", ctx))] }, "sap.m.DateTimePicker": { card: "Input.Text", note: "no date-time input in 1.5; `change` not raised", render(node, ctx) { unraised(node, ctx, ["change"]); return field(node, ctx, "value", (id, v) => ({ type: "Input.Text", id, value: text(v) })); } }, @@ -452,20 +454,20 @@ export const CONTROLS = { "sap.m.ToggleButton": { card: "Input.Toggle (`pressed`)", note: "`press` not raised - the state travels with the next action", render(node, ctx) { unraised(node, ctx, ["press"]); return toggle(node, ctx, "pressed", str(node, "text", ctx) || iconName(str(node, "icon", ctx))); } }, // ------------------------------------------------- collections & tables - "sap.m.Column": { card: "a column of its Table; `header` -> the header row", note: "popin and widths ignored", defaultAggregation: "header", render: () => [] }, - "sap.m.ColumnListItem": { card: "TableRow; `press` -> selectAction of its cells; `selected` -> a leading Input.Toggle in a selectable table", note: "", defaultAggregation: "cells", render: (node, ctx) => row(aggregation(node, "cells"), ctx) }, - "sap.m.Table": { card: "Table (1.5): header row from the columns, a TableRow per row of the list binding", note: "`itemPress` -> selectAction per row; `mode` *Select + `selected` binding -> Input.Toggle per row; growing ignored (every row is shown); `selectionChange` not raised", defaultAggregation: "items", render: table }, - "sap.m.List": { card: "FactSet (title -> value from description/info) - a Container per row when rows are pressable, selectable or custom", note: "`itemPress`/item `press` -> selectAction; `mode` *Select + `selected` binding -> Input.Toggle; `delete`/`selectionChange` not raised", defaultAggregation: "items", render: (node, ctx) => list(node, ctx) }, - "sap.m.StandardListItem": { card: "a fact (or row Container) of its List", note: "`icon`, `counter`, `highlight` dropped", defaultAggregation: "actions", render: (node, ctx) => [tb(str(node, "title", ctx))] }, - "sap.m.CustomListItem": { card: "a row Container of its List", note: "", defaultAggregation: "content", render: (node, ctx) => container(renderList(contentChildren(node), ctx)) }, - "sap.m.Tree": { card: "FactSet / row Containers, the tree flattened depth first (\"- \" per level)", note: "every node is shown expanded; `toggleOpenState` not raised", defaultAggregation: "items", render: (node, ctx) => list(node, ctx, { tree: true }) }, + "sap.m.Column": { card: "a column of its Table; `header` -> the header row", note: "popin and widths ignored", render: () => [] }, + "sap.m.ColumnListItem": { card: "TableRow; `press` -> selectAction of its cells; `selected` -> a leading Input.Toggle in a selectable table", note: "", render: (node, ctx) => row(aggregation(node, "cells"), ctx) }, + "sap.m.Table": { card: "Table (1.5): header row from the columns, a TableRow per row of the list binding", note: "`itemPress` -> selectAction per row; `mode` *Select + `selected` binding -> Input.Toggle per row; growing ignored (every row is shown); `selectionChange` not raised", render: table }, + "sap.m.List": { card: "FactSet (title -> value from description/info) - a Container per row when rows are pressable, selectable or custom", note: "`itemPress`/item `press` -> selectAction; `mode` *Select + `selected` binding -> Input.Toggle; `delete`/`selectionChange` not raised", render: (node, ctx) => list(node, ctx) }, + "sap.m.StandardListItem": { card: "a fact (or row Container) of its List", note: "`icon`, `counter`, `highlight` dropped", render: (node, ctx) => [tb(str(node, "title", ctx))] }, + "sap.m.CustomListItem": { card: "a row Container of its List", note: "", render: (node, ctx) => container(renderList(contentChildren(node), ctx)) }, + "sap.m.Tree": { card: "FactSet / row Containers, the tree flattened depth first (\"- \" per level)", note: "every node is shown expanded; `toggleOpenState` not raised", render: (node, ctx) => list(node, ctx, { tree: true }) }, "sap.m.StandardTreeItem": { card: "a fact of its Tree", note: "`icon` dropped", render: (node, ctx) => [tb(str(node, "title", ctx))] }, // ---------------------------------------------------- dialogs & popups "sap.m.Dialog": { card: "Container (style emphasis): title heading, content, buttons as one ActionSet", note: "rendered above the page, which turns read-only while it is open (modal); `afterClose` not raised", - defaultAggregation: "content", + render(node, ctx) { const out = []; const custom = aggregation(node, "customHeader"); @@ -479,7 +481,7 @@ export const CONTROLS = { "sap.m.Popover": { card: "Container (style emphasis): title heading, content, footer", note: "no anchor - shown above the page, which turns read-only while it is open; `afterClose` not raised", - defaultAggregation: "content", + render(node, ctx) { const out = []; if (node.attrs.title !== undefined) out.push(heading(str(node, "title", ctx))); diff --git a/lib/vendor/adaptive-cards/profiles/portable-v1.json b/lib/vendor/adaptive-cards/profiles/portable-v1.json new file mode 100644 index 0000000..995a272 --- /dev/null +++ b/lib/vendor/adaptive-cards/profiles/portable-v1.json @@ -0,0 +1,5116 @@ +{ + "$schema": "../schema/portable-profile.schema.json", + "profile": "portable", + "version": 1, + "status": "normative - profiles/portable.md is the text this file is generated for", + "generatedFrom": "the portable-profile census (2026-10-03): @abap2ui5/linter 0.8.5 view reconstruction over abap2UI5/samples, samples-stack, the addons and real-world apps (247 core apps) and samples-controls (642 ports)", + "documentRoots": [ + "sap.ui.core.mvc.View", + "sap.ui.core.FragmentDefinition" + ], + "slots": { + "required": [ + "MAIN", + "POPUP", + "POPOVER" + ], + "notInV1": [ + "NEST", + "NEST2" + ] + }, + "namespaces": [ + "sap.m", + "sap.ui.core", + "sap.ui.core.mvc", + "sap.ui.layout", + "sap.ui.layout.form", + "sap.tnt" + ], + "universalAttributes": [ + "id", + "class", + "visible", + "tooltip", + "busy", + "busyIndicatorDelay", + "fieldGroupIds", + "binding", + "layoutData", + "customData", + "dependents" + ], + "cssHelperClassPattern": "^sapUi(Tiny|Small|Medium|Large)?(Margin|Padding)(Top|Bottom|Begin|End|BeginEnd|TopBottom)?$|^sapUiContentPadding$|^sapUiResponsiveMargin$|^sapUiNoMargin|^sapUiNoContentPadding$", + "controls": { + "sap.m.Page": { + "category": "Layout & containers", + "tolerated": false, + "usage": { + "coreApps": 195, + "samplesControlsApps": 162 + }, + "properties": [ + { + "name": "backgroundDesign", + "core": 0, + "controls": 6 + }, + { + "name": "enableScrolling", + "core": 0, + "controls": 23 + }, + { + "name": "showFooter", + "core": 0, + "controls": 2 + }, + { + "name": "showHeader", + "core": 5, + "controls": 120 + }, + { + "name": "showNavButton", + "core": 185, + "controls": 36 + }, + { + "name": "title", + "core": 188, + "controls": 42 + }, + { + "name": "titleLevel", + "core": 0, + "controls": 2 + } + ], + "aggregations": [ + { + "name": "customHeader", + "core": 2, + "controls": 34 + }, + { + "name": "footer", + "core": 10, + "controls": 30 + }, + { + "name": "headerContent", + "core": 7, + "controls": 4 + }, + { + "name": "subHeader", + "core": 1, + "controls": 13 + } + ], + "defaultAggregation": "content", + "events": [ + { + "name": "navButtonPress", + "core": 185, + "controls": 20 + } + ], + "webComponent": "ui5-page (fiori) + ui5-bar in slot header/footer", + "fit": "adapt", + "mapping": "`ui5-page` has no title/nav button: build the header as `` with `startContent` = `` (only when `showNavButton`), default slot = `` with `title`, `endContent` = `headerContent`. `customHeader` replaces that bar; `subHeader` = a second bar; `footer` (a Toolbar/OverflowToolbar/Bar) -> slot `footer` (`fixed-footer`). `showHeader=false` drops the bar, `enableScrolling=false` -> `no-scrolling`, `backgroundDesign` -> `background-design`. `navButtonPress` = click of the nav button.", + "notInV1": [ + { + "kind": "aggregation", + "name": "landmarkInfo", + "core": 0, + "controls": 1 + } + ] + }, + "sap.m.Shell": { + "category": "Layout & containers", + "tolerated": false, + "usage": { + "coreApps": 193, + "samplesControlsApps": 15 + }, + "properties": [], + "aggregations": [], + "defaultAggregation": "app", + "events": [], + "webComponent": "(none) - plain root
", + "fit": "compose", + "mapping": "App frame only. Render its single child full-height; `appWidthLimited` (default true) = centred max-width (~1280px) letterbox. Do NOT map to `ui5-shellbar` (that is a header bar, not a frame).", + "notInV1": [ + { + "kind": "property", + "name": "appWidthLimited", + "core": 0, + "controls": 1 + } + ] + }, + "sap.m.VBox": { + "category": "Layout & containers", + "tolerated": false, + "usage": { + "coreApps": 72, + "samplesControlsApps": 194 + }, + "properties": [ + { + "name": "alignItems", + "core": 2, + "controls": 15 + }, + { + "name": "height", + "core": 5, + "controls": 14 + }, + { + "name": "justifyContent", + "core": 4, + "controls": 14 + }, + { + "name": "renderType", + "core": 0, + "controls": 9 + }, + { + "name": "width", + "core": 2, + "controls": 8 + }, + { + "name": "wrap", + "core": 0, + "controls": 1 + } + ], + "aggregations": [], + "defaultAggregation": "items", + "events": [], + "webComponent": "(none) -
", + "fit": "compose", + "mapping": "`alignItems`/`justifyContent`/`wrap`/`width`/`height` are CSS flexbox one-to-one (Start->flex-start, End->flex-end, Center, Stretch, SpaceBetween->space-between, …). `renderType` ignored.", + "notInV1": [ + { + "kind": "property", + "name": "fitContainer", + "core": 0, + "controls": 4 + }, + { + "kind": "property", + "name": "backgroundDesign", + "core": 0, + "controls": 1 + } + ] + }, + "sap.ui.layout.form.SimpleForm": { + "category": "Layout & containers", + "tolerated": false, + "usage": { + "coreApps": 70, + "samplesControlsApps": 115 + }, + "properties": [ + { + "name": "adjustLabelSpan", + "core": 2, + "controls": 18 + }, + { + "name": "columnsL", + "core": 1, + "controls": 44 + }, + { + "name": "columnsM", + "core": 1, + "controls": 44 + }, + { + "name": "columnsXL", + "core": 1, + "controls": 24 + }, + { + "name": "editable", + "core": 65, + "controls": 110 + }, + { + "name": "emptySpanL", + "core": 0, + "controls": 50 + }, + { + "name": "emptySpanM", + "core": 0, + "controls": 50 + }, + { + "name": "emptySpanS", + "core": 0, + "controls": 29 + }, + { + "name": "emptySpanXL", + "core": 0, + "controls": 14 + }, + { + "name": "labelSpanL", + "core": 3, + "controls": 65 + }, + { + "name": "labelSpanM", + "core": 3, + "controls": 65 + }, + { + "name": "labelSpanS", + "core": 2, + "controls": 41 + }, + { + "name": "labelSpanXL", + "core": 2, + "controls": 24 + }, + { + "name": "layout", + "core": 11, + "controls": 112 + }, + { + "name": "maxContainerCols", + "core": 0, + "controls": 30 + }, + { + "name": "singleContainerFullSize", + "core": 0, + "controls": 15 + }, + { + "name": "width", + "core": 0, + "controls": 32 + } + ], + "aggregations": [ + { + "name": "title", + "core": 53, + "controls": 45 + } + ], + "defaultAggregation": "content", + "events": [], + "webComponent": "ui5-form + ui5-form-group + ui5-form-item", + "fit": "adapt", + "mapping": "SimpleForm content is a FLAT sequence; the renderer must group it: a `core:Title` (or `Toolbar`/`Title`) opens a new ``; each `Label` opens a new `` (the label goes to slot `labelContent`) and every following field until the next Label/Title goes to the item's default slot. `labelSpanXL/L/M/S` -> `label-span=\"S12 M4 L4 XL4\"` (same notation), `emptySpan*` -> `empty-span`, `columnsXL/L/M` -> `layout=\"S1 M1 L2 XL2\"`, `title` aggregation -> `header-text`. `layout`/`adjustLabelSpan`/`singleContainerFullSize`/`maxContainerCols` ignored. `editable` only changes label/field density in UI5 - no WC counterpart (ignore).", + "notInV1": [ + { + "kind": "property", + "name": "backgroundDesign", + "core": 0, + "controls": 5 + }, + { + "kind": "aggregation", + "name": "toolbar", + "core": 0, + "controls": 3 + }, + { + "kind": "event", + "name": "validateFieldGroup", + "core": 0, + "controls": 1 + } + ] + }, + "sap.m.HBox": { + "category": "Layout & containers", + "tolerated": false, + "usage": { + "coreApps": 35, + "samplesControlsApps": 64 + }, + "properties": [ + { + "name": "alignItems", + "core": 12, + "controls": 25 + }, + { + "name": "height", + "core": 0, + "controls": 4 + }, + { + "name": "justifyContent", + "core": 1, + "controls": 18 + }, + { + "name": "renderType", + "core": 0, + "controls": 13 + }, + { + "name": "width", + "core": 0, + "controls": 7 + }, + { + "name": "wrap", + "core": 7, + "controls": 2 + } + ], + "aggregations": [], + "defaultAggregation": "items", + "events": [], + "webComponent": "(none) -
", + "fit": "compose", + "mapping": "As VBox, row direction. `wrap=\"Wrap\"` -> `flex-wrap:wrap`.", + "notInV1": [ + { + "kind": "property", + "name": "backgroundDesign", + "core": 0, + "controls": 2 + }, + { + "kind": "property", + "name": "fitContainer", + "core": 0, + "controls": 1 + } + ] + }, + "sap.m.Panel": { + "category": "Layout & containers", + "tolerated": false, + "usage": { + "coreApps": 15, + "samplesControlsApps": 71 + }, + "properties": [ + { + "name": "backgroundDesign", + "core": 0, + "controls": 14 + }, + { + "name": "expandable", + "core": 4, + "controls": 5 + }, + { + "name": "expanded", + "core": 3, + "controls": 4 + }, + { + "name": "headerText", + "core": 14, + "controls": 28 + }, + { + "name": "height", + "core": 0, + "controls": 13 + }, + { + "name": "stickyHeader", + "core": 0, + "controls": 1 + }, + { + "name": "width", + "core": 5, + "controls": 36 + } + ], + "aggregations": [ + { + "name": "headerToolbar", + "core": 0, + "controls": 21 + } + ], + "defaultAggregation": "content", + "events": [ + { + "name": "expand", + "core": 0, + "controls": 1 + } + ], + "webComponent": "ui5-panel", + "fit": "adapt", + "mapping": "`headerText` -> `header-text`; UI5 panels are NOT collapsible by default, WC panels are: set `fixed` unless `expandable=true`; `expanded` -> `!collapsed`; `headerToolbar` -> slot `header`; `width`/`height` CSS; `expand` <- `toggle` (`$parameters>/expand` = `!collapsed`). `backgroundDesign` -> CSS only.", + "notInV1": [ + { + "kind": "property", + "name": "accessibleRole", + "core": 0, + "controls": 2 + } + ] + }, + "sap.ui.layout.Grid": { + "category": "Layout & containers", + "tolerated": false, + "usage": { + "coreApps": 13, + "samplesControlsApps": 26 + }, + "properties": [ + { + "name": "defaultSpan", + "core": 11, + "controls": 25 + }, + { + "name": "hSpacing", + "core": 0, + "controls": 17 + }, + { + "name": "width", + "core": 0, + "controls": 11 + } + ], + "aggregations": [], + "defaultAggregation": "content", + "events": [], + "webComponent": "(none) - CSS grid, 12 columns", + "fit": "compose", + "mapping": "`defaultSpan=\"XL3 L3 M6 S12\"` -> per-breakpoint `grid-column: span n` (UI5 breakpoints S<600, M<1024, L<1440, XL); children may carry `GridData span/linebreak/indent` (tolerated). `hSpacing`/`vSpacing` (rem) -> `column-gap`/`row-gap`.", + "notInV1": [ + { + "kind": "property", + "name": "containerQuery", + "core": 0, + "controls": 3 + }, + { + "kind": "property", + "name": "defaultIndent", + "core": 0, + "controls": 2 + }, + { + "kind": "property", + "name": "vSpacing", + "core": 0, + "controls": 5 + }, + { + "kind": "property", + "name": "position", + "core": 0, + "controls": 1 + } + ] + }, + "sap.m.FlexBox": { + "category": "Layout & containers", + "tolerated": false, + "usage": { + "coreApps": 7, + "samplesControlsApps": 54 + }, + "properties": [ + { + "name": "alignItems", + "core": 5, + "controls": 36 + }, + { + "name": "columnGap", + "core": 0, + "controls": 1 + }, + { + "name": "direction", + "core": 1, + "controls": 12 + }, + { + "name": "fitContainer", + "core": 0, + "controls": 25 + }, + { + "name": "gap", + "core": 0, + "controls": 1 + }, + { + "name": "height", + "core": 2, + "controls": 7 + }, + { + "name": "justifyContent", + "core": 3, + "controls": 12 + }, + { + "name": "renderType", + "core": 1, + "controls": 5 + }, + { + "name": "rowGap", + "core": 0, + "controls": 1 + }, + { + "name": "width", + "core": 1, + "controls": 5 + }, + { + "name": "wrap", + "core": 1, + "controls": 25 + } + ], + "aggregations": [], + "defaultAggregation": "items", + "events": [], + "webComponent": "(none) - flex
", + "fit": "compose", + "mapping": "`direction` (Row/Column/RowReverse/ColumnReverse) -> `flex-direction`; `fitContainer` -> height:100%; `gap`/`rowGap`/`columnGap` -> CSS gap.", + "notInV1": [] + }, + "sap.m.ScrollContainer": { + "category": "Layout & containers", + "tolerated": false, + "usage": { + "coreApps": 4, + "samplesControlsApps": 24 + }, + "properties": [ + { + "name": "height", + "core": 4, + "controls": 20 + }, + { + "name": "horizontal", + "core": 1, + "controls": 15 + }, + { + "name": "vertical", + "core": 4, + "controls": 22 + }, + { + "name": "width", + "core": 0, + "controls": 12 + } + ], + "aggregations": [], + "defaultAggregation": "content", + "events": [], + "webComponent": "(none) -
", + "fit": "compose", + "mapping": "`vertical`/`horizontal` -> `overflow-y`/`overflow-x` (UI5 default: horizontal true, vertical false); `width`/`height` CSS. SCROLL_TO / SCROLL_INTO_VIEW actions address it by id.", + "notInV1": [ + { + "kind": "property", + "name": "focusable", + "core": 0, + "controls": 2 + } + ] + }, + "sap.m.IconTabFilter": { + "category": "Layout & containers", + "tolerated": false, + "usage": { + "coreApps": 4, + "samplesControlsApps": 35 + }, + "properties": [ + { + "name": "count", + "core": 0, + "controls": 17 + }, + { + "name": "design", + "core": 0, + "controls": 3 + }, + { + "name": "enabled", + "core": 0, + "controls": 1 + }, + { + "name": "icon", + "core": 2, + "controls": 16 + }, + { + "name": "iconColor", + "core": 0, + "controls": 9 + }, + { + "name": "key", + "core": 4, + "controls": 32 + }, + { + "name": "text", + "core": 4, + "controls": 29 + } + ], + "aggregations": [], + "defaultAggregation": "content", + "events": [], + "webComponent": "ui5-tab", + "fit": "adapt", + "mapping": "`text`, `icon` (sap-icon://x -> `icon=\"x\"`), `key` -> keep as `data-key` (WC tabs have no key), `count` -> `additional-text`, `enabled=false` -> `disabled`, `design` -> `design`, content -> default slot. `iconColor` (Positive/Critical/Negative) -> tab `design`.", + "notInV1": [ + { + "kind": "property", + "name": "showAll", + "core": 0, + "controls": 7 + }, + { + "kind": "aggregation", + "name": "items", + "core": 0, + "controls": 5 + }, + { + "kind": "property", + "name": "interactionMode", + "core": 0, + "controls": 3 + } + ] + }, + "sap.m.IconTabBar": { + "category": "Layout & containers", + "tolerated": false, + "usage": { + "coreApps": 3, + "samplesControlsApps": 30 + }, + "properties": [ + { + "name": "applyContentPadding", + "core": 1, + "controls": 2 + }, + { + "name": "expandable", + "core": 2, + "controls": 2 + }, + { + "name": "expanded", + "core": 2, + "controls": 9 + }, + { + "name": "headerBackgroundDesign", + "core": 0, + "controls": 3 + }, + { + "name": "headerMode", + "core": 1, + "controls": 4 + }, + { + "name": "selectedKey", + "core": 2, + "controls": 8 + } + ], + "aggregations": [ + { + "name": "content", + "core": 1, + "controls": 7 + }, + { + "name": "items", + "core": 3, + "controls": 30 + } + ], + "events": [ + { + "name": "select", + "core": 2, + "controls": 6 + } + ], + "webComponent": "ui5-tabcontainer", + "fit": "adapt", + "mapping": "`items` -> tabs; `selectedKey` (two-way) -> `selected` on the tab whose key matches, written back on `tab-select`; `select` <- `tab-select` with `$parameters>/key` and `$parameters>/selectedKey` = the tab's key; `expandable`/`expanded` -> `collapsed`; `headerBackgroundDesign` -> `header-background-design`. The IconTabBar-level `content` aggregation (shared content) has no slot: render it below the tab strip.", + "notInV1": [ + { + "kind": "property", + "name": "backgroundDesign", + "core": 0, + "controls": 2 + }, + { + "kind": "property", + "name": "stretchContentHeight", + "core": 0, + "controls": 2 + }, + { + "kind": "property", + "name": "upperCase", + "core": 0, + "controls": 5 + }, + { + "kind": "property", + "name": "enableTabReordering", + "core": 0, + "controls": 3 + }, + { + "kind": "property", + "name": "tabDensityMode", + "core": 0, + "controls": 2 + }, + { + "kind": "property", + "name": "tabsOverflowMode", + "core": 0, + "controls": 1 + }, + { + "kind": "property", + "name": "maxNestingLevel", + "core": 0, + "controls": 1 + } + ] + }, + "sap.ui.layout.VerticalLayout": { + "category": "Layout & containers", + "tolerated": false, + "usage": { + "coreApps": 2, + "samplesControlsApps": 174 + }, + "properties": [ + { + "name": "width", + "core": 2, + "controls": 110 + } + ], + "aggregations": [], + "defaultAggregation": "content", + "events": [], + "webComponent": "(none) - block
", + "fit": "compose", + "mapping": "Children stacked; `width` CSS.", + "notInV1": [] + }, + "sap.ui.core.Title": { + "category": "Layout & containers", + "tolerated": false, + "usage": { + "coreApps": 1, + "samplesControlsApps": 55 + }, + "properties": [ + { + "name": "text", + "core": 1, + "controls": 55 + } + ], + "aggregations": [], + "events": [], + "webComponent": "group header of ui5-form-group (inside forms); ui5-title elsewhere", + "fit": "adapt", + "mapping": "Only `text` is used (and `level`). Inside SimpleForm it is a group separator, not a visible control of its own.", + "notInV1": [] + }, + "sap.ui.layout.HorizontalLayout": { + "category": "Layout & containers", + "tolerated": false, + "usage": { + "coreApps": 0, + "samplesControlsApps": 52 + }, + "properties": [ + { + "name": "allowWrapping", + "core": 0, + "controls": 12 + } + ], + "aggregations": [], + "defaultAggregation": "content", + "events": [], + "webComponent": "(none) - inline-flex
", + "fit": "compose", + "mapping": "`allowWrapping` -> `flex-wrap`.", + "notInV1": [] + }, + "sap.m.OverflowToolbar": { + "category": "Toolbars & bars", + "tolerated": false, + "usage": { + "coreApps": 43, + "samplesControlsApps": 113 + }, + "properties": [ + { + "name": "height", + "core": 0, + "controls": 12 + }, + { + "name": "style", + "core": 0, + "controls": 22 + } + ], + "aggregations": [], + "defaultAggregation": "content", + "events": [], + "webComponent": "ui5-toolbar", + "fit": "adapt", + "mapping": "Toolbar children must be toolbar items: Button/OverflowToolbarButton -> `ui5-toolbar-button`, ToolbarSpacer -> `ui5-toolbar-spacer`, Select -> `ui5-toolbar-select`/`ui5-toolbar-select-option`, anything else (Title, Label, Input, SearchField, SegmentedButton, …) wrapped in `ui5-toolbar-item`. `style=\"Clear\"`/`design=\"Transparent\"` -> `design=\"Transparent\"`. `OverflowToolbarLayoutData priority` -> `overflow-priority` (NeverOverflow -> `NeverOverflow`, AlwaysOverflow -> `AlwaysOverflow`).", + "notInV1": [ + { + "kind": "property", + "name": "active", + "core": 0, + "controls": 3 + }, + { + "kind": "event", + "name": "press", + "core": 0, + "controls": 3 + }, + { + "kind": "property", + "name": "design", + "core": 0, + "controls": 4 + }, + { + "kind": "property", + "name": "enabled", + "core": 0, + "controls": 1 + }, + { + "kind": "property", + "name": "width", + "core": 0, + "controls": 5 + }, + { + "kind": "property", + "name": "ariaHasPopup", + "core": 0, + "controls": 1 + } + ] + }, + "sap.m.ToolbarSpacer": { + "category": "Toolbars & bars", + "tolerated": false, + "usage": { + "coreApps": 37, + "samplesControlsApps": 117 + }, + "properties": [ + { + "name": "width", + "core": 1, + "controls": 5 + } + ], + "aggregations": [], + "events": [], + "webComponent": "ui5-toolbar-spacer", + "fit": "direct", + "mapping": "`width` -> `width` (fixed spacer); without width it is the flexible spacer. Outside a `ui5-toolbar` (e.g. in `ui5-bar`) use a `flex:1` div.", + "notInV1": [] + }, + "sap.m.Toolbar": { + "category": "Toolbars & bars", + "tolerated": false, + "usage": { + "coreApps": 16, + "samplesControlsApps": 55 + }, + "properties": [ + { + "name": "height", + "core": 0, + "controls": 12 + }, + { + "name": "style", + "core": 1, + "controls": 0 + } + ], + "aggregations": [], + "defaultAggregation": "content", + "events": [], + "webComponent": "ui5-toolbar (or ui5-bar)", + "fit": "adapt", + "mapping": "Same mapping as OverflowToolbar. Most uses are the `headerToolbar` of a Table/List/Panel (Title + Spacer + Buttons).", + "notInV1": [ + { + "kind": "property", + "name": "width", + "core": 0, + "controls": 1 + }, + { + "kind": "property", + "name": "design", + "core": 0, + "controls": 1 + }, + { + "kind": "property", + "name": "active", + "core": 0, + "controls": 1 + } + ] + }, + "sap.m.Bar": { + "category": "Toolbars & bars", + "tolerated": false, + "usage": { + "coreApps": 2, + "samplesControlsApps": 40 + }, + "properties": [], + "aggregations": [ + { + "name": "contentLeft", + "core": 2, + "controls": 9 + }, + { + "name": "contentMiddle", + "core": 0, + "controls": 9 + }, + { + "name": "contentRight", + "core": 2, + "controls": 34 + } + ], + "events": [], + "webComponent": "ui5-bar", + "fit": "direct", + "mapping": "`contentLeft` -> slot `startContent`, `contentMiddle` -> default slot, `contentRight` -> slot `endContent`; `design` (Header/SubHeader/Footer) -> `design`.", + "notInV1": [] + }, + "sap.m.OverflowToolbarButton": { + "category": "Toolbars & bars", + "tolerated": false, + "usage": { + "coreApps": 2, + "samplesControlsApps": 37 + }, + "properties": [ + { + "name": "enabled", + "core": 0, + "controls": 1 + }, + { + "name": "icon", + "core": 2, + "controls": 37 + }, + { + "name": "text", + "core": 2, + "controls": 31 + }, + { + "name": "type", + "core": 0, + "controls": 30 + } + ], + "aggregations": [], + "events": [ + { + "name": "press", + "core": 2, + "controls": 12 + } + ], + "webComponent": "ui5-toolbar-button", + "fit": "direct", + "mapping": "Icon-only in the bar, text shown in the overflow -> `show-overflow-text` + `text`; `type` -> `design` (as Button); `press` <- `click`.", + "notInV1": [] + }, + "sap.m.Text": { + "category": "Display", + "tolerated": false, + "usage": { + "coreApps": 115, + "samplesControlsApps": 310 + }, + "properties": [ + { + "name": "emptyIndicatorMode", + "core": 0, + "controls": 1 + }, + { + "name": "maxLines", + "core": 0, + "controls": 1 + }, + { + "name": "renderWhitespace", + "core": 1, + "controls": 2 + }, + { + "name": "text", + "core": 102, + "controls": 307 + }, + { + "name": "textAlign", + "core": 0, + "controls": 1 + }, + { + "name": "width", + "core": 1, + "controls": 14 + }, + { + "name": "wrapping", + "core": 1, + "controls": 42 + } + ], + "aggregations": [], + "events": [], + "webComponent": "ui5-text", + "fit": "direct", + "mapping": "`text` -> text content; `wrapping=false` -> `max-lines=\"1\"`; `maxLines` -> `max-lines`; `renderWhitespace` -> CSS `white-space:pre-wrap`; `emptyIndicatorMode` -> `empty-indicator-mode`; `textAlign`/`width` CSS.", + "notInV1": [] + }, + "sap.m.Label": { + "category": "Display", + "tolerated": false, + "usage": { + "coreApps": 86, + "samplesControlsApps": 286 + }, + "properties": [ + { + "name": "design", + "core": 0, + "controls": 8 + }, + { + "name": "required", + "core": 2, + "controls": 8 + }, + { + "name": "showColon", + "core": 0, + "controls": 1 + }, + { + "name": "text", + "core": 76, + "controls": 285 + }, + { + "name": "textAlign", + "core": 0, + "controls": 1 + }, + { + "name": "width", + "core": 1, + "controls": 9 + }, + { + "name": "wrapping", + "core": 0, + "controls": 17 + } + ], + "aggregations": [], + "events": [], + "webComponent": "ui5-label", + "fit": "direct", + "mapping": "`text`, `required`, `showColon` -> `show-colon`, `wrapping` -> `wrapping-type`, `labelFor` -> `for` (needs the target's DOM id); `design=\"Bold\"`/`width`/`textAlign` CSS.", + "notInV1": [ + { + "kind": "property", + "name": "displayOnly", + "core": 0, + "controls": 2 + }, + { + "kind": "property", + "name": "wrappingType", + "core": 0, + "controls": 1 + } + ] + }, + "sap.m.Title": { + "category": "Display", + "tolerated": false, + "usage": { + "coreApps": 58, + "samplesControlsApps": 181 + }, + "properties": [ + { + "name": "level", + "core": 10, + "controls": 53 + }, + { + "name": "text", + "core": 51, + "controls": 178 + }, + { + "name": "textAlign", + "core": 0, + "controls": 1 + }, + { + "name": "titleStyle", + "core": 0, + "controls": 25 + }, + { + "name": "wrapping", + "core": 1, + "controls": 43 + } + ], + "aggregations": [], + "defaultAggregation": "content", + "events": [], + "webComponent": "ui5-title", + "fit": "direct", + "mapping": "`text` -> text content; `level` (H1..H6, Auto) -> `level`; `titleStyle` -> `size`; `wrapping=true` -> `wrapping-type=\"Normal\"`. A Title with a `content` child (link) renders that child inside.", + "notInV1": [ + { + "kind": "property", + "name": "wrappingType", + "core": 0, + "controls": 1 + } + ] + }, + "sap.m.ObjectStatus": { + "category": "Display", + "tolerated": false, + "usage": { + "coreApps": 17, + "samplesControlsApps": 51 + }, + "properties": [ + { + "name": "active", + "core": 0, + "controls": 2 + }, + { + "name": "icon", + "core": 2, + "controls": 6 + }, + { + "name": "inverted", + "core": 0, + "controls": 3 + }, + { + "name": "state", + "core": 11, + "controls": 50 + }, + { + "name": "text", + "core": 16, + "controls": 51 + }, + { + "name": "title", + "core": 6, + "controls": 18 + } + ], + "aggregations": [], + "events": [ + { + "name": "press", + "core": 0, + "controls": 1 + } + ], + "webComponent": "ui5-tag (non-interactive) or styled text", + "fit": "adapt", + "mapping": "No exact counterpart. `state` None/Success/Warning/Error/Information/IndicationNN -> `ui5-tag design` Neutral/Positive/Critical/Negative/Information/Set1..(color-scheme) - BUT a tag renders as a pill; apps expect coloured text (+ icon). Recommended: `` with theming colour vars (`--sapPositiveTextColor`, …) + optional `ui5-icon`; use `ui5-tag` only for `inverted=true`. `title` -> \"title: \" prefix; `active`/`press` -> `ui5-tag interactive` + `click`.", + "notInV1": [] + }, + "sap.m.Link": { + "category": "Display", + "tolerated": false, + "usage": { + "coreApps": 14, + "samplesControlsApps": 94 + }, + "properties": [ + { + "name": "emphasized", + "core": 0, + "controls": 2 + }, + { + "name": "enabled", + "core": 0, + "controls": 1 + }, + { + "name": "endIcon", + "core": 0, + "controls": 1 + }, + { + "name": "href", + "core": 11, + "controls": 45 + }, + { + "name": "icon", + "core": 0, + "controls": 1 + }, + { + "name": "target", + "core": 13, + "controls": 31 + }, + { + "name": "text", + "core": 12, + "controls": 93 + }, + { + "name": "wrapping", + "core": 0, + "controls": 1 + } + ], + "aggregations": [], + "events": [ + { + "name": "press", + "core": 3, + "controls": 35 + } + ], + "webComponent": "ui5-link", + "fit": "direct", + "mapping": "`text` -> text content, `href`, `target`, `enabled=false` -> `disabled`, `emphasized` -> `design=\"Emphasized\"`, `subtle` -> `design=\"Subtle\"`, `wrapping` -> `wrapping-type`, `icon`/`endIcon` -> `icon`/`end-icon`; `press` <- `click` (do not navigate when a press wire is set and no href).", + "notInV1": [ + { + "kind": "property", + "name": "subtle", + "core": 0, + "controls": 1 + }, + { + "kind": "property", + "name": "ariaHasPopup", + "core": 0, + "controls": 5 + } + ] + }, + "sap.m.ObjectIdentifier": { + "category": "Display", + "tolerated": false, + "usage": { + "coreApps": 7, + "samplesControlsApps": 50 + }, + "properties": [ + { + "name": "text", + "core": 7, + "controls": 45 + }, + { + "name": "title", + "core": 1, + "controls": 47 + }, + { + "name": "titleActive", + "core": 0, + "controls": 3 + } + ], + "aggregations": [], + "events": [ + { + "name": "titlePress", + "core": 0, + "controls": 3 + } + ], + "webComponent": "(none) - ui5-title/ui5-link + ui5-text", + "fit": "compose", + "mapping": "Two lines: `title` (bold; `ui5-link` when `titleActive`, firing `titlePress`) and `text` (secondary).", + "notInV1": [] + }, + "sap.ui.core.HTML": { + "category": "Display", + "tolerated": false, + "usage": { + "coreApps": 7, + "samplesControlsApps": 60 + }, + "properties": [ + { + "name": "content", + "core": 5, + "controls": 60 + } + ], + "aggregations": [], + "events": [], + "webComponent": "(none) - raw HTML in a
", + "fit": "compose", + "mapping": "`content` is injected as HTML. Used for (a) custom `