Skip to content

Latest commit

 

History

History
402 lines (327 loc) · 171 KB

File metadata and controls

402 lines (327 loc) · 171 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What is Mateu

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.

Repository Layout

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

Key Architecture Concepts

Two-Step Annotation Processing

@UI classes can live in a framework-agnostic module (no Spring/Quarkus dep, only io.mateu:uidl).

  1. Indexer AP (annotation-processor-indexer) — compile the UI module with this AP; it writes META-INF/mateu/ui-registrations into the jar.
  2. 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.

ViewModel Instantiation & DI (avoid singleton state!)

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.

Runtime Flow

Frontend → POST /{baseUrl}/mateu/v3/components/_/actionMateuController (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).

Route registry (specs/ui/routes.yaml) — 2026-08-12

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.yaml is the authored half, merged on top — authored wins, replacing the entry outright. Only the authored half short-circuits DefaultRoutedClassResolver.resolve: the derived half is what the RoutedClassProviders 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 orders screen.
  • Parameter precedence — identical on the server, in libs/mateu and in the VB core, because route resolution also runs in the browser: fixed > client state > path > query > defaults. Applied in RouteSegmentUtils.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. YamlUidlLoader uses the entry's definition instead of the specs/ui/<route>.yaml convention, and the entry's viewModel when the YAML declares none — so a shared definition must NOT declare modelView:, 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 in routes.yaml are 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.py and src/Mateu.Core/RouteRegistry.cs mirror the model, matching, precedence and definition lookup; neither has a bundle exporter. Both accept viewModel and view_model. User docs: doc/.../java-ui-definition/route-registry.md.

Generated JSON Schemas — do not edit by hand (2026-08-12)

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=true

Why 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/UserTriggerMenu, 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.

Frontend Renderers

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.

Helidon MP adapter (JAX-RS/CDI/Weld) — parity notes (2026-07-25)

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 have jersey-media-json-jackson on the runtime classpath (the ContextResolver is inert without Jackson's MessageBodyWriter).
  • MateuService isn't a CDI bean by itself. DefaultMateuService (framework-neutral core) carries only jakarta.inject metadata, which Weld's annotated discovery does NOT treat as a bean → the generated controllers' @Inject MateuService would 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) via CDI.select(rawClass) returns nothing for a parameterized bean (ComponentAdapter<Foo>) → the ComponentAdapter SPI silently falls back to reflectively rendering the POJO. Fix: resolve via BeanManager.getBeans(Object.class, @Any) + filter by raw type + getReference(bean, Object.class, …) — the same approach as QuarkusBeanProvider (plain CDI SPI, works verbatim on Weld).
  • Lazy static facade. CDI beans are lazy; DefaultInstanceFactory initializes the static MateuInstanceFactory facade 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). HelidonCDIProducer observes @Initialized(ApplicationScoped.class) and touches the bean at startup (the CDI equivalent of Quarkus' StartupEvent init).
  • Generated controllers need a bean archive. Without META-INF/beans.xml in the app, Jersey does not register the AP-generated @Path/@RequestScoped controllers → every route 404s (JAX-RS "Endpoint not found"). The generated RouteResolver also 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 /assets ONLY (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 finalName is the artifactId WITHOUT the version → the runnable artifact is helidon-app1.jar, not helidon-app1-1.0.0-SNAPSHOT.jar (a wrong reference in the CI start step made java -jar fail 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 testing (core integration harness)

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.

Figma design-to-code pipeline (IN PROGRESS, 2026-07-15)

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).

Creating a Release

Releases follow the pattern Mateu v3.0-alpha.N. To cut a new one:

  1. Check the latest release number:
    gh release list --limit 5
  2. 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 & Run Commands

Java Backend (from repo root or any Maven module)

# 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=YamlUidlLoaderTest

Use the settings.xml at repo root when you need to point to a custom Maven repo:

mvn -s settings.xml clean install

Frontend (from frontend/web/monorepo/)

# 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 test

Renderer VB/Redwood (from frontend/web/monorepo/apps/redwood/)

npm 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)

E2E Tests (from e2e/)

# 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 report

Generating Documentation Screenshots

Screenshots for the documentation in doc/src/content/docs/ are generated programmatically:

  1. Write the example Java class in the appropriate SUT module (usually e2e/sut/modules/sample1/ for pure UI classes, or e2e/sut/apps/mvc-app1/src/.../app/ for CRUD classes that need AutoCrud).
  2. Build the changed modules:
    cd e2e/sut/modules/sample1 && mvn clean install
    cd e2e/sut/apps/mvc-app1 && mvn clean install -DskipTests
  3. Start the MVC app (keep running in background):
    cd e2e/sut/apps/mvc-app1 && mvn spring-boot:run
  4. 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
  5. 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.


API Quick Reference

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).

