This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Mateu is a model-driven UI framework for Java. You annotate Java classes with @UI and Mateu generates forms, CRUD screens, navigation, and a full web UI automatically. Developers write zero frontend code for typical business apps.
backend/ ← Maven multi-module Java backend
shared/
core/ ← Framework kernel (hexagonal: application / domain / infra layers)
uidl/ ← Public API: annotations (@UI, @Action, @Button, etc.) and interfaces (CrudStore, Listing + capability interfaces, …)
dtos/ ← Wire DTOs exchanged between backend and frontend (UIIncrementDto, FormDto, …)
annotation-processor-core/ ← AP logic shared by all framework adapters
annotation-processor-indexer/← Writes META-INF/mateu/ui-registrations into module jars
frontend/
vaadin-lit/ ← Bundled vaadin web-component assets served by the Spring boot app
redwood/ ← Same for the Redwood (Visual Builder) renderer — resources generated by
`npm run copy` in frontend/web/monorepo/apps/redwood (do not edit by hand)
mvc/ ← Spring MVC adapter (SpaRedirectFilter, annotation-processor-mvc, …)
webflux/ ← Spring WebFlux adapter
micronaut/ ← Micronaut adapter
quarkus/ ← Quarkus adapter
helidon-mp/ ← Helidon MicroProfile adapter (JAX-RS/CDI/Weld; at full parity — see the
Helidon MP adapter note below)
backend/dotnet/ ← C# server-side (Mateu.NET) — ASP.NET reflection mapper emitting the same
/mateu/v3/sync wire model so existing renderers render a C# backend.
See backend/dotnet/DESIGN.md + README.md.
backend/python/ ← Python server-side — FastAPI + Pydantic reflection mapper emitting the same
/mateu/v3/sync wire model. Field modifiers via Annotated[...], class/method
features via decorators. See backend/python/DESIGN.md + README.md.
(A Go port was CONSIDERED AND DISCARDED 2026-07-11: struct tags can't carry
the declarative surface with acceptable DX and a 4th lockstep port isn't
worth it without demand — if it ever comes back, start as a fluent/builder
SDK emitting the wire DTOs, not a reflective mapper.)
frontend/web/monorepo/ ← TypeScript/Lit/Vite monorepo (workspaces: apps/*, libs/*)
libs/mateu/ ← Shared lib: API client, domain state, base web-components
apps/vaadin/ ← Vaadin-themed renderer (builds → backend/shared/frontend/vaadin-lit)
apps/redwood/ ← Redwood renderer sobre Oracle Visual Builder (movido 2026-07-30 desde
.dev/vb). OJO: cae bajo el glob de workspaces apps/* — su package.json
trae el tooling grunt de Oracle como devDependencies (tarballs del CDN
de Oracle), así que un yarn/npm install en la RAÍZ del monorepo los
descarga; para trabajar solo con él, npm install DENTRO del dir.
webApps/vbredwoodapp = la app VB (chains + bridge AMD
generado desde poc/); poc/ = fuente única del core (reduceContexts +
transport) con 32 tests de contrato (node test.mjs); `npm run build`
(grunt vb-build) → build/optimized; `npm run copy` empaqueta ese build
en backend/shared/frontend/redwood (jar io.mateu:redwood — añadirlo
como dependencia en lugar de vaadin-lit sirve la app VB: _index.html
con marcadores AQUIELTITULODELAPAGINA + AQUIUI/HASTAAQUIUI — el
<mateu-ui> que inyecta el controller queda display:none, transporta
el baseUrl Y es la señal del modo URL —, mateuBaseUrl → '' same-origin,
rutas Mateu POR PATH sin hash (/products; deep-link+popstate; el copy
inyecta vbInitConfig.BASE_URL='/version_<ts>/' porque la base de
módulos del visual-runtime deriva de location.pathname e ignora <base
href>; en serving estático vb-serve/VB hosteado los chains siguen en
hash #/ruta); JET/oj-sp/visual-runtime SIEMPRE desde el CDN de Oracle, nada
de static.oracle.com se vendoriza — ver su NOTICE.md). App de
referencia: demo/demo-vb (:9005). Ver README/DESIGN-NOTES/ROADMAP en
el propio directorio.
(Los renderers sapui5, redwood-oj (OJET), redhat/PatternFly y slds
fueron RETIRADOS — los web soportados son vaadin y la línea VB.)
frontend/app/ ← Native (non-browser) renderers — all speak the same /mateu/v3/sync API
react-native/ ← Mobile (Expo/TypeScript) — iOS & Android; boots via the app registry
intellij-plugin/ ← Desktop — IntelliJ IDEA plugin (tool windows, editor tabs, docking)
(the JavaFX and Compose renderers were REMOVED 2026-07-10)
demo/ ← Runnable demo apps (Spring MVC, WebFlux, Micronaut, Quarkus, Helidon).
demo-front-office (port 8594) is the reference app for the front-office
UX components: Check-In queue + wizard, Check-Out folio/payment, En Casa
360, Automatizaciones, with @AppContext + @Audience Staff/Cliente modes.
e2e/ ← Playwright end-to-end tests + SUT (subject under test) apps
@UI classes can live in a framework-agnostic module (no Spring/Quarkus dep, only io.mateu:uidl).
- Indexer AP (
annotation-processor-indexer) — compile the UI module with this AP; it writesMETA-INF/mateu/ui-registrationsinto the jar. - Framework AP (e.g.
annotation-processor-mvc) — compile the app module with this AP and with the UI module on the AP classpath; it reads the index and generates Spring MVC / WebFlux / Micronaut / Quarkus controllers.
Both the UI module and the AP must appear in <annotationProcessorPaths> of the app's pom.xml. See e2e/README.md for the canonical example.
A @UI/@Route ViewModel that is NOT a container-managed bean is instantiated by Mateu fresh on every request, and @Autowired/@Inject FIELDS are still injected — this is the default and preferred pattern (see doc/.../java-user-manual/concepts/execution-model.md). If you register the ViewModel as a Spring bean (e.g. to use constructor injection), it MUST be @Scope("prototype"): a singleton ViewModel shares its mutable form fields across all users and requests (user A sees user B's half-typed form). CRUD orchestrators (AutoCrud subclasses) that only hold final injected services are stateless and may stay singletons, but any class with mutable UI-state fields must be per-request. When generating or reviewing consumer code, flag @Component/@Service on a ViewModel with non-final fields as a bug unless it is prototype-scoped.
Frontend → POST /{baseUrl}/mateu/v3/components/_/action → MateuController (infra) → application use case → domain → reflection-based mappers → UIIncrementDto (JSON) → frontend renders via web-components.
The same UIIncrementDto carries commands (e.g. SetWindowTitle, navigateTo), messages (toasts/alerts), and fragments (partial UI updates).
A mount is a UI app served at a base path (what @UI declares; the annotated class is its root
view, the entry whose route is ""). Inside it, routes can also be declared as data, in a
routes.yaml next to the definitions — an entry binds a definition (layout), a viewModel and
fixedParams/defaultParams independently, so one screen can answer several routes with
different parameters pinned, one definition can serve several view models, and a route can exist
with no view model at all (the statically deployed case). An annotation can only ever express
the one-to-one case.
- Two producers, one table. The APs' indexes (
ui-registrations+route-registrations) are the derived half;routes.yamlis the authored half, merged on top — authored wins, replacing the entry outright. Only the authored half short-circuitsDefaultRoutedClassResolver.resolve: the derived half is what theRoutedClassProviders already carry, and they also serve the CRUD sub-routes (/new,/{id}/edit). - Routes are RELATIVE to the mount, so two federated domains can each have an
ordersscreen. - Parameter precedence — identical on the server, in
libs/mateuand in the VB core, because route resolution also runs in the browser:fixed > client state > path > query > defaults. Applied inRouteSegmentUtils.addParameterValues. The fixed ones are re-applied server-side rather than trusted from the client (a pin enforced only in the browser would be a suggestion). - The definition is layout only.
YamlUidlLoaderuses the entry'sdefinitioninstead of thespecs/ui/<route>.yamlconvention, and the entry'sviewModelwhen the YAML declares none — so a shared definition must NOT declaremodelView:, or it can only serve the class it names. - Static bundle: the authored table travels in
manifest.json(only the authored half — a class is what a bundle with no backend cannot use), and routes that exist only inroutes.yamlare exported too, including those with no view model (they render as a bare layout through the ordinary sync path — there is no client-side YAML renderer and none is needed). - Ports:
mateu_core/route_registry.pyandsrc/Mateu.Core/RouteRegistry.csmirror the model, matching, precedence and definition lookup; neither has a bundle exporter. Both acceptviewModelandview_model. User docs:doc/.../java-ui-definition/route-registry.md.
backend/shared/uidl/uidl-schema.json (the component catalog) and routes-schema.json (the route
registry) are generated from the records by UidlSchemaGenerator (uidl test sources) and pinned
by UidlSchemaTest, so adding a component or a RouteEntry field without regenerating fails the
build instead of shipping a schema that does not know about it. Both are published as the public
authoring contract (editors point YAML IntelliSense at the raw files on master).
mvn -pl shared/uidl test -Dtest=UidlSchemaTest -Duidl.schema.write=trueWhy generated: the hand-written schema was written in one commit and never updated while the catalog
nearly doubled — 53 of 116 components were unreachable from YAML, and it was also wrong (it
declared Badge.color as a $ref to BadgeColor when the record has it as String). The generator
covers BOTH polymorphic families: Component, and the UIDL interfaces used as field types
(Actionable/UserTrigger → Menu, RouteLink, RemoteMenu…). EXCLUDED (wire plumbing:
ServerSideComponent, ModelViewComponent, PageView) is itself guarded by a test, so widening the
authoring surface has to be a reviewed change.
Each renderer in frontend/web/monorepo/apps/<name>/ is a standalone Vite app. Its copy npm script builds and copies artifacts directly into the matching backend/shared/frontend/<name>-lit/src/main/resources folder so they are served as static assets.
The shared lib (libs/mateu) contains: MateuApiClient, SSE support, mateu-ux (root web component), mateu-dialog, mateu-grid, mateu-choice, and base infrastructure. Renderers import from mateu workspace package and provide renderer-specific web-components for each DTO type.
Brought to full parity with Micronaut/Quarkus and pinned by the shared e2e suite (e2e/sut/apps/helidon-app1, port 8086, a Playwright project running the same **/shared/** specs; 252/252 green). The adapter (backend/helidon-mp/helidon-mp-core + annotation-processor-helidon-mp) now supplies ALL the CDI wiring, so a consumer app needs only its @UI classes + a META-INF/beans.xml — NO Mateu glue (exactly like a Quarkus/Micronaut app). Non-obvious gotchas discovered building it (all of them silent failures — the server boots either way):
- JSON-B drops the wire discriminators. Helidon MP defaults to JSON-B (Yasson), which ignores Jackson
@JsonTypeInfo→ the"type"field is missing and the frontend renders empty components. Fix:MateuObjectMapperProvider(a@Provider ContextResolver<ObjectMapper>) forces Jackson, AND the app must havejersey-media-json-jacksonon the runtime classpath (the ContextResolver is inert without Jackson'sMessageBodyWriter). MateuServiceisn't a CDI bean by itself.DefaultMateuService(framework-neutral core) carries onlyjakarta.injectmetadata, which Weld'sannotateddiscovery does NOT treat as a bean → the generated controllers'@Inject MateuServicewould be unsatisfied.HelidonMateuService(@ApplicationScoped @Specializes DefaultMateuService, in the adapter jar — a bean archive) supplies it. It lives in the ADAPTER, not the app.- Parameterized-bean lookup.
HelidonMPBeanProvider.getBeans(ComponentAdapter.class)viaCDI.select(rawClass)returns nothing for a parameterized bean (ComponentAdapter<Foo>) → the ComponentAdapter SPI silently falls back to reflectively rendering the POJO. Fix: resolve viaBeanManager.getBeans(Object.class, @Any)+ filter by raw type +getReference(bean, Object.class, …)— the same approach asQuarkusBeanProvider(plain CDI SPI, works verbatim on Weld). - Lazy static facade. CDI beans are lazy;
DefaultInstanceFactoryinitializes the staticMateuInstanceFactoryfacade in its constructor, but nothing on the request path injects it → the FIRST filtered CRUD search throws "MateuInstanceFactory has not been initialized" (later ones work).HelidonCDIProducerobserves@Initialized(ApplicationScoped.class)and touches the bean at startup (the CDI equivalent of Quarkus'StartupEventinit). - Generated controllers need a bean archive. Without
META-INF/beans.xmlin the app, Jersey does not register the AP-generated@Path/@RequestScopedcontrollers → every route 404s (JAX-RS "Endpoint not found"). The generatedRouteResolveralso needs@ApplicationScoped(not just@Named) or Weld won't discover it → "Not found". - Static assets shadow routes. The frontend jar bundles assets at
/static/assets/*; serve them at/assetsONLY (server.static.classpath.context=/assets,location=/static/assets) — a context of/makes the terminal static handler shadow every JAX-RS route. - Jar name. The Helidon parent pom's
finalNameis the artifactId WITHOUT the version → the runnable artifact ishelidon-app1.jar, nothelidon-app1-1.0.0-SNAPSHOT.jar(a wrong reference in the CI start step madejava -jarfail silently and the unbounded readiness loop hung the runner — that wait step is now bounded to 3 min/port + dumps SUT logs on failure).
The AP templates (controller.ftl returns UIIncrementDto via .blockFirst() over JAX-RS @Context HttpHeaders/UriInfo; route.ftl emits @ApplicationScoped) are otherwise close copies of the Quarkus templates. User setup docs: doc/.../java-create-your-project/helidon.md.
backend/shared/core/src/test/.../testutil/TestMateu.java boots the ENTIRE core bean graph in-JVM (Spring test context understands the framework's jakarta.inject annotations), registers fixture classes exactly like the annotation processor's generated RoutedClassProviders, provides the platform beans adapters normally contribute (BeanProvider→MateuBeanProvider, ObjectMapper, "baseUrl" request attribute; extra beans via withUisAndBeans, e.g. fake Excel/Pdf exporters), and calls the same MateuService entry point the generated controllers call. One mateu.sync("/route") exercises route resolution → instance creation → reflective mapping → wire DTOs; mateu.run(RunActionRqDto...) drives any action. Feature suites live in core/src/test/.../application/*SyncTest.java (fields, layout, app/menu, crud lifecycle, wizards, archetypes, actions/commands/triggers, editable grid fields, validation, nested state). Wire-shape gotchas the suites document: fragment state lives on UIFragmentDto (not the component); grid columns nest inside CrudlDto metadata; Card title/content and form fields nest inside METADATA records (walkers must descend reflectively); AccordionLayoutDto.panels is empty on the wire (panels are children); filtered/sorted listing rows come back as maps; in-JVM the search Data still carries the typed ListingData. Coverage: plain mvn verify on backend/shared/core now runs JaCoCo end to end (agent → report at target/site/jacoco/ → check gate: BUNDLE line coverage ≥ ${jacoco.min.bundle.coverage}, set to 70% in core's pom — ratchet it up as suites grow; other modules default to 0%). Core's surefire argLine composes via @{argLine} — don't overwrite it or JaCoCo silently records nothing. Exclusions (backend/pom.xml jacoco config): DefaultMateuHttpClient and RemoteMenuHandler — thin wrappers over live HTTP that cannot run in-JVM (federated menus are fetched by the frontend in normal operation); they're exercised by the e2e suites. Repo-root lombok.config sets addLombokGeneratedAnnotation so lombok-generated members don't count. Line coverage as of 2026-07-05: 77.8% measured / gate at 75% (was 11%). The residual to 80% is dominated by catch/defensive branches and sub-20-line variant tails across mappers/converters — ratchet the gate as suites grow rather than writing noise tests against exception handlers.
design/figma/contract.json is the single source of truth of the Figma ⇄ Mateu mapping (64 components + 10 page templates, full catalog): Figma component names Mateu/<Category>/<Name>, variant axes named after the Mateu annotation/record params, #config text-layer convention for non-visual props (fieldId=email; actionId=save), text-layer → param mapping (headings map to title, the modux node field — NOT the Java param name), per-language construct notes, and a declarative sketch the plugin draws. Page templates (2026-07-19): the Mateu/Page Templates/* category carries full-page entries (Smart Search, To-do List, Calendar, Dashboard, Welcome, Hero Search, Collection Detail, General Overview, Item Overview, Foldout) with kinds like smartSearchPage/todoList/calendarPage and the pageWidth variant axis (fixed/fullWidth/edgeToEdge — the first RDS template parameter) on all of them; the modux importer + codegen need those kinds registered when the mirrors sync. design/figma/plugin (TS + esbuild, npm run build) is a data-driven Figma plugin that BUILDS the library from the contract when run inside Figma (one page per category, component sets from variant axes) — regenerate + republish when the catalog grows; verify with tsc only (Figma can't run headless). The reverse path lives in modux (model-driven-generator, application/usecases/project/importfigma/): Figma REST JSON → contract → PageEntity + UiComponentNodeEntity trees (params map added; kinds extended). Import conventions: top-level frames = pages, instance internals are chrome (texts harvested, never children), containers absorb the SIBLINGS that follow them (the @Section semantics), Mateu · * canvases are skipped. The contract is PUBLISHED (2026-08-12): design/figma/contract.json is packaged into the io.mateu:uidl jar at META-INF/mateu/contract.json (a build-time resource copy in uidl's pom, NOT a checked-in duplicate — that would just be a fourth mirror), pinned by FigmaContractPackagedTest. Consumers should READ IT FROM THE ARTIFACT instead of keeping a copy. The hand-kept mirrors in modux (model-driven-generator and figma-maven-plugin, both src/main/resources/figma/mateu-contract.json) had already drifted 15 components behind — the entire Page Templates category — so modux's importer/codegen cannot handle those frames at all; migrating them to the dependency is the pending cross-repo half. Build-time codegen (2026-07-15): modux's figma-maven-plugin (goal figma:generate, GENERATE_SOURCES) scans src/main/figma/*.json (downloaded GET /v1/files/:key payloads) and emits one view class per designed frame in java/csharp/python under target/generated-sources/figma (java joins the build via addCompileSourceRoot); fields/sections/notices/texts/bullets/separators/buttons come out ready, display components and wizard/crud frames as TODO skeletons. It embeds its own light reader (FigmaScreenReader, same conventions as the importer) + a THIRD contract mirror in its resources — sync all mirrors when contract.json changes. PENDING (fase 3): modux generation templates consuming the imported kinds (full-model path) + deeper emitters (wizard steps, crud wiring). User docs: doc/.../design-systems/figma.md (includes the end-to-end flow + maven plugin usage).
Releases follow the pattern Mateu v3.0-alpha.N. To cut a new one:
- Check the latest release number:
gh release list --limit 5
- Create the next release (increment N by 1):
gh release create v3.0-alpha.N --title "Mateu v3.0-alpha.N" --notes "- summary of changes"
GitHub Actions handle the rest (build, publish to Maven Central, etc.) automatically once the tag is pushed.
# Build entire backend (from backend/)
cd backend && mvn clean install -DskipTests
# Build a single module
cd backend/shared/core && mvn clean install
# Run a demo app
cd demo/demo-vaadin-mvc && mvn spring-boot:run
# Run tests for a module
cd backend/shared/core && mvn test
# Run a single test class
cd backend/shared/core && mvn test -Dtest=YamlUidlLoaderTestUse the settings.xml at repo root when you need to point to a custom Maven repo:
mvn -s settings.xml clean install# Install dependencies (first time or after package.json changes)
npm install # or yarn
# Dev server for a renderer
cd apps/vaadin && yarn dev # http://localhost:5173
# Build a renderer and copy assets into backend
cd apps/vaadin && yarn copy # builds + copies to backend/shared/frontend/vaadin-lit/...
# Unit tests of the shared lib (vitest; pure logic: weightEngine, interpolation, shortcuts, dirtyGuard…)
yarn test # from monorepo root (delegates to libs/mateu) or cd libs/mateu && yarn testnpm install # once, run INSIDE the dir; downloads Oracle's grunt tooling
npm run bridge # regenerate webApps/.../resources/js/mateu-bridge.js from poc/
npm test # 32 contract tests of the reducer (poc/test.mjs)
npm run build # grunt vb-build → build/optimized (exit code unreliable — check the artifact)
npm run serve # grunt vb-serve --port=9006 against demo-vb (:9005), CORS already open
npm run copy # package build/optimized into backend/shared/frontend/redwood (then mvn install)# 1. Build UI module
cd e2e/sut/modules/sample1 && mvn clean install
# 2. Start the test app (keep running)
cd e2e/sut/apps/mvc-app1 && mvn spring-boot:run
# 3. Run Playwright tests
cd e2e && npm test
npm run test:headed # with browser visible
npm run test:ui # interactive UI mode
npm run report # open last HTML reportScreenshots for the documentation in doc/src/content/docs/ are generated programmatically:
- Write the example Java class in the appropriate SUT module (usually
e2e/sut/modules/sample1/for pure UI classes, ore2e/sut/apps/mvc-app1/src/.../app/for CRUD classes that needAutoCrud). - Build the changed modules:
cd e2e/sut/modules/sample1 && mvn clean install cd e2e/sut/apps/mvc-app1 && mvn clean install -DskipTests
- Start the MVC app (keep running in background):
cd e2e/sut/apps/mvc-app1 && mvn spring-boot:run
- Take screenshots using the Playwright one-shot script (from
e2e/):# Full-page screenshot of a route node screenshot.mjs --url http://localhost:8080/<route> --output ../doc/public/images/docs/<topic>/<name>.png # Screenshot of a specific element only node screenshot.mjs --url http://localhost:8080/<route> --output ../doc/public/images/docs/<topic>/<name>.png --element vaadin-form-layout # Custom viewport node screenshot.mjs --url http://localhost:8080/<route> --output ../doc/public/images/docs/<topic>/<name>.png --width 1440 --height 900
- Reference the images in the doc Markdown as
/images/docs/<topic>/<name>.png.
The script waits for mateu-page to appear, then adds a 1.5 s settle delay so web components finish rendering before the screenshot is taken. Options: --wait-for, --element, --width, --height, --full-page, --settle, --timeout.
This approach also validates the documentation code examples — if a class does not compile or does not produce the expected UI, the screenshot will be blank or missing.
Every Mateu app exposes the same REST contract regardless of framework:
POST /{baseUrl}/mateu/v3/components/_/action
Body: { "route": "...", "actionId": "__load__" | "<methodName>", "componentState": {...} }
Response is UIIncrementDto (see backend/shared/dtos).
@UI("/path")on a class registers it as a routed view; no path means root.AutoCrud<T>/AutoCrudOrchestrator<T>give full CRUD with minimal code.CrudStore<T>is the data-access port; implement it inline or as a Spring@Serviceand return it fromstore()in yourAutoCrud<T>subclass. Renamed 2026-07-18 fromCrudRepository/repository()(a data-access adapter is not a domain-aggregate repository).store()is nowabstractand therepository()fallback method was removed (2026-07-23) — everyAutoCrud/FilteredAutoCrudsubclass MUST overridestore(); the oldresolveStore()fallback is gone. The deprecatedCrudRepository/CompositionCrudRepositoryinterface aliases were deleted 2026-07-29 (86 straggler type references migrated toCrudStore/CompositionCrudStore); therepository()method name was already gone — all overrides usestore().CrudStore.find(String searchText, T filters, Pageable pageable)→Page<T>: the single search+filter+sort+paginate entry pointAutoCruduses to fill the listing. It is adefaultmethod (so no existing implementer breaks): the default filtersfindAll()bysearchText(viaSearchableText.searchableText()/toString(); WORD-based: every whitespace-separated word must be contained, case-insensitive, any order), then by the field-level filters — a filter field counts as SET only when its value differs from a freshly-constructed instance of the filters class (no-arg constructor, or the canonical constructor fed null/zero/false for records), because the filters object is hydrated from the component state and untouched fields keep their initializers/primitive defaults; strings match by case-insensitive containment, other basics (numbers, booleans, enums, chars) by equality (flip side: filtering BY a default value needs an overriddenfind) — then sorts bypageable.sort()(read reflectively via getter/record-accessor/field byprivate statichelpers inCrudStore—uidlhas no property reader), then paginates in memory. Range/multi-select filters (criteria): the example object can't say "between" or "in", so those conditions travel asFilterCriterion(field, FilterOperator between|gte|lte|in, values)(uidl.data), built byFilterCriteriaBuilder(core, crud package) from the component state — range bounds in<field>_from/<field>_tokeys, multi-select values as a list (or comma-joined string after URL restore) — with the values coerced viaTypeCoercionHelper; the consumed keys are STRIPPED from the state before hydrating the example (a list into an enum field would break it; a single bare enum string intentionally stays → example equality, old-client compatible). Applies only on the AutoCrud path (getFilters(..., crudFilterSemantics=true)fromListRouteResolver): temporal fields (LocalDate/LocalDateTime/LocalTime) → stereotypedateRangeBY DEFAULT,@RangeFilter-annotated numerics →numberRange, enums →multiSelect; declarative Listings keep single-value widgets (their custom Filters hydration wouldn't survive the new wire shapes). Criteria flow:SearchActionHandler(builds theSearchRequest, criteria included) →Crud.search(SearchRequest, HttpRequest)→FilteredAutoCrud.fetchRows/5→CrudStore.find/4(new default overload evaluating criteria in memory; called ONLY when criteria exist so 3-arg overrides keep working — DB repos override find/4 for DB-side ranges). TYPED filters on declarative Listings (layer 4): a customListing<Filters,Row>Filters class can declareDateRange/NumberRange(uidl.data records withcontains/isEmptyhelpers) orSet<SomeEnum>fields — they render range/multi widgets on ANY listing (explicit type = explicit ask, independent of crudFilterSemantics;PageListingBuilder.isTypedFilterbuilds the filter FormField DIRECTLY with id/label/stereotype/options because the standard mapper would turn a record field into a nested form, and the Collection exclusion + isBasic guard both special-case them); before hydrationFilterStateAssembler(uidl.interfaces, called fromSearchRequestBuilder— which servesListing/ReactiveListing— ANDSearchActionHandler) replaces the flat<field>_from/_tokeys and value lists/comma-joined strings with ready-made typed instances, which coercion passes through (TypeCoercionHelperexact-class match;FieldValueConvertergained anisInstancepass-through — it used to THROW pouring a LinkedHashSet into aSetfield); blank/unparseable bounds and stale enum constants are dropped rather than failing the search. Demo:/typed-filters(BookingsListing). Tests:TypedFiltersSyncTest.compareValuescompares Numbers numerically whatever their boxed type. Frontend: the listing filters render as a SMART SEARCH BAR (after the Redwood Smart Search pattern): one search field hosting the free-text keyword search (Enter commits it as a chip) and a "Filter by" panel (opened by clicking the bar or typing — NOT on focus, orautoFocusOnSearchTextwould pop it on page load) with a type-specific widget per filter (option list for selects, Yes/No for booleans, input+Apply for text/number, from–to inputs+Apply fordateRange/numberRange, checkable rows formultiSelect— toggles re-search but keep the panel open); applied conditions are chips with ✕ (a range chip clears BOTH_from/_tokeys), chip add/remove re-runs the search, "Clear filters" resets;mateu-table-crud._filterIdsexpands range filters to their_from/_tokeys so URL sync keeps working. Two implementations kept in sync: the sharedmateu-filter-bar(Vaadin/sapui5/redhat — a LitElement with Lumo-var styling; the wire contract withmateu-table-crudis untouched:value-changed/search-requested/filter-reset-requested;filtersLayoutandmainFilterare no longer consulted) and redwood-oj'srenderFilterBarhook (stateless — panel/draft state in a WeakMap keyed by the crud element, hand-styled divs). User docs:doc/.../ux-patterns/filters-and-listing.md. Override it to push everything to the database. .NET/Python parity (2026-07-08): both Cruds emit the samefiltersmetadata from the entity (CrudMetadataDto.Filters/CrudMetadata.filters, FormField entries with fieldId/dataType/label/stereotype/options; rules inReflectionMapper.MapCrudFilters/mapper.crud_filters: enum→multiSelect+options, DateOnly/DateTime resp. date/datetime→dateRange,[RangeFilter]/RangeFilter()numerics→numberRange) and APPLY the componentState values in-memory overFetch()in the search handlers (SyncHandler.MatchesFilters/_matches_filters+InRange/_in_range+MultiValues/_multi_values: presence-based — a filter applies when its key exists and is non-blank, no defaults-diff needed since the state only carries applied filters; strings contain, bools/numbers equality, enums IN over list or comma-joined,_from/_toat date granularity). No repository/criteria layer there (in-memory only, matrix says 🟡). Tests: 43 golden each (Bookingsfixtures). dataType mismatch fixed the same day: Java emitsbool, .NETboolean—isBooleanFilterin BOTH filter bars (sharedmateu-filter-bar+ redwoodrenderFilterBar) now accepts both (before the fix Java boolean filters silently rendered as text inputs, not Yes/No).Page<T>already carriestotalElements, so there is no separatecountmethod — a DB impl runs the count + page queries insidefind. Wired inFilteredAutoCrud.fetchRows(core), which now delegates tostore().find(searchText, (T) filters, pageable)instead of doing the in-memoryfindAll().stream().filter().subList()itself; overridefetchRows(...)on theAutoCrudsubclass only when you need theHttpRequest. Types live inio.mateu.uidl.data:Page,Pageable(page,size,List<Sort>),Sort(field,Direction),Direction{ascending,descending}.- CRUD create/edit in a drawer (
editInDrawer()): override thisCruddefault (false) so New/row clicks open the crud form in aDrawerover the listing (Redwood "Create and Edit - Drawer" template) instead of navigating to/new—/{id}/edit. Mechanics:NewActionHandler/EditActionHandlerreturn aDrawer(built byCrudDrawerBuilder:CrudFormComponentBuilder.build— made public — as content,FormViewModel.toMap(entity)asinitialDataso fields arrive populated; the routed pages get their state via a State fragment, the drawer via initialData); in drawer modeview→edit drawer (no view page; readOnly keeps navigating),cancel-new/cancel-edit→[closeModal, markAsClean], and save/create (PersistActionHandler) responds ONLY[message, markAsClean, closeModal(Crud.SAVED_IN_DRAWER_EVENT)]— no re-render/navigation, because re-rendering the host would kill the drawer's owner before the close command runs; the listing refreshes becauseCrudTriggersBuildersubscribes it to that event →search. Frontend fixes that made it work:ConnectedElement.closeModalqueries(shadowRoot ?? this)(light-DOM shells like redwood-oj have no shadow root — the overlay was never found and a husk blocked the page) and splices the overlay out ofcomponent.children(an Add-fragment overlay would be resurrected by the next re-render).editDrawerWidth()(36rem) also overridable. Demo/drawer-crud-demo; testsEditInDrawerSyncTest; doc ux-patterns/drawer.md. Full parity (2026-07-17): .NETCrud<T>.EditInDrawervirtual / Python@edit_in_drawerclass decorator — new/view/edit answer the form in a Drawer (Add fragment), save answersCloseModal(saved event)+RunAction search(the ports have no trigger subscription on listings, so the refresh rides a RunAction command instead of the Java event subscription); RN's controller already closed the top overlay AND dispatched the named event (verified withscripts/drawer-probe.tsagainst a live backend). RAIL: .NET[WizardProgress("rail")]/ Python@wizard_progress("rail")compose the same two-column rail;ProgressStepscarriesverticalin all three backends; RN's stepper is vertical by design; IntelliJ'srenderProgressStepshonors the flag. Ports closed the archetype gap (2026-07-17, later same day): both ports gained a fluentFormFieldprimitive (.NETFormField : ComponentBasemapped in ComponentMapper; Pythonfluent.FormField+ fluentVerticalLayout/HorizontalLayoutwhich Python lacked) →FormFieldMetadataDto/FormFieldMetadata, bound to componentState by fieldId; on it,CollectionDetail<TRow>/GeneralOverview<TRow>(.NET Archetypes.cs) andCollectionDetail/GeneralOverview(Python mateu_uidl) mirror the Java orchestrators. Runtime pieces: tree-supplier views seed scalar properties intoinitialData(state round-trip) and anIRefreshOnChange(.NET) /__mateu_refresh_action__(Python) marker emits the{type:"AutoSave", actionId, debounceMillis}trigger the shared frontend already honors; SyncHandler handlesselectCollectionItem/filterCollection/switchRecordby mutating bound state and re-rendering (no navigation). Tests:ArchetypeTests(.NET),test_archetypes_collection_overview.py(Python). - CRUD button labels: override any of
newLabel(),saveLabel(),cancelLabel(),deleteLabel(),editLabel(),addAnotherLabel(),backToListLabel(),importLabel(),historyLabel()in anAutoCrudsubclass to replace the corresponding built-in English label. The default values are the method names in plain English (e.g.newLabel()→"New"). Implemented inCrud.java; consumed byListRouteResolver,CrudFormComponentBuilder, andViewToolbarBuilder. - Bean validation annotations (
@NotNull,@NotEmpty,@Min,@Max) drive client-side and server-side validation automatically. Identifiableinterface on record/entity marks the ID field for CRUD.HttpRequestcan be added to any method signature; Mateu injects it automatically.- i18n: implement
Translatoror rely on the defaultDefaultTranslator. - The
uidlmodule is the only dependency needed for writing@UIclasses in a framework-agnostic module. - Multi-column zone layouts (
@Zones/@Zone): annotate a form class with@Zones({@Zone(name="left", width="64%"), @Zone(name="right", width="36%")})to lay sections out side by side. Each@Section(zone="left")is assigned to the matching zone column; a section pointing at an unrecognised (non-blank) zone falls into a trailing flexible column. A section with no zone (blankzone()) is a full-width band, stacked in declaration order and interleaved with the zoned rows — so[header (no zone), left, right]renders the header full-width across the top and the two zones as columns below it (a maximal run of consecutive zoned sections collapses into one side-by-side row at its position).SectionFormRenderer.renderZones/flushZonedRowhandle this; when every section is zoned the single row is emitted directly (unchanged wire shape). Responsive + right-edge alignment (2026-07-15): the zoned row iswrap(true)and each zone column isflex: 1 1 calc(<width> - var(--lumo-space-m, 1rem)); min-width: min(20rem, 100%);— the basis subtracts the spacing gap because withflex-wrapline breaks are computed from the HYPOTHETICAL (basis) sizes, so a plain62% + 38% + gap > 100%wrapped immediately (and without wrap it overflowed past the full-width bands' right edge, the oldflex: 0 0 62%bug); the min-width is the wrap point — a column squeezed under 20rem drops below the previous one and grows to the full row. Mirrored in .NETBuildZones/ Pythonbuild_zones(HorizontalLayoutMetadataDto.Wrap/HorizontalLayoutMetadata.wrapadded); React Native'sLayoutRendererhonors the wire'smin-width: min(20rem…)marker on zone columns (minWidth 300 → stacks at phone widths). Tests:LayoutSyncTest.zoneWidthsBecomeFlexBasisStyles, .NET/PythonZones_lay_sections_out...updated. A blank/whitespace section title emits no heading, so an untitled band or column leaves no empty header line. Section grouping compares EVERY@Sectionattribute (2026-07-15): a new section starts when the annotation's value OR zone OR columns/style/sticky/propertyList/frameless changes (FormSectionGrouper.sameSection; mirrored in .NETSameSection/ Python dataclass equality) — so two consecutive untitled ("") sections pointing at different zones stay separate (they used to merge, emptying the second zone); the" "-vs-""trick is only needed when ALL attributes are identical. Zones are incompatible with@FoldedLayout(zones take precedence). Tests:LayoutSyncTest(sectionWithoutZoneRendersAsFullWidthBandAboveTheZonedRow); demo:demo-front-officeIdentidadStep(arrival header band + contact/preferences columns). - Section column count (
@Section(columns=N), fixed 2026-07-27): the annotation default is now0= unset (was1, which made an explicitcolumns = 1indistinguishable from unset —SectionFormRenderer.sectionColumnsonly honored> 1and silently fell back to the form default). Any explicit N ≥ 1 now wins, socolumns = 1forces single-column stacking on a form whose other sections stay multi-column; unset (0) inherits@FormLayout(columns=…)on the class, else 2.FormSectionGrouper.sameSectionstill compares rawcolumns()(unset carries the 0 sentinel intoSectionFields). Java-only — the .NET/PythonSectionmirrors never hadcolumns. Tests:LayoutSyncTest.sectionColumnsHonorsAnExplicitSingleColumn,FormSectionGrouperTest. NOTE: pre-existing@Section(..., columns = 1)declarations (demo-admin-panel check-in screens) now really render single-column — that was their declared intent, but the old rendering was the class default. - Sticky sections index (
@Toc) + pinned sections (@Section(sticky=true)): for long "docs-style" pages with many sections stacked vertically, annotate the page class with@Tocto show a sticky right-hand index (table of contents) listing every section title; clicking an entry smooth-scrolls to that section and the active entry highlights as the user scrolls (scrollspy).@Tocis tri-state: absent → auto (the frontend shows the index only when there are > 4 section cards stacked vertically and the form is not a@Zones/@FoldedLayouthorizontal layout),@Toc/@Toc(true)→ force on,@Toc(false)→ suppress. Independently, mark any@Section(sticky=true)so its card is pinned (position: sticky) and never leaves the viewport while the rest scrolls (e.g. a guests list on a check-in screen). In docs mode the page header is also pinned (themateu-content-headergets asticky-headerclass,top:0), and multiple sticky sections stack directly under it without overlapping:mateu-page._layoutStickyTops()measures the header height (published as the--mateu-header-hCSS var, also used for the index'stop) and sets each sticky card'stopto the header height plus the cumulative height of the sticky cards declared before it, with a small gap between each so stacked pinned cards never touch (recomputed on resize). Pipeline:@Toc→PageMetadataExtractor.getToc(a nullableBoolean, read viaMetaAnnotations) →PageView.toc→PageMapper→PageDto.toc→ frontendForm.toc;@Section.sticky()→SectionFormRendereradds themateu-sectionmarker class to every section card (plusmateu-section--sticky+position: stickystyle when sticky) — note the reflective@Sectionpath emits section cards as fluentCards (→CardMapper→cardRenderer.ts), notformSectionRenderer.ts, andCardMapperhardcodes the card id, so the index anchors by DOM element reference + the marker class, not by a server id. Frontend lives entirely inmateu-page.ts: it enumeratesvaadin-card.mateu-sectionacross the slotted subtree, reads each card's title ([slot="title"]or the firsth1..h6heading), lays the body out as a 2-column grid (content + sticky<aside class="page-toc">), and runs a scroll-listener scrollspy on the nearest scrollable ancestor (the app'svaadin-scroller). The active entry is the section occupying the reading line just below the pinned region — the scrollspy measures the pinned region bottom straight from the rendered rects (pinned header + whichever sticky cards are butted up against it) so a section hidden behind a pinned sticky is never marked active, and a pinned sticky section itself is highlighted while it's the one in view. Clicking an index entry scrolls with an offset (viascrollBy, notscrollIntoView) so the target lands just below the pinned region (header + any sticky section above it), not hidden behind it; the scrollspy uses the same pinned-region bottom (plus the same landing gap) so the highlighted entry stays consistent with click-to-scroll. After a click the scrollspy is locked to the clicked entry until the next manual scroll gesture (wheel/touchstart/keydown), so a section near the bottom that can't scroll all the way up to the reading line still stays highlighted. Keyboard shortcuts: while the index is shown,Ctrl+Alt+1..9jump to the first nine sections (same as clicking the entry, and it locks the highlight the same way); the shortcut number is shown as a faint badge on each index entry (matched viae.code— bothDigit1..9and the numeric keypadNumpad1..9— so it's layout- and NumLock-independent). This is on by default — no annotation attribute needed. Demo:demo-admin-panel/.../checkin/CheckInFormV2.java(/checkin/:id/v2) — every section stacked vertically with the Huéspedes section pinned; the client-info tabs are split into their own top-level sections (CardexSection,CompanyDataSection,CardDataSection,ClientHistorySection,PreferencesSection) so all of it is visible and indexed instead of hidden behind tabs (note:@Tab→@Sectiononly works at the top level — a nested@Inlinetype's fields are grouped into tabs, not sub-sections). Contrast with v1/v3 which stay@Zonesmaster-detail with the shared tabbedClientInfoSection. - Capability listings (
Listing<Row>+ interfaces de capacidad, 2026-07-29) — the listing/CRUD surface is ADDITIVE by declaration: a listing is a class implementingio.mateu.uidl.interfaces.Listing<Row>with the singleListingData<Row> search(SearchRequest, HttpRequest)(SearchRequest= uidl.data recordsearchText/filters/criteria/pageable— new inputs become fields, never overloads;SearchRequestBuilderbuilds it accepting sort keysfieldIdANDfield). Input capabilities are DECLARED:Searchable(marker → search box; without it searchText arrives empty — was hardcoded-on before) andFilterable<F>(filters type → filter bar;filters(request)typed accessor). Interaction capabilities are IMPLEMENTED:Navigable<Detail,Id>(view(id)→ clickable rows +/:id),Editable<Editor,Id>(edit(id)+save(); WITHOUT Navigable the editor opens in a drawer over the listing — the "editable listing" idiom, reusingeditInDrawer),Creatable<Form,Id>(creationForm()+create()→ New +/new),Deletable<Id>(deleteAllById(ids)→ selection + Delete). A plain@UIPOJO with interaction capabilities is promoted to the CRUD mediator by theCapabilityCrudbridge (hooked inRunActionUseCaseafter instance creation, pattern AdaptedComponentTree; advertises the LISTING class as serverSideType via theMultiView.serverSideTypeName()hook so round-trips re-bridge;metadataSource()/stateSource()/behaviourSource()hooks delegate class annotations, state and METHOD-based behaviour — @ListToolbarButton/@Toolbar/@ViewToolbarButton bulk methods, UploadEnabled/Auditable, row/view method invocation — to the target) serving ONLY the declared routes/buttons (gates:canView/canEdit/canCreate/canDeleteconsulted byListRouteResolverand the New/Edit route resolvers; all default true onCrud— the full pack — and are narrowed by the bridge or subtracted via@Not*).Crud<View,Editor,CreationForm,Filters,Row,IdType>IS aListingimplementing all the capabilities (itssaveNewis nowcreate,savereturnsIdType,getIdFieldForRowhas a default;MultiView.supportsActionmaterializes the broadActionHandlerdefault so orchestrators keep claiming delete/bulk/action-on-row-* overListing's narrow "search");AutoCrud/FilteredAutoCrudunchanged outside (entity +store(), subtractive@Not*). RENAMES:ListingBackend→Listing(absorbed the deleted declarative base classcore.infra.declarative.Listing: @Toolbar action advertising, Selector select-glue inhandleActionOnRowdefault +ListingRowActionRunner(core) for reflective row-method invocation — excluded for RouteHandlers; export flags; Selectors now carry their ownfieldId/withFieldId),ReactiveListingBackend→ReactiveListing<Row>, entity interfaceSearchable→SearchableText(freed the name for the capability; the@SearchableANNOTATION is a third, unrelated thing — lookup fields). The legacyDeleteableview-class marker was DELETED same day (zero implementers; custom Cruds now show the Delete button by default — subtract with@NotDeletable). Tests:CapabilityListingSyncTest(7 combinations pinned). .NET/Python parity (same day): .NETIListing<TRow>+SearchRequest+ISearchable/IFilterable<F>/INavigable/IEditable/ICreatable/IDeletable(Capabilities.cs; capability methods take TYPED objects, not HttpRequest — the port's idiom;CapabilityProfilein CapabilityCrud.cs;Crud<T>implements all 7 with virtualCanView/CanEdit/CanCreate/CanDelete; wire addsCrudMetadataDto.RowsSelectionEnabled+GridColumnMetaDto.ActionId; tests CapabilityListingTests) / PythonListing[R]+SearchRequest+mixinsSearchable/Filterable[F]/Navigable/Editable/Creatable/Deletable(mateu_uidl;Searchableis DUAL-ROLE — listing capability AND selector-field marker; real generic resolver_resolved_generic_argsin mapper;resolve_listing/handle_listingin sync_handler; wireCrudMetadata.rows_selection_enabled+GridColumnMeta.action_id; tests test_capability_listing.py). Both mirror the editable-sin-Navigable→drawer contract (save → CloseModal(SAVED_IN_DRAWER)+RunAction search);criteriaalways empty in the ports (no criteria layer, matrix 🟡);ListingData(rows, totalElements?)= pushdown when totalElements set, in-memory sort+page otherwise. Docs:java-user-manual/build/capability-listings.md. Listing.gridLayout(): override this default method to force a specific grid layout instead of letting the renderer auto-select. Values:GridLayout.auto(default),table,list,cards,masterDetail,tree(hierarchical rows carrying a self-referentialchildrenlist; never auto-selected).- Grid column widths (
@ColumnWidth): annotate a grid row field with@ColumnWidth("9rem")for a fixed-width column (rendered withflex-grow:0), or@ColumnWidth("auto")to have the column size to its content (header + widest cell) so nothing truncates — this adapts to the current density, which is why it's the right choice for a grid shown in both compact and non-compact screens (a fixed3remtruncates to"A."/"H."once non-compact padding eats the width). No@ColumnWidth→ the column keeps the defaultflex-growand shares the remaining space. Handled inGridColumnBuilder(auto→GridColumn.autoWidth(true)+ null width +flex-grow:0); theautoWidth/width/flexGrowfields flow throughGridColumnMapperto thevaadin-grid-columninrenderColumn.ts. Cells always ellipsis-truncate (renderColumn.tscolumnRenderer), so column width is the only lever against truncation. - Wizards: extend
Wizardand declare fields implementingWizardStepfor each step. The penultimate step shows the@WizardCompletionActionbutton; the last step is a read-only result screen shown after the action executes. The last step is instantiated automatically if null (preserving field defaults), or the wizard can set it explicitly inside the completion method. The progress bar starts at 0 and shows 100% on the result step. No navigation buttons are shown on the result step. The wizard title is derived viagetTitle()(respects@Title,TitleSupplier, or falls back to the class name). Branching: overridestepApplies(String stepFieldName)(default true) to skip steps conditionally based on earlier answers — evaluated on every render/navigation; skipped steps are jumped in both directions (Wizard.nextApplicable/previousApplicable, used byWizardActionDispatcherandWizardButtonBuilder), excluded from the progress bar (applicableSteps/applicablePosition) and from the ACCORDION/ACCUMULATIVE recaps; the result step always applies, and when the penultimate step is skipped the@WizardCompletionActionbutton moves to the last applicable step. Also:WizardStateSerializer.toMaptreats enum-typed fields as basic values (they used to crash state building with a Jackson MismatchedInputException). Progress style (@WizardProgress, 2026-07-15): the wizard's progress indicator defaults to the classicProgressBar; annotate the wizard with@WizardProgress(WizardProgressStyle.STEPS)to show connected step bullets instead (the existingProgressStepscomponent — one dot per applicable non-result step with done/current/upcoming states, all done on the result step; skipped branching steps are excluded), orWizardProgressStyle.RAIL(2026-07-17, the Redwood "Guided Process" template) for a sticky right-hand rail: bigcurrent | totalcounter (non-result steps only) over the VERTICAL step list —Wizard.component()wraps the form in a two-column HorizontalLayout (progressRail(), flex 0 0 15rem + border-left) andProgressStepsgained averticalflag (record +ProgressStepsDto.vertical+:host([vertical])styles inmateu-progress-steps; ports/RN ignore the flag gracefully). Demo:/branching-wizard(RAIL + branching). Implemented inWizard.progressIndicator(); no new wire types, so every renderer that showsProgressStepsrenders it. Ports: .NET[WizardProgress("steps")]/ Python@wizard_progress("steps")(their wizards' numbered step groups emit "Step N" bullets). Demo:demo-front-officeCheckInWizard. Tests:WizardProgressStyleSyncTest, .NET/Python inSectionFeatureTests/test_section_features.py. Cross-step state survives (fixed 2026-07-14): values set in ANY step persist across navigation in both directions and reach the completion action — the nested per-step state maps used to carry the raw Jackson husk (with{}entries for holder fields), so the whole-stepfromMapfailed silently and NON-CURRENT steps reset to their field initializers on every action;WizardStateSerializer.toMapnow replaces each non-basic field's husk with the cleaned nested map it already computes for flattening (FormViewSerializergot the symmetric fix for nested POJOs in regular forms). No mirror-field workarounds needed. Tests:WizardCrossStepStateSyncTest(JSON round-trips the state between requests AND integerizes whole doubles exactly like the JS client). Demo:demo-admin-panel/.../wizards/BranchingSignupWizard.java(/branching-wizard); unit tests inWizardBranchingTest. - State coercion & holder fields (fixed 2026-07-14, front-office dogfooding): (1) Numeric widening — the JS client integerizes whole doubles (a
Doublefield's343.0comes back as343);FieldValueConverter(write path) andTypeCoercionHelper(read path) now widen anyNumberinto the target numeric field type (Double/double/Float/float/BigDecimal/Long/Integer); before, the conversion threw,Hydraterswallowed it and the field silently reset to its initializer. The .NET/Python ports already coerced (System.Text.JsonGetDouble(),float(raw)) — pinned with tests. Tests:NestedStateSyncTest. (2) Component-holder fields stay out of the state — fields typedCallable/Supplier/Runnable/io.mateu.uidl.fluent.Componentused to serialize as{}husks and null out on rehydration (NPE on re-render) unless@JsonIgnore'd;HolderFieldChecker.isNonDataHolder(coreinfra/reflection/read/) is now applied symmetrically byFormViewSerializer/WizardStateSerializer(drop on write) andHydrater(skip on read, initializers survive), so@JsonIgnoreis unnecessary (still honored). Tests:HolderFieldsSyncTest. (3)AllEditableFieldsProvidernowlog.warns once per final field dropped from an editable form (they are silently excluded byisNotInjected— the warning makes it discoverable). - Dashboards (
Dashboardarchetype): extendDashboard(coreorchestrators/dashboard/) and declare component-holding fields — consecutiveMetricCardfields group into a full-widthScoreboardKPI band; component fields annotated@Panel(title, subtitle, colSpan, rowSpan)become titled tiles on a responsive CSS grid; other component fields land on the grid as-is; overridecolumns()to fix the column count (0 = auto-fit).MetricCard.actionIdmakes the tile clickable — the frontend dispatches the standardaction-requested, so an@Actionmethod with that name runs (drill-in navigation). Everything is also usable fluently:DashboardLayout/DashboardPanel/Scoreboard/MetricCardare UIDL data records (mapped byDashboardLayoutMapper/DashboardPanelMapperinLayoutComponentDispatcher,MetricCardMapper/ScoreboardMapperinDisplayComponentDispatcher; wire DTOsDashboardLayoutDtoetc.; frontenddashboardRenderer.ts— design-system-neutral divs + Lumo CSS vars with fallbacks, so sapui5/redwood-oj claim support in their SUPPORTED_TYPES). Demo:demo-admin-panel/.../dashboard/SalesDashboard.java(/dashboard-demo). User docs:doc/.../ux-patterns/dashboard.md. - Collection-detail pages (
CollectionDetail<Row>archetype, 2026-07-17): the Redwood "Collection Detail" template — searchable card list left (TaskQueue;rows/idOf/titleOf+ optionalcaptionOf/badgesOf/listLabel/listWidth), selected item's detail right (detail(Row, rq)— any component, or a DetailIsland when it needs own actions),emptyDetail()before selection; selection (selectCollectionItem,_item) and debounced search re-render in place. The archetype declares its ownAutoSaveTriggervia TriggersSupplier because CLASS annotations are NOT inherited by subclasses (a general gotcha for archetype base classes; field annotations DO work — the@Colspan(2) Callableisland and@Hidden _selectedIdlive on the base). Demo/collection-detail-demo; testsCollectionDetailSyncTest; doc ux-patterns/collection-detail.md. - Record overview pages (
GeneralOverview<Row>archetype, 2026-07-17): the Redwood "General Overview" template — a record CONTEXT SWITCHER on top (therecordString field turned into a select via the archetype's OptionsSuppliersupports("record")+switcherOptions) and the selected record's overview below (load(id)+overview(Row, rq)— typicallyEntityHeadertitle/badges/facts/metric over property cards), first option auto-selected, switching re-renders in place (same TriggersSupplier auto-save mechanism as CollectionDetail). Demo/general-overview-demo; testsGeneralOverviewSyncTest; doc ux-patterns/general-overview.md. - Foldout record pages (
Foldoutarchetype): extendFoldout(coreorchestrators/foldout/) — the first component field without@Panelis the always-visible overview (left);@Panel(title, subtitle, icon, open)component fields are lateral fold-out panels (closed = narrow strip with rotated title, click to fold out; several open side by side, horizontal scroll on overflow;open=falsestarts folded). Fluent:FoldoutLayout(overview +List<FoldoutPanel>) mapped byFoldoutLayoutMapperinLayoutComponentDispatcher; wireFoldoutLayoutDtocarriesFoldoutPanelInfoDtoheaders while overview/panel contents travel as slotted children (overview,panel-N). Per-panel width (2026-07-27):FoldoutPanel.width(optional CSS length, fluent-only) →FoldoutPanelInfoDto.width→ flex-basis of the expanded panel in BOTH the sharedmateu-foldoutandmateu-vaadin-foldout(whose carousel snapping now uses actual section offsets, not a uniform stride — sections may have different widths); null keeps the 22rem default. Ports mirror it (.NETFoldoutPanel.Width, PythonFoldoutPanel.width). NOTE: the Vaadin shell renders foldouts withmateu-vaadin-foldout(apps/vaadin/src/renderers) — a horizontal carousel of ALWAYS-EXPANDED sections that ignoresopenby design (only the sharedmateu-foldoutcollapses to strips). Demo:demo-front-office-evolutionReservaOverview(check-in foldout: huéspedes overview + Operaciones 38rem + Perfil del cliente 15rem). Frontend:mateu-foldout.tsLitElement (owns the open/closed Set state locally, no server round-trip) +foldoutRenderer.tsin the shared switch; sapui5 + redwood-oj claim it in SUPPORTED_TYPES. Demo:demo-admin-panel/.../foldout/BookingFoldout.java(/foldout-demo). User docs:doc/.../ux-patterns/foldout.md. - Hero search pages (
HeroSearch<Filters, Row>archetype): extend it (coreorchestrators/herosearch/) and implementsearch(...)exactly like a declarativeListing— the archetype composes a centeredHeroSection(overrideheroTitle()/heroSubtitle()/heroImage()) + the standard listing built byPageListingBuilder.getCrud(made public for this), inside a STRETCHVerticalLayout. Results default toGridLayout.cards(overridegridLayout()); Filters record fields become the facet bar; starts empty and searches on enter (add@Trigger(OnLoad, "search")to preload).HeroSectionis also a standalone UIDL component (title, subtitle, background image with dark overlay,centered, slotted content children) mapped inLayoutComponentDispatcher, rendered byheroRenderer.ts(shared switch; sapui5/redwood-oj claim it) — reused by welcome pages. Demo:demo-admin-panel/.../herosearch/HotelSearch.java(/hotel-search). User docs:doc/.../ux-patterns/hero-search.md. - Item overview pages (
ItemOverviewarchetype): extend it (coreorchestrators/itemoverview/) — the first component field without@Panelbecomes the key-info panel (left,position: stickyCard, width viapanelWidth(), default 22rem);@Panel(title)component fields become tabs in aTabLayouton the right. Pure composition of existing components (Card + TabLayout + HorizontalLayout) — no new UIDL types, so it works on every renderer that already supports those. Demo:demo-admin-panel/.../itemoverview/ProductOverview.java(/product-overview). User docs:doc/.../ux-patterns/item-overview.md. - Welcome pages (
Welcomearchetype): extend it (coreorchestrators/welcome/) —Button(uidl data) fields become CTAs inside a centeredHeroSection(theiractionIdruns the matching@Actionmethod; return aURIto navigate);@Panel(title)component fields become highlight tiles on aDashboardLayoutgrid below; overrideheroTitle()/heroSubtitle()/heroImage(). Pure composition of HeroSection + DashboardLayout — no new UIDL types. Demo:demo-admin-panel/.../welcome/WelcomeDemo.java(/welcome-demo). User docs:doc/.../ux-patterns/welcome-page.md. - Smart search pages (
SmartSearchPage<Filters, Row>archetype, 2026-07-19): the Redwood "Smart Search" template — a standalone, search-first page: optionalpageSubtitle()intro line (aTextidpage-subtitle) over the standard smart-search listing (PageListingBuilder.getCrud, typed facets included), read-only, starts EMPTY (noOnLoad→searchtrigger — declarative listings get none; add@Trigger(OnLoad, "search")to preload). LikeHeroSearchminus the hero and without forcingcards. Ports: .NETSmartSearchPage<TFilters,TRow>+ISmartSearchPage(MapListing omits the OnLoad trigger + emits the subtitle Text) / PythonSmartSearchPage[F, R](same inmap_listing). Demo/smart-search-demo(redwood showcase, preloads via the trigger); testsSmartSearchPageSyncTest; doc ux-patterns/smart-search.md. - To-do list pages (
TodoList<Row>archetype, 2026-07-19): the Redwood "To-do list" template — pending work as aTaskQueueof counted buckets (groupOf(row)→ "Today (2)" labels,groupOrder()overrides first-appearance order) withcaptionOf/badgesOfcards; clicking a card ACTS (actionOn(row)→ URI/Message/component) instead of selecting for a detail pane (wire actionIdopenTodoItem,_itemparam);emptyState()("All caught up! 🎉") when no rows. Form-view wiring like CollectionDetail (@Hidden _itemId+@Colspan Callableisland + TriggersSupplier for the request). Ports: .NETTodoList<TRow>+ITodoList(SyncHandler branch) / PythonTodoList(open_todo_itemcamelCase-dispatched). Demo/todo-list-demo; testsTodoListSyncTest; doc ux-patterns/to-do-list.md. - Calendar pages (
CalendarPagearchetype, 2026-07-19): the Redwood "Calendar" template — calendar toolbar (‹/Today/› buttons + optional primary "+ Create") over theCalendarmonth grid; the displayed month is page state (@Hidden _month), navigation re-runsevents(month, rq); the archetype re-stamps every event withactionId="openCalendarEvent"and the click arrives withparameters._clickedEvent(a MAP carrying the eventid) →actionOn(event);showCreate()+createAction()for the create flow. Week/day/list RDS views NOT built in. Ports: .NETCalendarPage+ICalendarPage(Month/EventId as ISO strings) / PythonCalendarPage(month/event_idWITHOUT underscore — underscore fields are excluded from initialData seeding, the round-trip would break). Demo/calendar-demo; testsCalendarPageSyncTest; doc ux-patterns/calendar.md. - Page width (
@PageWidth+PageWidthSupplier, 2026-07-19): the first parameter of the RDS page templates —PageWidthStyle { FIXED, FULL_WIDTH, EDGE_TO_EDGE }(measured in the RDS Toolkit 24C Figma: fixed = 1408px cap centered, margins 24px <1536 / auto ≥1536; fullWidth = fluid 24px always; edgeToEdge = 0 margins; header/strip/#F1EFEDcanvas/#FBF9F8content common to all three). Resolution:@PageWidthon the concrete view WINS >PageWidthSupplier.pageWidth()hook (Foldout declares EDGE_TO_EDGE) > null (renderer infers). Wire:ServerSideComponentDto.pageWidth(the universal envelope — set in ComponentTreeSupplierMapper, buildPageUIFragment, ComponentStateHelper, FutureComponentMapper viaPageWidthResolver) ANDPageDto.pageWidth(reflected pages), values"fixed"|"fullWidth"|"edgeToEdge". Frontend: sharedresolvePageWidth(...)(libs/mateulayout/pageWidth.ts: wrapper → Page metadata → inference — gantt/planning/kanban/bpmn/map → edge, compact or inline-editable crud → full, else fixed) stampsdata-page-widthonmateu-uxinapplyFragment(re-evaluated per navigation); redwood-oj's index.css paints the three modes on the page mounts (max-width: min(1408px, 100% - 48px)/ 24px gutters / 0) and the foldout's negative-margin hack is neutralized under edge. Ports: .NET[PageWidth]+IPageWidthSupplier/ Python@page_width(PageWidth.X). TestsPageWidthSyncTest; note in ux-patterns/page-templates.md. - Empty states & skeletons:
EmptyStateUIDL component (icon emoji/text, title, description,actionId+actionLabelCTA dispatching the standard action mechanism) andSkeleton(variantstext/card/grid/formviaSkeletonVariant,countrepeats the shape; rendered by themateu-skeletonLitElement with a shimmer animation —:hostcarriesflex: 1 1 0so skeletons share width inside Horizontal layouts). Both mapped inDisplayComponentDispatcher, rendered byemptyStateRenderer.ts, claimed by sapui5/redwood-oj. Grids/listings now render the sharedemptyStateTemplate(...)block instead of bare "No data." text — patched inmateu-table-crud.ts(list/cards/masterDetail/table spots),gridRenderer.tsandmateu-table.ts, keepingemptyStateMessageas the message. Demo:demo-admin-panel/.../emptystates/EmptyStatesDemo.java(/empty-skeleton-demo). User docs:doc/.../ux-patterns/empty-states-and-skeletons.md. - Gantt/timeline component:
GanttUIDL record (list ofGanttTask(id, title, LocalDate start/end, progress 0-100, color)), mapped byGanttMapperinDisplayComponentDispatcher(dates serialized ISO inGanttTaskDto), rendered by the dependency-freemateu-ganttLitElement (CSS grid: labels column + time lanes, month headers derived from the tasks' date range, per-task progress fill, today marker, bar tooltips; themes via Lumo CSS vars with fallbacks so it's design-system neutral and dark-mode aware; per-taskcoloroverrides--mateu-gantt-fill). Read-only by design. Claimed by sapui5/redwood-oj. Demo:demo-admin-panel/.../gantt/ProjectPlan.java(/gantt-demo). User docs:doc/.../ux-patterns/gantt.md. - Separators (
Separatorcomponent +@SeparatorBefore) & text sizes (@Text(size=…)/Text.size):@SeparatorBeforeon a field paints a full-width<hr>above it — for separating groups of contents inside a section/form without starting a new section. Pipeline:FormLayoutBuilder.toFormLayoutflatMaps aSeparator(uidl.data record with anattributesmap carryingdata-colspan= the form's columns) before the field;buildRowsgives anySeparatoraFormRowof its own (flushing the pending row); fluentSeparatoralso usable anywhere (mapped bySeparatorMapperinDisplayComponentDispatcher→SeparatorDto); frontendseparatorRenderer.tsemits the styled<hr>(Lumo contrast var,data-colspanso it spans the vaadin-form-layout row); claimed by sapui5/redhat/redwood-oj. Text sizes:TextSize { xl, l, m, s, xs }(uidl.data) on the@Textannotation (size(), defaultm) and the fluentText.size—m/absent applies nothing, the rest emitfont-size: var(--lumo-font-size-*)intextRenderer.ts(TextDto.size, a plain string on the wire). Independently,@Text(noMargins=true)/ fluentText.noMargins(TextDto.noMargins; .NETText.NoMargins, PythonText.no_margins) drops the container's block margins (margin-block-start/end: 0— a default<p>margin dwarfs an xs caption); combine both for tight sized captions. Tests:SeparatorAndTextSizeSyncTest. Full parity (2026-07-15): .NET[SeparatorBefore]+ fluentSeparator/Text { Size }(MapFieldshelper inserts the separator,FormRowsgives it its own row;SeparatorMetadataDto,TextMetadataDto.Size; tests inSectionFeatureTests); PythonSeparatorBefore()marker +fluent.Separator/Text(size=…)(map_fields/form_rows;SeparatorMetadata,TextMetadata.size; tests intest_section_features.py); React Native (Separatorcase → hairline View, Text case maps size→fontSize 22/18/12.5/11); IntelliJ plugin ("Separator"→JSeparator,"Text"derives the font ×1.5/1.25/0.875/0.78 — and the plugin also gained"BulletedList"(renderBulletedList) plus thepropertyRow/bulletedListfield branches in itsFormFieldRenderer, verified with./gradlew renderProbeover a captured front-office wizard increment). - Property-list sections (
@Section(propertyList=true)) & frameless sections (@Section(frameless=true)): two@Sectionattributes for key-info panels.propertyList=truerenders every DATA field of the section as a read-only property row — plain-text value, label left / value right, divider line between rows, single column regardless ofcolumns— without annotating each field; component-holding fields in the section travel untouched. Pipeline:SectionFormRenderer.buildFormLayout→asPropertyListrecursively transforms theFormLayoutBuilderoutput (VerticalLayout → FormLayout → FormRow), REPLACING the responsive FormLayout with a STRETCHVerticalLayoutof the rows (the form layout would size rows to its column width, leaving dividers short of the card edge) and marking eachFormField.toBuilder().propertyRow(true).readOnly(true);propertyRowflowsFormField→FieldMapper→FormFieldDto→ frontendFormField.ts;mateu-field.renderPropertyRowField(dispatched FIRST, before badge/plainText) and redwood-oj'srenderPropertyRowFieldrender the flex row (money/bool handling copied from plainText).frameless=truedrops the outlined section Card AND its padding (both card paths inSectionFormRendereremit the bare content, like the_inlineembedded-mediator path) — for bands whose content brings its own chrome (header cards, progress banners); frameless sections are not enumerated by the@Tocindex. NOTE: the anonymousSectioninFormSectionGroupermust implement every new@Sectionattribute. Demo:demo-front-officeIdentidadStep(Documento = propertyList; header band + registroPax = frameless). Tests:PropertyListSyncTest,FramelessSectionSyncTest. .NET/Python parity (2026-07-15):[Section(PropertyList = true, Frameless = true)]/Section(caption, property_list=True, frameless=True)— the section grouping carries the marker (parallelsectionAttrs/section_markerslist) intoSectionCard/section_card, which swaps the FormLayout for a stretch VerticalLayout ofPropertyRow+ReadOnlyfields resp. drops the FormSection/Card wrapper for a bare Div; flags apply on the stacked and zones paths (not the tabs/fold inference paths). Wire:FormFieldMetadataDto.PropertyRow/FormFieldMetadata.property_row. Port tests: .NETSectionFeatureTests, Pythontest_section_features.py. React Native renders both (2026-07-15):FormFieldRendererearly-returns apropertyRowflex row (label left, value right, hairline divider; money/bool formatting) before the label+input container, and thebulletedListstereotype renders bullet rows inrenderInput; the fluent component isBulletedListRenderer(DisplayRenderer.tsx, case inComponentRenderer). Verified with expo web + demo-front-office on :8592 (the RN dev fallback port) driving the check-in queue → wizard. - Notices (
Noticecomponent +@Noticefield annotation): a compact INLINE banner — rounded theme-tinted strip with a small circular severity icon, one line of text (e.g. "2 quejas pendientes") and an optional right-aligned action (actionLabel+actionId, standard dispatch) — embeddable anywhere (cards, columns, forms). Declarative:@Notice(theme=…, slim=…, fullWidth=…, actionLabel/actionId)on a String field renders the field's VALUE as the notice text (emitted as a${state.<field>}template, interpolated innoticeRenderer.ts) — a null/blank value hides the notice entirely, so the field doubles as its own visibility switch (ReflectionFormFieldMapper, branch before@Text; note bothio.mateu.uidl.annotations.Noticeandio.mateu.uidl.data.Noticeexist — double star-imports need an explicit import, like Badge).slimdrops block margins + tightens padding and setsline-height: normal(the@Text(noMargins)analogue);fullWidthspans all form columns (the renderer stampsdata-colspan=99, clamped by vaadin-form-layout — andcustomFieldRendererpropagates it to the wrappingvaadin-custom-fieldwhen the notice sits inside a component-holder field). Arbitrary content:Notice.content(List<Component>, fluent only) renders ANY components inside the tinted strip below the text — travels as slotted children (the HeroSection/Drawer pattern;NoticeMappernow takes the dispatch context),mateu-noticeshows them via<slot>, and a text-less notice with content is NOT hidden (the blank-hides rule only applies when there's no content either). Ports carryContent/contentthe same way; RN/IJ render the children below the text. Smaller thanCalloutCard(no title/CTA block) and independent of the page-level@Banners.theme: info|success|warning|danger (default info);iconoverrides the theme glyph (ℹ ✓ ! !). Pipeline:Notice(uidl.data) →NoticeMapperinDisplayComponentDispatcher→NoticeDto→ sharedmateu-noticeLitElement (always-light pastel bg + dark ink per theme, like the page banners) vianoticeRenderer.ts; claimed by sapui5/redhat/redwood-oj. Parity: .NETNotice { Theme, ActionLabel… }/ Pythonfluent.Notice(...)→NoticeMetadataDto/NoticeMetadata; RNNoticeRenderer(DisplayRenderer.tsx), IntelliJrenderNotice. Demo:demo-front-officeIdentidadStep.quejas. Tests:NoticeSyncTest+ port suites. - Bulleted lists (
BulletedListcomponent +@BulletedListfield annotation): render a plain<ul>of text items — the lightweight counterpart ofStatusListfor read-only enumerations (preferences, highlights, notes). Fluent:io.mateu.uidl.data.BulletedList(id, List<String> items, style, cssClasses)mapped byBulletedListMapperinDisplayComponentDispatcher→BulletedListDto. Declarative:@BulletedList(uidl annotation, FIELD + ANNOTATION_TYPE so it composes) on aList<String>field = shorthand for stereotypeFieldStereotype.bulletedList(mapped inFieldTypeMapper.getStereotype, checked right after@Badge);mateu-field.tsand redwood-oj'srenderField.tsbranch on it before the readOnly dispatch and render the sharedmateu-bulleted-listLitElement (DS-neutral, Lumo vars with fallbacks; a non-array value renders as a single item). Claimed by sapui5/redhat/redwood-oj in SUPPORTED_TYPES. Demo:demo-front-officeIdentidadStep.preferencias(guest preferences as a<ul>above theStatusList). Tests:BulletedListSyncTest. .NET/Python parity (2026-07-15): fluentBulletedList { Items }/fluent.BulletedList(items=...)→BulletedListMetadataDto/BulletedListMetadata(wire typeBulletedList), and the[BulletedList]attribute /BulletedList()Annotated marker → stereotypebulletedList(first check inStereotypeOf/stereotype_of). Port tests: .NETSectionFeatureTests, Pythontest_section_features.py. - Welcome banner element (
@WelcomeBanner, 2026-07-19): any view class can carry the Redwood "Welcome Banner" — the annotation (title falling back to the page@Title, subtitle, image) is mapped by ReflectionPageMapper to a centeredHeroSection(idwelcome-banner) prepended to the page content (no new wire type). Accent strip rule: the strip only shows on pages WITHOUT a welcome banner —hasWelcomeBanner(...)in libs/mateulayout/pageWidth.ts(any HeroSection in the tree) stampsdata-has-welcome-banneron mateu-ux;mateu-page._showHeaderBand()skips the band, and redwood-oj's renderFilterBar suppresses its listing strip via theuxHost()ancestor walker;.mateu-herogets a soft Redwood gradient band in redwood-oj's index.css. Ports: .NET[WelcomeBanner](MapView prepend) / Python@welcome_banner(...)(map_view prepend). TestsWelcomeBannerSyncTest; docs ux-patterns/welcome-page.md. - Coarse page type (
@PageTemplate+PageTypeResolver, 2026-07-19): every page carries apageTypeon the wire (ServerSideComponentDto.pageType+PageDto.pageType) from the Redwood template families —landing/collection/detail/form/process/dashboard.PageTypeResolver(componentmapper) maps the ModelView shape:@PageTemplate(PageType.X)wins > archetype family (Dashboard→dashboard, Welcome/HeroSearch→landing, SmartSearchPage/TodoList/CalendarPage/CollectionDetail/Crud/Listing→collection, Wizard/ImportWizard→process, Foldout/ItemOverview/GeneralOverview/MasterDetailView/EditableView→detail) >MetricCardfield → dashboard > defaultform. Frontend:pageTypeOfinlayout/pageWidth.tsfeeds the width chain (declaration > app-shell edge > type default (form/process/landing always fixed) > content inference > fixed) andmateu-uxstampsdata-page-typenext todata-page-width. Ports: .NET[PageTemplate]+PageTypeOf(MapView/MapListing/MapCrud/MapWizard/MapEntityForm) / Python@page_template(PageType.X)+page_type_of(5 ServerSideComponent sites + Page metadata). TestsPageTypeResolverSyncTest(13-case archetype map + wire); docs note in ux-patterns/page-templates.md. - Labels-aside inference (
labelsAsidewire flag, 2026-07-19): the dense backoffice idiom (label LEFT of the field in a 10rem column) is inferred per form inLabelsAsideInference(componentmapper) and emitted on every FormLayout (FormLayoutBuilder + TabFormLayoutBuilder): aside only when single-column (getFormColumns == 1) AND ≥6 fields AND all labels ≤20 chars AND all single-line widgets (no textarea/richText/Collection/Map/Callable/nested). Explicit@FormLayout(labelsAside = LabelsAsideMode.ASIDE|TOP)(new uidl attribute + enum) always wins overAUTO(infer). Frontend: sharedrenderLayoutssets--vaadin-form-item-label-width: 10remwhen aside (fields fill the rest; when NOT aside, the layout getsexpand-fieldsso fields span the full column). TestsLabelsAsideInferenceSyncTest(7 cases); docsjava-ui-definition/forms.md#labels-on-top-vs-labels-aside. - Canonical page header — overline & title placeholder (2026-08-12): the two remaining Redwood header text elements, closing delta #9 of
design/vb-template-coverage-audit.md.@Overline("Requisitions")→ the quiet line ABOVE the title;@TitlePlaceholder("New booking…")→ what the header shows while the title is still EMPTY (create-mode affordance). Both follow the house rule for page metadata, the same as@Subtitle: the supplier interface (OverlineSupplier/TitlePlaceholderSupplier) WINS over the annotation, because one is dynamic and the other static. Chain:PageMetadataExtractor.getOverline/getTitlePlaceholder→PageView→PageDto.overline/.titlePlaceholder→mateu-content-header(overline muted above theh2; the placeholder INSIDE theh2so it keeps the size, dimmed so it never reads as a real title). It is a placeholder, not a default: the wire emits it as declared and the RENDERER suppresses it once a title exists, so the wire stays descriptive and each renderer decides how to present it.hasMainHeader/_showHeaderBandaccount for both, so a page declaring only a placeholder still gets a header. Ports:[Overline]/[TitlePlaceholder](.NET) and@overline/@title_placeholder(Python) — declarative form only, same convention as@Subtitle. Tests:PageOverlineSyncTest(Java),PageOverlineTests(.NET),test_page_overline.py(Python). User docs:doc/.../ux-patterns/page-templates.md. - Canonical page header — peer navigation & timestamp (Fase 0 RDS, 2026-07-20): two header elements added to the shared page header (
PageDto/mateu-content-header, reused by every renderer) as part of adopting the Redwood page-template header grammar (design plan:design/redwood-page-templates-plan.md). Peer navigation (Redwood "next/previous object"): a page implementsPeerNavigationSupplier.peers(HttpRequest)→PeerNav(prevLabel, prevRoute, nextLabel, nextRoute)(uidl.data) →PageView.peerNav→PageDto.peerNav(PeerNavDto) → prev/next arrow buttons inmateu-content-header(navigate like breadcrumb links; anullroute disables that side; optionalrenderPeerNavrenderer hook for DS-specific styling). Timestamp (Redwood "last updated"):@Timestamp("Last updated")on a field →PageMetadataExtractor.getTimestamp(prefix + value.toString, first field wins) →PageView.timestamp→PageDto.timestamp→ muted text under the subtitle; the field is excluded from the form body byFormFieldFilter(like@BadgeInHeader). Contextual info was already covered by field-level@KPI(label/value pairs in the header), so no@HeaderFactwas added. Ports: .NETIPeerNavigationSupplier/PeerNav→PageMetadataDto.PeerNav+[Timestamp]→PageMetadataDto.Timestamp(ReflectionMapper.PeerNavOf/TimestampOf, excluded viaVisible); PythonPeerNavigationSupplier/PeerNav→PageMetadata.peer_nav+Timestamp()marker→PageMetadata.timestamp(mapper.peer_nav/timestamp_of, excluded viavisible). Tests:PeerNavigationSyncTest+PageHeaderExtrasSyncTest(Java),PeerNavTests(.NET),test_peer_nav.py(Python). Demo:/peer-nav-demo(EmployeeRecord1..3, redwood-oj). Semantic button roles (primary/save/cancel/secondary) were DEFERRED — the header already places cancel/back left and actions right viaisNavButton+ emphasizes primary viabuttonStyle, and arolefield would break the fluentButtonrecord's constructors + ~23new Button(...)sites for marginal value. User docs:doc/.../ux-patterns/page-templates.md+ the decision guidechoosing-a-page-template.md. - General Drawer — read-only detail enrichment (Fase 1 RDS, 2026-07-20): the Redwood "General Drawer" template (extra read-only info about an object without leaving the page) is NOT a new archetype — it's the existing
Drawerenriched with four header extras:subtitle,size(DrawerSize {s,m,l,xl}→ 464/648/968/90vw,widthstill overrides),maximizable(a ⤢ button that steps the drawer up the size ladder), andpeerNav(reuses the Fase 0PeerNav— prev/next-object arrows in the drawer header). Pipeline:Drawer/DrawerSize(uidl.data) →DrawerDto(subtitle/size/maximizable/peerNav) →DrawerMapper→Drawer.ts→mateu-drawer.ts(subtitle under title, width derived from size, Maximize is CLIENT-SIDE local statemaximizeStepswith no round-trip, peer arrows navigate like breadcrumb links). Ports: .NETDrawerSize+Drawer { Subtitle, Size, Maximizable, PeerNav }→DrawerMetadataDto(ComponentMapper); PythonDrawerSize+Drawer(subtitle, size, maximizable, peer_nav)→DrawerMetadata(mapper). Tests:DrawerSyncTest(Java),SyncHandlerTests.General_drawer_carries_...(.NET),test_sync_handler.py::test_general_drawer_carries_...(Python). Demo:/general-drawer-demo. User docs:doc/.../ux-patterns/drawer.md(General Drawer section). - Bottom Drawer (Fase 1 RDS, 2026-07-20): the Redwood "Bottom Drawer" template — a full-width panel docked at the bottom edge — is the existing
Drawerwith a newDrawerPosition.bottomplus acollapsibleflag. Frontendmateu-drawer.ts: thebottomposition anchors the panel toleft:0;right:0;bottom:0, full width, sliding UP (height--mateu-drawer-heightdefault 50vh, cap 90vh;widthis ignored for bottom), andcollapsibleadds a ▾/▴ handle in the header that toggles between the collapsed header strip and the expanded panel — CLIENT-SIDE local@state collapsed, no round-trip (same pattern as the General Drawer's maximizemaximizeSteps). Wire:DrawerPosition.bottom(uidl enum +DrawerPositionDto+DrawerMapper.mapPosition) +Drawer.collapsible→DrawerDto.collapsible. Ports: .NETDrawerPosition.Bottom+Drawer.Collapsible→DrawerMetadataDto.Collapsible; PythonDrawerPosition.bottom+Drawer(collapsible=…)→DrawerMetadata.collapsible. Tests:DrawerSyncTest.bottomDrawerCarriesTheBottomPositionAndCollapsibleFlag(Java),SyncHandlerTests.Bottom_drawer_carries_...(.NET),test_sync_handler.py::test_bottom_drawer_...(Python). Demo:/bottom-drawer-demo. Fase 1 (both base drawers) done → unblocks Fase 2 (Gantt page + Data Management use them as bottom/side panels). - Gantt page & Data management archetypes (Fase 2 RDS, 2026-07-20): two full-page templates built as ComponentTreeSupplier archetypes (pure composition, no new wire types → render on every renderer). GanttPage (
orchestrators/ganttpage/,PageWidthSupplier→EDGE_TO_EDGE): composes a heading (ReflectionPageMapper.getTitle— a ComponentTreeSupplier gets no mateu-page wrapper, so it composes its own title) + theGanttcanvas (fromtasks(rq)) + an optional dockeddetail(rq)Card. Interaction:GanttgainedonTaskSelectionActionId(wire field, uidl/dtos/mapper +mateu-gantt.tsdispatchesaction-requestedwithparameters._clickedTaskIdon a.bar.clickableclick); GanttPage wires it toselectGanttTask, whose@Actionreads_clickedTaskId, finds the task and returns a sideDrawer(taskDrawer/taskDetailoverridable).PageTypeResolver→DETAIL. DataManagement (orchestrators/datamanagement/,PageWidthSupplier→FULL_WIDTH): the developer suppliesgridView(rq)/ganttView(rq); a toolbar switcher (two Buttons) flips the active view kept in_viewstate —@Action switchToGrid/switchToGanttset it and returnthisto re-render in place; heading from@Title.PageTypeResolver→COLLECTION. Ports: .NET (GanttPage/IGanttPage,DataManagement/IDataManagementin Archetypes.cs; SyncHandler branches onselectGanttTask/switchToGrid+switchToGantt;GanttMetadataDto.OnTaskSelectionActionId) and Python (GanttPage,DataManagement; sync_handler branches;GanttMetadata.on_task_selection_action_id; DataManagement usesviewWITHOUT underscore so it seeds into initialData and round-trips). Tests:GanttPageSyncTest/DataManagementSyncTest(Java),ArchetypeTests(.NET),test_gantt_page.py/test_data_management.py(Python). Demos/gantt-page-demo(verified visually — canvas + bar-click drawer),/data-management-demo(grid ⇄ gantt switch). Docs: ux-patterns/gantt.md (Gantt page section) + ux-patterns/data-management.md. NOTE: a ComponentTreeSupplier archetype returning a plain layout gets NO page header/mateu-page— it must compose its own title/toolbar (as these do). - Inline editing on CRUD listings (class-level
@InlineEditing): annotate anAutoCrudclass with@InlineEditing(the annotation now also targets TYPE) and every data column of the table listing becomes an in-place editor (@ReadOnlyfields stay display-only); each committed cell persists its row immediately — the frontend dispatches the crud'supdate-rowaction withparameters._editedRow, handled byUpdateRowActionHandler→Crud.updateRow(Map, HttpRequest)(defaultUnsupportedOperationException;FilteredAutoCrudrebuilds the entity viaMateuInstanceFactoryand callsstore().save). Pipeline:ListingColumnBuilder.getColumnsetseditable/editorType/editorOptionswhen the listing instance's class carries@InlineEditing(reusingGridColumnBuilder.getEditorType/getEditorOptions, made package-visible);ListRouteResolvernow passes the orchestrator (not itself) as thegetColumnsinstance;update-rowadvertised inCrudActionsBuilder(list + mediator). Frontend:renderEditableCell's commit falls back toaction-requested update-rowwhen there's no enclosing form-grid field, and skips no-op commits (vaadin-checkbox fireschecked-changedon initialization — without the guard every boolean row saved on page load). Demo:demo-admin-panel/.../inlinecrud/StockCrud.java(/inline-crud-demo). User docs:doc/.../ux-patterns/inline-crud-editing.md. - Mediator navigation on renderer shells (Redwood/SLDS/PatternFly): these shells serve MANY routes from one page (unlike Vaadin, where each route is its own page with its own baseUrl), so
MateuRendererApp's upstream subscription re-points the innermateu-uxwhen a mediator fragment arrives withstate._route. Three subtleties fixed 2026-07-05: (1) on a direct URL load (no menu click)selectedConsumedRoutewas never set, so the composed route lost its prefix — the handler now falls back to the inner ux'sconsumedRoute; (2)chooseRoute()gives the app state's_routeprecedence overselectedRoute, so the handler must clearstate._routebefore remounting (exactly like Vaadin'smateu-app._selectRoutedoes) or the remount reloads the listing instead of e.g. the crud's/newform; (3) the browser URL for a mediator-internalPushStateToHistory(e.g./new) is prefixed with the inner ux'sconsumedRouteinmateu-ux.routeChangedListener(no-op on Vaadin where consumedRoute is''/_empty).mateu-redwood-app.tsis a copy ofMateuRendererApp— keep both in sync. Also: redwood-oj'srenderFilterBarmust NOT rendermetadata.toolbar(the sharedmateu-table-crudheader already renders the crud toolbar — rendering both duplicated every button), andmateu-redwood-tablerenders columns carrying anactionId(e.g. the crud's first column,actionId="view") as link-style borderless-button cells via the existingojAction → handleCellButtonActionpath — note JET's CSP expression evaluator does NOT exposeString(), use'' + cell.datain template bindings. Embedded mediator islands on these shells (fixed 2026-07-05, check-in cardex): (a) the App fragment of the shell can re-arrive mid-navigation with a fresh uuid, remounting the shell — the inner ux DOM id must be stable across remounts (derived from the navigation route / pathname,contentUxId), or responses to in-flight loads target the dead incarnation and the page renders blank (e.g./checkin/3); (b) a state-only fragment (no component) arriving at amateu-uxthat already shows routed content must merge, not replace, inapplyFragment— otherwise a host-page push emitted while an embedded mediator loads blanks the content; (c) an embedded MEDIATOR-variant shell must NOT intercept unclaimedaction-requestedevents (handleUnhandledActionreturns early forAppVariant.MEDIATOR): they may belong to an ANCESTOR component — the cardex'sreloadPaxbubbles from the entity view inside the island up to the enclosingAutoEditableView's mateu-component, which advertises it (Vaadin'smateu-apphas no such interceptor, so this only broke on the shells); after the ancestor claims it, the island re-renders purely by property flow (state._routeflip →chooseRoute→ inner ux route change → re-fetch); (d)captureActionSSTmust ignore actions whoseserverSideComponentRoutecarries_embeddedMediator=1— recording the island's initiator would make the outer shell treat the island'sstate._routeresponse (the/view↔/reload flip) as a page navigation and remount the whole routed content; (e)ComponentElementre-registers itsOnCustomEventlisteners inconnectedCallback— a Lit re-render of an ancestor can detach+re-attach the element without changing itscomponentproperty, anddisconnectedCallbackdropped the listeners, leaving subscriptions (e.g. the cardex'spax-selected) deaf. Menu navigation on these shells (2026-07-05): a click on a LOCAL menu option (no own baseUrl, no target-specific serverSideType — the App metadata's RouteLinks only echo the app's own class — and no actionId) is handled as a full URL-load navigation:selectRoute/navigateconsultdirtyGuard.confirmLeave(), dispatchroute-changed(pushes the URL via the top ux →mateu-ui) andnavigate-to-requested(mateu-ui re-routes the top-level_ux, which swaps the whole shell for the target route's own App). This makes menu clicks byte-identical to direct URL loads / browser back-forward (one uniform boot path) instead of an in-place content load that left the shell with stale selected*/lastActionInitiator state (source of several bugs). Remote menu options (own baseUrl/serverSideType, e.g. remote apps under a uriPrefix) and action menu entries keep the in-place load; embedded MEDIATOR shells never take this path. Also fixed: the drawer<li>data attributes were written unhyphenated (data-consumedroute) whileselected()read camelCase dataset keys (dataset.consumedRoute⇒ attributedata-consumed-route), so option metadata NEVER reached selectRoute — attributes are now hyphenated, reads are normalized (ds()maps Lit's literal "undefined" to undefined), and the TABS variant resolves the clicked option frommetadata.menu(it renders no[data-path]elements). NOTE: a plain route like /products renders as ~3 sequential loads by design (App shell → the crud's MEDIATOR component → the listing route inside it) — the chrome-less MEDIATOR-variant shell nested in the content area is the crud mediator, NOT a double-booted app. - AI chat: when
sseUrlis set in the app metadata, a floating round button (.ai-fab,position: fixed, bottom-right) toggles amateu-chatside panel. The button and chat are rendered once outside all variant-specific layouts inappRenderer.ts. Do not add an IA button to variant headers — the FAB is the only entry point. - Command center (Ask-Oracle pattern, 2026-07-20):
@App(commandCenter=true)shows an always-present FAB (bottom-right) that opens a full-screen palette unifying navigation (whole menu flattened), global entity search (reusesGlobalSearchSupplier/_globalsearch), recent screens and an AI hand-off — the discoverable/touch/chromeless sibling of the ⌘K palette.@App(chromeless=true)additionally drops the nav chrome (implies commandCenter; on the Vaadin shell an earlyif (metadata.chromeless)branch inrenderApprenders content full-bleed + FAB only). It's one shared DS-neutral LitElementmateu-command-center.ts(inline SVG icons, Lumo vars + fallbacks) mounted document-wide singleton by the shell base classes'updated()viacommandCenterMount.ts::syncCommandCenter(host)(appended to the shell'srenderRoot— MateuApp covers Vaadin/redwood-oj/sapui5, MateuRendererApp covers redhat/slds; MEDIATOR shells skipped so nested crud shells don't dup the FAB). FAB auto-offset: the element measures sibling FABs (.ai-fab/.app-fab/.page-fab) in its own root and stacks its FAB above them (fabOffset = count*4rem), re-measuring via aMutationObserverso it reacts when the AI fab appears/disappears (chat open/close) — so it never overlaps the AI/app FABs on any shell, with NO per-shell offset code (the old appRenderer-side Vaadin-only shift was removed). It navigates by dispatching the sharedroute-changed+navigate-to-requestedpair (works on every shell, no glue), records recents inrecentRoutesStore.ts(localStoragemateu-recent-routes, scoped by app serverSideType), and the "Ask AI" row dispatchesmateu-open-ai(mateu-app opens the chat). WhencommandCenterEnabled, mateu-app's own ⌘K palette + keydown stand down (guards onmetadata.commandCenterEnabled). Wire:@App.commandCenter()/chromeless()→AppMapper.getCommandCenter/getChromeless(chromeless ORs into commandCenter) →AppDto.commandCenterEnabled/chromeless→App.ts. Opt-in (default false); non-breaking. Demo:Home2(@App(commandCenter=true)+ GlobalSearchSupplier). Tests:CommandCenterSyncTest. Doc:ux-patterns/command-center.md. .NET/Python parity:[App(CommandCenter=true, Chromeless=true)](AppAttributeprops) /@app(command_center=True, chromeless=True)(decorator sets__mateu_app_command_center__/__mateu_app_chromeless__) →AppMetadataDto.CommandCenterEnabled/Chromeless/AppMetadata.command_center_enabled/chromeless(chromeless ORs into commandCenter in bothReflectionMapper.Map…appandmapperapp builder). Tests: .NETSyncHandlerTests(Command_center/Chromeless facts), Pythontest_command_center.py. The command-center UI itself is frontend-only (same sharedmateu-command-center), so the ports only emit the two wire flags. - Dark/light mode toggle: add
themeToggle = trueto@Appto show a moon/sun icon button in the header of all app variants. The theme system works in layers: (1)index.htmlinline script applieslocalStorage['mateu-theme']first, then falls back to the OSprefers-color-schememedia query — the OS change listener only fires if no user choice is stored; (2)MateuApp.toggleTheme()setsdocument.documentElement.setAttribute('theme', 'dark'|'light')and saves tolocalStorage; (3)MateuApp.connectedCallbackreads the already-set attribute. Always usesetAttribute('theme', 'light'|'dark')— neverremoveAttribute— to stay consistent with Vaadin's convention. The toggle button and the theme-change listener forHAMBURGUER_MENUare rendered in the right-side widgets container (themargin-left:auto<div>that also holds the widgets slot), so they stay right-aligned without affecting the hamburger button on the left. - Application context selector (
@AppContext): a field of the@UIapp class annotated@AppContext(label=…)renders as a compact select on the APP HEADER (next to the theme toggle, all shared-appRenderer variants + redwood header) that fixes a value for every screen — active hotel, company, fiscal year. Options come from the field's TYPE: enum constants, or aLookupOptionsSupplierinstantiated viaInstanceFactoryand searched with empty text (first 100) at app build time (AppMapper.getContextSelectors→AppDto.contextSelectors→AppContextSelectorDto(fieldName,label,options)). The picked value is persisted client-side in localStorage keymateu-app-context(one object per origin,{fieldName: value}) byappContextStore.ts;AxiosMateuApiClient.runActionmerges it into the appState of EVERY request (explicit appState entries win), so the server reads it anywhere viaHttpRequest.appContext(fieldName)(null when unset/blank). Changing the value reloads the page (uniform reactivity — the current route rebuilds against the new context), and astorage-event watcher reloads the OTHER tabs of the origin too (cross-tab sync). The widget: on the VAADIN shell (the shared appRenderer) it'smateu-vaadin-app-context-picker(2026-07-15) — Vaadin's own widgets: ≤7 options →vaadin-select(with a leading "—" clear item), more →vaadin-combo-boxwith a lazy dataProvider running the same_appcontext-search-<field>action (pick guards value equality so the initvalue-changeddoesn't reload-loop); the sapui5/redhat/redwood/slds shells keep the DS-neutralmateu-app-context-pickerLitElement: ≤7 options → native select; more → button + SEARCHABLE panel whose typing filters client-side and (debounced 300ms) asks the server via the_appcontext-search-<field>action (AppContextSearchActionRunner, core/domain/act — resolves the field on the instance hierarchy or the request serverSideType, since the routed instance may be the shell wrapper; respondsData{_appcontext_<field>: page}like the lookupsearch-<field>action); the picked label persists inmateu-app-context-labelsso the button can display selections not among the loaded options.@AppContextfields are EXCLUDED from form bodies byFormFieldFilter(like@BadgeInHeader). .NET/Python parity:[AppContext]attribute (enum property or method returning OptionDto list) /@app_contextdecorator (method returning Option/(value,label) pairs or an Enum return annotation) emit the samecontextSelectorswire field. Native renderer parity: React Native (drawer-top selectors; session-scoped; a contextVersion state remounts the current screen); its picker with >7 options is SEARCHABLE with remote search (same_appcontext-search-<field>contract: route = app homeRoute, serverSideType = the app class,parameters.searchText; responsefragments[].data._appcontext_<field>.content= value/label options; TextInput under the drawer selector, setTimeout 300). Root-app dispatch fix (2026-07-07): on an app at route "" the_appcontext-search-*action used to answer "Not found." (AppMenuResolver.resolveInApp found no menu actionable for the empty route → Mono.empty → RunActionUseCase.switchIfEmpty) — the WEB picker silently hid this behind its client-side filter;ActionInstanceCreator.instantiateWithKnownTypenow treats_appcontext-search-*as app-level (like terminal routes) and returns the app instance without menu resolution (isAppLevelAction). Test:AppContextSyncTest.appContextSearchActionWorksOnTheRootApp(own TestMateu instance — registering a root app changes route resolution for every other fixture in a shared harness). Demo:Home2.hotel(+greet()echoes it). Tests:AppContextSyncTest. User docs:doc/.../ux-patterns/app-context.md. - App header actions (
AppActionsSupplier): the@UIapp class implementsAppActionsSupplier.appActions(HttpRequest)returningAppHeaderAction(actionId, label, icon, children)records — buttons on the app header next to the@AppContextpickers; an action withchildren(built viaAppHeaderAction.menu(...)) renders as a dropdown and only the children dispatch. Evaluated per shell build (server-side visibility). Wire:AppMapper.getContextActions→AppDto.contextActions(recursiveAppHeaderActionDto). Dispatch is APP-LEVEL like_appcontext-search-*(ActionInstanceCreator.isAppLevelActionflattens children before matching, so it works on root apps); the shared frontend dispatch lives inappHeaderActions.ts(dispatchAppHeaderAction: SSE-flavored runAction against the app's serverSideType, with the on-screenmateu-component— found by piercing shadow roots — as initiator so streamed LongTask fragments land on a real component). Each shell renders it with its OWN widgets (2026-07-16): vaadinvaadin-menu-bar/vaadin-button(appRenderer), sapui5ui5-menu, redhat PF menu markup, redwoodoj-c-menu-button, sldsslds-dropdown(icon names are Vaadin-specific and intentionally not rendered on the DS shells). .NET/Python parity:IAppActionsSupplier.AppActions()/AppActionsSupplier.app_actions()→ samecontextActionswire field; app-level dispatch needed no handler change there. Tests:AppHeaderActionsSyncTest(core), SyncHandler tests in both ports. User docs: the "Header actions" section ofdoc/.../ux-patterns/app-context.md. RN/IntelliJ: N/A by design (drawer/sidebar shells without a top app bar). - Enterprise pattern wave (2026-07-17, one release per pattern) — twelve patterns landed with tri-backend + all-web-renderer parity; each has a ux-patterns page, a SyncTest suite and (where wire-visible) .NET/Python ports. Quick map: Bulk actions —
@ListToolbarButton(confirmationRequired, rowsSelectedRequired=true)on a Crud method; typedList<Row>param getscrud_selected_itemshydrated (HttpRequest.getSelectedRows); label honors@Label; DS tables all write<id>_selected_items(BulkActionsSyncTest). Saved views — client-side named condition sets on the smart search bar (savedViewsStore.ts, localStoragemateu-saved-views, scope=pathname; ★=default view auto-applied when URL has no params; redwood has its own port in renderFilterBar). Totals & grouping —@Aggregate(AggregateFunction)+@GroupByon row fields →CrudStore.summaries(in-memory default, DB-overridable) →ListingData.aggregates/groups+CrudlDto.groupBy/GridColumnDto.aggregate; group column becomes implicit primary sort (ListingSummarySpec); sharedlistingGroups.tsinterleaves marker rows; markers excluded from selection/click/editing (AggregatesSyncTest). Optimistic locking —@Versionint/long field;OptimisticLock.check/bumpin FilteredAutoCrud.save + FilteredAutoCrud.updateRow; conflict → Spanish Dialog (Recargar/Sobrescribir with_forceOverwrite, update-row re-sends_editedRow); overwrite adopts stored version then bumps (OptimisticLockSyncTest). Column chooser — client-sidecolumnPrefsStore.ts/applyColumnPrefsapplied at mateu-table-crud's derivedeffectiveComponent(single choke point for all five renderers); identifier/action/select columns protected. Notification inbox — app class implementsNotificationsSupplier→AppDto.notificationsEnabled+ app-level_notifications-list/-read(ids list or "all") →Data{_notifications}; sharedmateu-notification-bellmounted in all five shells (NotificationsSyncTest). Guided import —ImportWizard<Row>archetype (upload@FileUpload/paste CSV → mapping grid with select editors → validation report → import valid rows); newFieldStereotype.fileUpload(file → data URI). Undo —Message.undoable(text, actionId, params)→ MessageDto undo fields → shared SSEService toast with Undo button dispatching on the initiator (UndoableMessageSyncTest). Session expiry — axios 401 interceptor →onSessionExpired/mateu-session-expiredevent with {retry, giveUp}; one retry, opt-in (sessionGuard.ts). Audit diff —AuditEntry.whencarries@GroupBy: the History dialog groups by save moment = version-by-version diff. Planning board —PlanningBoard/PlanningResource/PlanningBlock(resources × days tape chart, drag dispatchesmoveActionIdwith_blockId/_resourceId/_start/_end, clickselectActionId); demo/planning-demo. Global search — app class implementsGlobalSearchSupplier→AppDto.globalSearchEnabled+ app-level_globalsearch{searchText}→Data{_globalsearch}; the ⌘K palette (mateu-app) shows entity hits grouped by category under the menu results (GlobalSearchSyncTest). All app-level actions are exempted from menu resolution inActionInstanceCreator.isAppLevelAction. - Slow/unreliable network resilience (2026-08-03) — the transport layer's answer to "the backend is slow": almost all of it is frontend-only in
libs/mateu, so ONE implementation covers vaadin + redwood + every shell. Pure-logic modules (vitest-tested, singleton-module shape likeloopGuard/dirtyGuard):infra/http/requestPolicy.tsclassifies a transport failure into{kind, message, retryable}— kindsoffline|timeout|server|unauthorized|notFound|client|cancelled|unknown, user-facing English text instead of axios jargon (ERR_CANCELED→cancelled/silent, axios reusesECONNABORTEDfor its own TIMEOUT not for aborts, no-response+ERR_NETWORK→offline even when the flag claims online);infra/http/retryPolicy.tsdecides whether we MAY repeat:isIdempotentAction(actionId, declared)(ALWAYS_SAFE=''← the route load,mateu-uxfiresactionId: ''NOT__load__—,__load__,search,_globalsearch,_notifications-list; prefixessearch-,_appcontext-search-; the wire flag is an opt-IN that never opts a known read out;undefined≠''),shouldRetry(timeout|server only —offlineis deliberately excluded, reconnection isconnectivity's job), MAX_RETRIES 2,retryDelayMs300·3^(n-1) ±25% jitter;infra/http/connectivity.tstreatsnavigator.onLineas a hard NEGATIVE only (it lies on captive portals) and takes the positive from our own traffic (noteReachable/noteUnreachable), withwhenBack(cb)waiters;infra/ui/pendingActions.tsin-flight registry keyedcomponentId::actionIdwith a 120s stale valve. UI:infra/ui/pendingIndicator.tsmarks the pressed controldata-mateu-pending+aria-busyand pulses it via a stylesheet adopted into the element's OWN root (document-level CSS can't cross a shadow boundary) — it animates the HOST's opacity, NOT a::afterspinner: a pseudo-element on a shadow host is NOT PAINTED (verified on a livevaadin-button:inset:0;background:redrenders nothing), and vaadin/ui5/oj buttons are all shadow hosts;decorable()only decorates a closed list of button-like selectors so a container is never dimmed;originOf(e)usescomposedPath()[0](target is retargeted) and the origin ridesdetail._originElementacross bubbling re-dispatches. Wiring:mateu-component.requestActionCallToServerclaims the slot + marks the control (skipped entirely forbackgroundactions; reads claim NO exclusive slot — blocking them would drop the type-ahead search for "mad" while "ma" is in flight), releases onbackend-succeeded/failed/cancelled-eventfiltered at-target (those events bubble through from child components) and ondisconnectedCallback. Transport:wrap()now takes a THUNK so N retries = ONE reported outcome, marks the error__mateuReportedsoHttpService's catch stops raising a SECOND toast (pre-existing duplicate-toast bug), and puts{failure, retry}on the failed event;RunActionOptions{timeoutMillis, idempotent, retry}threadsmateu-component→server-side-action-requesteddetail →mateu-ux→ HttpService/SSEService →post(uri, data, timeoutMillis); theretryclosure re-entersHttpService.runAction, NOT the api client — a response nobody handles changes nothing on screen.mateu-api-caller: veil delay 300ms→600ms, scrim--lumo-base-color(was hardcoded white = a flash in dark mode), friendly text + a Retry control (new genericToastMessage.actionLabel/onActionin the Notifier port, implemented in BOTHneutralNotifierandVaadinNotifier— cleaner than piggybacking on undo).mateu-uxrenders a skeleton (400ms delay) only when a route has NO content yet (a re-load keeps stale content under the veil).mateu-connectivity-banner.ts= document-wide singleton strip mounted frommateu-ui(the composition root, next toregisterNeutralNotifier), always-light pastels, and it PUSHES the body down (padding-block-startfrom its measured height) instead of covering the page title. Wire:@Action(timeoutMillis, idempotent)→ fluentAction→ActionDto→Action.ts; ports .NET[ActionOptions(TimeoutMillis, Idempotent)](+WithActionOptionsin ReflectionMapper) and Python@action_options(...)(+with_action_optionsin mapper) — the ports had no@Actionequivalent, so the knobs got their own composable attribute/decorator. Tests: 35 vitest (requestPolicy/retryPolicy/connectivity/pendingActions),ActionsAndCommandsSyncTest(+2), Pythontest_action_options_travel_on_the_action_dto, .NETAction_options_travel_on_the_action_dto; golden JSON intest_sync_handler.py/test_ux_components.py/SyncHandlerTests.csgrew the two trailing fields.e2e/slow-network-probe.mjsis the real-browser harness (10 checks: skeleton, busy control, no double submit, banner, plain-language error, recovery announced, Retry re-runs AND applies the response, read-recovers-from-503, write-sent-exactly-once) — run it against any SUT app, exits non-zero on failure. Docs:ux-patterns/slow-connections.md+ the@Actionsection ofreference/key-annotations.md. - Accessibility (2026-08-03) — the generator is the only place a11y has to be right, so fixing it fixes every app at once; all of it is frontend, most in
libs/mateu. Measured baseline first: axe-core over 8 SUT routes found only 2 violations (both upstreamvaadin-tabs) because Vaadin's own components carry their field a11y — the real gaps were the ones axe CANNOT see. Shared helpers ininfra/a11y/:focusTrap.ts(trapFocus(container)→ move focus in + cycle Tab inside + restore on release;tabbablesWithinwalks OPEN SHADOW ROOTS and slots becausequerySelectorAllstops at the boundary and every overlay renders through nested custom elements;deepActiveElement/isInsideuse host-walking, notcontains),announcer.ts(ONE live region per politeness, created at BOOT — a region filled in the same tick it is created is frequently never announced —, hidden by clip not display:none, re-announces an identical string by clearing first),activate.ts(onActivate= Enter/Space like a native button, Space preventDefault'd, Enter not;nextIndexForKeyroving-tabindex arrows; pure, no lit import) andfocusStyles.ts(activatableFocusStyles, a CSSResult interpolated into each component'sstatic styles— a document rule would never cross the shadow boundary). Fase 1 (forms+overlays):mateu-field.tsnow setsinvalid/errorMessageON the control inupdated()(found by PROPERTY PROBING'invalid' in el, not by tag name) so the DS wiresaria-invalid/aria-describedbyinside its own shadow root — before, errors were a detached<ul>and a screen-reader user pressing Save heard NOTHING; the fallback<ul role="alert">only renders when the control has no validity state of its own (else it'd be read twice);mateu-component.focusFirstInvalidFieldmoves focus to the first rejected field (walks shadow roots, retries 3 frames because the flags are set by each field's ownupdated());mateu-dialoggainedrole="dialog"+aria-modal+aria-label(it had NONE) and both overlays trap+restore focus (panel located by[role="dialog"], NOT by class — the drawer's is.panel, the dialog's.dialog; modeless/layout drawers deliberately don't claim focus); error toasts arerole="alert"+assertive in the neutral notifier and explicitlyannounce()d in VaadinNotifier (vaadin-notification's overlay is not a live region). Fase 2 (keyboard): 25 sites across 19 components gotrole+tabindex="0"+@keydown=onActivate(sameExpr)(applied by a scripted transform, then hand-refined: faq→aria-expanded, task-queue→role=option/aria-selectedin arole=listbox, gantt bars→aria-labelwith title/dates/progress). Fase 3 (orientation):mateu-skip-link.ts(document-wide singleton prepended to body, hidden by transform NOT display:none — a display:none element isn't focusable —, finds the content by walking shadow roots because ahref="#id"can't cross them),role="main"on all 4 appRenderer shell variants,announce(document.title)on theSetWindowTitlecommand (the one reliable per-route signal every renderer gets), and focus-to-heading on route change inmateu-ux— gated to TOP-level ux + real route change + not the first load, or it would yank focus out of a field mid-edit. Fase 4 (net):e2e/tests/shared/accessibility.spec.ts— 8 axe routes + 8 behaviour tests (skip link is the first Tab stop, main landmark, live regions exist at boot, navigation announces, rejected save marks+focuses, drawer AND dialog own focus through 12 Tabs and give it back on Esc, no interactive role without a tab stop) running in the SHARED suite so all 5 adapters are covered;e2e/a11y-audit.mjs(npm run a11y) for ad-hoc sweeps. New SUT fixtureOverlayForm/OverlayContentForm(/overlays) — the SUT had no dialog/drawer to test against. Known carve-out, scoped to one rule on one element:vaadin-tabsisrole=tablistwith a<div part="tabs" tabindex="-1">in its OWN shadow root; axe flags it and only upstream can fix it. Testing note:libs/mateu's vitest default env staysnode; the two files whose SUBJECT is the DOM opt into jsdom with a// @vitest-environment jsdomdocblock (jsdom added as a devDependency). Docs:ux-patterns/accessibility.md. GOTCHA worth remembering: a::before/::afteron a shadow HOST is not painted — verified with aninset:0;background:redon a livevaadin-button— so any decoration of a DS control must animate the host's own properties (this is why the pending indicator pulses opacity instead of drawing a spinner). - Accessibility of the NATIVE renderers (2026-08-03) — same guarantees as the web, expressed in each platform's own API. Both started at literally ZERO (no
accessibility*prop anywhere in RN; noAccessibleContextcall anywhere in the plugin). React Native (src/a11y/a11y.ts):fieldA11ycomposes the control's NAME as"<label>, required, invalid: <error>"— RN has nolabelFor/aria-labelledby, so a<Text>label above aTextInputis unrelated to it and the field announces as a bare "text field"; and the error goes in the NAME not the hint, because hints are announced last and can be switched OFF in the OS settings;buttonA11y(role + optional label + state),modalA11y,headingA11y,announce(AccessibilityInfo.announceForAccessibility). Applied: onea11ydescriptor computed once inFormFieldRendererand spread onto all 7 TextInput branches + the custom widgets (neededdescriptionadded to itsFieldMeta);accessibilityRoleon 105 touchables across 14 files (a TouchableOpacity IS accessible and collects child text, but is NOT announced as a button without a role — scripted transform, skippingonPress={() => {}}no-op tap-swallowers); explicit labels on the icon-only ones (✕/➤/💬/‹/›/✎/📷);accessibilityViewIsModalon the 4 Modals;announce()on validation failure and onSetWindowTitleinMateuViewController.App.tsx's hardcoded backend port is nowEXPO_PUBLIC_MATEU_BACKEND_PORT-overridable (needed to point the app at the SUT for verification). IntelliJ plugin (ui/A11y.kt):accessibleName/accessibleDescription/JLabel.labelling(component)(setLabelFor + copies the name, minus the " *" marker) + reflectiveannounceviaAccessibleAnnouncerUtil(2022.3+, silent on failure). Applied at the ONE choke point inFormFieldRendererwhere caption and input meet:caption.labelling(input), required/@Helpfolded into the accessible DESCRIPTION (the asterisk is read as punctuation or skipped), the validation error appended to it, andinput.putClientProperty("mateu.fieldId", …)soAppContext.focusFirstInvalidFieldcan focus the rejected field;Buttons.renderButtonnames every Mateu button from the wire label.labellingalso names the first FOCUSABLE DESCENDANT — a date field is a textbox+button and a stepper is a JSpinner wrapping a formatted field, and the screen reader follows the FOCUS, so a name on the wrapper alone is never read (found via the probe: the inner JBTextField had no name). Two real bugs found and fixed while verifying: the date picker's trigger button hadisFocusable = false(browsing months was mouse-ONLY) and was named only on the icon-less fallback path. Verification tooling:RenderProbe.dumpnow printsa11yName/a11yDesc/labelFor(a control with no accessible name is indistinguishable from a correct one in a screenshot) and the gradle task forwards-Pprobe.baseUrl(without it nested loads hit the default backend and the probe reports an app shell with NO content — that cost me a wrong "0 labelFor" reading);e2e/rn-a11y-probe.mjsdrives expo web and asserts every control has a name, inputs are named, tappables are buttons and a rejected field carries the error in its name (RN-for-Web maps the props toaria-*, which is what makes this checkable without a device farm). Verified: plugin probe shows 4/4 fields bound with zero unnamed focusables; RN probe 5/5 with"Name, required, invalid: Cannot be empty". Docs: the Native renderers section ofux-patterns/accessibility.md. - Resiliencia de red en el renderer VB/Redwood (2026-08-03) — el port de lo de
libs/mateuaapps/redwood, que no comparte NADA de core con los renderers web: su transporte espoc/transport.mjsconfetchpelado, así que ninguna garantía se hereda. Nuevopoc/resilience.mjs(cuarto fichero de la fuente única, concatenado pormake-amd.mjsENTRE reduceContexts y transport — el orden importa, todo cae en un scope sin imports). Lo que cambia respecto a la versión axios, y que es la razón de que no sea copy-paste: fetch no tiene timeout (sin AbortController una petición cuelga para siempre), fetch no rechaza ante 4xx/5xx (resuelve conres.ok===false, hay que leer y ADJUNTAR el status al error o abajo no se distingue un 500 de un cable desenchufado) y un fallo de red es unTypeErrorgenérico sin código. Marca__mateuTimedOutpara distinguir un abort NUESTRO (cancelación silenciosa) del que disparó el timeout (sí es noticia);timeoutMillis < 0= SIN ceiling, para el stream de un LongTask (un 0/ausente usa el default de 60s — el primer intento puso-1y caía en el default, matando el stream).runMateuActionaplica el guard de doble envío con las lecturas exentas (mismo razonamiento que en web: el type-ahead). UI en la shell VB:setTransportHooks/connectivity.subscribese cablean enloadMateuShell.jsANTES del primer bootstrap (si el backend está caído al arrancar, se ve un mensaje en vez de pantalla muerta) y alimentan tres variables de app nuevas (mateuBusy/mateuOffline/mateuLastError) que pintan barra de ocupado (retardo de 500ms por ANIMACIÓN, no por temporizador), banda de sin-conexión sostenida y banda de error ya traducida + chaindismissMateuError. Tests:poc/test.mjspasa de 32 a 47; los 4 async necesitaron un runneratestSERIALIZADO — lanzados a la vez se pisan elglobalThis.fetchy el reintento de uno acaba hablando con el doble del siguiente (me pasó y costó ver). Verificación en navegador:e2e/vb-slow-network-probe.mjs(4/4 contra el renderer servido en :9006 con demo-vb en :9005). Tras tocarlo:npm run bridge && npm run build && npm run copy+mvn -pl shared/frontend/redwood install. NO portado (queda pendiente): esqueleto de navegación, indicador de ocupado en el control pulsado, y el control "Reintentar" en el error. - Accesibilidad del renderer VB/Redwood (2026-08-03) — como con la resiliencia, aquí NO se hereda nada: nuevo
poc/a11y.mjs(quinto fichero de la fuente única del bridge). Medido antes de escribir (axe sobre la app servida, 4 pantallas): la composiciónoj-sp-*sale casi limpia —los componentes traen su accesibilidad, igual que Vaadin— y el marcado propio tiene CERO clics sobre elementos no focusables, así que la Fase 2 de web aquí no aplica. Dos defectos REALES encontrados y arreglados: (1) las header actions eran ilegibles — losoj-buttonborderless heredan--oj-core-text-color-primary(tinta casi negra) sobre la cabecera OSCURA de Redwood, contraste 1.33 frente al 4.5 de AA, y son etiquetas de negocio (@AppActionsSupplier), no decoración; arreglado con CSS acotado aoj-sp-global-headerusando--oj-core-text-color-inverse(vacío en este tema → manda el fallback); (2) los iconos de colapsar del menú sin nombre —oj-navigation-listlos emite como<a role="button">VACÍOS; se nombran desde el texto de su grupo tras el refresh (nameCollapseIconsen loadMateuShell). Lo de SPA, que axe no ve:installAnnouncer(2 regiones vivas creadas al boot),mountSkipLink(primer hijo del body, oculto por transform NO display:none),role="main"enoj-vb-content, yannounceNavigationen onMateuNavigate. Dos gotchas que costaron: (a)focusContentencontraba un<h1> VACÍO(la shell lo pinta antes de que llegue el título) yfocus()no prendía en silencio — ahora filtra encabezados con texto Y COMPRUEBA que el foco prendió, cayendo al contenedor si no; (b) VB actualiza los bindings de forma ASÍNCRONA, así que al terminar la chain el contenido nuevo aún no está en el DOM y el foco prendía sobre el viejo y se perdía al reemplazarlo — de ahífocusContentSoon(reintenta hasta 12 frames). Y la regla que ya había aprendido en web: la primera carga NO mueve el foco, o el enlace de salto queda por detrás del punto de partida y el primer tabulador del usuario ya no lo alcanza. De 15 violaciones en 3 reglas a 3 en 1; la que queda es estructura interna de JET (el icono de colapsar es unrole=buttondentro de la lista) — carve-out acotado a esa regla en#mateuNavList. Verificación:e2e/vb-a11y-probe.mjs(10/10: axe en 4 pantallas + 6 de comportamiento). Ojo: las dos sondas VB se pisan si se lanzan seguidas justo tras reiniciar el serve — correr cada una por su lado. - Redwood/VB: lo visual de la resiliencia (2026-08-03) — las tres piezas que faltaban del port. Esqueleto de carga cuando la ruta aún no tiene contenido (
mateuBusy && !mateuHostTitle); con contenido ya pintado se conserva bajo la barra de ocupado (contenido viejo conserva el contexto, un esqueleto lo tira). Control pulsado marcado ocupado: seguido a nivel de DOCUMENTO (trackPressedControls, listener de click en captura + ventana de gracia de 400ms) y emparejado con los hooks del transporte, NO enhebrando el evento por cada chain — los botones pasan por chains distintas (toolbar, listado, wizard, isla) y cualquiera nueva se olvidaría; anima la opacidad del host, no un::after(misma razón que en web). Reintentar en la banda de error: guarda un DESCRIPTOR ({kind:'navigate',route}/{kind:'action',actionId,parameters}), no un cierre — un cierre atrapa elcontextde VB de la ejecución que falló y para cuando el usuario pulsa ya no sirve: la llamada no hace nada y falla EN SILENCIO (me pasó); el de navegación llama aonMateuNavigatecon contexto fresco, el de acción viaja como evento de aplicación NUEVOmateuRetryActionporque la acción vive en la página de contenido y no se puede llamar su chain desde la shell. GOTCHA que costó dos vueltas: los eventListeners de esta app usan la clave"chain", no"chainId", y las chains NO se declaran en un mapachains(VB las resuelve por fichero en*-chains/) — mis dos primeros listeners usabanchainId+ un mapa inventado y NUNCA se disparaban, aunque el botón se pintara (el test sólo comprobaba que existía). Sondae2e/vb-slow-network-probe.mjsampliada a 8/8;vb-a11y-probe.mjssigue 10/10. - FABs (
@Fab): annotate methods with@Fab(icon="vaadin:plus", label="...", order=0)to create floating action buttons. At app level (@UIclass), FABs appear globally stacked above the AI FAB atright: 1.5rem. At page level (any page class), FABs appear stacked atright: 5.5remand are scoped to that page. FAB actions are dispatched via the standardaction-requestedevent mechanism.FabDtois the wire type;Fab.tsis the frontend TS interface. - Page banners (
@Banner/BannerSupplier): show messages below the page header and above the first form section, rendered asvaadin-card. Two approaches — declarative: annotate methods with@Banner(theme=BannerTheme.INFO, title="...")(method may returnStringfor a dynamic description); programmatic: implementBannerSupplier.banners()returningList<PageBanner>(takes precedence over annotations, same pattern asToolbarSupplier/ButtonsSupplier). Themes:INFO(blue),SUCCESS(green),WARNING(amber),DANGER(red). Wire type:BannerDtovia existingPageDto.bannersfield; frontend rendering inmateu-page.ts. Dark mode: banner backgrounds are always light pastels so text and title slot must usecolor: #1a1a1aexplicitly — CSS shadow rules don't reach theslot="title"light DOM child, so the color is applied inline on the span. Extra options on@Banner:closeable = trueadds a dismiss button;timeoutSeconds = Nauto-dismisses after N seconds. Both also work onPageBannerconstructor fields. - Action-returned banners: action methods (e.g.
@Toolbar) can returnPageBanner,List<PageBanner>, orPageBannersto show banners on the current page dynamically. They are carried inUIIncrementDto.banners, dispatched viapage-banners-receivedDOM event, and shown alongside the static@Bannerbanners inmateu-page.ts. Replace vs append: returning a barePageBanner/List<PageBanner>replaces all existing action banners (default). UsePageBanners.replace(banner…)for explicit replace orPageBanners.append(banner…)to accumulate banners across multiple action calls. Action banners are automatically cleared when the user navigates to a different page (i.e. whenmateu-page.componentchanges). Implementation note:PageBannerandPageBannersare excluded fromFragmentListMapper— returning them from an action produces only banner DTOs, never a spurious UI fragment. - Expression interpolation in labels: any string attribute that accepts a label or title supports
${...}template expressions evaluated against the currentstateanddatacontext. Supported locations: tab labels, section titles (@Section), subsection titles, field labels, accordion panel labels, button labels, toolbar button labels, CRUD titles/subtitles, column header labels (including group headers), filter bar active-filter badges, banner title/description, and KPI titles. Page titles/subtitles/KPI text already usedpossiblyHtml()which supports the same syntax. Example:@Tab("${state.nombre} — Details")or@Section("Customer: ${state.customerName}"). - CRUD URL pagination: navigating directly to a URL with
?page=N,?sort=field:asc, or filter query params now correctly loads that page/sort/filters instead of always showing page 0. The frontend reads the URL params after component init and triggers a new search if any param differs from the default. - Page-level badges (
@BadgeInHeader/BadgeSupplier): show small status chips in the page header strip (rendered bymateu-content-header.tsviaFormDto.badges). Important:@Badgeis NOT the header badge — it is a shorthand for@Stereotype(FieldStereotype.badge)which renders a boolean chip inside the form body. The header-strip annotation is@BadgeInHeader, placed on fields (not methods). Two approaches — declarative: annotate a boolean or String field with@BadgeInHeader(label="...", color="success"). For boolean fields the badge is shown when the value istrue, text =label(or field name if empty). For String fields the field value is used as badge text;null/blank hides the badge. Fields annotated with@BadgeInHeaderare automatically excluded from the form body byFormFieldFilter. Programmatic: implementBadgeSupplier.badges()returningList<Badge>(takes precedence over@BadgeInHeaderfields, same pattern asBannerSupplier). Colors follow Vaadin Lumo badge themes (normal,success,error,warning,contrast). Pipeline:PageMetadataExtractor.getBadges()→ReflectionPageMapper→PageView.badges→PageMapper→FormDto.badges→ frontend. Important timing note: badges are part of the component metadata (built once on initial render), so they must be based on data available at that time. @Inlinefields with actions (@Toolbar/@Buttonon nested types): annotating a field with@Inlineexpands the nested type's fields directly into the parent@Sectioncard without adding aCardwrapper. If the nested class has methods annotated with@Toolbar, those buttons appear on the same row as the section title (title left, buttons right, rendered viaSectionFormRenderer.buildTitleRow). Methods annotated with@Buttonappear below the section content as a right-aligned button row. Action dispatch follows the"nested-form-action-<fieldName>-<methodName>"prefix, handled byRunMethodActionRunner. Button labels respect@Label(viaFieldMetadataExtractor.getLabel(method)). The nested class must carry its own class-level annotations (@PlainText,@Compact, etc.) since they are not inherited from the parent form. Use@Inlineon dense, multi-section screens (@Compact+@Zones) where the extra card chrome of a non-inline subform would add visual noise.@Inlineon embedded orchestrator fields (MultiViewsubclasses, e.g.AutoEditableView): when the host field is annotated@Inline, the embedded mediator drops its badges/kpis, demotes its title fromh2toh3(so it nests visually under the host@Section/@Tab), and, when the inner form has a single section, drops the outlined Card wrapper around it. The parent@Sectionthat hosts an@Inlineembedded mediator also drops its own title row so the two don't visually compete — the embeddedh3title + toolbar buttons render as a single coherent row, with the parent card providing the framing. Use this when the embedded view lives inside a host tab or section that should own the framing (e.g. an editablePersonalDataViewinside a "Datos personales" section, or a read-onlyCardexViewinside an "Info Cardex" tab). Mechanism:EmbeddedOrchestratorFieldBuilderappends_inline=1next to_embeddedMediator=1on the marked route and seeds it intoinitialData;EditableViewcallsisInline(httpRequest)to setPageView.level=1and drop badges/kpis;SectionFormRenderer.render()skips theCard outlinedwrap on the single-section path whenEmbeddedOrchestratorFieldBuilder.isInlineRequest(httpRequest)is true;SectionFormRenderer.renderSections()hides the parent section title viahostsInlineEmbeddedMediator(). For tabs (which don't carry their own title row), the embeddedh3becomes the only visible title — remove@Titlefrom the inner model to suppress it entirely.- Multi-state embedded islands (backend-driven state machines) + island state seeding: an
@InlineembeddedEditableViewwhoseview(...)returns a DIFFERENT model per backend state is the pattern for an in-page element with N server-decided states (demo:demo-front-officeDocumentoViewinsideIdentidadStep— 3 states: sin datos →@Notice(theme="warning")String + scanButton; hay datos →@Section(propertyList=true, frameless=true)property list + the built-in Edit toolbar button; editor → the standard editor/save cycle landing back on view). Framework pieces that make it work: (1) island initialData seeding —EmbeddedOrchestratorFieldBuilder.build(...)takes the HOST instance andseedInstanceState(...)copies the field VALUE's simple fields (String/Number/Boolean/primitive/enum, non-null; e.g. a configuredstayId) into the island'sinitialData, so the host passes context by just setting fields on the orchestrator instance (documento = new DocumentoView(); documento.setStayId(...)inload()); (2)mateu-ux.initialState— the MEDIATOR branch ofappRenderer.tsfeeds the fragment state into the innermateu-ux, whose initial__load__(and route-flip reloads) now send it ascomponentStateinstead of loading empty — without it the island's first render had no seeded state; (3) the async leg (scan) is aLongTaskSSE action (Action.sse(true)advertised inactions()) whose.withCommand(UICommand.dispatchEvent("evento"))fires the bus event that a@SubscribeToon the LOADED MODEL (the empty-state class, not the orchestrator) turns into the island's reload action — which uses the standard route-flip (setRouteTo(flip ? "/view" : "/")+return new State(this)) to force the embedded mediator to re-render; (4) hide the Edit button in the empty state by overridingreadOnly(). Also fixed while building this:ComponentToFragmentDtoMapper'sComponentTreeSupplierbranch now honorsDtoSupplierfirst (an orchestrator dropped into a composite component tree used to throw viacomponent()). Tests:EmbeddedIslandStateSeedingSyncTest. Gotcha: the host is re-created per request, so it must derive its own context (e.g. the id from the route) inload()before seeding the island. Re-pointing the island at another entity (pax selector band, 2026-07-16): the same bus re-targets the island — the host band (IdentidadStep.registroPax, aNoticewhosethemetracks completeness success/warning, customicon👥,content= oneButtonper pax labeledk/N,color=successwhen that pax's data is in,buttonStyle=primaryfor the selected one,Button.parameters=Map.of("paxIndex", k)) dispatchesselectPaxon the wizard, which returnsList.of(this, UICommand.dispatchEvent("pax-seleccionado", Map.of("paxIndex", n)))(an action CAN return state + commands in one Collection —FragmentListMappermaps the fragments,CommandMappercollects theUICommands); every island model carries@SubscribeTo(event="pax-seleccionado", action="cambiarPax")and the handler readspaxIndexfrom the event-detail parameters and route-flips. The wizard itself also subscribes to the island'sdocumento-escaneadoto re-render the band colors. A custommateu-noticeicon (emoji) renders at natural size without the severity circle (.icon.custom). Domain:Companioncarries document/verified/email/phone (identityComplete()),Stay.companionAt(paxNumber)/registerCompanion(paxNumber, c)pad gaps withCompanion.pending(n); pax 1 = the Guest aggregate (uniform access viaDocumentoView's privatePaxinterface). - High-density mode (
@Compact): annotate a page class with@Compactto render it in condensed mode — smaller control heights, tighter spacing, and smaller field labels — so information-dense screens fit without scrolling. Implemented by injecting theStyleConstants.COMPACTCSS custom-property overrides onto the page container; because CSS custom properties cascade through shadow DOM, every Vaadin/Lumo component inside is automatically condensed. Font size is intentionally left at the normal Lumo value so text stays legible. The annotation also shrinks the auto-responsive form-layout minimum column width to7em(vs the standard default), allowing more columns to fit at the same viewport width. Additionally,@Compactpages emit acompact-changedevent frommateu-pageon render;mateu-applistens and adds ano-paddingCSS class to theapp-contentelement, removing the standard content area padding so the form fills edge-to-edge. Combine with@Zones/@Style(StyleConstants.FULL_WIDTH_WITH_PADDING)for data-heavy operational screens (e.g. hotel check-in forms). On grids/tables the flag also applies Vaadin's built-incompactrow theme.StyleConstants.COMPACTincludes a--mateu-compact:1CSS custom property marker used by frontend components to detect compact mode. Opt-in and non-breaking — pages without@Compactare unaffected. Alternatively, composeStyleConstants.COMPACTdirectly via@Stylewhen you need compact mode on only a part of the page, or want to blend it with other style constants. @PlainTextat class level:@PlainTextcan be applied at the class level (in addition to field level) to make all fields in that class render as plain read-only text. This is particularly useful on inline nested types (@Inline) or wizard result steps where every field should be display-only without input chrome.@Multiline: annotate a@PlainTextfield (or a class) with@Multilineto allow the plain-text content to wrap across multiple lines instead of truncating with an ellipsis. Has no effect on non-plain-text fields.AppVariant.AUTOheuristic:@App(AppVariant.AUTO)(which is now the default sincevalue()defaults toAUTO) auto-selects the app shell variant. The rule lives inAppMetadataExtractor.getVariant(...): an explicit non-AUTO@App(...)value always wins; otherwise, when the menu hasMenuitems — a deep menu (any top-levelMenuwith a nestedMenusubmenu) →TILES; more than 7 top-level items →HAMBURGUER_MENU; else →MENU_ON_TOP. When there are noMenuitems at all →TABS. Adjust the thresholds/selection there.AppLayouton@App: the@Appannotation has a second attributelayoutof typeAppLayout(defaultSINGLE_SLOT).AppLayout.SPLITrenders the content area as a two-pane split layout. This is distinct fromAppVariant(which controls navigation chrome) —layoutcontrols how the page content area itself is arranged.@AutoSave: annotate a page class with@AutoSave(debounceMillis=800, action="save")to automatically invoke the named action method whenever the user changes a field value. The call is debounced: the framework waits until the user has been idle fordebounceMillisms before dispatching. Default action name is"save". Useful for settings screens and draft editors where explicit save buttons are unwanted.- Keyboard shortcuts: any action method can be bound to a keyboard shortcut via
@Action(shortcut="ctrl+s")(works alongside@Toolbar/@Buttonon the same method). The shortcut string is+-separated modifiers and key (ctrl,alt,shift,meta). Shortcuts work inside subforms (nested types): the action is collected into the component'sactionslist with the nested id (nested-form-action-<field>-<method>) byActionMapper.addNestedFormsActions→FieldActionCollector(which preserves the@Actionshortcut), andmateu-component._keydownListenerscans that list.@Action(runOnEnter=true)is equivalent toshortcut="enter". The matchermateu-component._shortcutMatchesEventmatches bye.keyore.code(KeyX/DigitX/NumpadX), so modifier+letter/digit shortcuts are keyboard-layout independent (important on e.g. Spanish layouts whereCtrl+Alt+<letter>/AltGr remapse.keyto a symbol) and the numeric keypad works. The button's shortcut also shows as atitletooltip (buttonRenderer.ts) — for whichButtonMapperpropagatesButton.shortcuttoButtonDto. Demo: every action onCheckInFormV2(/checkin/:id/v2) is bound to aCtrl+Alt+<letter>shortcut across its (shared and V2-specific) section classes. - Tab keyboard shortcuts (
@Tab(shortcut="alt+1")): select a tab by keyboard, same shortcut syntax as@Action. Pipeline:Tab.shortcut()(uidl annotation) →FormLayoutBuildersetsio.mateu.uidl.data.Tab.shortcut(fluent record, new field) →TabMapper→TabDto.shortcut→ frontendTab.ts.shortcut. The frontend emits it as adata-shortcutattribute on each<vaadin-tab>(renderLayouts.ts renderTabLayout);mateu-component._handleTabShortcut(called first in_keydownListener) does a pure DOM lookup (_collectShortcutTabs()—this.renderRoot.querySelectorAll('vaadin-tab[data-shortcut]')plus the shadow roots of anymateu-drawer/mateu-dialogin the render root, sincequerySelectorAlldoes not pierce an overlay's shadow boundary — so tab shortcuts also work when the tab strip is inside a drawer/dialog opened by the component), matches via the shared_shortcutMatchesEvent, and sets the enclosingvaadin-tabs.selectedto the tab's index — in-place, no server round-trip. Gotcha: tabs are grouped by consecutive fields sharing the same@Tabname within a section; putting a@Sectionon each tab's fields splits the form into several separate one-tab strips (each its ownvaadin-tabsheet), so for one tab strip don't mix per-tab@Section. Demo:demo-admin-panel/.../tabs/TabsShortcutDemo.java(/tabs-shortcuts). User docs:doc/.../ux-patterns/keyboard-shortcuts.md. - Default open tab (
@Tab(open=true)): by default a tab strip selects the first-declared tab on first render; mark a different@Tabwithopen=trueto make it the initial selection instead (independent ofshortcut, which only selects on demand; if several tabs in one strip declareopen=true, the first wins). Pipeline:@Tab.open()(uidl annotation) →FormLayoutBuildersetsio.mateu.uidl.data.Tab.active(fluent record field; also settable when building aTabLayoutdynamically) →TabMapper→TabDto.active(the DTO field already existed, previously always false) → frontendTab.ts.active;renderLayouts.ts renderTabLayoutcomputesactiveIndex(first child whose metadata.active, else 0) and its@items-changedhandler setsvaadin-tabs.selected = activeIndex(both theadaptableand plainvaadin-tabsheetbranches share the one handler). Additive + backward-compatible (opendefaults false → previous first-tab behaviour). Tests:LayoutSyncTest.tabMarkedOpenIsActiveOnTheWireAndOthersAreNot. User docs:doc/.../ux-patterns/keyboard-shortcuts.md,reference/key-annotations.md. Ported to C# and Python (same wireactive,opendefaults false): .NETTabAttribute.Open→ReflectionMapper.TabLayoutcomputes the active index (also fluentTabPanel.Activehonored inComponentMapper) →TabMetadataDto.Active; PythonTab(open=...)→mapper.tab_layout→TabMetadata.active. Port tests: .NETLayoutInferenceTests.Tab_marked_open_is_active_on_the_wire_and_others_are_not, Pythontest_tab_marked_open_is_active_on_the_wire_and_others_are_not. Demo: the check-in drawer variantCheckInFormV4(/checkin/:id/v4) +CheckInReferenceDrawer— the essential check-in stays on the page; a single@Toolbarbutton (ctrl+alt+d) opens a modelessDrawerwhose content is aModelViewComponent-wrapped tabbed panel (the same section components as v2) with the Cardex tabopen=trueand per-tabalt+1..9shortcuts. Contrast with v2 (stacked + sticky@Toc) and v1/v3 (@Zonesmaster-detail). - Unsaved-changes navigation guard (
@ConfirmOnNavigationIfDirty): annotate a form class to warn the user before they leave it with unsaved changes. The confirmation covers every way of leaving: in-app menu navigation, browser back/forward (popstate, with URL restore on cancel), and reloading/closing the tab (beforeunload). CRUD create/edit views opt in automatically. Control the state programmatically by returningUICommand.markAsDirty()/UICommand.markAsClean()from any action (typicallymarkAsClean()after a successful save; a backend-drivenNavigateTomarks clean instead of prompting). Frontend architecture (centralized in commit "centralize unsaved-changes navigation guard"):dirtyGuard.tsis the single source of truth — it owns the dirty flag, wires the document-level dirty/clean listeners once, installs thebeforeunloadguard, and exposesconfirmLeave().mateu-app(route selection) andmateu-ui(browser back/forward) both delegate to it;mateu-componentresets the dirty flag when a tracked form (re)loads, tied to the lifecycle that rebuildsformerState(so reset no longer depends on the backend sendingMarkAsClean). Annotation lives inuidl(ConfirmOnNavigationIfDirty.java), surfaced viaServerSideComponentDto. Documented for users indoc/.../reference/key-annotations.md,ux-patterns/partial-forms.md, andfluent-components/fluent-commands.md.EditableView.navigateauto-cleans on/view: when anEditableView'ssaveorcancel-editlands on/view,navigate(...)automatically appendsUICommand.markAsClean()to the response. Required because the unmount of the edit-modemateu-componentdoes NOT fireclean(that lifecycle hook fires only when the new component is tracked, and view mode is not tracked); without the explicit command the global dirty flag would stay set after save/cancel and the host page would keep prompting on every navigation. Same pattern AutoCrud uses after a persist. - Money formatting on read-only fields (
@Stereotype(FieldStereotype.money)): mark a numeric field (BigDecimal,double, etc.) with@Stereotype(FieldStereotype.money)so it renders as a formatted currency amount. In a plain-text context (the field or its declaring class is@PlainText) the field keeps the dense plain-text rendering but is taggedFieldDataType.moneyso the front-end formats the value (thousands separator + 2 decimals viaIntl.NumberFormat,de-DEby default;Amountvalues use their ownlocale/currency). Logic inFieldTypeMapper(getDataType/getStereotype, helpersisMoneyStereotype/isPlainTextContext): the money stereotype yieldsplainTextfor layout whiledataType=moneycarries the formatting intent. Front-end formatting is in theplainTextbranch ofmateu-field.ts.Amount-typed fields already getdataType=moneyautomatically. Used in the check-in demo (FoliosSection: límite crédito, entrega a cuenta, saldo pendiente). - Inter-component communication (
@SubscribeTo/@Emits+UICommand.dispatchEvent): components talk to each other by emitting named custom events and subscribing to them. Emit: returnUICommand.dispatchEvent(eventName)ordispatchEvent(eventName, payload)from any action; the frontend (ConnectedElement.applyCommand,DispatchEventbranch) dispatches a realbubbles+composedDOMCustomEventfrom the emitting component element, stampingdetail.__sourcewith the emitter's logical name (@Emits(name=...), falling back toserverSideType) — only on object payloads, so legacy events keep their shape. Subscribe: annotate a class with@SubscribeTo(event=..., action=..., source=..., from=..., condition=...)(repeatable via@SubscribesTo); when the event fires Mateu runsactionserver-side on that component passingevent.detailas parameters. Scope =SubscriptionSource:DOCUMENT(default — global bus, the listener is attached todocumentso it reaches sibling/unrelated components),COMPONENT(listens ondocumentbut filters bydetail.__source === from),SELF(legacy: listens on the component's own element, only catches events bubbling up from descendants). A raw@Trigger(type=OnCustomEvent)maps toSELF(backward compatible). Pipeline:@SubscribeTo/@Emits→TriggerMapper/EmitsMapper→OnCustomEventTriggerDto(source,from)+ServerSideComponentDto.emitsName→ frontendComponentElement.registerCustomEventListeners()(attaches todocumentorthisper scope; removed indisconnectedCallback) +customEventManager(filters by__source, onlystopPropagationfor SELF).@Emitsis mostly declarative; its only runtime effect is supplying thenamestamped as__source. Used in the check-in demo:GuestsSection(@Emits(name="guests-section")) emitscheckin-confirmed;CheckInForm(@SubscribeTo(event="checkin-confirmed", action="load", source=DOCUMENT)) refreshes itself in place instead of navigating away. - Drawer overlay (
Drawer+UICommand.closeModal(eventName[, payload])): return aDrawerfrom any action to open itscontent(typically a form) in a panel sliding in from a viewport edge — the native side-panel counterpart ofDialog. Record inuidl/data/Drawer.java:headerTitle,header,content,footer,position(DrawerPosition.start|end, default end),width,noPadding,modeless(no backdrop),initialData. Pipeline mirrors Dialog exactly:DrawerMapperinOverlayComponentDispatcher→DrawerDto(registered inComponentMetadataDtosubtypes +YamlUidlMapperFactory) → fragment emitted with action Add (FragmentDataSerializer.isOverlay, shared with dialogs) so it stacks on the page instead of replacing it → sharedmateu-drawer.tsLitElement (design-system neutral: Lumo vars with fallbacks; slide transition, backdrop, header ✕, Esc closes only the topmost overlay in its root) viadrawerRenderer.ts; sapui5/redwood-oj/redhat claim it in SUPPORTED_TYPES and fall through to the shared switch. Close-with-result contract:UICommand.closeModal()closes the topmost overlay (dialog OR drawer —ConnectedElement.closeModalqueriesmateu-dialog, mateu-drawerand closes the last in DOM order);closeModal(eventName)/closeModal(eventName, payload)additionally emit the named custom event through the standard@SubscribeTobus (frontend: the CloseModal branch calls the samedispatchNamedEventhelper as DispatchEvent, stamping__source), so the host page refreshes in place or receives the overlay's result as action parameters. Closing via ✕/Esc/backdrop emits nothing (the "dismissed without saving" path). Overlays nest; each close unwinds only the topmost. Tests:DrawerSyncTest. Demo:demo-admin-panel/.../drawer/DrawerDemo.java(/drawer-demo) — toolbar action opens a contact editor in a drawer; save persists, closes withcloseModal("contact-saved", payload)and the subscribed host reloads. User docs:doc/.../ux-patterns/drawer.md. - Grid row selection (
@OnRowSelected): annotate a grid list field (@Stereotype(FieldStereotype.grid)) with@OnRowSelected("methodName")to run a developer method when the user selects (clicks) a row. The clicked row is auto-injected into a method parameter typed as the row class (HttpRequest.getClickedRow(...), injected byRunMethodActionRunner.createParameters). Works on read-only grids (unlike the default<fieldId>_selectedCRUD detail-edit path, which is also broken for nested grids), so it's the way to build master/detail. Pipeline:GridColumnBuilder.getOnItemSelectionActionId()setsFormField.onItemSelectionActionIdto the routed action id — bare method name at page level, ornested-form-action-<prefix><method>when the grid is inside a nested@Inlinesection (e.g.nested-form-action-guestList-onGuestSelected); the frontendmateu-grid.tsactive-item-changedhandler (fires on row click regardless of the selection column / read-only) dispatchesaction-requestedwithparameters: { _clickedRow: item }. The action is auto-registered in the component'sactionslist byFieldActionCollector(for each@OnRowSelectedfield) — required becausemateu-component.manageActionRequestedEventonly sends an action to the server if it is inactions; otherwise it bubbles unclaimed and is dropped. Commonly combined with@Emits/UICommand.dispatchEvent+ a@SubscribeToto update another panel. Keyboard row selection:@OnRowSelected(value=…, shortcut="ctrl+shift")lets the user select a row by position — the base combo plus a digit selects that row (ctrl+shift+1→ first row … ninth; top-row or numeric keypad, matched viae.code). Pipeline:@OnRowSelected.shortcut()→GridColumnBuilder.getRowSelectionShortcut→FormField.rowSelectionShortcut→FormFieldDto→mateu-grid.ts, whose documentkeydownhandler resolves the row and calls the sharedselectRow(item)(same path as a click: setsselectedItems+ dispatchesaction-requestedwith_clickedRow). Demo:GuestsSection.guestsis@OnRowSelected(value="onGuestSelected", shortcut="ctrl+shift")→ emitspax-selected(Ctrl+Shift+N reloads the cardex with the N-th guest). - Self-reloading embedded component (master/detail via events): to make a panel reload itself (not the whole page) when an event fires, extract it into its own class and embed it as an independent
ServerSideComponentby making it aMultiView(e.g. a read-onlyAutoEditableView<T>— see the embedded-orchestrator field mechanism). Three rules make the self-reload work: (1) put@SubscribeToon the loaded entity (the model view), not the orchestrator —MultiView.wrapViewmaps the embedded component's triggers from the loaded entity; (2) advertise the reload action by overridingactions(HttpRequest)to add it (so the embedded component claims it and routes it tohandleAction); (3) inhandleAction, after updating the (static, demo) holder, alternate the always-view route (setRouteTo(flip ? "/view" : "/")) andreturn new State(this)— the embedded mediator only re-renders on a route change, so a fixed route updates on the first reload only. Demo:Cardex(@SubscribeTo("pax-selected", action="reloadPax")) +CardexView extends AutoEditableView<Cardex>(@UI,@ReadOnly) embedded inCheckInForm; selecting a guest row reloads only the cardex with that pax's full data. Documented indoc/.../ux-patterns/component-communication.md. (A cleaner long-term fix would force the embedded mediator to re-render on every action regardless of route change, removing the route-flip workaround.) - Layout inference (
@AutoLayout): annotate a class with@AutoLayout(uidl; composable;@AutoLayout(false)opts out when themateu.layout.inferencesystem property enables it globally) and Mateu infers the UX patterns from the amount and structure of the declared information — the developer only declares data; explicit layout annotations always win. Decision table incore/.../componentmapper/LayoutInference.java(the REFERENCE for the C#/Python ports — same rules and thresholds so the wire JSON stays identical): field weight units (textarea/richText/html/markdown/image/uploadableImage=4, grid=6, radio/checkbox=2, array/component=6, else 1); fold-optionals — editable form, single unnamed section, no tab/inline/composition/component fields, weight > 16, ≥1 required and ≥4 optional → required fields stay visible, optionals collapse into a one-panelAccordionLayoutlabeled "More options" (SectionFormRenderer.buildSectionBody); sections→tabs — read-only view (ctx readOnly or class@ReadOnly), ≥5 sections, weight ≥ 30, no sticky section and no explicit@Toc(@Zones/@FoldedLayoutchecked earlier) → one tab per section (SectionFormRenderer.tabsFromSections, id_tabs); small-enum→radio — enum with ≤4 constants renders as radio buttons (FieldTypeMapper;@UseRadioButtonsforces radio at any size — that annotation was dead until 2026-07-05: it had no@Retention(RUNTIME)). Wire:TabLayoutDto(and fluentTabLayout) carrygroupRelationship(alternative/sequential/simultaneous— semantic relation between the groups; dev-declared tabs always emitalternative) andadaptable(true under inference → renderers may degrade tabs to an accordion on narrow viewports without losing the disclosure semantics). Tests:LayoutInferenceSyncTest(also asserts non-@AutoLayoutclasses keep the previous behaviour). - Page-level inference (
@AutoPage+ArchetypeAdvisor, 2026-07-21): the page-altitude sibling of@AutoLayout(design plan:design/page-level-inference-plan.md). Fase 0 (always on):ArchetypeAdvisor(componentmapper, hooked atPageTypeResolver's shape-fallback branches) logs a one-time INFO hint when a plain reflected form structurally resembles an archetype — ≥1MetricCardfield → "looks like a Dashboard"; aListfield with@OnRowSelectednext to a component-holder field → "looks like a CollectionDetail";@PageTemplatesilences it. Fase 1 (opt-in@AutoPage, uidl, composable; also enabled by themateu.layout.inferenceproperty with@AutoPage(false)opt-out): fully-derivable shapes stop advising and COMPOSE —PageInference.composesDashboard(enabled + ≥1 MetricCard field + NOT a Component/Crud/Listing) makesReflectionUiIncrementMapper.mapsubstitute the instance with theInferredDashboardbridge (same pattern asAdaptedComponentTree, right after the adapter branch): it advertises the MODEL asserverSideType(actions keep routing), carries@PageTemplate(DASHBOARD)for the wirepageType, honors the model's@Style, and composes viaDashboardComposer— theDashboardarchetype's composition extracted so subclassing and inference share one implementation (Dashboard.component()now delegates; behavior pinned byArchetypesSyncTest). Welcome rule (same day):PageInference.composesWelcome— ≥1Buttonfield AND all fields presentational (Button/Component/holder; ONE data field keeps it a form) →InferredWelcomebridge (WelcomeComposerextracted likewise; hero title derived from the class@Title, subtitle/image stay subclass-only); dashboard checked first (stronger signal); advisor stands down viaPageInference.composes. Only fully-derivable archetypes compose; shapes needing undeclared suppliers (CollectionDetail id/title functions) stay advisory. Demo/auto-dashboard-demo(InferredOpsDashboard, demo-admin-panel). Fase 2 (2026-07-21): full ports — .NET[AutoPage]+PageInference(Mateu.Core; compositions extracted to publicArchetypeComposersin Archetypes.cs, MapView composes at the tree-supplier branch,PageTypeOfgained welcome→landing; testsAutoPageTests) and Python@auto_page+mateu_core/page_inference.py(component_treecomposes via the existingcompose_dashboard/compose_welcome, made tolerant of plain instances;page_type_ofgained welcome→landing; teststest_auto_page.py); JavaPageTypeResolvergained the same welcome→landing branch for resolver/fingerprint consistency. CI tooling:PageFingerprint(componentmapper) — stable"pageType=… composes=…"per class + sorted batch form, meant for golden tests that fail on inferred flips (testsPageFingerprintTest; docs in layout-inference.md "Guarding against flips in CI"). Tests:ArchetypeAdvisorTest,AutoPageSyncTest. Docs:ux-patterns/layout-inference.md(Page-level inference section) + the-mateu-way.md. - Semantic (composed) annotations: any Mateu field, method or class annotation can be used as a meta-annotation to build a single domain annotation that bundles configuration — e.g.
@Lookup(search=…, label=…) @interface ProveedorId {}then@ProveedorId String proveedorId;, or@Stereotype(money) @Label(…) @Help(…) @interface ImporteTotal {}, or method@Toolbar @Label("Guardar") @interface AccionGuardar {}, or class@Compact @interface PantallaCompacta {}. Resolved byio.mateu.core.infra.reflection.MetaAnnotations.find/isPresent(element, X.class)(like Spring'sfindMergedAnnotation, minimal — first match, no@AliasFor). Every framework annotation read in core goes throughMetaAnnotationsand every field/method/class uidl annotation carriesElementType.ANNOTATION_TYPEin its@Target. Exception: routing annotations (@UI/@Route/@Routes/@HomeRoute) are NOT composable — they're resolved by the annotation processor at compile time (not meta-aware), so their reads stayaClass.getAnnotation(...)directly. To make a NEW annotation composable: addANNOTATION_TYPEto its@Targetand read it viaMetaAnnotations. Demo:demo-admin-panel/.../proveedores/(/proveedor-demo). User docs:doc/.../java-ui-definition/annotations/semantic-annotations.md. - Component adapters (
ComponentAdapter<T>SPI): lets a developer render an arbitrary domain object that is NOT a Mateu component and carries no Mateu form annotations, and round-trip it back from state. Interface inuidl/.../interfaces/ComponentAdapter.java:type(),adapt(T, HttpRequest) → AdaptedView,deserialize(Map state, HttpRequest) → T.AdaptedView(uidl/.../data/AdaptedView.java) bundlescomponents+state+data+actions(action ids the view exposes). Register the adapter as a bean (@Service); discovered viaMateuBeanProvider.getBeans(ComponentAdapter.class). Pipeline:AdapterRegistry(coreinfra/adapters/) finds the adapter by type;ReflectionUiIncrementMapper.mapsubstitutes the instance with anAdaptedComponentTreebridge (implementsComponentTreeSupplier+StateSupplier+DataSupplier+ActionSupplier) so the adapter's components/state/data/actions flow through the normal mappers unchanged; the bridge advertises the model's type name asserverSideType(via the newComponentTreeSupplier.serverSideType()default) so state routes back.AdapterInstanceFactory(InstanceFactory, priority 100) wins for adapted types and callsdeserializeto rebuild the model from the incoming state — this also builds the initial route instance from an empty state, sodeserializemust guard each assignment withstate.containsKey(...)to preserve the model's field initializers. Top-level: give the model@UI("/route")(routing only — the adapter owns the whole UI) + field initializers to seed the initial view. Nested: an adapted-type field of a normal form is rendered as an independent island —ReflectionFormFieldMapperwrapsnew AdaptedComponentTree(value, adapter)in aCustomField, whichComponentToFragmentDtoMappermaps to its ownServerSideComponentDto(own serverSideType+state+actions), so its buttons round-trip through the adapter on their own (a real secondmateu-componentboundary). Demo:demo-admin-panel/.../adapters/—Pedido(@UI("/adapter-demo"), plain POJO) +PedidoAdapter+AdapterNestedDemo(/adapter-nested-demo). User docs:doc/.../java-ui-definition/interfaces/component-adapter.md. - Capture fields (
@Signature,@PhotoCapture): String fields that CAPTURE instead of uploading —@Signaturerendersmateu-signature-pad(pointer-events canvas, Clear/Accept → PNG data URI committed via the standard value-changed; existing value shows as image with Sign again/Delete) and@PhotoCapturerendersmateu-camera-capture(getUserMedia live preview + shutter → JPEG data URI; fallback<input type=file capture>when the camera is unavailable — on phones it opens the native camera). Same self-contained data-URI round-trip as@UploadableImage(no upload endpoint); stereotypessignature/cameramapped inFieldTypeMapper; the read-only image branch covers both; both shared LitElements are DS-neutral (Lumo vars + fallbacks) and reused by the redwood renderField. Demo:/image-field. Tests:CaptureFieldsSyncTest. Parity: .NET[Signature]/[PhotoCapture]and PythonSignature()/PhotoCapture()markers emit the stereotypes; React Native captures BOTH for real (CaptureFields.tsx): the signature pad captures strokes with a PanResponder, renders them as react-native-svg polylines and rasterizes on Accept via react-native-view-shot (captureRef(..., result: "data-uri")→ PNG); the photo field uses expo-camera (useCameraPermissions+CameraView+takePictureAsync({base64})→ JPEG data URI, permission plugin registered in app.json). Deps added: expo-camera, react-native-svg, react-native-view-shot (Expo SDK 52 versions). - Tree selects (
@TreeSelect) & tree selectors:Option/OptionDtocarry achildrenlist (canonical constructors grew — 6-arg convenience kept;FieldMapper.mapOptionmaps recursively).@TreeSelect(leavesOnly=…)→ stereotypetreeSelect+FormField(Dto).treeLeavesOnly; the hierarchy comes from the view'sOptionsSupplierreturning nested Options; frontendmateu-tree-select(shared LitElement: button + expandable-nodes panel, leavesOnly makes group nodes expand-only) used by mateu-field and redwood renderField. TREE LOOKUP SELECTORS: aSelectorListing withgridLayout() = GridLayout.treeand a self-referentialchildrenrow list shows the lookup dialog as a tree —mateu-table-crud.renderTreerenders theselectcolumn as the Select button (sameaction-on-row-selectdispatch as the flat grids' renderActionCell; it used to render as an empty plain column, making tree selectors unselectable). Demo:/tree-select(ZoneSelector). Tests:TreeSelectSyncTest. Parity: .NET[TreeSelect(leavesOnly)]+IOptionsSupplier(uidlOptionrecord with Children, mapped recursively) and PythonTreeSelect(leaves_only=…)+ the view'soptions(field_name)method (Option children) emit the same wire; RN renders an expandable dropdown — the caret is its own tap target so selectable groups can still expand. - Uploadable image field (
@UploadableImage): shorthand for@Stereotype(FieldStereotype.uploadableImage)(new enum value) on aStringfield — renders the image preview + Upload/Replace + Delete actions. Self-contained, no upload endpoint: the picked file is read client-side into a data URI (base64) and stored as the field value, so the image travels in the string and round-trips like any other field; the value may also be a plain URL. Pipeline:@UploadableImage(uidl, mirrors@Badge) →FieldTypeMapper.getStereotype(viaMetaAnnotations) →FieldStereotype.uploadableImage→ frontendmateu-field.ts: editable branch renders<img>+ a hidden<input type=file>+vaadin-buttons;imageUploadusesFileReader.readAsDataURL→value-changed,imageDeletesets value'',triggerImageUploadclicks the hidden input; the read-onlyimagebranch also handlesuploadableImage(shows just the<img>).stereotypeis a plain string on the wire, so no DTO change. Demo:demo-admin-panel/.../images/ImageFieldDemo.java(/image-field). User docs:doc/.../java-ui-definition/annotations/field-types.md(@UploadableImage). - Permission-driven field/button state (
@EyesOnlyon fields,@ReadOnlyUnless,@DisabledUnless): the same identity dimensions as@EyesOnly(roles/groups/scopes/permissions, resolved from the JWT Bearer token byAuthorizer) now drive three field states, not just menu visibility. Hide:@EyesOnlyon a form field hides it when unauthorized — evaluated inFormFieldFilter.filterField(so it also applies to inline subforms and listing columns; previously@EyesOnlyonly gated menus inAppMenuBuilder/MenuEntryMapper). Read-only:@ReadOnlyUnless(...)(field or class level) makes the field/view read-only unless authorized — evaluated inPageFormBuilder.readOnlyByPermission, called from bothPageFormBuilder.isReadOnlyandReflectionFormFieldMapper.isReadOnly(staticreadOnlybool onFormFieldDto). Disabled:@DisabledUnless(...)(field or@Button/@Toolbarmethod) disables unless authorized — for fields it emits a client-sidedisabledRule inRuleMapper.createRules(mirrors@Disabled); for buttons it's OR-ed into the disabled flag inPageButtonsBuilderviadisabledByPermission(...). All three reuseAuthorizer.isAuthorized(...)(refactored to a shared privatematches(roles,groups,scopes,permissions,httpRequest)core with overloads per annotation). Matching is AND across declared dimensions, OR within each; no dimension declared → unrestricted; no request/token → unauthorized. They compose for layered access, e.g.@EyesOnly(roles="staff") @ReadOnlyUnless(roles="manager")→ hidden to non-staff, read-only to staff, editable to managers. Caveat:MetaAnnotationshas no@AliasFor, so a composed (semantic) annotation wrapping these cannot override their dimension attributes. Annotations inuidl(ReadOnlyUnless,DisabledUnless). Demo:demo-admin-panel/.../security/FieldAccessDemo.java. User docs:doc/.../reference/key-annotations.md.