Important Conventions

  • @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 @Service and return it from store() in your AutoCrud<T> subclass. Renamed 2026-07-18 from CrudRepository/repository() (a data-access adapter is not a domain-aggregate repository). store() is now abstract and the repository() fallback method was removed (2026-07-23) — every AutoCrud/FilteredAutoCrud subclass MUST override store(); the old resolveStore() fallback is gone. The deprecated CrudRepository/CompositionCrudRepository interface aliases were deleted 2026-07-29 (86 straggler type references migrated to CrudStore/CompositionCrudStore); the repository() method name was already gone — all overrides use store().
  • CrudStore.find(String searchText, T filters, Pageable pageable)Page<T>: the single search+filter+sort+paginate entry point AutoCrud uses to fill the listing. It is a default method (so no existing implementer breaks): the default filters findAll() by searchText (via SearchableText.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 overridden find) — then sorts by pageable.sort() (read reflectively via getter/record-accessor/field by private static helpers in CrudStoreuidl has 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 as FilterCriterion(field, FilterOperator between|gte|lte|in, values) (uidl.data), built by FilterCriteriaBuilder (core, crud package) from the component state — range bounds in <field>_from/<field>_to keys, multi-select values as a list (or comma-joined string after URL restore) — with the values coerced via TypeCoercionHelper; 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) from ListRouteResolver): temporal fields (LocalDate/LocalDateTime/LocalTime) → stereotype dateRange BY 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 the SearchRequest, criteria included) → Crud.search(SearchRequest, HttpRequest)FilteredAutoCrud.fetchRows/5CrudStore.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 custom Listing<Filters,Row> Filters class can declare DateRange/NumberRange (uidl.data records with contains/isEmpty helpers) or Set<SomeEnum> fields — they render range/multi widgets on ANY listing (explicit type = explicit ask, independent of crudFilterSemantics; PageListingBuilder.isTypedFilter builds 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 hydration FilterStateAssembler (uidl.interfaces, called from SearchRequestBuilder — which serves Listing/ReactiveListing — AND SearchActionHandler) replaces the flat <field>_from/_to keys and value lists/comma-joined strings with ready-made typed instances, which coercion passes through (TypeCoercionHelper exact-class match; FieldValueConverter gained an isInstance pass-through — it used to THROW pouring a LinkedHashSet into a Set field); blank/unparseable bounds and stale enum constants are dropped rather than failing the search. Demo: /typed-filters (BookingsListing). Tests: TypedFiltersSyncTest. compareValues compares 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, or autoFocusOnSearchText would 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 for dateRange/numberRange, checkable rows for multiSelect — toggles re-search but keep the panel open); applied conditions are chips with ✕ (a range chip clears BOTH _from/_to keys), chip add/remove re-runs the search, "Clear filters" resets; mateu-table-crud._filterIds expands range filters to their _from/_to keys so URL sync keeps working. Two implementations kept in sync: the shared mateu-filter-bar (Vaadin/sapui5/redhat — a LitElement with Lumo-var styling; the wire contract with mateu-table-crud is untouched: value-changed/search-requested/filter-reset-requested; filtersLayout and mainFilter are no longer consulted) and redwood-oj's renderFilterBar hook (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 same filters metadata from the entity (CrudMetadataDto.Filters / CrudMetadata.filters, FormField entries with fieldId/dataType/label/stereotype/options; rules in ReflectionMapper.MapCrudFilters / mapper.crud_filters: enum→multiSelect+options, DateOnly/DateTime resp. date/datetime→dateRange, [RangeFilter]/RangeFilter() numerics→numberRange) and APPLY the componentState values in-memory over Fetch() 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/_to at date granularity). No repository/criteria layer there (in-memory only, matrix says 🟡). Tests: 43 golden each (Bookings fixtures). dataType mismatch fixed the same day: Java emits bool, .NET booleanisBooleanFilter in BOTH filter bars (shared mateu-filter-bar + redwood renderFilterBar) now accepts both (before the fix Java boolean filters silently rendered as text inputs, not Yes/No). Page<T> already carries totalElements, so there is no separate count method — a DB impl runs the count + page queries inside find. Wired in FilteredAutoCrud.fetchRows (core), which now delegates to store().find(searchText, (T) filters, pageable) instead of doing the in-memory findAll().stream().filter().subList() itself; override fetchRows(...) on the AutoCrud subclass only when you need the HttpRequest. Types live in io.mateu.uidl.data: Page, Pageable(page,size,List<Sort>), Sort(field,Direction), Direction{ascending,descending}.
  • CRUD create/edit in a drawer (editInDrawer()): override this Crud default (false) so New/row clicks open the crud form in a Drawer over the listing (Redwood "Create and Edit - Drawer" template) instead of navigating to /new/{id}/edit. Mechanics: NewActionHandler/EditActionHandler return a Drawer (built by CrudDrawerBuilder: CrudFormComponentBuilder.build — made public — as content, FormViewModel.toMap(entity) as initialData so fields arrive populated; the routed pages get their state via a State fragment, the drawer via initialData); in drawer mode view→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 because CrudTriggersBuilder subscribes it to that event → search. Frontend fixes that made it work: ConnectedElement.closeModal queries (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 of component.children (an Add-fragment overlay would be resurrected by the next re-render). editDrawerWidth() (36rem) also overridable. Demo /drawer-crud-demo; tests EditInDrawerSyncTest; doc ux-patterns/drawer.md. Full parity (2026-07-17): .NET Crud<T>.EditInDrawer virtual / Python @edit_in_drawer class decorator — new/view/edit answer the form in a Drawer (Add fragment), save answers CloseModal(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 with scripts/drawer-probe.ts against a live backend). RAIL: .NET [WizardProgress("rail")] / Python @wizard_progress("rail") compose the same two-column rail; ProgressSteps carries vertical in all three backends; RN's stepper is vertical by design; IntelliJ's renderProgressSteps honors the flag. Ports closed the archetype gap (2026-07-17, later same day): both ports gained a fluent FormField primitive (.NET FormField : ComponentBase mapped in ComponentMapper; Python fluent.FormField + fluent VerticalLayout/HorizontalLayout which Python lacked) → FormFieldMetadataDto/FormFieldMetadata, bound to componentState by fieldId; on it, CollectionDetail<TRow>/GeneralOverview<TRow> (.NET Archetypes.cs) and CollectionDetail/GeneralOverview (Python mateu_uidl) mirror the Java orchestrators. Runtime pieces: tree-supplier views seed scalar properties into initialData (state round-trip) and an IRefreshOnChange (.NET) / __mateu_refresh_action__ (Python) marker emits the {type:"AutoSave", actionId, debounceMillis} trigger the shared frontend already honors; SyncHandler handles selectCollectionItem/filterCollection/switchRecord by 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 an AutoCrud subclass to replace the corresponding built-in English label. The default values are the method names in plain English (e.g. newLabel()"New"). Implemented in Crud.java; consumed by ListRouteResolver, CrudFormComponentBuilder, and ViewToolbarBuilder.
  • Bean validation annotations (@NotNull, @NotEmpty, @Min, @Max) drive client-side and server-side validation automatically.
  • Identifiable interface on record/entity marks the ID field for CRUD.
  • HttpRequest can be added to any method signature; Mateu injects it automatically.
  • i18n: implement Translator or rely on the default DefaultTranslator.
  • The uidl module is the only dependency needed for writing @UI classes 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 (blank zone()) 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/flushZonedRow handle 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 is wrap(true) and each zone column is flex: 1 1 calc(<width> - var(--lumo-space-m, 1rem)); min-width: min(20rem, 100%); — the basis subtracts the spacing gap because with flex-wrap line breaks are computed from the HYPOTHETICAL (basis) sizes, so a plain 62% + 38% + gap > 100% wrapped immediately (and without wrap it overflowed past the full-width bands' right edge, the old flex: 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 .NET BuildZones / Python build_zones (HorizontalLayoutMetadataDto.Wrap / HorizontalLayoutMetadata.wrap added); React Native's LayoutRenderer honors the wire's min-width: min(20rem…) marker on zone columns (minWidth 300 → stacks at phone widths). Tests: LayoutSyncTest.zoneWidthsBecomeFlexBasisStyles, .NET/Python Zones_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 @Section attribute (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 .NET SameSection / 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-office IdentidadStep (arrival header band + contact/preferences columns).
  • Section column count (@Section(columns=N), fixed 2026-07-27): the annotation default is now 0 = unset (was 1, which made an explicit columns = 1 indistinguishable from unset — SectionFormRenderer.sectionColumns only honored > 1 and silently fell back to the form default). Any explicit N ≥ 1 now wins, so columns = 1 forces single-column stacking on a form whose other sections stay multi-column; unset (0) inherits @FormLayout(columns=…) on the class, else 2. FormSectionGrouper.sameSection still compares raw columns() (unset carries the 0 sentinel into SectionFields). Java-only — the .NET/Python Section mirrors never had columns. 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 @Toc to 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). @Toc is 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/@FoldedLayout horizontal 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 (the mateu-content-header gets a sticky-header class, top:0), and multiple sticky sections stack directly under it without overlapping: mateu-page._layoutStickyTops() measures the header height (published as the --mateu-header-h CSS var, also used for the index's top) and sets each sticky card's top to 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: @TocPageMetadataExtractor.getToc (a nullable Boolean, read via MetaAnnotations) → PageView.tocPageMapperPageDto.toc → frontend Form.toc; @Section.sticky()SectionFormRenderer adds the mateu-section marker class to every section card (plus mateu-section--sticky + position: sticky style when sticky) — note the reflective @Section path emits section cards as fluent Cards (→ CardMappercardRenderer.ts), not formSectionRenderer.ts, and CardMapper hardcodes the card id, so the index anchors by DOM element reference + the marker class, not by a server id. Frontend lives entirely in mateu-page.ts: it enumerates vaadin-card.mateu-section across the slotted subtree, reads each card's title ([slot="title"] or the first h1..h6 heading), 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's vaadin-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 (via scrollBy, not scrollIntoView) 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..9 jump 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 via e.code — both Digit1..9 and the numeric keypad Numpad1..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@Section only works at the top level — a nested @Inline type's fields are grouped into tabs, not sub-sections). Contrast with v1/v3 which stay @Zones master-detail with the shared tabbed ClientInfoSection.
  • Capability listings (Listing<Row> + interfaces de capacidad, 2026-07-29) — the listing/CRUD surface is ADDITIVE by declaration: a listing is a class implementing io.mateu.uidl.interfaces.Listing<Row> with the single ListingData<Row> search(SearchRequest, HttpRequest) (SearchRequest = uidl.data record searchText/filters/criteria/pageable — new inputs become fields, never overloads; SearchRequestBuilder builds it accepting sort keys fieldId AND field). Input capabilities are DECLARED: Searchable (marker → search box; without it searchText arrives empty — was hardcoded-on before) and Filterable<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, reusing editInDrawer), Creatable<Form,Id> (creationForm()+create() → New + /new), Deletable<Id> (deleteAllById(ids) → selection + Delete). A plain @UI POJO with interaction capabilities is promoted to the CRUD mediator by the CapabilityCrud bridge (hooked in RunActionUseCase after instance creation, pattern AdaptedComponentTree; advertises the LISTING class as serverSideType via the MultiView.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/canDelete consulted by ListRouteResolver and the New/Edit route resolvers; all default true on Crud — the full pack — and are narrowed by the bridge or subtracted via @Not*). Crud<View,Editor,CreationForm,Filters,Row,IdType> IS a Listing implementing all the capabilities (its saveNew is now create, save returns IdType, getIdFieldForRow has a default; MultiView.supportsAction materializes the broad ActionHandler default so orchestrators keep claiming delete/bulk/action-on-row-* over Listing's narrow "search"); AutoCrud/FilteredAutoCrud unchanged outside (entity + store(), subtractive @Not*). RENAMES: ListingBackendListing (absorbed the deleted declarative base class core.infra.declarative.Listing: @Toolbar action advertising, Selector select-glue in handleActionOnRow default + ListingRowActionRunner (core) for reflective row-method invocation — excluded for RouteHandlers; export flags; Selectors now carry their own fieldId/withFieldId), ReactiveListingBackendReactiveListing<Row>, entity interface SearchableSearchableText (freed the name for the capability; the @Searchable ANNOTATION is a third, unrelated thing — lookup fields). The legacy Deleteable view-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): .NET IListing<TRow>+SearchRequest+ISearchable/IFilterable<F>/INavigable/IEditable/ICreatable/IDeletable (Capabilities.cs; capability methods take TYPED objects, not HttpRequest — the port's idiom; CapabilityProfile in CapabilityCrud.cs; Crud<T> implements all 7 with virtual CanView/CanEdit/CanCreate/CanDelete; wire adds CrudMetadataDto.RowsSelectionEnabled+GridColumnMetaDto.ActionId; tests CapabilityListingTests) / Python Listing[R]+SearchRequest+mixins Searchable/Filterable[F]/Navigable/Editable/Creatable/Deletable (mateu_uidl; Searchable is DUAL-ROLE — listing capability AND selector-field marker; real generic resolver _resolved_generic_args in mapper; resolve_listing/handle_listing in sync_handler; wire CrudMetadata.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); criteria always 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-referential children list; never auto-selected).
  • Grid column widths (@ColumnWidth): annotate a grid row field with @ColumnWidth("9rem") for a fixed-width column (rendered with flex-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 fixed 3rem truncates to "A."/"H." once non-compact padding eats the width). No @ColumnWidth → the column keeps the default flex-grow and shares the remaining space. Handled in GridColumnBuilder (autoGridColumn.autoWidth(true) + null width + flex-grow:0); the autoWidth/width/flexGrow fields flow through GridColumnMapper to the vaadin-grid-column in renderColumn.ts. Cells always ellipsis-truncate (renderColumn.ts columnRenderer), so column width is the only lever against truncation.
  • Wizards: extend Wizard and declare fields implementing WizardStep for each step. The penultimate step shows the @WizardCompletionAction button; 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 via getTitle() (respects @Title, TitleSupplier, or falls back to the class name). Branching: override stepApplies(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 by WizardActionDispatcher and WizardButtonBuilder), 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 @WizardCompletionAction button moves to the last applicable step. Also: WizardStateSerializer.toMap treats 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 classic ProgressBar; annotate the wizard with @WizardProgress(WizardProgressStyle.STEPS) to show connected step bullets instead (the existing ProgressSteps component — one dot per applicable non-result step with done/current/upcoming states, all done on the result step; skipped branching steps are excluded), or WizardProgressStyle.RAIL (2026-07-17, the Redwood "Guided Process" template) for a sticky right-hand rail: big current | total counter (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) and ProgressSteps gained a vertical flag (record + ProgressStepsDto.vertical + :host([vertical]) styles in mateu-progress-steps; ports/RN ignore the flag gracefully). Demo: /branching-wizard (RAIL + branching). Implemented in Wizard.progressIndicator(); no new wire types, so every renderer that shows ProgressSteps renders it. Ports: .NET [WizardProgress("steps")] / Python @wizard_progress("steps") (their wizards' numbered step groups emit "Step N" bullets). Demo: demo-front-office CheckInWizard. Tests: WizardProgressStyleSyncTest, .NET/Python in SectionFeatureTests/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-step fromMap failed silently and NON-CURRENT steps reset to their field initializers on every action; WizardStateSerializer.toMap now replaces each non-basic field's husk with the cleaned nested map it already computes for flattening (FormViewSerializer got 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 in WizardBranchingTest.
  • State coercion & holder fields (fixed 2026-07-14, front-office dogfooding): (1) Numeric widening — the JS client integerizes whole doubles (a Double field's 343.0 comes back as 343); FieldValueConverter (write path) and TypeCoercionHelper (read path) now widen any Number into the target numeric field type (Double/double/Float/float/BigDecimal/Long/Integer); before, the conversion threw, Hydrater swallowed it and the field silently reset to its initializer. The .NET/Python ports already coerced (System.Text.Json GetDouble(), float(raw)) — pinned with tests. Tests: NestedStateSyncTest. (2) Component-holder fields stay out of the state — fields typed Callable/Supplier/Runnable/io.mateu.uidl.fluent.Component used to serialize as {} husks and null out on rehydration (NPE on re-render) unless @JsonIgnore'd; HolderFieldChecker.isNonDataHolder (core infra/reflection/read/) is now applied symmetrically by FormViewSerializer/WizardStateSerializer (drop on write) and Hydrater (skip on read, initializers survive), so @JsonIgnore is unnecessary (still honored). Tests: HolderFieldsSyncTest. (3) AllEditableFieldsProvider now log.warns once per final field dropped from an editable form (they are silently excluded by isNotInjected — the warning makes it discoverable).
  • Dashboards (Dashboard archetype): extend Dashboard (core orchestrators/dashboard/) and declare component-holding fields — consecutive MetricCard fields group into a full-width Scoreboard KPI 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; override columns() to fix the column count (0 = auto-fit). MetricCard.actionId makes the tile clickable — the frontend dispatches the standard action-requested, so an @Action method with that name runs (drill-in navigation). Everything is also usable fluently: DashboardLayout / DashboardPanel / Scoreboard / MetricCard are UIDL data records (mapped by DashboardLayoutMapper/DashboardPanelMapper in LayoutComponentDispatcher, MetricCardMapper/ScoreboardMapper in DisplayComponentDispatcher; wire DTOs DashboardLayoutDto etc.; frontend dashboardRenderer.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 + optional captionOf/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 own AutoSaveTrigger via TriggersSupplier because CLASS annotations are NOT inherited by subclasses (a general gotcha for archetype base classes; field annotations DO work — the @Colspan(2) Callable island and @Hidden _selectedId live on the base). Demo /collection-detail-demo; tests CollectionDetailSyncTest; 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 (the record String field turned into a select via the archetype's OptionsSupplier supports("record") + switcherOptions) and the selected record's overview below (load(id) + overview(Row, rq) — typically EntityHeader title/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; tests GeneralOverviewSyncTest; doc ux-patterns/general-overview.md.
  • Foldout record pages (Foldout archetype): extend Foldout (core orchestrators/foldout/) — the first component field without @Panel is 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=false starts folded). Fluent: FoldoutLayout (overview + List<FoldoutPanel>) mapped by FoldoutLayoutMapper in LayoutComponentDispatcher; wire FoldoutLayoutDto carries FoldoutPanelInfoDto headers 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 shared mateu-foldout and mateu-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 (.NET FoldoutPanel.Width, Python FoldoutPanel.width). NOTE: the Vaadin shell renders foldouts with mateu-vaadin-foldout (apps/vaadin/src/renderers) — a horizontal carousel of ALWAYS-EXPANDED sections that ignores open by design (only the shared mateu-foldout collapses to strips). Demo: demo-front-office-evolution ReservaOverview (check-in foldout: huéspedes overview + Operaciones 38rem + Perfil del cliente 15rem). Frontend: mateu-foldout.ts LitElement (owns the open/closed Set state locally, no server round-trip) + foldoutRenderer.ts in 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 (core orchestrators/herosearch/) and implement search(...) exactly like a declarative Listing — the archetype composes a centered HeroSection (override heroTitle()/heroSubtitle()/heroImage()) + the standard listing built by PageListingBuilder.getCrud (made public for this), inside a STRETCH VerticalLayout. Results default to GridLayout.cards (override gridLayout()); Filters record fields become the facet bar; starts empty and searches on enter (add @Trigger(OnLoad, "search") to preload). HeroSection is also a standalone UIDL component (title, subtitle, background image with dark overlay, centered, slotted content children) mapped in LayoutComponentDispatcher, rendered by heroRenderer.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 (ItemOverview archetype): extend it (core orchestrators/itemoverview/) — the first component field without @Panel becomes the key-info panel (left, position: sticky Card, width via panelWidth(), default 22rem); @Panel(title) component fields become tabs in a TabLayout on 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 (Welcome archetype): extend it (core orchestrators/welcome/) — Button (uidl data) fields become CTAs inside a centered HeroSection (their actionId runs the matching @Action method; return a URI to navigate); @Panel(title) component fields become highlight tiles on a DashboardLayout grid below; override heroTitle()/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: optional pageSubtitle() intro line (a Text id page-subtitle) over the standard smart-search listing (PageListingBuilder.getCrud, typed facets included), read-only, starts EMPTY (no OnLoad→search trigger — declarative listings get none; add @Trigger(OnLoad, "search") to preload). Like HeroSearch minus the hero and without forcing cards. Ports: .NET SmartSearchPage<TFilters,TRow> + ISmartSearchPage (MapListing omits the OnLoad trigger + emits the subtitle Text) / Python SmartSearchPage[F, R] (same in map_listing). Demo /smart-search-demo (redwood showcase, preloads via the trigger); tests SmartSearchPageSyncTest; 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 a TaskQueue of counted buckets (groupOf(row) → "Today (2)" labels, groupOrder() overrides first-appearance order) with captionOf/badgesOf cards; clicking a card ACTS (actionOn(row) → URI/Message/component) instead of selecting for a detail pane (wire actionId openTodoItem, _item param); emptyState() ("All caught up! 🎉") when no rows. Form-view wiring like CollectionDetail (@Hidden _itemId + @Colspan Callable island + TriggersSupplier for the request). Ports: .NET TodoList<TRow> + ITodoList (SyncHandler branch) / Python TodoList (open_todo_item camelCase-dispatched). Demo /todo-list-demo; tests TodoListSyncTest; doc ux-patterns/to-do-list.md.
  • Calendar pages (CalendarPage archetype, 2026-07-19): the Redwood "Calendar" template — calendar toolbar (‹/Today/› buttons + optional primary "+ Create") over the Calendar month grid; the displayed month is page state (@Hidden _month), navigation re-runs events(month, rq); the archetype re-stamps every event with actionId="openCalendarEvent" and the click arrives with parameters._clickedEvent (a MAP carrying the event id) → actionOn(event); showCreate()+createAction() for the create flow. Week/day/list RDS views NOT built in. Ports: .NET CalendarPage + ICalendarPage (Month/EventId as ISO strings) / Python CalendarPage (month/event_id WITHOUT underscore — underscore fields are excluded from initialData seeding, the round-trip would break). Demo /calendar-demo; tests CalendarPageSyncTest; 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/#F1EFED canvas/#FBF9F8 content common to all three). Resolution: @PageWidth on 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 via PageWidthResolver) AND PageDto.pageWidth (reflected pages), values "fixed"|"fullWidth"|"edgeToEdge". Frontend: shared resolvePageWidth(...) (libs/mateu layout/pageWidth.ts: wrapper → Page metadata → inference — gantt/planning/kanban/bpmn/map → edge, compact or inline-editable crud → full, else fixed) stamps data-page-width on mateu-ux in applyFragment (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). Tests PageWidthSyncTest; note in ux-patterns/page-templates.md.
  • Empty states & skeletons: EmptyState UIDL component (icon emoji/text, title, description, actionId+actionLabel CTA dispatching the standard action mechanism) and Skeleton (variants text/card/grid/form via SkeletonVariant, count repeats the shape; rendered by the mateu-skeleton LitElement with a shimmer animation — :host carries flex: 1 1 0 so skeletons share width inside Horizontal layouts). Both mapped in DisplayComponentDispatcher, rendered by emptyStateRenderer.ts, claimed by sapui5/redwood-oj. Grids/listings now render the shared emptyStateTemplate(...) block instead of bare "No data." text — patched in mateu-table-crud.ts (list/cards/masterDetail/table spots), gridRenderer.ts and mateu-table.ts, keeping emptyStateMessage as 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: Gantt UIDL record (list of GanttTask(id, title, LocalDate start/end, progress 0-100, color)), mapped by GanttMapper in DisplayComponentDispatcher (dates serialized ISO in GanttTaskDto), rendered by the dependency-free mateu-gantt LitElement (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-task color overrides --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 (Separator component + @SeparatorBefore) & text sizes (@Text(size=…) / Text.size): @SeparatorBefore on 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.toFormLayout flatMaps a Separator (uidl.data record with an attributes map carrying data-colspan = the form's columns) before the field; buildRows gives any Separator a FormRow of its own (flushing the pending row); fluent Separator also usable anywhere (mapped by SeparatorMapper in DisplayComponentDispatcherSeparatorDto); frontend separatorRenderer.ts emits the styled <hr> (Lumo contrast var, data-colspan so 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 @Text annotation (size(), default m) and the fluent Text.sizem/absent applies nothing, the rest emit font-size: var(--lumo-font-size-*) in textRenderer.ts (TextDto.size, a plain string on the wire). Independently, @Text(noMargins=true) / fluent Text.noMargins (TextDto.noMargins; .NET Text.NoMargins, Python Text.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] + fluent Separator / Text { Size } (MapFields helper inserts the separator, FormRows gives it its own row; SeparatorMetadataDto, TextMetadataDto.Size; tests in SectionFeatureTests); Python SeparatorBefore() marker + fluent.Separator / Text(size=…) (map_fields/form_rows; SeparatorMetadata, TextMetadata.size; tests in test_section_features.py); React Native (Separator case → 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 the propertyRow/bulletedList field branches in its FormFieldRenderer, verified with ./gradlew renderProbe over a captured front-office wizard increment).
  • Property-list sections (@Section(propertyList=true)) & frameless sections (@Section(frameless=true)): two @Section attributes for key-info panels. propertyList=true renders 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 of columns — without annotating each field; component-holding fields in the section travel untouched. Pipeline: SectionFormRenderer.buildFormLayoutasPropertyList recursively transforms the FormLayoutBuilder output (VerticalLayout → FormLayout → FormRow), REPLACING the responsive FormLayout with a STRETCH VerticalLayout of the rows (the form layout would size rows to its column width, leaving dividers short of the card edge) and marking each FormField.toBuilder().propertyRow(true).readOnly(true); propertyRow flows FormFieldFieldMapperFormFieldDto → frontend FormField.ts; mateu-field.renderPropertyRowField (dispatched FIRST, before badge/plainText) and redwood-oj's renderPropertyRowField render the flex row (money/bool handling copied from plainText). frameless=true drops the outlined section Card AND its padding (both card paths in SectionFormRenderer emit the bare content, like the _inline embedded-mediator path) — for bands whose content brings its own chrome (header cards, progress banners); frameless sections are not enumerated by the @Toc index. NOTE: the anonymous Section in FormSectionGrouper must implement every new @Section attribute. Demo: demo-front-office IdentidadStep (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 (parallel sectionAttrs/section_markers list) into SectionCard/section_card, which swaps the FormLayout for a stretch VerticalLayout of PropertyRow+ReadOnly fields 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: .NET SectionFeatureTests, Python test_section_features.py. React Native renders both (2026-07-15): FormFieldRenderer early-returns a propertyRow flex row (label left, value right, hairline divider; money/bool formatting) before the label+input container, and the bulletedList stereotype renders bullet rows in renderInput; the fluent component is BulletedListRenderer (DisplayRenderer.tsx, case in ComponentRenderer). Verified with expo web + demo-front-office on :8592 (the RN dev fallback port) driving the check-in queue → wizard.
  • Notices (Notice component + @Notice field 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 in noticeRenderer.ts) — a null/blank value hides the notice entirely, so the field doubles as its own visibility switch (ReflectionFormFieldMapper, branch before @Text; note both io.mateu.uidl.annotations.Notice and io.mateu.uidl.data.Notice exist — double star-imports need an explicit import, like Badge). slim drops block margins + tightens padding and sets line-height: normal (the @Text(noMargins) analogue); fullWidth spans all form columns (the renderer stamps data-colspan=99, clamped by vaadin-form-layout — and customFieldRenderer propagates it to the wrapping vaadin-custom-field when 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; NoticeMapper now takes the dispatch context), mateu-notice shows 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 carry Content/content the same way; RN/IJ render the children below the text. Smaller than CalloutCard (no title/CTA block) and independent of the page-level @Banners. theme: info|success|warning|danger (default info); icon overrides the theme glyph (ℹ ✓ ! !). Pipeline: Notice (uidl.data) → NoticeMapper in DisplayComponentDispatcherNoticeDto → shared mateu-notice LitElement (always-light pastel bg + dark ink per theme, like the page banners) via noticeRenderer.ts; claimed by sapui5/redhat/redwood-oj. Parity: .NET Notice { Theme, ActionLabel… } / Python fluent.Notice(...)NoticeMetadataDto/NoticeMetadata; RN NoticeRenderer (DisplayRenderer.tsx), IntelliJ renderNotice. Demo: demo-front-office IdentidadStep.quejas. Tests: NoticeSyncTest + port suites.
  • Bulleted lists (BulletedList component + @BulletedList field annotation): render a plain <ul> of text items — the lightweight counterpart of StatusList for read-only enumerations (preferences, highlights, notes). Fluent: io.mateu.uidl.data.BulletedList(id, List<String> items, style, cssClasses) mapped by BulletedListMapper in DisplayComponentDispatcherBulletedListDto. Declarative: @BulletedList (uidl annotation, FIELD + ANNOTATION_TYPE so it composes) on a List<String> field = shorthand for stereotype FieldStereotype.bulletedList (mapped in FieldTypeMapper.getStereotype, checked right after @Badge); mateu-field.ts and redwood-oj's renderField.ts branch on it before the readOnly dispatch and render the shared mateu-bulleted-list LitElement (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-office IdentidadStep.preferencias (guest preferences as a <ul> above the StatusList). Tests: BulletedListSyncTest. .NET/Python parity (2026-07-15): fluent BulletedList { Items } / fluent.BulletedList(items=...)BulletedListMetadataDto/BulletedListMetadata (wire type BulletedList), and the [BulletedList] attribute / BulletedList() Annotated marker → stereotype bulletedList (first check in StereotypeOf/stereotype_of). Port tests: .NET SectionFeatureTests, Python test_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 centered HeroSection (id welcome-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/mateu layout/pageWidth.ts (any HeroSection in the tree) stamps data-has-welcome-banner on mateu-ux; mateu-page._showHeaderBand() skips the band, and redwood-oj's renderFilterBar suppresses its listing strip via the uxHost() ancestor walker; .mateu-hero gets a soft Redwood gradient band in redwood-oj's index.css. Ports: .NET [WelcomeBanner] (MapView prepend) / Python @welcome_banner(...) (map_view prepend). Tests WelcomeBannerSyncTest; docs ux-patterns/welcome-page.md.
  • Coarse page type (@PageTemplate + PageTypeResolver, 2026-07-19): every page carries a pageType on 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) > MetricCard field → dashboard > default form. Frontend: pageTypeOf in layout/pageWidth.ts feeds the width chain (declaration > app-shell edge > type default (form/process/landing always fixed) > content inference > fixed) and mateu-ux stamps data-page-type next to data-page-width. Ports: .NET [PageTemplate] + PageTypeOf (MapView/MapListing/MapCrud/MapWizard/MapEntityForm) / Python @page_template(PageType.X) + page_type_of (5 ServerSideComponent sites + Page metadata). Tests PageTypeResolverSyncTest (13-case archetype map + wire); docs note in ux-patterns/page-templates.md.
  • Labels-aside inference (labelsAside wire flag, 2026-07-19): the dense backoffice idiom (label LEFT of the field in a 10rem column) is inferred per form in LabelsAsideInference (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 over AUTO (infer). Frontend: shared renderLayouts sets --vaadin-form-item-label-width: 10rem when aside (fields fill the rest; when NOT aside, the layout gets expand-fields so fields span the full column). Tests LabelsAsideInferenceSyncTest (7 cases); docs java-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/getTitlePlaceholderPageViewPageDto.overline/.titlePlaceholdermateu-content-header (overline muted above the h2; the placeholder INSIDE the h2 so 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/_showHeaderBand account 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 implements PeerNavigationSupplier.peers(HttpRequest)PeerNav(prevLabel, prevRoute, nextLabel, nextRoute) (uidl.data) → PageView.peerNavPageDto.peerNav (PeerNavDto) → prev/next arrow buttons in mateu-content-header (navigate like breadcrumb links; a null route disables that side; optional renderPeerNav renderer hook for DS-specific styling). Timestamp (Redwood "last updated"): @Timestamp("Last updated") on a field → PageMetadataExtractor.getTimestamp (prefix + value.toString, first field wins) → PageView.timestampPageDto.timestamp → muted text under the subtitle; the field is excluded from the form body by FormFieldFilter (like @BadgeInHeader). Contextual info was already covered by field-level @KPI (label/value pairs in the header), so no @HeaderFact was added. Ports: .NET IPeerNavigationSupplier/PeerNavPageMetadataDto.PeerNav + [Timestamp]PageMetadataDto.Timestamp (ReflectionMapper.PeerNavOf/TimestampOf, excluded via Visible); Python PeerNavigationSupplier/PeerNavPageMetadata.peer_nav + Timestamp() marker→PageMetadata.timestamp (mapper.peer_nav/timestamp_of, excluded via visible). 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 via isNavButton + emphasizes primary via buttonStyle, and a role field would break the fluent Button record's constructors + ~23 new Button(...) sites for marginal value. User docs: doc/.../ux-patterns/page-templates.md + the decision guide choosing-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 Drawer enriched with four header extras: subtitle, size (DrawerSize {s,m,l,xl} → 464/648/968/90vw, width still overrides), maximizable (a ⤢ button that steps the drawer up the size ladder), and peerNav (reuses the Fase 0 PeerNav — prev/next-object arrows in the drawer header). Pipeline: Drawer/DrawerSize (uidl.data) → DrawerDto (subtitle/size/maximizable/peerNav) → DrawerMapperDrawer.tsmateu-drawer.ts (subtitle under title, width derived from size, Maximize is CLIENT-SIDE local state maximizeSteps with no round-trip, peer arrows navigate like breadcrumb links). Ports: .NET DrawerSize + Drawer { Subtitle, Size, Maximizable, PeerNav }DrawerMetadataDto (ComponentMapper); Python DrawerSize + 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 Drawer with a new DrawerPosition.bottom plus a collapsible flag. Frontend mateu-drawer.ts: the bottom position anchors the panel to left:0;right:0;bottom:0, full width, sliding UP (height --mateu-drawer-height default 50vh, cap 90vh; width is ignored for bottom), and collapsible adds 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 maximize maximizeSteps). Wire: DrawerPosition.bottom (uidl enum + DrawerPositionDto + DrawerMapper.mapPosition) + Drawer.collapsibleDrawerDto.collapsible. Ports: .NET DrawerPosition.Bottom + Drawer.CollapsibleDrawerMetadataDto.Collapsible; Python DrawerPosition.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) + the Gantt canvas (from tasks(rq)) + an optional docked detail(rq) Card. Interaction: Gantt gained onTaskSelectionActionId (wire field, uidl/dtos/mapper + mateu-gantt.ts dispatches action-requested with parameters._clickedTaskId on a .bar.clickable click); GanttPage wires it to selectGanttTask, whose @Action reads _clickedTaskId, finds the task and returns a side Drawer (taskDrawer/taskDetail overridable). PageTypeResolver→DETAIL. DataManagement (orchestrators/datamanagement/, PageWidthSupplier→FULL_WIDTH): the developer supplies gridView(rq)/ganttView(rq); a toolbar switcher (two Buttons) flips the active view kept in _view state — @Action switchToGrid/switchToGantt set it and return this to re-render in place; heading from @Title. PageTypeResolver→COLLECTION. Ports: .NET (GanttPage/IGanttPage, DataManagement/IDataManagement in Archetypes.cs; SyncHandler branches on selectGanttTask / switchToGrid+switchToGantt; GanttMetadataDto.OnTaskSelectionActionId) and Python (GanttPage, DataManagement; sync_handler branches; GanttMetadata.on_task_selection_action_id; DataManagement uses view WITHOUT 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 an AutoCrud class with @InlineEditing (the annotation now also targets TYPE) and every data column of the table listing becomes an in-place editor (@ReadOnly fields stay display-only); each committed cell persists its row immediately — the frontend dispatches the crud's update-row action with parameters._editedRow, handled by UpdateRowActionHandlerCrud.updateRow(Map, HttpRequest) (default UnsupportedOperationException; FilteredAutoCrud rebuilds the entity via MateuInstanceFactory and calls store().save). Pipeline: ListingColumnBuilder.getColumn sets editable/editorType/editorOptions when the listing instance's class carries @InlineEditing (reusing GridColumnBuilder.getEditorType/getEditorOptions, made package-visible); ListRouteResolver now passes the orchestrator (not itself) as the getColumns instance; update-row advertised in CrudActionsBuilder (list + mediator). Frontend: renderEditableCell's commit falls back to action-requested update-row when there's no enclosing form-grid field, and skips no-op commits (vaadin-checkbox fires checked-changed on 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 inner mateu-ux when a mediator fragment arrives with state._route. Three subtleties fixed 2026-07-05: (1) on a direct URL load (no menu click) selectedConsumedRoute was never set, so the composed route lost its prefix — the handler now falls back to the inner ux's consumedRoute; (2) chooseRoute() gives the app state's _route precedence over selectedRoute, so the handler must clear state._route before remounting (exactly like Vaadin's mateu-app._selectRoute does) or the remount reloads the listing instead of e.g. the crud's /new form; (3) the browser URL for a mediator-internal PushStateToHistory (e.g. /new) is prefixed with the inner ux's consumedRoute in mateu-ux.routeChangedListener (no-op on Vaadin where consumedRoute is ''/_empty). mateu-redwood-app.ts is a copy of MateuRendererApp — keep both in sync. Also: redwood-oj's renderFilterBar must NOT render metadata.toolbar (the shared mateu-table-crud header already renders the crud toolbar — rendering both duplicated every button), and mateu-redwood-table renders columns carrying an actionId (e.g. the crud's first column, actionId="view") as link-style borderless-button cells via the existing ojAction → handleCellButtonAction path — note JET's CSP expression evaluator does NOT expose String(), use '' + cell.data in 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 a mateu-ux that already shows routed content must merge, not replace, in applyFragment — otherwise a host-page push emitted while an embedded mediator loads blanks the content; (c) an embedded MEDIATOR-variant shell must NOT intercept unclaimed action-requested events (handleUnhandledAction returns early for AppVariant.MEDIATOR): they may belong to an ANCESTOR component — the cardex's reloadPax bubbles from the entity view inside the island up to the enclosing AutoEditableView's mateu-component, which advertises it (Vaadin's mateu-app has no such interceptor, so this only broke on the shells); after the ancestor claims it, the island re-renders purely by property flow (state._route flip → chooseRoute → inner ux route change → re-fetch); (d) captureActionSST must ignore actions whose serverSideComponentRoute carries _embeddedMediator=1 — recording the island's initiator would make the outer shell treat the island's state._route response (the /view/ reload flip) as a page navigation and remount the whole routed content; (e) ComponentElement re-registers its OnCustomEvent listeners in connectedCallback — a Lit re-render of an ancestor can detach+re-attach the element without changing its component property, and disconnectedCallback dropped the listeners, leaving subscriptions (e.g. the cardex's pax-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/navigate consult dirtyGuard.confirmLeave(), dispatch route-changed (pushes the URL via the top ux → mateu-ui) and navigate-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) while selected() read camelCase dataset keys (dataset.consumedRoute ⇒ attribute data-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 from metadata.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 sseUrl is set in the app metadata, a floating round button (.ai-fab, position: fixed, bottom-right) toggles a mateu-chat side panel. The button and chat are rendered once outside all variant-specific layouts in appRenderer.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 (reuses GlobalSearchSupplier/_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 early if (metadata.chromeless) branch in renderApp renders content full-bleed + FAB only). It's one shared DS-neutral LitElement mateu-command-center.ts (inline SVG icons, Lumo vars + fallbacks) mounted document-wide singleton by the shell base classes' updated() via commandCenterMount.ts::syncCommandCenter(host) (appended to the shell's renderRoot — 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 a MutationObserver so 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 shared route-changed+navigate-to-requested pair (works on every shell, no glue), records recents in recentRoutesStore.ts (localStorage mateu-recent-routes, scoped by app serverSideType), and the "Ask AI" row dispatches mateu-open-ai (mateu-app opens the chat). When commandCenterEnabled, mateu-app's own ⌘K palette + keydown stand down (guards on metadata.commandCenterEnabled). Wire: @App.commandCenter()/chromeless()AppMapper.getCommandCenter/getChromeless (chromeless ORs into commandCenter) → AppDto.commandCenterEnabled/chromelessApp.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)] (AppAttribute props) / @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 both ReflectionMapper.Map…app and mapper app builder). Tests: .NET SyncHandlerTests (Command_center/Chromeless facts), Python test_command_center.py. The command-center UI itself is frontend-only (same shared mateu-command-center), so the ports only emit the two wire flags.
  • Dark/light mode toggle: add themeToggle = true to @App to show a moon/sun icon button in the header of all app variants. The theme system works in layers: (1) index.html inline script applies localStorage['mateu-theme'] first, then falls back to the OS prefers-color-scheme media query — the OS change listener only fires if no user choice is stored; (2) MateuApp.toggleTheme() sets document.documentElement.setAttribute('theme', 'dark'|'light') and saves to localStorage; (3) MateuApp.connectedCallback reads the already-set attribute. Always use setAttribute('theme', 'light'|'dark') — never removeAttribute — to stay consistent with Vaadin's convention. The toggle button and the theme-change listener for HAMBURGUER_MENU are rendered in the right-side widgets container (the margin-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 @UI app 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 a LookupOptionsSupplier instantiated via InstanceFactory and searched with empty text (first 100) at app build time (AppMapper.getContextSelectorsAppDto.contextSelectorsAppContextSelectorDto(fieldName,label,options)). The picked value is persisted client-side in localStorage key mateu-app-context (one object per origin, {fieldName: value}) by appContextStore.ts; AxiosMateuApiClient.runAction merges it into the appState of EVERY request (explicit appState entries win), so the server reads it anywhere via HttpRequest.appContext(fieldName) (null when unset/blank). Changing the value reloads the page (uniform reactivity — the current route rebuilds against the new context), and a storage-event watcher reloads the OTHER tabs of the origin too (cross-tab sync). The widget: on the VAADIN shell (the shared appRenderer) it's mateu-vaadin-app-context-picker (2026-07-15) — Vaadin's own widgets: ≤7 options → vaadin-select (with a leading "—" clear item), more → vaadin-combo-box with a lazy dataProvider running the same _appcontext-search-<field> action (pick guards value equality so the init value-changed doesn't reload-loop); the sapui5/redhat/redwood/slds shells keep the DS-neutral mateu-app-context-picker LitElement: ≤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; responds Data{_appcontext_<field>: page} like the lookup search-<field> action); the picked label persists in mateu-app-context-labels so the button can display selections not among the loaded options. @AppContext fields are EXCLUDED from form bodies by FormFieldFilter (like @BadgeInHeader). .NET/Python parity: [AppContext] attribute (enum property or method returning OptionDto list) / @app_context decorator (method returning Option/(value,label) pairs or an Enum return annotation) emit the same contextSelectors wire 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; response fragments[].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.instantiateWithKnownType now 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 @UI app class implements AppActionsSupplier.appActions(HttpRequest) returning AppHeaderAction(actionId, label, icon, children) records — buttons on the app header next to the @AppContext pickers; an action with children (built via AppHeaderAction.menu(...)) renders as a dropdown and only the children dispatch. Evaluated per shell build (server-side visibility). Wire: AppMapper.getContextActionsAppDto.contextActions (recursive AppHeaderActionDto). Dispatch is APP-LEVEL like _appcontext-search-* (ActionInstanceCreator.isAppLevelAction flattens children before matching, so it works on root apps); the shared frontend dispatch lives in appHeaderActions.ts (dispatchAppHeaderAction: SSE-flavored runAction against the app's serverSideType, with the on-screen mateu-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): vaadin vaadin-menu-bar/vaadin-button (appRenderer), sapui5 ui5-menu, redhat PF menu markup, redwood oj-c-menu-button, slds slds-dropdown (icon names are Vaadin-specific and intentionally not rendered on the DS shells). .NET/Python parity: IAppActionsSupplier.AppActions() / AppActionsSupplier.app_actions() → same contextActions wire field; app-level dispatch needed no handler change there. Tests: AppHeaderActionsSyncTest (core), SyncHandler tests in both ports. User docs: the "Header actions" section of doc/.../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; typed List<Row> param gets crud_selected_items hydrated (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, localStorage mateu-saved-views, scope=pathname; ★=default view auto-applied when URL has no params; redwood has its own port in renderFilterBar). Totals & grouping@Aggregate(AggregateFunction) + @GroupBy on row fields → CrudStore.summaries (in-memory default, DB-overridable) → ListingData.aggregates/groups + CrudlDto.groupBy/GridColumnDto.aggregate; group column becomes implicit primary sort (ListingSummarySpec); shared listingGroups.ts interleaves marker rows; markers excluded from selection/click/editing (AggregatesSyncTest). Optimistic locking@Version int/long field; OptimisticLock.check/bump in 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-side columnPrefsStore.ts/applyColumnPrefs applied at mateu-table-crud's derived effectiveComponent (single choke point for all five renderers); identifier/action/select columns protected. Notification inbox — app class implements NotificationsSupplierAppDto.notificationsEnabled + app-level _notifications-list/-read (ids list or "all") → Data{_notifications}; shared mateu-notification-bell mounted in all five shells (NotificationsSyncTest). Guided importImportWizard<Row> archetype (upload @FileUpload/paste CSV → mapping grid with select editors → validation report → import valid rows); new FieldStereotype.fileUpload (file → data URI). UndoMessage.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-expired event with {retry, giveUp}; one retry, opt-in (sessionGuard.ts). Audit diffAuditEntry.when carries @GroupBy: the History dialog groups by save moment = version-by-version diff. Planning boardPlanningBoard/PlanningResource/PlanningBlock (resources × days tape chart, drag dispatches moveActionId with _blockId/_resourceId/_start/_end, click selectActionId); demo /planning-demo. Global search — app class implements GlobalSearchSupplierAppDto.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 in ActionInstanceCreator.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 like loopGuard/dirtyGuard): infra/http/requestPolicy.ts classifies a transport failure into {kind, message, retryable} — kinds offline|timeout|server|unauthorized|notFound|client|cancelled|unknown, user-facing English text instead of axios jargon (ERR_CANCELED→cancelled/silent, axios reuses ECONNABORTED for its own TIMEOUT not for aborts, no-response+ERR_NETWORK→offline even when the flag claims online); infra/http/retryPolicy.ts decides whether we MAY repeat: isIdempotentAction(actionId, declared) (ALWAYS_SAFE = ''the route load, mateu-ux fires actionId: '' NOT __load__ —, __load__, search, _globalsearch, _notifications-list; prefixes search-, _appcontext-search-; the wire flag is an opt-IN that never opts a known read out; undefined''), shouldRetry (timeout|server only — offline is deliberately excluded, reconnection is connectivity's job), MAX_RETRIES 2, retryDelayMs 300·3^(n-1) ±25% jitter; infra/http/connectivity.ts treats navigator.onLine as a hard NEGATIVE only (it lies on captive portals) and takes the positive from our own traffic (noteReachable/noteUnreachable), with whenBack(cb) waiters; infra/ui/pendingActions.ts in-flight registry keyed componentId::actionId with a 120s stale valve. UI: infra/ui/pendingIndicator.ts marks the pressed control data-mateu-pending+aria-busy and 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 ::after spinner: a pseudo-element on a shadow host is NOT PAINTED (verified on a live vaadin-button: inset:0;background:red renders 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) uses composedPath()[0] (target is retargeted) and the origin rides detail._originElement across bubbling re-dispatches. Wiring: mateu-component.requestActionCallToServer claims the slot + marks the control (skipped entirely for background actions; reads claim NO exclusive slot — blocking them would drop the type-ahead search for "mad" while "ma" is in flight), releases on backend-succeeded/failed/cancelled-event filtered at-target (those events bubble through from child components) and on disconnectedCallback. Transport: wrap() now takes a THUNK so N retries = ONE reported outcome, marks the error __mateuReported so HttpService's catch stops raising a SECOND toast (pre-existing duplicate-toast bug), and puts {failure, retry} on the failed event; RunActionOptions{timeoutMillis, idempotent, retry} threads mateu-componentserver-side-action-requested detail → mateu-ux → HttpService/SSEService → post(uri, data, timeoutMillis); the retry closure re-enters HttpService.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 generic ToastMessage.actionLabel/onAction in the Notifier port, implemented in BOTH neutralNotifier and VaadinNotifier — cleaner than piggybacking on undo). mateu-ux renders 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 from mateu-ui (the composition root, next to registerNeutralNotifier), always-light pastels, and it PUSHES the body down (padding-block-start from its measured height) instead of covering the page title. Wire: @Action(timeoutMillis, idempotent) → fluent ActionActionDtoAction.ts; ports .NET [ActionOptions(TimeoutMillis, Idempotent)] (+WithActionOptions in ReflectionMapper) and Python @action_options(...) (+with_action_options in mapper) — the ports had no @Action equivalent, so the knobs got their own composable attribute/decorator. Tests: 35 vitest (requestPolicy/retryPolicy/connectivity/pendingActions), ActionsAndCommandsSyncTest (+2), Python test_action_options_travel_on_the_action_dto, .NET Action_options_travel_on_the_action_dto; golden JSON in test_sync_handler.py/test_ux_components.py/SyncHandlerTests.cs grew the two trailing fields. e2e/slow-network-probe.mjs is 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 @Action section of reference/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 upstream vaadin-tabs) because Vaadin's own components carry their field a11y — the real gaps were the ones axe CANNOT see. Shared helpers in infra/a11y/: focusTrap.ts (trapFocus(container) → move focus in + cycle Tab inside + restore on release; tabbablesWithin walks OPEN SHADOW ROOTS and slots because querySelectorAll stops at the boundary and every overlay renders through nested custom elements; deepActiveElement/isInside use host-walking, not contains), 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; nextIndexForKey roving-tabindex arrows; pure, no lit import) and focusStyles.ts (activatableFocusStyles, a CSSResult interpolated into each component's static styles — a document rule would never cross the shadow boundary). Fase 1 (forms+overlays): mateu-field.ts now sets invalid/errorMessage ON the control in updated() (found by PROPERTY PROBING 'invalid' in el, not by tag name) so the DS wires aria-invalid/aria-describedby inside 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.focusFirstInvalidField moves focus to the first rejected field (walks shadow roots, retries 3 frames because the flags are set by each field's own updated()); mateu-dialog gained role="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 are role="alert"+assertive in the neutral notifier and explicitly announce()d in VaadinNotifier (vaadin-notification's overlay is not a live region). Fase 2 (keyboard): 25 sites across 19 components got role+tabindex="0"+@keydown=onActivate(sameExpr) (applied by a scripted transform, then hand-refined: faq→aria-expanded, task-queue→role=option/aria-selected in a role=listbox, gantt bars→aria-label with 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 a href="#id" can't cross them), role="main" on all 4 appRenderer shell variants, announce(document.title) on the SetWindowTitle command (the one reliable per-route signal every renderer gets), and focus-to-heading on route change in mateu-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 fixture OverlayForm/OverlayContentForm (/overlays) — the SUT had no dialog/drawer to test against. Known carve-out, scoped to one rule on one element: vaadin-tabs is role=tablist with 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 stays node; the two files whose SUBJECT is the DOM opt into jsdom with a // @vitest-environment jsdom docblock (jsdom added as a devDependency). Docs: ux-patterns/accessibility.md. GOTCHA worth remembering: a ::before/::after on a shadow HOST is not painted — verified with an inset:0;background:red on a live vaadin-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; no AccessibleContext call anywhere in the plugin). React Native (src/a11y/a11y.ts): fieldA11y composes the control's NAME as "<label>, required, invalid: <error>"RN has no labelFor/aria-labelledby, so a <Text> label above a TextInput is 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: one a11y descriptor computed once in FormFieldRenderer and spread onto all 7 TextInput branches + the custom widgets (needed description added to its FieldMeta); accessibilityRole on 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, skipping onPress={() => {}} no-op tap-swallowers); explicit labels on the icon-only ones (✕/➤/💬/‹/›/✎/📷); accessibilityViewIsModal on the 4 Modals; announce() on validation failure and on SetWindowTitle in MateuViewController. App.tsx's hardcoded backend port is now EXPO_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) + reflective announce via AccessibleAnnouncerUtil (2022.3+, silent on failure). Applied at the ONE choke point in FormFieldRenderer where caption and input meet: caption.labelling(input), required/@Help folded into the accessible DESCRIPTION (the asterisk is read as punctuation or skipped), the validation error appended to it, and input.putClientProperty("mateu.fieldId", …) so AppContext.focusFirstInvalidField can focus the rejected field; Buttons.renderButton names every Mateu button from the wire label. labelling also 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 had isFocusable = false (browsing months was mouse-ONLY) and was named only on the icon-less fallback path. Verification tooling: RenderProbe.dump now prints a11yName/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.mjs drives 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 to aria-*, 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 of ux-patterns/accessibility.md.
  • Resiliencia de red en el renderer VB/Redwood (2026-08-03) — el port de lo de libs/mateu a apps/redwood, que no comparte NADA de core con los renderers web: su transporte es poc/transport.mjs con fetch pelado, así que ninguna garantía se hereda. Nuevo poc/resilience.mjs (cuarto fichero de la fuente única, concatenado por make-amd.mjs ENTRE 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 con res.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 un TypeError genérico sin código. Marca __mateuTimedOut para 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 -1 y caía en el default, matando el stream). runMateuAction aplica 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.subscribe se cablean en loadMateuShell.js ANTES 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 + chain dismissMateuError. Tests: poc/test.mjs pasa de 32 a 47; los 4 async necesitaron un runner atest SERIALIZADO — lanzados a la vez se pisan el globalThis.fetch y 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ón oj-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 — los oj-button borderless 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 a oj-sp-global-header usando --oj-core-text-color-inverse (vacío en este tema → manda el fallback); (2) los iconos de colapsar del menú sin nombreoj-navigation-list los emite como <a role="button"> VACÍOS; se nombran desde el texto de su grupo tras el refresh (nameCollapseIcons en 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" en oj-vb-content, y announceNavigation en onMateuNavigate. Dos gotchas que costaron: (a) focusContent encontraba un <h1> VACÍO (la shell lo pinta antes de que llegue el título) y focus() 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 un role=button dentro 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 el context de 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 a onMateuNavigate con contexto fresco, el de acción viaja como evento de aplicación NUEVO mateuRetryAction porque 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 mapa chains (VB las resuelve por fichero en *-chains/) — mis dos primeros listeners usaban chainId + un mapa inventado y NUNCA se disparaban, aunque el botón se pintara (el test sólo comprobaba que existía). Sonda e2e/vb-slow-network-probe.mjs ampliada a 8/8; vb-a11y-probe.mjs sigue 10/10.
  • FABs (@Fab): annotate methods with @Fab(icon="vaadin:plus", label="...", order=0) to create floating action buttons. At app level (@UI class), FABs appear globally stacked above the AI FAB at right: 1.5rem. At page level (any page class), FABs appear stacked at right: 5.5rem and are scoped to that page. FAB actions are dispatched via the standard action-requested event mechanism. FabDto is the wire type; Fab.ts is the frontend TS interface.
  • Page banners (@Banner / BannerSupplier): show messages below the page header and above the first form section, rendered as vaadin-card. Two approaches — declarative: annotate methods with @Banner(theme=BannerTheme.INFO, title="...") (method may return String for a dynamic description); programmatic: implement BannerSupplier.banners() returning List<PageBanner> (takes precedence over annotations, same pattern as ToolbarSupplier/ButtonsSupplier). Themes: INFO (blue), SUCCESS (green), WARNING (amber), DANGER (red). Wire type: BannerDto via existing PageDto.banners field; frontend rendering in mateu-page.ts. Dark mode: banner backgrounds are always light pastels so text and title slot must use color: #1a1a1a explicitly — CSS shadow rules don't reach the slot="title" light DOM child, so the color is applied inline on the span. Extra options on @Banner: closeable = true adds a dismiss button; timeoutSeconds = N auto-dismisses after N seconds. Both also work on PageBanner constructor fields.
  • Action-returned banners: action methods (e.g. @Toolbar) can return PageBanner, List<PageBanner>, or PageBanners to show banners on the current page dynamically. They are carried in UIIncrementDto.banners, dispatched via page-banners-received DOM event, and shown alongside the static @Banner banners in mateu-page.ts. Replace vs append: returning a bare PageBanner / List<PageBanner> replaces all existing action banners (default). Use PageBanners.replace(banner…) for explicit replace or PageBanners.append(banner…) to accumulate banners across multiple action calls. Action banners are automatically cleared when the user navigates to a different page (i.e. when mateu-page.component changes). Implementation note: PageBanner and PageBanners are excluded from FragmentListMapper — 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 current state and data context. 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 used possiblyHtml() 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 by mateu-content-header.ts via FormDto.badges). Important: @Badge is 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 is true, 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 @BadgeInHeader are automatically excluded from the form body by FormFieldFilter. Programmatic: implement BadgeSupplier.badges() returning List<Badge> (takes precedence over @BadgeInHeader fields, same pattern as BannerSupplier). Colors follow Vaadin Lumo badge themes (normal, success, error, warning, contrast). Pipeline: PageMetadataExtractor.getBadges()ReflectionPageMapperPageView.badgesPageMapperFormDto.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.
  • @Inline fields with actions (@Toolbar / @Button on nested types): annotating a field with @Inline expands the nested type's fields directly into the parent @Section card without adding a Card wrapper. 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 via SectionFormRenderer.buildTitleRow). Methods annotated with @Button appear below the section content as a right-aligned button row. Action dispatch follows the "nested-form-action-<fieldName>-<methodName>" prefix, handled by RunMethodActionRunner. Button labels respect @Label (via FieldMetadataExtractor.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 @Inline on dense, multi-section screens (@Compact + @Zones) where the extra card chrome of a non-inline subform would add visual noise.
  • @Inline on embedded orchestrator fields (MultiView subclasses, e.g. AutoEditableView): when the host field is annotated @Inline, the embedded mediator drops its badges/kpis, demotes its title from h2 to h3 (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 @Section that hosts an @Inline embedded mediator also drops its own title row so the two don't visually compete — the embedded h3 title + 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 editable PersonalDataView inside a "Datos personales" section, or a read-only CardexView inside an "Info Cardex" tab). Mechanism: EmbeddedOrchestratorFieldBuilder appends _inline=1 next to _embeddedMediator=1 on the marked route and seeds it into initialData; EditableView calls isInline(httpRequest) to set PageView.level=1 and drop badges/kpis; SectionFormRenderer.render() skips the Card outlined wrap on the single-section path when EmbeddedOrchestratorFieldBuilder.isInlineRequest(httpRequest) is true; SectionFormRenderer.renderSections() hides the parent section title via hostsInlineEmbeddedMediator(). For tabs (which don't carry their own title row), the embedded h3 becomes the only visible title — remove @Title from the inner model to suppress it entirely.
  • Multi-state embedded islands (backend-driven state machines) + island state seeding: an @Inline embedded EditableView whose view(...) returns a DIFFERENT model per backend state is the pattern for an in-page element with N server-decided states (demo: demo-front-office DocumentoView inside IdentidadStep — 3 states: sin datos@Notice(theme="warning") String + scan Button; 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 seedingEmbeddedOrchestratorFieldBuilder.build(...) takes the HOST instance and seedInstanceState(...) copies the field VALUE's simple fields (String/Number/Boolean/primitive/enum, non-null; e.g. a configured stayId) into the island's initialData, so the host passes context by just setting fields on the orchestrator instance (documento = new DocumentoView(); documento.setStayId(...) in load()); (2) mateu-ux.initialState — the MEDIATOR branch of appRenderer.ts feeds the fragment state into the inner mateu-ux, whose initial __load__ (and route-flip reloads) now send it as componentState instead of loading empty — without it the island's first render had no seeded state; (3) the async leg (scan) is a LongTask SSE action (Action.sse(true) advertised in actions()) whose .withCommand(UICommand.dispatchEvent("evento")) fires the bus event that a @SubscribeTo on 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 overriding readOnly(). Also fixed while building this: ComponentToFragmentDtoMapper's ComponentTreeSupplier branch now honors DtoSupplier first (an orchestrator dropped into a composite component tree used to throw via component()). Tests: EmbeddedIslandStateSeedingSyncTest. Gotcha: the host is re-created per request, so it must derive its own context (e.g. the id from the route) in load() 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, a Notice whose theme tracks completeness success/warning, custom icon 👥, content = one Button per pax labeled k/N, color=success when that pax's data is in, buttonStyle=primary for the selected one, Button.parameters=Map.of("paxIndex", k)) dispatches selectPax on the wizard, which returns List.of(this, UICommand.dispatchEvent("pax-seleccionado", Map.of("paxIndex", n))) (an action CAN return state + commands in one Collection — FragmentListMapper maps the fragments, CommandMapper collects the UICommands); every island model carries @SubscribeTo(event="pax-seleccionado", action="cambiarPax") and the handler reads paxIndex from the event-detail parameters and route-flips. The wizard itself also subscribes to the island's documento-escaneado to re-render the band colors. A custom mateu-notice icon (emoji) renders at natural size without the severity circle (.icon.custom). Domain: Companion carries document/verified/email/phone (identityComplete()), Stay.companionAt(paxNumber)/registerCompanion(paxNumber, c) pad gaps with Companion.pending(n); pax 1 = the Guest aggregate (uniform access via DocumentoView's private Pax interface).
  • High-density mode (@Compact): annotate a page class with @Compact to render it in condensed mode — smaller control heights, tighter spacing, and smaller field labels — so information-dense screens fit without scrolling. Implemented by injecting the StyleConstants.COMPACT CSS 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 to 7em (vs the standard default), allowing more columns to fit at the same viewport width. Additionally, @Compact pages emit a compact-changed event from mateu-page on render; mateu-app listens and adds a no-padding CSS class to the app-content element, 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-in compact row theme. StyleConstants.COMPACT includes a --mateu-compact:1 CSS custom property marker used by frontend components to detect compact mode. Opt-in and non-breaking — pages without @Compact are unaffected. Alternatively, compose StyleConstants.COMPACT directly via @Style when you need compact mode on only a part of the page, or want to blend it with other style constants.
  • @PlainText at class level: @PlainText can 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 @PlainText field (or a class) with @Multiline to 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.AUTO heuristic: @App(AppVariant.AUTO) (which is now the default since value() defaults to AUTO) auto-selects the app shell variant. The rule lives in AppMetadataExtractor.getVariant(...): an explicit non-AUTO @App(...) value always wins; otherwise, when the menu has Menu items — a deep menu (any top-level Menu with a nested Menu submenu) → TILES; more than 7 top-level items → HAMBURGUER_MENU; else → MENU_ON_TOP. When there are no Menu items at all → TABS. Adjust the thresholds/selection there.
  • AppLayout on @App: the @App annotation has a second attribute layout of type AppLayout (default SINGLE_SLOT). AppLayout.SPLIT renders the content area as a two-pane split layout. This is distinct from AppVariant (which controls navigation chrome) — layout controls 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 for debounceMillis ms 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/@Button on 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's actions list with the nested id (nested-form-action-<field>-<method>) by ActionMapper.addNestedFormsActionsFieldActionCollector (which preserves the @Action shortcut), and mateu-component._keydownListener scans that list. @Action(runOnEnter=true) is equivalent to shortcut="enter". The matcher mateu-component._shortcutMatchesEvent matches by e.key or e.code (KeyX/DigitX/NumpadX), so modifier+letter/digit shortcuts are keyboard-layout independent (important on e.g. Spanish layouts where Ctrl+Alt+<letter>/AltGr remaps e.key to a symbol) and the numeric keypad works. The button's shortcut also shows as a title tooltip (buttonRenderer.ts) — for which ButtonMapper propagates Button.shortcut to ButtonDto. Demo: every action on CheckInFormV2 (/checkin/:id/v2) is bound to a Ctrl+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) → FormLayoutBuilder sets io.mateu.uidl.data.Tab.shortcut (fluent record, new field) → TabMapperTabDto.shortcut → frontend Tab.ts.shortcut. The frontend emits it as a data-shortcut attribute 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 any mateu-drawer/mateu-dialog in the render root, since querySelectorAll does 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 enclosing vaadin-tabs.selected to the tab's index — in-place, no server round-trip. Gotcha: tabs are grouped by consecutive fields sharing the same @Tab name within a section; putting a @Section on each tab's fields splits the form into several separate one-tab strips (each its own vaadin-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 @Tab with open=true to make it the initial selection instead (independent of shortcut, which only selects on demand; if several tabs in one strip declare open=true, the first wins). Pipeline: @Tab.open() (uidl annotation) → FormLayoutBuilder sets io.mateu.uidl.data.Tab.active (fluent record field; also settable when building a TabLayout dynamically) → TabMapperTabDto.active (the DTO field already existed, previously always false) → frontend Tab.ts.active; renderLayouts.ts renderTabLayout computes activeIndex (first child whose metadata .active, else 0) and its @items-changed handler sets vaadin-tabs.selected = activeIndex (both the adaptable and plain vaadin-tabsheet branches share the one handler). Additive + backward-compatible (open defaults 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 wire active, open defaults false): .NET TabAttribute.OpenReflectionMapper.TabLayout computes the active index (also fluent TabPanel.Active honored in ComponentMapper) → TabMetadataDto.Active; Python Tab(open=...)mapper.tab_layoutTabMetadata.active. Port tests: .NET LayoutInferenceTests.Tab_marked_open_is_active_on_the_wire_and_others_are_not, Python test_tab_marked_open_is_active_on_the_wire_and_others_are_not. Demo: the check-in drawer variant CheckInFormV4 (/checkin/:id/v4) + CheckInReferenceDrawer — the essential check-in stays on the page; a single @Toolbar button (ctrl+alt+d) opens a modeless Drawer whose content is a ModelViewComponent-wrapped tabbed panel (the same section components as v2) with the Cardex tab open=true and per-tab alt+1..9 shortcuts. Contrast with v2 (stacked + sticky @Toc) and v1/v3 (@Zones master-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 returning UICommand.markAsDirty() / UICommand.markAsClean() from any action (typically markAsClean() after a successful save; a backend-driven NavigateTo marks clean instead of prompting). Frontend architecture (centralized in commit "centralize unsaved-changes navigation guard"): dirtyGuard.ts is the single source of truth — it owns the dirty flag, wires the document-level dirty/clean listeners once, installs the beforeunload guard, and exposes confirmLeave(). mateu-app (route selection) and mateu-ui (browser back/forward) both delegate to it; mateu-component resets the dirty flag when a tracked form (re)loads, tied to the lifecycle that rebuilds formerState (so reset no longer depends on the backend sending MarkAsClean). Annotation lives in uidl (ConfirmOnNavigationIfDirty.java), surfaced via ServerSideComponentDto. Documented for users in doc/.../reference/key-annotations.md, ux-patterns/partial-forms.md, and fluent-components/fluent-commands.md. EditableView.navigate auto-cleans on /view: when an EditableView's save or cancel-edit lands on /view, navigate(...) automatically appends UICommand.markAsClean() to the response. Required because the unmount of the edit-mode mateu-component does NOT fire clean (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 tagged FieldDataType.money so the front-end formats the value (thousands separator + 2 decimals via Intl.NumberFormat, de-DE by default; Amount values use their own locale/currency). Logic in FieldTypeMapper (getDataType / getStereotype, helpers isMoneyStereotype / isPlainTextContext): the money stereotype yields plainText for layout while dataType=money carries the formatting intent. Front-end formatting is in the plainText branch of mateu-field.ts. Amount-typed fields already get dataType=money automatically. 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: return UICommand.dispatchEvent(eventName) or dispatchEvent(eventName, payload) from any action; the frontend (ConnectedElement.applyCommand, DispatchEvent branch) dispatches a real bubbles+composed DOM CustomEvent from the emitting component element, stamping detail.__source with the emitter's logical name (@Emits(name=...), falling back to serverSideType) — 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 runs action server-side on that component passing event.detail as parameters. Scope = SubscriptionSource: DOCUMENT (default — global bus, the listener is attached to document so it reaches sibling/unrelated components), COMPONENT (listens on document but filters by detail.__source === from), SELF (legacy: listens on the component's own element, only catches events bubbling up from descendants). A raw @Trigger(type=OnCustomEvent) maps to SELF (backward compatible). Pipeline: @SubscribeTo/@EmitsTriggerMapper / EmitsMapperOnCustomEventTriggerDto(source,from) + ServerSideComponentDto.emitsName → frontend ComponentElement.registerCustomEventListeners() (attaches to document or this per scope; removed in disconnectedCallback) + customEventManager (filters by __source, only stopPropagation for SELF). @Emits is mostly declarative; its only runtime effect is supplying the name stamped as __source. Used in the check-in demo: GuestsSection (@Emits(name="guests-section")) emits checkin-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 a Drawer from any action to open its content (typically a form) in a panel sliding in from a viewport edge — the native side-panel counterpart of Dialog. Record in uidl/data/Drawer.java: headerTitle, header, content, footer, position (DrawerPosition.start|end, default end), width, noPadding, modeless (no backdrop), initialData. Pipeline mirrors Dialog exactly: DrawerMapper in OverlayComponentDispatcherDrawerDto (registered in ComponentMetadataDto subtypes + YamlUidlMapperFactory) → fragment emitted with action Add (FragmentDataSerializer.isOverlay, shared with dialogs) so it stacks on the page instead of replacing it → shared mateu-drawer.ts LitElement (design-system neutral: Lumo vars with fallbacks; slide transition, backdrop, header ✕, Esc closes only the topmost overlay in its root) via drawerRenderer.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.closeModal queries mateu-dialog, mateu-drawer and closes the last in DOM order); closeModal(eventName) / closeModal(eventName, payload) additionally emit the named custom event through the standard @SubscribeTo bus (frontend: the CloseModal branch calls the same dispatchNamedEvent helper 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 with closeModal("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 by RunMethodActionRunner.createParameters). Works on read-only grids (unlike the default <fieldId>_selected CRUD detail-edit path, which is also broken for nested grids), so it's the way to build master/detail. Pipeline: GridColumnBuilder.getOnItemSelectionActionId() sets FormField.onItemSelectionActionId to the routed action id — bare method name at page level, or nested-form-action-<prefix><method> when the grid is inside a nested @Inline section (e.g. nested-form-action-guestList-onGuestSelected); the frontend mateu-grid.ts active-item-changed handler (fires on row click regardless of the selection column / read-only) dispatches action-requested with parameters: { _clickedRow: item }. The action is auto-registered in the component's actions list by FieldActionCollector (for each @OnRowSelected field) — required because mateu-component.manageActionRequestedEvent only sends an action to the server if it is in actions; otherwise it bubbles unclaimed and is dropped. Commonly combined with @Emits/UICommand.dispatchEvent + a @SubscribeTo to 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 via e.code). Pipeline: @OnRowSelected.shortcut()GridColumnBuilder.getRowSelectionShortcutFormField.rowSelectionShortcutFormFieldDtomateu-grid.ts, whose document keydown handler resolves the row and calls the shared selectRow(item) (same path as a click: sets selectedItems + dispatches action-requested with _clickedRow). Demo: GuestsSection.guests is @OnRowSelected(value="onGuestSelected", shortcut="ctrl+shift") → emits pax-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 ServerSideComponent by making it a MultiView (e.g. a read-only AutoEditableView<T> — see the embedded-orchestrator field mechanism). Three rules make the self-reload work: (1) put @SubscribeTo on the loaded entity (the model view), not the orchestrator — MultiView.wrapView maps the embedded component's triggers from the loaded entity; (2) advertise the reload action by overriding actions(HttpRequest) to add it (so the embedded component claims it and routes it to handleAction); (3) in handleAction, after updating the (static, demo) holder, alternate the always-view route (setRouteTo(flip ? "/view" : "/")) and return 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 in CheckInForm; selecting a guest row reloads only the cardex with that pax's full data. Documented in doc/.../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 the mateu.layout.inference system 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 in core/.../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-panel AccordionLayout labeled "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/@FoldedLayout checked earlier) → one tab per section (SectionFormRenderer.tabsFromSections, id _tabs); small-enum→radio — enum with ≤4 constants renders as radio buttons (FieldTypeMapper; @UseRadioButtons forces radio at any size — that annotation was dead until 2026-07-05: it had no @Retention(RUNTIME)). Wire: TabLayoutDto (and fluent TabLayout) carry groupRelationship (alternative/sequential/simultaneous — semantic relation between the groups; dev-declared tabs always emit alternative) and adaptable (true under inference → renderers may degrade tabs to an accordion on narrow viewports without losing the disclosure semantics). Tests: LayoutInferenceSyncTest (also asserts non-@AutoLayout classes 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 at PageTypeResolver's shape-fallback branches) logs a one-time INFO hint when a plain reflected form structurally resembles an archetype — ≥1 MetricCard field → "looks like a Dashboard"; a List field with @OnRowSelected next to a component-holder field → "looks like a CollectionDetail"; @PageTemplate silences it. Fase 1 (opt-in @AutoPage, uidl, composable; also enabled by the mateu.layout.inference property with @AutoPage(false) opt-out): fully-derivable shapes stop advising and COMPOSE — PageInference.composesDashboard (enabled + ≥1 MetricCard field + NOT a Component/Crud/Listing) makes ReflectionUiIncrementMapper.map substitute the instance with the InferredDashboard bridge (same pattern as AdaptedComponentTree, right after the adapter branch): it advertises the MODEL as serverSideType (actions keep routing), carries @PageTemplate(DASHBOARD) for the wire pageType, honors the model's @Style, and composes via DashboardComposer — the Dashboard archetype's composition extracted so subclassing and inference share one implementation (Dashboard.component() now delegates; behavior pinned by ArchetypesSyncTest). Welcome rule (same day): PageInference.composesWelcome — ≥1 Button field AND all fields presentational (Button/Component/holder; ONE data field keeps it a form) → InferredWelcome bridge (WelcomeComposer extracted likewise; hero title derived from the class @Title, subtitle/image stay subclass-only); dashboard checked first (stronger signal); advisor stands down via PageInference.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 public ArchetypeComposers in Archetypes.cs, MapView composes at the tree-supplier branch, PageTypeOf gained welcome→landing; tests AutoPageTests) and Python @auto_page + mateu_core/page_inference.py (component_tree composes via the existing compose_dashboard/compose_welcome, made tolerant of plain instances; page_type_of gained welcome→landing; tests test_auto_page.py); Java PageTypeResolver gained 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 (tests PageFingerprintTest; 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 by io.mateu.core.infra.reflection.MetaAnnotations.find/isPresent(element, X.class) (like Spring's findMergedAnnotation, minimal — first match, no @AliasFor). Every framework annotation read in core goes through MetaAnnotations and every field/method/class uidl annotation carries ElementType.ANNOTATION_TYPE in 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 stay aClass.getAnnotation(...) directly. To make a NEW annotation composable: add ANNOTATION_TYPE to its @Target and read it via MetaAnnotations. 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 in uidl/.../interfaces/ComponentAdapter.java: type(), adapt(T, HttpRequest) → AdaptedView, deserialize(Map state, HttpRequest) → T. AdaptedView (uidl/.../data/AdaptedView.java) bundles components + state + data + actions (action ids the view exposes). Register the adapter as a bean (@Service); discovered via MateuBeanProvider.getBeans(ComponentAdapter.class). Pipeline: AdapterRegistry (core infra/adapters/) finds the adapter by type; ReflectionUiIncrementMapper.map substitutes the instance with an AdaptedComponentTree bridge (implements ComponentTreeSupplier+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 as serverSideType (via the new ComponentTreeSupplier.serverSideType() default) so state routes back. AdapterInstanceFactory (InstanceFactory, priority 100) wins for adapted types and calls deserialize to rebuild the model from the incoming state — this also builds the initial route instance from an empty state, so deserialize must guard each assignment with state.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 — ReflectionFormFieldMapper wraps new AdaptedComponentTree(value, adapter) in a CustomField, which ComponentToFragmentDtoMapper maps to its own ServerSideComponentDto (own serverSideType+state+actions), so its buttons round-trip through the adapter on their own (a real second mateu-component boundary). 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 — @Signature renders mateu-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 @PhotoCapture renders mateu-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); stereotypes signature/camera mapped in FieldTypeMapper; 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 Python Signature()/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/OptionDto carry a children list (canonical constructors grew — 6-arg convenience kept; FieldMapper.mapOption maps recursively). @TreeSelect(leavesOnly=…) → stereotype treeSelect + FormField(Dto).treeLeavesOnly; the hierarchy comes from the view's OptionsSupplier returning nested Options; frontend mateu-tree-select (shared LitElement: button + expandable-nodes panel, leavesOnly makes group nodes expand-only) used by mateu-field and redwood renderField. TREE LOOKUP SELECTORS: a Selector Listing with gridLayout() = GridLayout.tree and a self-referential children row list shows the lookup dialog as a tree — mateu-table-crud.renderTree renders the select column as the Select button (same action-on-row-select dispatch 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 (uidl Option record with Children, mapped recursively) and Python TreeSelect(leaves_only=…) + the view's options(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 a String field — 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 (via MetaAnnotations) → FieldStereotype.uploadableImage → frontend mateu-field.ts: editable branch renders <img> + a hidden <input type=file> + vaadin-buttons; imageUpload uses FileReader.readAsDataURLvalue-changed, imageDelete sets value '', triggerImageUpload clicks the hidden input; the read-only image branch also handles uploadableImage (shows just the <img>). stereotype is 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 (@EyesOnly on fields, @ReadOnlyUnless, @DisabledUnless): the same identity dimensions as @EyesOnly (roles/groups/scopes/permissions, resolved from the JWT Bearer token by Authorizer) now drive three field states, not just menu visibility. Hide: @EyesOnly on a form field hides it when unauthorized — evaluated in FormFieldFilter.filterField (so it also applies to inline subforms and listing columns; previously @EyesOnly only gated menus in AppMenuBuilder/MenuEntryMapper). Read-only: @ReadOnlyUnless(...) (field or class level) makes the field/view read-only unless authorized — evaluated in PageFormBuilder.readOnlyByPermission, called from both PageFormBuilder.isReadOnly and ReflectionFormFieldMapper.isReadOnly (static readOnly bool on FormFieldDto). Disabled: @DisabledUnless(...) (field or @Button/@Toolbar method) disables unless authorized — for fields it emits a client-side disabled Rule in RuleMapper.createRules (mirrors @Disabled); for buttons it's OR-ed into the disabled flag in PageButtonsBuilder via disabledByPermission(...). All three reuse Authorizer.isAuthorized(...) (refactored to a shared private matches(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: MetaAnnotations has no @AliasFor, so a composed (semantic) annotation wrapping these cannot override their dimension attributes. Annotations in uidl (ReadOnlyUnless, DisabledUnless). Demo: demo-admin-panel/.../security/FieldAccessDemo.java. User docs: doc/.../reference/key-annotations.md.