You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by da...@apache.org on 2026/09/03 16:42:09 UTC

(causeway) branch htmx updated (0827bd9164b -> d30392886a5)

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a change to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git


    from 0827bd9164b docs(openspec): archive application-authored HTMX shell
     new ea372fc6ddb docs(openspec): plan generic Vue web component viewer
     new afdb0b317a3 feat(webcomponents): add generic Vue viewer
     new 39607fd9249 docs(webcomponents): add Petclinic run and test scripts
     new bef375a250d docs(openspec): archive generic Vue web component viewer
     new fb4af5e2bb3 docs(openspec): plan Vue Petclinic reconciliation
     new 4f46b220d4f feat(webcomponents): reconcile Vue Petclinic presentation
     new 509d1b4b6fd docs(openspec): archive Vue Petclinic reconciliation
     new 39199d115c7 docs(openspec): plan web component logout and local redirects
     new a7a6cc96501 feat(webcomponents): support logout policy and local redirects
     new 0406fbad441 fix(webcomponents): suppress Logout in Vue menus
     new b7453c794be docs(openspec): archive web component logout and redirects
     new 325ca7434e2 docs(openspec): plan secured Vue Petclinic variant
     new b2ad141a494 feat(webcomponents): add secured Vue Petclinic variant
     new f9c2a33f7c1 docs(openspec): sync secured Vue authentication specs
     new 6606499af6c docs(openspec): archive secured Vue Petclinic variant
     new d30392886a5 fix(webcomponents): activate secured Vue source profile

The 16 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../.openspec.yaml                                 |    0
 .../design.md                                      |  174 +
 .../proposal.md                                    |   39 +
 .../specs/generic-vue-web-component-viewer/spec.md |  347 ++
 .../tasks.md                                       |   70 +
 .../.openspec.yaml                                 |    0
 .../design.md                                      |   97 +
 .../proposal.md                                    |   30 +
 .../specs/generic-vue-web-component-viewer/spec.md |   40 +
 .../spec.md                                        |   70 +
 .../vue-viewer-local-secman-authentication/spec.md |  179 +
 .../spec.md                                        |   68 +
 .../tasks.md                                       |   49 +
 .../.openspec.yaml                                 |    0
 .../design.md                                      |  111 +
 .../proposal.md                                    |   31 +
 .../specs/generic-vue-web-component-viewer/spec.md |   61 +
 .../tasks.md                                       |   45 +
 .../.openspec.yaml                                 |    0
 .../design.md                                      |  104 +
 .../proposal.md                                    |   33 +
 .../specs/domain-web-components/spec.md            |   55 +
 .../generic-htmx-web-component-viewer/spec.md      |   60 +
 .../specs/generic-vue-web-component-viewer/spec.md |   84 +
 .../tasks.md                                       |   44 +
 .../add-generic-vue-web-component-viewer/design.md |   99 -
 .../proposal.md                                    |   36 -
 .../specs/generic-vue-web-component-viewer/spec.md |   92 -
 .../add-generic-vue-web-component-viewer/tasks.md  |   29 -
 openspec/specs/domain-web-components/spec.md       |   54 +
 .../generic-htmx-web-component-viewer/spec.md      |   59 +
 .../specs/generic-vue-web-component-viewer/spec.md |  505 +++
 .../spec.md                                        |   56 +-
 .../vue-viewer-local-secman-authentication/spec.md |  185 ++
 .../spec.md                                        |   74 +
 viewers/webcomponents/README.adoc                  |   27 +-
 viewers/webcomponents/foundation/USAGE.adoc        |   13 +-
 .../webcomponents/foundation/src/action-result.mjs |   70 +
 .../foundation/src/host-operation-policy.mjs       |   43 +
 viewers/webcomponents/foundation/src/index.mjs     |    3 +
 .../foundation/src/local-resource-policy.mjs       |   99 +
 .../foundation/src/menubar-element.mjs             |   21 +-
 .../foundation/src/menubars-element.mjs            |   12 +
 .../foundation/src/object-context-controller.mjs   |   26 +-
 .../foundation/src/service-action-context.mjs      |   34 +-
 viewers/webcomponents/foundation/src/types.mjs     |    2 +
 .../test/application-menu-client.test.mjs          |   50 +
 .../foundation/test/element-prefix.test.mjs        |    3 +-
 .../test/host-navigation-policy.test.mjs           |   99 +
 .../foundation/test/menubar-component.test.mjs     |    5 +
 .../webcomponents/htmx-security-secman/README.adoc |   13 +-
 viewers/webcomponents/htmx-security-secman/pom.xml |    9 +-
 ...oduleViewerWebcomponentsHtmxSecuritySecman.java |    6 +-
 .../secman/HtmxSecmanSecurityConfiguration.java    |   28 +-
 .../secman/HtmxSecmanHttpSecurityTest.java         |    6 +-
 viewers/webcomponents/htmx/README.adoc             |    6 +
 .../causeway/webcomponents/htmx/document.html      |    2 +-
 .../resources/causeway-htmx/causeway-htmx.mjs      |   46 +-
 .../htmx/HtmxViewerControllerTest.java             |    1 +
 viewers/webcomponents/pom.xml                      |   25 +
 .../sample-htmx-petclinic/README.adoc              |   33 +-
 .../webcomponents/sample-htmx-petclinic/pom.xml    |    5 +
 .../sample-htmx-petclinic/run-secured.sh           |   31 +
 viewers/webcomponents/sample-htmx-petclinic/run.sh |   31 +
 .../htmx/petclinic/PetClinicHtmxApplication.java   |    2 +-
 .../PetClinicHtmxSecuredApplication.java           |    2 +-
 .../PetClinicSecmanDataConfiguration.java          |    2 +-
 .../petclinic/PetClinicHtmxPlaywrightTest.java     |   32 +-
 .../PetClinicHtmxApplication_IntegTest.java        |   10 +-
 .../webcomponents/sample-htmx-petclinic/test.sh    |   47 +
 .../pom.xml                                        |   67 +-
 .../sample}/petclinic/domain/Pet.java              |    2 +-
 .../domain/PetClinicDataConfiguration.java         |   10 +-
 .../petclinic/domain/PetClinicDomainModule.java    |    2 +-
 .../petclinic/domain/PetClinicHomePage.java        |    2 +-
 .../domain/PetCollectionFilterService.java         |    2 +-
 .../sample}/petclinic/domain/PetOwner.java         |    2 +-
 .../domain/PetOwnerCollectionFilterService.java    |    2 +-
 .../petclinic/domain/PetOwnerRepository.java       |    2 +-
 .../petclinic/domain/PetOwner_bookVisit.java       |    2 +-
 .../sample}/petclinic/domain/PetOwner_visits.java  |    2 +-
 .../sample}/petclinic/domain/PetOwners.java        |    2 +-
 .../sample}/petclinic/domain/PetSpecies.java       |    2 +-
 .../sample/petclinic/domain/ViewerFallback.java    |   99 +
 .../sample}/petclinic/domain/Visit.java            |    2 +-
 .../domain/VisitCollectionFilterService.java       |    2 +-
 .../sample}/petclinic/domain/VisitRepository.java  |    2 +-
 .../sample}/petclinic/domain/Visits.java           |    2 +-
 .../resources/petclinic-local-resource.html        |   29 +
 .../src/main/resources/menubars.layout.xml         |    0
 .../sample}/petclinic/domain/Pet.layout.xml        |    0
 .../PetClinicHomePage#futureVisits.columnOrder.txt |    0
 .../PetClinicHomePage#petOwners.columnOrder.txt    |    0
 .../petclinic/domain/PetClinicHomePage.layout.xml  |    0
 .../petclinic/domain/PetOwner#pets.columnOrder.txt |    0
 .../domain/PetOwner#visits.columnOrder.txt         |    0
 .../sample}/petclinic/domain/PetOwner.layout.xml   |    0
 .../sample}/petclinic/domain/Visit.layout.xml      |    0
 .../sample-vue-petclinic/PRESENTATION-PARITY.adoc  |   65 +
 .../webcomponents/sample-vue-petclinic/README.adoc |  110 +
 .../sample-vue-petclinic/frontend/index.html       |   35 +
 .../frontend/package-lock.json                     | 1946 +++++++++++
 .../sample-vue-petclinic/frontend/package.json     |   27 +
 .../sample-vue-petclinic/frontend/src/App.vue      |  117 +
 .../frontend/src/authentication.ts                 |  106 +
 .../frontend/src/env.d.ts}                         |   14 +-
 .../sample-vue-petclinic/frontend/src/main.ts      |   86 +
 .../frontend/src/pages/HomePage.vue                |   85 +
 .../frontend/src/pages/PetOwnerPage.vue            |  177 +
 .../frontend/src/pages/PetPage.vue                 |   72 +
 .../frontend/src/pages/VisitPage.vue               |   72 +
 .../frontend/src/petclinic.css                     |  401 +++
 .../sample-vue-petclinic/frontend/tsconfig.json    |   14 +
 .../frontend/vite.config.ts}                       |   32 +-
 viewers/webcomponents/sample-vue-petclinic/pom.xml |  256 ++
 .../sample-vue-petclinic/run-secured.sh            |   31 +
 viewers/webcomponents/sample-vue-petclinic/run.sh  |   31 +
 .../vue/petclinic/PetClinicVueApplication.java}    |   24 +-
 .../petclinic/PetClinicVueRouteController.java}    |   23 +-
 .../src/main/resources/application.properties      |   25 +
 .../resources/static/vue/assets/index-Bs2eP4Py.js  |    2 +
 .../static/vue/assets/index-Bs2eP4Py.js.map        |    1 +
 .../resources/static/vue/assets/index-DAyo-XFi.css |    1 +
 .../src/main/resources/static/vue/index.html       |   36 +
 .../vue/petclinic/PetClinicVuePlaywrightTest.java  |  359 ++
 .../PetClinicVueSecuredPlaywrightAcceptance.java   |  217 ++
 .../PetClinicVueSecuredApplication_IntegTest.java  |  198 ++
 .../PetClinicSecmanDataConfiguration.java          |    4 +-
 .../PetClinicVueSecuredApplication.java}           |   15 +-
 .../PetClinicVueSecuredRouteController.java        |   69 +
 .../resources/secured-application.properties       |   23 +
 .../PetClinicVueApplicationIntegTest.java          |  110 +
 viewers/webcomponents/sample-vue-petclinic/test.sh |   54 +
 viewers/webcomponents/security-secman/README.adoc  |   44 +
 .../pom.xml                                        |   32 +-
 ...AuthenticationConverterOfSecmanUserDetails.java |    2 +-
 ...ayModuleViewerWebcomponentsSecuritySecman.java} |   10 +-
 .../SecmanSpringAuthenticationConfiguration.java   |   56 +
 .../security/secman/SecmanUserDetails.java         |    2 +-
 .../security/secman/SecmanUserDetailsService.java  |    2 +-
 .../secman/SecmanAuthenticationBridgeTest.java     |   34 +-
 .../webcomponents/vue-security-secman/README.adoc  |  127 +
 .../pom.xml                                        |   35 +-
 ...oduleViewerWebcomponentsVueSecuritySecman.java} |   16 +-
 .../VueSecmanAuthenticationContextFilter.java      |   95 +
 .../secman/VueSecmanAuthenticationEntryPoint.java  |   55 +
 .../VueSecmanAuthenticationFailureHandler.java     |   52 +
 .../VueSecmanAuthenticationSuccessHandler.java     |   74 +
 .../security/secman/VueSecmanLoginPageFilter.java  |  153 +
 .../vue/security/secman/VueSecmanPaths.java        |   89 +
 .../secman/VueSecmanSecurityConfiguration.java}    |  116 +-
 .../secman/VueSecmanSecurityProperties.java        |  120 +
 .../secman/VueSecmanSecurityValidator.java         |   67 +
 .../secman/VueSecmanHttpSecurityTest.java}         |   88 +-
 .../secman/VueSecmanSecurityFoundationTest.java    |  101 +
 LICENSE => viewers/webcomponents/vue/LICENSE       |    0
 viewers/webcomponents/vue/NOTICE                   |    5 +
 viewers/webcomponents/vue/README.adoc              |  166 +
 viewers/webcomponents/vue/dist/boundary.d.ts       |   19 +
 viewers/webcomponents/vue/dist/boundary.d.ts.map   |    1 +
 viewers/webcomponents/vue/dist/causeway-vue.css    |    1 +
 viewers/webcomponents/vue/dist/causeway-vue.js     |  639 ++++
 viewers/webcomponents/vue/dist/causeway-vue.js.map |    1 +
 .../vue/dist/components/GenericObjectPage.vue.d.ts |    5 +
 .../dist/components/GenericObjectPage.vue.d.ts.map |    1 +
 .../vue/dist/components/HomePage.vue.d.ts          |    4 +
 .../vue/dist/components/HomePage.vue.d.ts.map      |    1 +
 .../vue/dist/components/ObjectRoutePage.vue.d.ts   |    4 +
 .../dist/components/ObjectRoutePage.vue.d.ts.map   |    1 +
 viewers/webcomponents/vue/dist/contracts.d.ts      |   73 +
 viewers/webcomponents/vue/dist/contracts.d.ts.map  |    1 +
 viewers/webcomponents/vue/dist/host-operation.d.ts |    8 +
 .../webcomponents/vue/dist/host-operation.d.ts.map |    1 +
 viewers/webcomponents/vue/dist/index.d.ts          |   13 +
 viewers/webcomponents/vue/dist/index.d.ts.map      |    1 +
 viewers/webcomponents/vue/dist/local-resource.d.ts |   21 +
 .../webcomponents/vue/dist/local-resource.d.ts.map |    1 +
 viewers/webcomponents/vue/dist/plugin.d.ts         |   11 +
 viewers/webcomponents/vue/dist/plugin.d.ts.map     |    1 +
 viewers/webcomponents/vue/dist/policy.d.ts         |   17 +
 viewers/webcomponents/vue/dist/policy.d.ts.map     |    1 +
 viewers/webcomponents/vue/dist/registry.d.ts       |    9 +
 viewers/webcomponents/vue/dist/registry.d.ts.map   |    1 +
 viewers/webcomponents/vue/dist/route-codec.d.ts    |   21 +
 .../webcomponents/vue/dist/route-codec.d.ts.map    |    1 +
 viewers/webcomponents/vue/dist/routes.d.ts         |   16 +
 viewers/webcomponents/vue/dist/routes.d.ts.map     |    1 +
 viewers/webcomponents/vue/package-lock.json        | 3515 ++++++++++++++++++++
 viewers/webcomponents/vue/package.json             |   51 +
 viewers/webcomponents/vue/pom.xml                  |  119 +
 viewers/webcomponents/vue/src/boundary.ts          |   72 +
 .../vue/src/components/GenericObjectPage.vue       |   43 +
 .../webcomponents/vue/src/components/HomePage.vue  |   86 +
 .../vue/src/components/ObjectRoutePage.vue         |  183 +
 viewers/webcomponents/vue/src/contracts.ts         |  102 +
 .../domain/PetSpecies.java => vue/src/env.d.ts}    |   14 +-
 viewers/webcomponents/vue/src/host-operation.ts    |   43 +
 .../src/index.ts}                                  |   24 +-
 viewers/webcomponents/vue/src/local-resource.ts    |   97 +
 viewers/webcomponents/vue/src/plugin.ts            |   87 +
 viewers/webcomponents/vue/src/policy.ts            |  264 ++
 viewers/webcomponents/vue/src/registry.ts          |   53 +
 viewers/webcomponents/vue/src/route-codec.ts       |  122 +
 viewers/webcomponents/vue/src/routes.ts            |   93 +
 viewers/webcomponents/vue/src/theme.css            |   80 +
 .../webcomponents/vue/test/local-resource.test.ts  |   59 +
 .../vue/test/policy-integration.test.ts            |  198 ++
 .../vue/test/registry-boundary.test.ts             |   82 +
 viewers/webcomponents/vue/test/route-codec.test.ts |   83 +
 .../webcomponents/vue/test/vue-components.test.ts  |  110 +
 viewers/webcomponents/vue/tsconfig.json            |   18 +
 viewers/webcomponents/vue/verify-package.mjs       |   47 +
 .../PetOwner_visits.java => vue/vite.config.ts}    |   57 +-
 213 files changed, 16253 insertions(+), 762 deletions(-)
 rename openspec/{planned-changes/web-component-viewer/add-generic-vue-web-component-viewer => changes/archive/2026-09-03-add-generic-vue-web-component-viewer}/.openspec.yaml (100%)
 create mode 100644 openspec/changes/archive/2026-09-03-add-generic-vue-web-component-viewer/design.md
 create mode 100644 openspec/changes/archive/2026-09-03-add-generic-vue-web-component-viewer/proposal.md
 create mode 100644 openspec/changes/archive/2026-09-03-add-generic-vue-web-component-viewer/specs/generic-vue-web-component-viewer/spec.md
 create mode 100644 openspec/changes/archive/2026-09-03-add-generic-vue-web-component-viewer/tasks.md
 copy openspec/changes/archive/{2026-09-03-add-application-authored-htmx-shell => 2026-09-03-add-secured-vue-petclinic-variant}/.openspec.yaml (100%)
 create mode 100644 openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/design.md
 create mode 100644 openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/proposal.md
 create mode 100644 openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/specs/generic-vue-web-component-viewer/spec.md
 create mode 100644 openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/specs/htmx-viewer-local-secman-authentication/spec.md
 create mode 100644 openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/specs/vue-viewer-local-secman-authentication/spec.md
 create mode 100644 openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/specs/web-component-secman-spring-authentication-bridge/spec.md
 create mode 100644 openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/tasks.md
 copy openspec/changes/archive/{2026-09-03-add-application-authored-htmx-shell => 2026-09-03-reconcile-vue-petclinic-with-htmx}/.openspec.yaml (100%)
 create mode 100644 openspec/changes/archive/2026-09-03-reconcile-vue-petclinic-with-htmx/design.md
 create mode 100644 openspec/changes/archive/2026-09-03-reconcile-vue-petclinic-with-htmx/proposal.md
 create mode 100644 openspec/changes/archive/2026-09-03-reconcile-vue-petclinic-with-htmx/specs/generic-vue-web-component-viewer/spec.md
 create mode 100644 openspec/changes/archive/2026-09-03-reconcile-vue-petclinic-with-htmx/tasks.md
 copy openspec/changes/archive/{2026-09-03-add-application-authored-htmx-shell => 2026-09-03-support-web-component-logout-and-local-redirects}/.openspec.yaml (100%)
 create mode 100644 openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/design.md
 create mode 100644 openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/proposal.md
 create mode 100644 openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/specs/domain-web-components/spec.md
 create mode 100644 openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/specs/generic-htmx-web-component-viewer/spec.md
 create mode 100644 openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/specs/generic-vue-web-component-viewer/spec.md
 create mode 100644 openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/tasks.md
 delete mode 100644 openspec/planned-changes/web-component-viewer/add-generic-vue-web-component-viewer/design.md
 delete mode 100644 openspec/planned-changes/web-component-viewer/add-generic-vue-web-component-viewer/proposal.md
 delete mode 100644 openspec/planned-changes/web-component-viewer/add-generic-vue-web-component-viewer/specs/generic-vue-web-component-viewer/spec.md
 delete mode 100644 openspec/planned-changes/web-component-viewer/add-generic-vue-web-component-viewer/tasks.md
 create mode 100644 openspec/specs/generic-vue-web-component-viewer/spec.md
 create mode 100644 openspec/specs/vue-viewer-local-secman-authentication/spec.md
 create mode 100644 openspec/specs/web-component-secman-spring-authentication-bridge/spec.md
 create mode 100644 viewers/webcomponents/foundation/src/action-result.mjs
 create mode 100644 viewers/webcomponents/foundation/src/host-operation-policy.mjs
 create mode 100644 viewers/webcomponents/foundation/src/local-resource-policy.mjs
 create mode 100644 viewers/webcomponents/foundation/test/host-navigation-policy.test.mjs
 create mode 100755 viewers/webcomponents/sample-htmx-petclinic/run-secured.sh
 create mode 100755 viewers/webcomponents/sample-htmx-petclinic/run.sh
 create mode 100755 viewers/webcomponents/sample-htmx-petclinic/test.sh
 copy viewers/webcomponents/{htmx-security-secman => sample-petclinic-domain}/pom.xml (51%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/Pet.java (98%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/PetClinicDataConfiguration.java (95%)
 copy viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/PetClinicDomainModule.java (93%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/PetClinicHomePage.java (96%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/PetCollectionFilterService.java (96%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/PetOwner.java (99%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/PetOwnerCollectionFilterService.java (96%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/PetOwnerRepository.java (96%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/PetOwner_bookVisit.java (97%)
 copy viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/PetOwner_visits.java (94%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/PetOwners.java (97%)
 copy viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/PetSpecies.java (91%)
 create mode 100644 viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/ViewerFallback.java
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/Visit.java (98%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/VisitCollectionFilterService.java (96%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/VisitRepository.java (96%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/Visits.java (95%)
 create mode 100644 viewers/webcomponents/sample-petclinic-domain/src/main/resources/META-INF/resources/petclinic-local-resource.html
 rename viewers/webcomponents/{sample-htmx-petclinic => sample-petclinic-domain}/src/main/resources/menubars.layout.xml (100%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/Pet.layout.xml (100%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/PetClinicHomePage#futureVisits.columnOrder.txt (100%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/PetClinicHomePage#petOwners.columnOrder.txt (100%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/PetClinicHomePage.layout.xml (100%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/PetOwner#pets.columnOrder.txt (100%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/PetOwner#visits.columnOrder.txt (100%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/PetOwner.layout.xml (100%)
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample}/petclinic/domain/Visit.layout.xml (100%)
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/PRESENTATION-PARITY.adoc
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/README.adoc
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/frontend/index.html
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/frontend/package-lock.json
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/frontend/package.json
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/frontend/src/App.vue
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/frontend/src/authentication.ts
 copy viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetSpecies.java => sample-vue-petclinic/frontend/src/env.d.ts} (77%)
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/frontend/src/main.ts
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/HomePage.vue
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/PetOwnerPage.vue
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/PetPage.vue
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/VisitPage.vue
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/frontend/src/petclinic.css
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/frontend/tsconfig.json
 copy viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicDomainModule.java => sample-vue-petclinic/frontend/vite.config.ts} (62%)
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/pom.xml
 create mode 100755 viewers/webcomponents/sample-vue-petclinic/run-secured.sh
 create mode 100755 viewers/webcomponents/sample-vue-petclinic/run.sh
 copy viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxApplication.java => sample-vue-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVueApplication.java} (66%)
 copy viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicDomainModule.java => sample-vue-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVueRouteController.java} (64%)
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/src/main/resources/application.properties
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-Bs2eP4Py.js
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-Bs2eP4Py.js.map
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-DAyo-XFi.css
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVuePlaywrightTest.java
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/src/secured-playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicVueSecuredPlaywrightAcceptance.java
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/src/secured-test/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicVueSecuredApplication_IntegTest.java
 copy viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx => sample-vue-petclinic/src/secured/java/org/apache/causeway/viewer/webcomponents/sample/vue}/petclinicsecured/PetClinicSecmanDataConfiguration.java (96%)
 copy viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinicsecured/PetClinicHtmxSecuredApplication.java => sample-vue-petclinic/src/secured/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicVueSecuredApplication.java} (79%)
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/src/secured/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicVueSecuredRouteController.java
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/src/secured/resources/secured-application.properties
 create mode 100644 viewers/webcomponents/sample-vue-petclinic/src/test/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVueApplicationIntegTest.java
 create mode 100755 viewers/webcomponents/sample-vue-petclinic/test.sh
 create mode 100644 viewers/webcomponents/security-secman/README.adoc
 copy viewers/webcomponents/{htmx-security-secman => security-secman}/pom.xml (69%)
 rename viewers/webcomponents/{htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx => security-secman/src/main/java/org/apache/causeway/viewer/webcomponents}/security/secman/AuthenticationConverterOfSecmanUserDetails.java (97%)
 copy viewers/webcomponents/{htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/CausewayModuleViewerWebcomponentsHtmxSecuritySecman.java => security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/security/secman/CausewayModuleViewerWebcomponentsSecuritySecman.java} (71%)
 create mode 100644 viewers/webcomponents/security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/security/secman/SecmanSpringAuthenticationConfiguration.java
 rename viewers/webcomponents/{htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx => security-secman/src/main/java/org/apache/causeway/viewer/webcomponents}/security/secman/SecmanUserDetails.java (98%)
 rename viewers/webcomponents/{htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx => security-secman/src/main/java/org/apache/causeway/viewer/webcomponents}/security/secman/SecmanUserDetailsService.java (98%)
 rename viewers/webcomponents/{htmx-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/htmx => security-secman/src/test/java/org/apache/causeway/viewer/webcomponents}/security/secman/SecmanAuthenticationBridgeTest.java (86%)
 create mode 100644 viewers/webcomponents/vue-security-secman/README.adoc
 copy viewers/webcomponents/{htmx-security-secman => vue-security-secman}/pom.xml (78%)
 copy viewers/webcomponents/{htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/CausewayModuleViewerWebcomponentsHtmxSecuritySecman.java => vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/CausewayModuleViewerWebcomponentsVueSecuritySecman.java} (61%)
 create mode 100644 viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanAuthenticationContextFilter.java
 create mode 100644 viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanAuthenticationEntryPoint.java
 create mode 100644 viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanAuthenticationFailureHandler.java
 create mode 100644 viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanAuthenticationSuccessHandler.java
 create mode 100644 viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanLoginPageFilter.java
 create mode 100644 viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanPaths.java
 copy viewers/webcomponents/{htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/HtmxSecmanSecurityConfiguration.java => vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanSecurityConfiguration.java} (55%)
 create mode 100644 viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanSecurityProperties.java
 create mode 100644 viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanSecurityValidator.java
 copy viewers/webcomponents/{htmx-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/HtmxSecmanHttpSecurityTest.java => vue-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanHttpSecurityTest.java} (78%)
 create mode 100644 viewers/webcomponents/vue-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanSecurityFoundationTest.java
 copy LICENSE => viewers/webcomponents/vue/LICENSE (100%)
 create mode 100644 viewers/webcomponents/vue/NOTICE
 create mode 100644 viewers/webcomponents/vue/README.adoc
 create mode 100644 viewers/webcomponents/vue/dist/boundary.d.ts
 create mode 100644 viewers/webcomponents/vue/dist/boundary.d.ts.map
 create mode 100644 viewers/webcomponents/vue/dist/causeway-vue.css
 create mode 100644 viewers/webcomponents/vue/dist/causeway-vue.js
 create mode 100644 viewers/webcomponents/vue/dist/causeway-vue.js.map
 create mode 100644 viewers/webcomponents/vue/dist/components/GenericObjectPage.vue.d.ts
 create mode 100644 viewers/webcomponents/vue/dist/components/GenericObjectPage.vue.d.ts.map
 create mode 100644 viewers/webcomponents/vue/dist/components/HomePage.vue.d.ts
 create mode 100644 viewers/webcomponents/vue/dist/components/HomePage.vue.d.ts.map
 create mode 100644 viewers/webcomponents/vue/dist/components/ObjectRoutePage.vue.d.ts
 create mode 100644 viewers/webcomponents/vue/dist/components/ObjectRoutePage.vue.d.ts.map
 create mode 100644 viewers/webcomponents/vue/dist/contracts.d.ts
 create mode 100644 viewers/webcomponents/vue/dist/contracts.d.ts.map
 create mode 100644 viewers/webcomponents/vue/dist/host-operation.d.ts
 create mode 100644 viewers/webcomponents/vue/dist/host-operation.d.ts.map
 create mode 100644 viewers/webcomponents/vue/dist/index.d.ts
 create mode 100644 viewers/webcomponents/vue/dist/index.d.ts.map
 create mode 100644 viewers/webcomponents/vue/dist/local-resource.d.ts
 create mode 100644 viewers/webcomponents/vue/dist/local-resource.d.ts.map
 create mode 100644 viewers/webcomponents/vue/dist/plugin.d.ts
 create mode 100644 viewers/webcomponents/vue/dist/plugin.d.ts.map
 create mode 100644 viewers/webcomponents/vue/dist/policy.d.ts
 create mode 100644 viewers/webcomponents/vue/dist/policy.d.ts.map
 create mode 100644 viewers/webcomponents/vue/dist/registry.d.ts
 create mode 100644 viewers/webcomponents/vue/dist/registry.d.ts.map
 create mode 100644 viewers/webcomponents/vue/dist/route-codec.d.ts
 create mode 100644 viewers/webcomponents/vue/dist/route-codec.d.ts.map
 create mode 100644 viewers/webcomponents/vue/dist/routes.d.ts
 create mode 100644 viewers/webcomponents/vue/dist/routes.d.ts.map
 create mode 100644 viewers/webcomponents/vue/package-lock.json
 create mode 100644 viewers/webcomponents/vue/package.json
 create mode 100644 viewers/webcomponents/vue/pom.xml
 create mode 100644 viewers/webcomponents/vue/src/boundary.ts
 create mode 100644 viewers/webcomponents/vue/src/components/GenericObjectPage.vue
 create mode 100644 viewers/webcomponents/vue/src/components/HomePage.vue
 create mode 100644 viewers/webcomponents/vue/src/components/ObjectRoutePage.vue
 create mode 100644 viewers/webcomponents/vue/src/contracts.ts
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetSpecies.java => vue/src/env.d.ts} (77%)
 create mode 100644 viewers/webcomponents/vue/src/host-operation.ts
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicDomainModule.java => vue/src/index.ts} (60%)
 create mode 100644 viewers/webcomponents/vue/src/local-resource.ts
 create mode 100644 viewers/webcomponents/vue/src/plugin.ts
 create mode 100644 viewers/webcomponents/vue/src/policy.ts
 create mode 100644 viewers/webcomponents/vue/src/registry.ts
 create mode 100644 viewers/webcomponents/vue/src/route-codec.ts
 create mode 100644 viewers/webcomponents/vue/src/routes.ts
 create mode 100644 viewers/webcomponents/vue/src/theme.css
 create mode 100644 viewers/webcomponents/vue/test/local-resource.test.ts
 create mode 100644 viewers/webcomponents/vue/test/policy-integration.test.ts
 create mode 100644 viewers/webcomponents/vue/test/registry-boundary.test.ts
 create mode 100644 viewers/webcomponents/vue/test/route-codec.test.ts
 create mode 100644 viewers/webcomponents/vue/test/vue-components.test.ts
 create mode 100644 viewers/webcomponents/vue/tsconfig.json
 create mode 100644 viewers/webcomponents/vue/verify-package.mjs
 rename viewers/webcomponents/{sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner_visits.java => vue/vite.config.ts} (53%)


(causeway) 07/16: docs(openspec): archive Vue Petclinic reconciliation

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 509d1b4b6fd2365f5f30eeb8ef67fb8703bf2931
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 3 11:33:49 2026 +0100

    docs(openspec): archive Vue Petclinic reconciliation
---
 .../.openspec.yaml                                 |  0
 .../design.md                                      |  0
 .../proposal.md                                    |  0
 .../specs/generic-vue-web-component-viewer/spec.md |  0
 .../tasks.md                                       |  0
 .../specs/generic-vue-web-component-viewer/spec.md | 53 +++++++++++++++++-----
 6 files changed, 42 insertions(+), 11 deletions(-)

diff --git a/openspec/changes/reconcile-vue-petclinic-with-htmx/.openspec.yaml b/openspec/changes/archive/2026-09-03-reconcile-vue-petclinic-with-htmx/.openspec.yaml
similarity index 100%
rename from openspec/changes/reconcile-vue-petclinic-with-htmx/.openspec.yaml
rename to openspec/changes/archive/2026-09-03-reconcile-vue-petclinic-with-htmx/.openspec.yaml
diff --git a/openspec/changes/reconcile-vue-petclinic-with-htmx/design.md b/openspec/changes/archive/2026-09-03-reconcile-vue-petclinic-with-htmx/design.md
similarity index 100%
rename from openspec/changes/reconcile-vue-petclinic-with-htmx/design.md
rename to openspec/changes/archive/2026-09-03-reconcile-vue-petclinic-with-htmx/design.md
diff --git a/openspec/changes/reconcile-vue-petclinic-with-htmx/proposal.md b/openspec/changes/archive/2026-09-03-reconcile-vue-petclinic-with-htmx/proposal.md
similarity index 100%
rename from openspec/changes/reconcile-vue-petclinic-with-htmx/proposal.md
rename to openspec/changes/archive/2026-09-03-reconcile-vue-petclinic-with-htmx/proposal.md
diff --git a/openspec/changes/reconcile-vue-petclinic-with-htmx/specs/generic-vue-web-component-viewer/spec.md b/openspec/changes/archive/2026-09-03-reconcile-vue-petclinic-with-htmx/specs/generic-vue-web-component-viewer/spec.md
similarity index 100%
rename from openspec/changes/reconcile-vue-petclinic-with-htmx/specs/generic-vue-web-component-viewer/spec.md
rename to openspec/changes/archive/2026-09-03-reconcile-vue-petclinic-with-htmx/specs/generic-vue-web-component-viewer/spec.md
diff --git a/openspec/changes/reconcile-vue-petclinic-with-htmx/tasks.md b/openspec/changes/archive/2026-09-03-reconcile-vue-petclinic-with-htmx/tasks.md
similarity index 100%
rename from openspec/changes/reconcile-vue-petclinic-with-htmx/tasks.md
rename to openspec/changes/archive/2026-09-03-reconcile-vue-petclinic-with-htmx/tasks.md
diff --git a/openspec/specs/generic-vue-web-component-viewer/spec.md b/openspec/specs/generic-vue-web-component-viewer/spec.md
index af6fba198a5..7ff3612bcbb 100644
--- a/openspec/specs/generic-vue-web-component-viewer/spec.md
+++ b/openspec/specs/generic-vue-web-component-viewer/spec.md
@@ -298,30 +298,61 @@ The reference application SHALL support wide and narrow layouts, light and dark
 ### Requirement: Executable Vue Petclinic acceptance application
 
 The project SHALL provide a production-built Vue Petclinic application that reuses the same deterministic Petclinic domain, public logical types, and fixture identities as the HTMX sample without copying domain implementation.
-The application SHALL demonstrate a source-visible custom Vue page, generic fallback, stable shell, menus, interactions, results, canonical routing, and server refresh fallback.
+The application SHALL demonstrate source-visible exact-type Vue pages, generic fallback, a stable application-owned shell, menus, interactions, results, canonical routing, and server refresh fallback.
+For the shell and the `petclinic.HomePage`, `petclinic.PetOwner`, `petclinic.Pet`, and `petclinic.Visit` routes, the HTMX Petclinic application SHALL be the authoritative presentation reference.
+Presentation equivalence SHALL preserve user-visible information architecture and responsive relationships without requiring framework wrapper markup or browser pixels to be identical.
 
-#### Scenario: Custom Petclinic page loads
+#### Scenario: Reconciled Petclinic page loads
 
-- **WHEN** the configured Petclinic logical type is opened through a direct canonical bookmark
-- **THEN** its registered Vue single-file component binds the declared object context and renders semantic members
-- **AND** the source demonstrates application-owned layout and interaction-controller placement
+- **WHEN** HomePage, PetOwner, Pet, or Visit is opened through a direct canonical Vue bookmark
+- **THEN** an exact-type Vue single-file component binds the declared object context and interaction controller
+- **AND** it presents the same ordered headings, sections, selected semantic members, action placement, result outlets, descriptions, collection columns, paging, filtering, sorting, and row-preview affordances as the corresponding HTMX Petclinic page
+- **AND** it does not expose technical or additional members omitted by that HTMX page
 
-#### Scenario: Generic Petclinic page loads
+#### Scenario: Vue PetOwner page matches the reference composition
 
-- **WHEN** another Petclinic logical type has no registration
+- **WHEN** the deterministic PetOwner fixture is rendered by Vue
+- **THEN** Identity, Contact, and Details occupy the reference details column while Pets and Visits occupy the reference collections column
+- **AND** the owner actions include the empty-result action and the declared standalone related-owner result
+- **AND** the derived last-visit value, visit notes column, paging values, parameter presentation, and nested preview declarations match the HTMX page
+
+#### Scenario: Vue shell is presented at a wide viewport
+
+- **WHEN** a reconciled route is ready at the documented wide acceptance viewport
+- **THEN** branding, primary menu order, utility-menu grouping, header geometry, content inset, typography, palette, footer content, and document-title suffix are equivalent to the HTMX shell
+- **AND** the route uses the available width and reference column relationships without an accidental persistent container outline
+- **AND** keyboard users retain a visible focus indication on an appropriate heading or route landmark
+
+#### Scenario: Vue shell is presented at a narrow viewport
+
+- **WHEN** a reconciled route is ready at the documented narrow acceptance viewport
+- **THEN** shell navigation and page columns collapse in the same order and at an equivalent breakpoint to the HTMX presentation
+- **AND** menus, actions, results, fields, collections, previews, and pagination remain visible and keyboard operable without horizontal page overflow
+
+#### Scenario: Generic fallback fixture loads
+
+- **WHEN** a deliberately unregistered acceptance logical type is opened through a direct canonical Vue bookmark
 - **THEN** the generic Vue page renders `<cw-object>` for the same GraphQL identity
 - **AND** direct refresh and browser history retain that route
+- **AND** none of the four reconciled Petclinic route types is used to demonstrate fallback
 
 #### Scenario: Existing HTMX Petclinic is rebuilt
 
-- **WHEN** the Petclinic domain is shared with the new Vue application
-- **THEN** existing HTMX routes, pages, shell, seed cardinalities, stable identities, and acceptance behavior remain unchanged
-- **AND** the shared module introduces no Vue dependency
+- **WHEN** Vue presentation is reconciled with the HTMX reference
+- **THEN** existing HTMX routes, page composition, shell, seed cardinalities, stable identities, and acceptance behavior remain unchanged
+- **AND** the shared domain introduces no Vue dependency or presentation authority
+
+#### Scenario: Presentation parity is regression tested
+
+- **WHEN** the Vue browser profile executes
+- **THEN** it verifies semantic parity for all four reconciled route types and high-value computed shell and layout relationships at representative wide and narrow viewports
+- **AND** comparisons use bounded structural or computed-style assertions rather than pixel-perfect screenshot equality
+- **AND** it covers both supported component-toolkit policies where their presentation differs
 
 #### Scenario: Acceptance suite runs
 
 - **WHEN** the Vue browser profile executes
-- **THEN** it covers direct links, refresh, back and forward, exact custom-page precedence, generic fallback, menus, property and action interactions, object and collection results, partial errors, invalid routes, absent objects, responsive layout, keyboard focus, announcements, and both supported component toolkit policies
+- **THEN** it covers direct links, refresh, back and forward, exact custom-page precedence, deliberate generic fallback, menus, property and action interactions, object and collection results, partial errors, invalid routes, absent objects, responsive layout, keyboard focus, announcements, and both supported component toolkit policies
 - **AND** browser console errors, page errors, failed resource requests, and accessibility violations fail the suite
 
 ### Requirement: Reactor and release integration


(causeway) 01/16: docs(openspec): plan generic Vue web component viewer

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit ea372fc6ddba56cc7045688f64c4ccc119de5089
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 3 07:38:45 2026 +0100

    docs(openspec): plan generic Vue web component viewer
---
 .../.openspec.yaml                                 |   0
 .../add-generic-vue-web-component-viewer/design.md | 174 +++++++++++
 .../proposal.md                                    |  39 +++
 .../specs/generic-vue-web-component-viewer/spec.md | 347 +++++++++++++++++++++
 .../add-generic-vue-web-component-viewer/tasks.md  |  70 +++++
 .../add-generic-vue-web-component-viewer/design.md |  99 ------
 .../proposal.md                                    |  36 ---
 .../specs/generic-vue-web-component-viewer/spec.md |  92 ------
 .../add-generic-vue-web-component-viewer/tasks.md  |  29 --
 9 files changed, 630 insertions(+), 256 deletions(-)

diff --git a/openspec/planned-changes/web-component-viewer/add-generic-vue-web-component-viewer/.openspec.yaml b/openspec/changes/add-generic-vue-web-component-viewer/.openspec.yaml
similarity index 100%
rename from openspec/planned-changes/web-component-viewer/add-generic-vue-web-component-viewer/.openspec.yaml
rename to openspec/changes/add-generic-vue-web-component-viewer/.openspec.yaml
diff --git a/openspec/changes/add-generic-vue-web-component-viewer/design.md b/openspec/changes/add-generic-vue-web-component-viewer/design.md
new file mode 100644
index 00000000000..1fb35e0329a
--- /dev/null
+++ b/openspec/changes/add-generic-vue-web-component-viewer/design.md
@@ -0,0 +1,174 @@
+## Context
+
+Vue provides component composition, application plugins, dynamic components, async components, and a mature router.
+Causeway's custom elements already own GraphQL execution, semantic context, object layout, menus, editors, validation, interaction dispatch, action-result rendering, and domain-state concurrency.
+Applications can declare those elements directly in Vue single-file component templates.
+The Vue viewer should therefore be an integration package for browser routing, route-value binding, semantic-event policy, and framework lifecycle rather than a Vue rewrite of the component library.
+
+The accepted HTMX viewer established two relevant ownership rules.
+The application owns the stable application shell, while each replaceable route page owns its semantic object context and interaction controller.
+The Vue design applies the same semantic split through framework-native source templates rather than through classpath HTML resources.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Provide canonical deep-linkable object routes through an application-owned Vue Router instance.
+- Support exact-logical-type custom Vue pages and a generic `<cw-object>` fallback.
+- Keep the GraphQL provider, menu coordination, result outlet, and application chrome stable across route changes.
+- Bind endpoint and route identity into authored custom elements without imperatively creating them.
+- Bridge semantic navigation and result events into replaceable Vue policy.
+- Package a typed reusable Vue integration and a production-built Petclinic acceptance application.
+- Preserve deterministic lifecycle, accessibility, theming, and cross-viewer route meaning.
+
+**Non-Goals:**
+
+- Wrapping each Causeway custom element as a Vue component.
+- Reimplementing GraphQL operations, metadata interpretation, domain validation, object layout, menus, editors, or result rendering in Vue.
+- Making `<cw-object>` inspect Vue Router, Vue injection state, or the custom-page registry.
+- Introducing Pinia or another duplicate domain-state store.
+- Owning an application's complete router, HTML document, deployment topology, authentication UI, or static-resource server.
+- Supporting Nuxt, server-side rendering, streaming hydration, or server-only route data in the first version.
+- Adding inherited-logical-type page matching or runtime page-registry mutation in the first version.
+
+## Decisions
+
+### Deliver an optional typed Vue package
+
+The primary frontend artifact is an npm package under `viewers/webcomponents/vue` targeting Vue 3 and Vue Router 4.
+Vue and Vue Router are peer dependencies and are external to the library bundle, so installing or building the framework-neutral component modules does not acquire a Vue runtime.
+The package exports ECMAScript modules and TypeScript declarations and is built reproducibly with Vite library mode from a committed lockfile.
+Package verification includes unit tests, production build, declaration generation, package-content inspection, dependency licensing, and an install-from-packed-artifact smoke test.
+
+A lightweight Maven module integrates verification and release metadata with the web-components reactor without making Vue transitive to the foundation or HTMX artifacts.
+Any generated browser assets committed or packaged for a sample are reproducible from locked sources and retain the required third-party notices.
+
+### Integrate with an application-owned router
+
+The package does not create or hide a router instance.
+It exports namespaced Causeway route records and a plugin that an application installs beside its own Vue Router instance.
+The application remains free to add unrelated routes, guards, scroll behavior, and deployment-specific history configuration.
+
+The application supplies the history base to `createWebHistory`, mounts the returned Causeway records at the documented viewer path, and configures its server to return the Vue document for valid client routes.
+The package documents this direct-refresh contract but does not catch unrelated backend, GraphQL, or asset URLs.
+Hash history is not the default because it would change the cross-viewer bookmark grammar.
+
+### Use an independent canonical route codec
+
+Route creation and route acceptance use one Vue viewer codec for the canonical `<base-path>/object/<logical-type>/<identifier>` grammar.
+The codec applies the same canonical UTF-8 percent encoding, separator rejection, dot-segment rejection, control-character rejection, malformed-Unicode rejection, and encoded-segment bound as the existing viewer contract.
+It validates the raw URL path before binding decoded values because Vue Router parameters alone cannot prove that the original encoding was canonical.
+
+The Vue codec is tested against `viewers/webcomponents/canonical-route-fixtures.yaml` and against generated round trips.
+Navigation policy pushes codec-produced paths rather than relying on implicit router-parameter encoding.
+Invalid routes render a bounded invalid-route page and never reach a semantic object context.
+
+### Normalize an immutable exact-type page registry
+
+Plugin creation accepts a map from exact public logical type to either a Vue component or an async component loader.
+Registration keys are normalized once, and empty, malformed, or duplicate effective registrations fail during application startup.
+Arbitrary factories and inheritance fallback are excluded from the first contract because Vue components and async loaders already cover synchronous and code-split pages without introducing a second lifecycle model.
+
+The object route controller validates canonical identity and chooses the exact registration before mounting route content.
+If no registration exists, it selects the package's generic route page.
+Async loading is associated with the current route key, exposes an accessible loading and error presentation, and cannot mount a component after that route has been superseded.
+
+### Keep semantic route boundaries application-authored
+
+Every custom route-page component receives immutable `logicalTypeName`, `objectId`, and `routeKey` props.
+Its source template declares one marked route-level `<cw-object-context>`, binds the identity props to that element, and declares one `<cw-interaction-controller>` within the context.
+The generic route page follows the same contract and additionally declares `<cw-object editable>`.
+
+The package may render an ordinary non-semantic wrapper for lifecycle observation, but it does not create, move, select, or repair semantic provider elements.
+After each page mount, a bounded development validator confirms the route-context and interaction-controller cardinality, containment, and bound identity.
+An invalid page fails closed to a safe diagnostic rather than querying through an ancestor context or retaining stale route content.
+
+`RouterView` uses a canonical route key rather than component type alone.
+Changing logical type or identifier therefore disconnects the old object context and lets the established component disconnect contract release requirements, subscriptions, cancellation state, and obsolete responses.
+
+### Keep the stable shell application-owned
+
+The application's root `App.vue` owns the complete stable body composition inside the Vue mount point.
+It declares exactly one shell-level `<cw-graphql-client>`, binds the configured endpoint, and places `<cw-menubars>`, branding, authentication chrome when applicable, route loading, announcements, a default `<cw-action-results>` outlet, and the router-view region beneath that provider.
+The provider and menus remain mounted while keyed route pages change.
+
+The package exports composables and small route-view helpers, not a mandatory visual shell hierarchy.
+A reference shell and theme are supplied as copyable source in the Vue Petclinic application and documentation.
+A bounded shell validator diagnoses missing or duplicate protocol landmarks during development and tests, but never manufactures or relocates them.
+
+The HTML document remains application-owned and loads the framework-neutral Causeway registration module and selected toolkit assets before or alongside the bundled Vue entry point.
+Applications retain authority over CSP, base URL, locale, initial color scheme, global CSS, authentication metadata, and deployment-specific script delivery.
+
+### Treat custom elements as the data plane
+
+Both the package build and documented consuming Vite configuration classify `cw-` tags as native custom elements through Vue compiler options.
+String identity values may use ordinary bindings, while structured services and values use DOM-property bindings, including Vue's explicit `.prop` modifier where necessary.
+Native `CustomEvent` payloads remain authoritative and are not converted into Vue component-emits contracts.
+
+The plugin installs a scoped event bridge on the stable shell boundary and removes it when the Vue application unmounts.
+The bridge receives semantic navigation, interaction completion, and result events and invokes injected viewer policy.
+It does not alter GraphQL documents, validation outcomes, authoritative identity, interaction ordering, or component refresh behavior.
+
+### Define default navigation, home, and result policy
+
+Default object navigation and object results use the canonical codec and `router.push` to reach the corresponding object route.
+The policy accepts only complete advertised public identity and presents a bounded unsupported result when canonical navigation identity is unavailable.
+
+The home route uses the accepted application-entry semantics and permits an application-provided home component or handler.
+An object-valued default home outcome navigates canonically, while an unavailable home remains an accessible landing state.
+
+Scalar and collection results are assigned to the declared active-page result outlet when exactly one valid outlet exists, otherwise to the one shell-level default outlet.
+The existing `<cw-action-results>` and `<cw-standalone-collection>` components retain presentation authority.
+Void results refresh the current object context when it still exists and return home only when an authoritative refresh establishes that the target disappeared.
+Applications may replace navigation, home, and result handlers, and a documented claim protocol prevents both default and application policy from handling the same event.
+
+### Separate route failure from domain failure
+
+Malformed or non-canonical paths fail before object lookup.
+A valid route then relies on object-context observable states for schema loading, object loading, ready, partial error, and terminal error.
+Not-found and access-denied presentation does not reveal domain state or authorization rules, and the default unavailable presentation does not claim to distinguish outcomes that GraphQL intentionally makes indistinguishable.
+
+Route navigation sets the route region busy, announces completion or failure through the authored live region, and moves focus to the current page heading or route container according to documented policy.
+Superseded async imports and obsolete context responses cannot update the current route's focus, announcement, or status.
+
+### Reuse Petclinic rather than duplicate it
+
+A small shared Petclinic domain module is extracted from the existing HTMX sample so the HTMX and Vue host applications use the same entities, public logical types, fixtures, GraphQL behavior, and deterministic identities.
+The extraction changes Java packaging only where required and must not alter domain semantics, HTMX routes, resource-page lookup, seed cardinalities, or existing acceptance selectors.
+
+The new Vue Petclinic module owns its Spring Boot entry point, Vite application, `index.html`, `App.vue`, custom logical-type pages, generic fallback demonstration, and client-route forwarding.
+It uses production Vite output served by the application rather than a development server or runtime template compiler.
+At least one Petclinic logical type has a source-visible custom Vue page, and another uses the generic route fallback.
+
+### Keep first release client-rendered
+
+The supported lifecycle is a browser-created Vue application using Vue Router history.
+Nuxt, SSR, streaming hydration, and server-rendered custom-element upgrade require separate compatibility work and are explicitly unsupported rather than partially emulated.
+
+## Risks / Trade-offs
+
+- [Raw route encoding can be lost by router normalization] → Validate the raw path with the canonical codec before using decoded route parameters and test all shared invalid fixtures.
+- [Vue can resolve `cw-` tags as Vue components] → Configure `isCustomElement` in library and consuming builds and test compiled output.
+- [Vue can assign an attribute where a DOM property is required] → Document and test property bindings, including explicit `.prop` bindings for structured values.
+- [Route reuse can retain obsolete object state] → Key the rendered route component by canonical identity and verify disconnect and cancellation behavior.
+- [An authored page can omit or duplicate its semantic boundary] → Validate cardinality, containment, and bound identity and fail closed without repair.
+- [An async page can resolve after navigation] → Scope imports, diagnostics, focus, and announcements to the active route key.
+- [A duplicate Vue domain store can emerge] → Keep only router and viewer policy in Vue state and forbid mirroring GraphQL object, member, validation, or interaction state.
+- [Browser-history refresh can be mistaken for a frontend-only concern] → Specify and test the application server fallback independently of package route resolution.
+- [Sharing Petclinic can regress the HTMX sample] → Preserve logical types and fixture identities and run the existing HTMX Java and Playwright suites after extraction.
+- [Frontend tooling can leak into unrelated builds] → Keep Vue dependencies in the opt-in package and sample profiles and verify unaffected foundation and HTMX consumers.
+
+## Migration Plan
+
+The Vue viewer is additive and installed only by applications that select it.
+Applications create or reuse a Vue Router instance, add the exported route records, install the viewer plugin, author their stable shell, and configure server fallback for the chosen history base.
+They may begin with the generic route page and incrementally register exact logical-type custom pages.
+Rollback removes the plugin, route records, and Vue application assets without changing GraphQL or semantic component contracts.
+The Petclinic domain extraction is performed in one compatibility-preserving step before the Vue host application depends on it.
+
+## Deferred Work
+
+- Inherited or assignable logical-type page matching.
+- Runtime mutation of the page registry.
+- Nuxt, SSR, streaming hydration, and server route-data integration.
+- A framework-specific authentication package beyond application-owned shell chrome and existing GraphQL transport policy.
diff --git a/openspec/changes/add-generic-vue-web-component-viewer/proposal.md b/openspec/changes/add-generic-vue-web-component-viewer/proposal.md
new file mode 100644
index 00000000000..f057563a0b4
--- /dev/null
+++ b/openspec/changes/add-generic-vue-web-component-viewer/proposal.md
@@ -0,0 +1,39 @@
+## Why
+
+Vue applications need a complete generic Causeway viewer rather than examples that only place individual custom elements.
+The accepted component contracts now provide application entry points, semantic object contexts, object composition, menus, interactions, result outlets, collection previews, and toolkit-backed presentation.
+The remaining Vue-specific work is therefore deliberately thin: own browser routing and host lifecycle policy, bind route and endpoint values into application-authored semantic markup, and leave authoritative domain behavior in the framework-neutral components.
+
+## What Changes
+
+- Add an optional Vue 3 and Vue Router 4 package with TypeScript declarations, reproducible Vite library output, and no Vue dependency in the framework-neutral component artifact.
+- Export an application-owned-router integration consisting of route-record creation, a Vue plugin, route codecs, route-page components, composables, and replaceable navigation, home, and result policies.
+- Implement the canonical `<base-path>/object/<logical-type>/<identifier>` bookmark grammar and validate it against the shared cross-viewer route fixtures.
+- Resolve an exact logical-type Vue component or async component before falling back to a generic Vue route page containing `<cw-object>`.
+- Require application-authored Vue route pages to declare one `<cw-object-context>` and one route interaction controller, with Vue binding canonical identity rather than manufacturing semantic elements.
+- Require the application root component to own the stable shell, including one `<cw-graphql-client>`, `<cw-menubars>`, announcements, loading presentation, a result outlet, and the keyed router-view region.
+- Bridge authoritative custom-element navigation and interaction-result events into replaceable Vue policy without constructing GraphQL operations or mirroring domain state into a Vue store.
+- Configure and document native custom-element compilation, property binding, event handling, upgrade ordering, and deterministic cleanup.
+- Provide accessible route lifecycle, invalid-route, unavailable, partial-error, terminal-error, theme, and responsive behavior.
+- Add a production-built Vue Petclinic acceptance application that reuses the deterministic Petclinic domain without duplicating it and supports direct browser-history refresh through an application-owned server fallback.
+- Integrate package, browser, accessibility, Maven-reactor, licensing, and distribution verification while keeping npm installation and Vue runtime opt-in.
+
+## Capabilities
+
+### New Capabilities
+
+- `generic-vue-web-component-viewer`: Provides a router-led generic Vue Causeway viewer over the framework-neutral semantic web-component library.
+
+### Modified Capabilities
+
+None.
+
+## Impact
+
+- Adds Vue viewer package sources and generated distribution metadata under `viewers/webcomponents/vue`.
+- Adds a Vue Petclinic sample and the minimal shared Petclinic domain module needed by the HTMX and Vue host applications.
+- Updates the web-components reactor, test profiles, documentation, shared route fixtures, and release/licensing checks.
+- Relies on the accepted `graphql-web-component-context`, `domain-web-components`, rich GraphQL application-entry and interaction capabilities, and their declarative ownership contracts.
+- Preserves the existing GraphQL schema, public logical types, deterministic Petclinic identities, HTMX routes, and component semantics.
+- Does not require Vue, Vue Router, Vite, or a frontend build runtime for HTMX, vanilla HTML, or direct framework-neutral component consumers.
+- Does not add Nuxt, server-side rendering, streaming hydration, or a Vue-specific domain data store.
diff --git a/openspec/changes/add-generic-vue-web-component-viewer/specs/generic-vue-web-component-viewer/spec.md b/openspec/changes/add-generic-vue-web-component-viewer/specs/generic-vue-web-component-viewer/spec.md
new file mode 100644
index 00000000000..299abc8f28b
--- /dev/null
+++ b/openspec/changes/add-generic-vue-web-component-viewer/specs/generic-vue-web-component-viewer/spec.md
@@ -0,0 +1,347 @@
+## ADDED Requirements
+
+### Requirement: Optional packaged Vue viewer
+
+The project SHALL provide an explicitly installed Vue 3 viewer package that uses Vue Router 4 for host routing and semantic Causeway custom elements for domain behavior.
+Vue, Vue Router, and Vue build tooling MUST NOT become runtime or build dependencies of the framework-neutral component, HTMX viewer, or vanilla HTML artifacts.
+
+#### Scenario: Vue viewer is installed
+
+- **WHEN** a Vue application installs the package and its router integration
+- **THEN** the documented Causeway route records, route pages, composables, and policy hooks are available
+- **AND** authoritative domain state remains owned by semantic components
+
+#### Scenario: Vue viewer is absent
+
+- **WHEN** an application does not install the package
+- **THEN** Vue, Vue Router, and Vite are not required to consume the framework-neutral component library
+- **AND** existing HTMX and vanilla applications retain their established behavior
+
+#### Scenario: Package artifact is inspected
+
+- **WHEN** the Vue package is built and packed from its lockfile
+- **THEN** it contains ECMAScript library output, TypeScript declarations, documented peer dependencies, licenses, and only intended public files
+- **AND** a clean consumer can install and import the packed artifact
+
+### Requirement: Application-owned Vue Router integration
+
+The viewer SHALL export namespaced Causeway route records and installation helpers for an application-owned Vue Router instance.
+The viewer MUST NOT create a hidden router or prevent the application from defining unrelated routes, guards, history behavior, or scroll policy.
+
+#### Scenario: Causeway routes are added
+
+- **WHEN** an application adds the exported records to its router and installs the viewer plugin
+- **THEN** Causeway home, object, invalid-route, and fallback states coexist with application routes
+- **AND** viewer navigation uses that same router instance and history
+
+#### Scenario: Viewer is mounted beneath a base
+
+- **WHEN** the application configures a valid non-root browser-history base and route mount
+- **THEN** home, object, asset, refresh, and history URLs consistently honor that deployment
+- **AND** the viewer does not assume origin-root deployment
+
+#### Scenario: Direct history refresh reaches the application
+
+- **WHEN** a canonical Vue object bookmark is requested directly from the application server
+- **THEN** application-owned server fallback returns the Vue document for that client route
+- **AND** does not intercept GraphQL, static asset, authentication, or unrelated backend routes
+
+### Requirement: Canonical Vue bookmark routing
+
+The viewer SHALL map a public logical type and opaque identifier to the documented `<base-path>/object/<logical-type>/<identifier>` grammar.
+Route generation and raw-path acceptance MUST apply the same canonical UTF-8 percent encoding, separator rules, malformed-input rejection, and encoded-segment bound as the cross-viewer contract.
+
+#### Scenario: Canonical bookmark loads
+
+- **WHEN** a valid authorized bookmark is opened, refreshed, restored, or revisited through browser history
+- **THEN** Vue Router renders the page for the exact logical type and identifier
+- **AND** the browser location has the same route meaning as the HTMX viewer
+
+#### Scenario: Semantic navigation is requested
+
+- **WHEN** a component publishes complete authoritative object identity
+- **THEN** default Vue navigation pushes the codec-produced canonical path
+- **AND** does not reinterpret or truncate the identifier
+
+#### Scenario: Route encoding is invalid
+
+- **WHEN** a raw route contains malformed escapes, empty values, encoded separators, control characters, dot segments, malformed Unicode, overlong encoded segments, or non-canonical encoding
+- **THEN** the viewer presents a bounded invalid-route state before mounting an object context
+- **AND** does not disclose submitted route content, object state, authorization rules, or raw decoder exceptions
+
+#### Scenario: Shared route fixtures are exercised
+
+- **WHEN** the Vue route codec is verified
+- **THEN** every valid and invalid entry in `viewers/webcomponents/canonical-route-fixtures.yaml` has the same outcome as the established viewer contract
+- **AND** generated valid identities round-trip without loss
+
+### Requirement: Immutable exact-type Vue page registry
+
+The viewer SHALL normalize an application-supplied registry from exact public logical type to a Vue component or async component loader during plugin creation.
+Exact registration SHALL take precedence over the generic route page, and runtime registry mutation or inheritance fallback SHALL NOT be part of the initial contract.
+
+#### Scenario: Vue page is registered
+
+- **WHEN** a canonical route resolves a logical type with an exact synchronous registration
+- **THEN** the registered Vue component receives immutable canonical route props
+- **AND** the generic page is not mounted
+
+#### Scenario: Async Vue page is registered
+
+- **WHEN** the exact registration is an async component loader
+- **THEN** the viewer presents the current route's accessible loading state until that component resolves
+- **AND** loader rejection presents a bounded current-route error
+
+#### Scenario: No Vue page is registered
+
+- **WHEN** no exact registration exists
+- **THEN** the route renders the package's declarative generic page
+- **AND** that page contains `<cw-object editable>` within the route object context
+
+#### Scenario: Registry is defective
+
+- **WHEN** registration has an empty or malformed logical type, an unsupported value, or a duplicate effective key
+- **THEN** plugin installation fails with a bounded diagnostic
+- **AND** no arbitrary registration is selected
+
+### Requirement: Declarative Vue route context boundary
+
+Every custom and generic Vue object page SHALL declare exactly one marked route-level `<cw-object-context>` bound to the canonical logical type and identifier and exactly one `<cw-interaction-controller>` contained by that context.
+The viewer SHALL validate this authored boundary without creating, moving, selecting, or repairing semantic elements.
+
+#### Scenario: Valid custom page mounts
+
+- **WHEN** a registered page declares and binds the required context and interaction controller
+- **THEN** semantic descendants obtain the established GraphQL, object, and interaction services
+- **AND** Vue does not mirror object, member, validation, or interaction state
+
+#### Scenario: Declared context awaits Vue bindings
+
+- **WHEN** an authored context connects before Vue has supplied complete route props
+- **THEN** it issues no object query while identity is incomplete
+- **AND** becomes operational after complete canonical identity is bound
+
+#### Scenario: Route boundary is invalid
+
+- **WHEN** the mounted page omits or duplicates the marked context, misplaces the interaction controller, or binds identity different from the canonical route
+- **THEN** the route fails closed to a bounded diagnostic
+- **AND** does not query through an ancestor context, retain prior content, or manufacture a replacement
+
+#### Scenario: Generic component connects
+
+- **WHEN** `<cw-object>` renders the route object
+- **THEN** it uses effective or fallback grid behavior from the semantic component contract
+- **AND** does not inspect Vue Router, Vue injection state, or the page registry
+
+### Requirement: Stable application-authored Vue shell
+
+The application root Vue component SHALL declare one stable `<cw-graphql-client>` containing application chrome, `<cw-menubars>`, loading and announcement landmarks, one default result outlet, and the router-view region.
+The viewer SHALL expose endpoint and policy values for binding but SHALL NOT manufacture or prescribe the visual placement of those semantic elements.
+
+#### Scenario: Vue object route changes
+
+- **WHEN** the keyed router-view region changes canonical object identity
+- **THEN** the declared GraphQL client and menu coordination remain connected
+- **AND** the obsolete route context and interaction controller disconnect deterministically
+
+#### Scenario: Application chooses its shell layout
+
+- **WHEN** an application places menus, branding, results, route content, auxiliary regions, or footer differently from the reference shell
+- **THEN** routing, menu events, loading, results, announcements, focus, and context disposal retain their documented behavior
+- **AND** no vertical-menu or other visual hierarchy is imposed by the viewer package
+
+#### Scenario: Shell protocol is defective
+
+- **WHEN** the root composition omits or duplicates its provider, route region, default result outlet, loading landmark, or announcement landmark
+- **THEN** bounded development and test diagnostics identify the invalid contract
+- **AND** the viewer does not create, move, or silently select a landmark
+
+#### Scenario: Application document is authored
+
+- **WHEN** the Vue application loads
+- **THEN** its application-owned HTML document selects Causeway registration, toolkit assets, locale, CSP, base URL, theme, and Vue entry assets
+- **AND** the viewer package does not replace that document policy
+
+### Requirement: Native Vue custom-element integration
+
+The package and documented consuming build SHALL classify `cw-` tags as native custom elements and preserve their attributes, DOM properties, slots, `CustomEvent` payloads, upgrade behavior, and disconnect lifecycle.
+Structured values and services SHALL be assigned as DOM properties rather than serialized into unintended attributes.
+
+#### Scenario: Vue template is compiled
+
+- **WHEN** a package or application single-file component contains a `cw-` tag
+- **THEN** Vue compiles it as a native custom element rather than resolving a Vue component
+- **AND** authored descendants and slots remain available to the element's declarative capture lifecycle
+
+#### Scenario: Structured value is bound
+
+- **WHEN** Vue supplies a structured executor, result, or other property-only value to a custom element
+- **THEN** the value is assigned through the element's DOM property contract
+- **AND** is not stringified as an HTML attribute
+
+#### Scenario: Semantic event is published
+
+- **WHEN** a Causeway component emits navigation, interaction, or result semantics
+- **THEN** the scoped Vue bridge forwards the authoritative payload once to replaceable viewer policy
+- **AND** does not modify the originating GraphQL operation or semantic outcome
+
+#### Scenario: Vue application unmounts
+
+- **WHEN** the host application is unmounted
+- **THEN** the bridge removes its listeners and pending viewer lifecycle work
+- **AND** disconnected semantic elements perform their normal cleanup
+
+### Requirement: Vue navigation, home, and result policy
+
+The viewer SHALL provide replaceable policy for object navigation, application-entry home behavior, and scalar, object, collection, and void action results.
+Application handlers SHALL receive semantic payloads through a documented single-claim protocol without replacing component interaction behavior.
+
+#### Scenario: Object result uses default policy
+
+- **WHEN** an interaction returns complete navigable object identity and no application handler claims it
+- **THEN** default policy navigates to the canonical Vue object route
+- **AND** the component-provided identity remains authoritative
+
+#### Scenario: Object result lacks navigation identity
+
+- **WHEN** an object-shaped result lacks the required public logical type or opaque identifier
+- **THEN** default policy presents a bounded unsupported result
+- **AND** does not infer identity from title, type name, current route, or Vue state
+
+#### Scenario: Non-object result uses a page outlet
+
+- **WHEN** a scalar or collection result is unclaimed and the active page declares exactly one eligible result outlet
+- **THEN** default policy assigns the normalized result to that outlet
+- **AND** established semantic result components retain presentation authority
+
+#### Scenario: Non-object result uses the shell outlet
+
+- **WHEN** a scalar or collection result is unclaimed and the active page has no eligible result outlet
+- **THEN** default policy assigns it to the one declared shell outlet
+- **AND** duplicate or disconnected outlets fail closed rather than receiving an arbitrary result
+
+#### Scenario: Void result retains its target
+
+- **WHEN** a successful void action leaves the current route object available
+- **THEN** default policy refreshes the current object context without adding a history entry
+- **AND** preserves accessible result status
+
+#### Scenario: Void result removes its target
+
+- **WHEN** the authoritative post-action refresh establishes that the current route object no longer exists
+- **THEN** default policy replaces the route with configured home
+- **AND** does not leave a stale object page in history state
+
+#### Scenario: Application replaces policy
+
+- **WHEN** an application handler claims a navigation, home, or result event
+- **THEN** the handler receives the unchanged semantic payload
+- **AND** default policy does not also handle that event
+
+### Requirement: Accessible generation-scoped route lifecycle
+
+The viewer SHALL coordinate accessible loading, ready, invalid-route, unavailable, partial-error, unsupported, and terminal-error states for the current route generation.
+Focus, busy state, and announcements MUST belong only to the current canonical route.
+
+#### Scenario: Navigation begins
+
+- **WHEN** Vue Router starts a Causeway route transition or an async page begins loading
+- **THEN** the authored route region exposes a busy state and the authored live region announces bounded progress
+- **AND** the previous route cannot overwrite that generation's status
+
+#### Scenario: Navigation completes
+
+- **WHEN** the current page is mounted and reaches its presentable state
+- **THEN** focus moves to the current page heading or documented route container
+- **AND** the live region announces the current authoritative title without duplicating component-local announcements
+
+#### Scenario: Async page is superseded
+
+- **WHEN** navigation changes before an async registered page resolves or rejects
+- **THEN** the obsolete page, error, focus movement, and announcement are discarded
+- **AND** the current route remains authoritative
+
+#### Scenario: Domain data is partially available
+
+- **WHEN** the current object context reports a partial GraphQL error with usable sibling data
+- **THEN** the page preserves usable semantic content and exposes bounded partial-error status
+- **AND** does not replace the whole route with a terminal state
+
+#### Scenario: Object cannot be presented
+
+- **WHEN** a canonical object route yields unavailable or denied data
+- **THEN** the default presentation reveals neither domain state nor authorization rules
+- **AND** does not claim to distinguish outcomes that the authoritative transport leaves indistinguishable
+
+### Requirement: Vue theme and responsive shell contract
+
+The viewer SHALL provide a copyable reference theme and stable styling hooks while leaving application CSS and shell geometry authoritative.
+The reference application SHALL support wide and narrow layouts, light and dark color preferences, reduced motion, keyboard operation, and visible focus.
+
+#### Scenario: Application supplies its own theme
+
+- **WHEN** an application overrides documented custom properties or shell classes
+- **THEN** routing and semantic component behavior remain unchanged
+- **AND** component shadow-part and custom-property contracts remain available
+
+#### Scenario: Narrow or reduced-motion presentation applies
+
+- **WHEN** viewport width or motion preference changes
+- **THEN** shell content remains operable without obscuring the active route or result
+- **AND** transitions do not interfere with focus or announcements
+
+### Requirement: Executable Vue Petclinic acceptance application
+
+The project SHALL provide a production-built Vue Petclinic application that reuses the same deterministic Petclinic domain, public logical types, and fixture identities as the HTMX sample without copying domain implementation.
+The application SHALL demonstrate a source-visible custom Vue page, generic fallback, stable shell, menus, interactions, results, canonical routing, and server refresh fallback.
+
+#### Scenario: Custom Petclinic page loads
+
+- **WHEN** the configured Petclinic logical type is opened through a direct canonical bookmark
+- **THEN** its registered Vue single-file component binds the declared object context and renders semantic members
+- **AND** the source demonstrates application-owned layout and interaction-controller placement
+
+#### Scenario: Generic Petclinic page loads
+
+- **WHEN** another Petclinic logical type has no registration
+- **THEN** the generic Vue page renders `<cw-object>` for the same GraphQL identity
+- **AND** direct refresh and browser history retain that route
+
+#### Scenario: Existing HTMX Petclinic is rebuilt
+
+- **WHEN** the Petclinic domain is shared with the new Vue application
+- **THEN** existing HTMX routes, pages, shell, seed cardinalities, stable identities, and acceptance behavior remain unchanged
+- **AND** the shared module introduces no Vue dependency
+
+#### Scenario: Acceptance suite runs
+
+- **WHEN** the Vue browser profile executes
+- **THEN** it covers direct links, refresh, back and forward, exact custom-page precedence, generic fallback, menus, property and action interactions, object and collection results, partial errors, invalid routes, absent objects, responsive layout, keyboard focus, announcements, and both supported component toolkit policies
+- **AND** browser console errors, page errors, failed resource requests, and accessibility violations fail the suite
+
+### Requirement: Reactor and release integration
+
+The Vue package, shared sample domain, and Vue Petclinic application SHALL integrate with the web-components reactor and project release checks while retaining opt-in frontend dependency installation.
+Reproducible frontend regeneration SHALL be separated from unrelated runtime consumers and documented for maintainers.
+
+#### Scenario: Ordinary project build runs
+
+- **WHEN** the normal Maven reactor builds the web-components modules
+- **THEN** Java artifacts and committed or packaged Vue outputs are verified without making Vue runtime classes transitive to non-Vue modules
+- **AND** licensing and stale-generated-output checks remain enforceable
+
+#### Scenario: Vue verification profile runs
+
+- **WHEN** the documented Vue verification profile or frontend command is selected
+- **THEN** locked dependencies are installed, unit tests and type checks run, production artifacts are regenerated, package contents are verified, and browser acceptance can be selected
+- **AND** a generated-output difference fails verification
+
+### Requirement: Initial client-rendered scope
+
+The first generic Vue viewer SHALL document browser client rendering as its supported lifecycle and SHALL NOT imply verified Nuxt, SSR, or streaming-hydration compatibility.
+
+#### Scenario: SSR support is requested
+
+- **WHEN** an application requires Nuxt, SSR, streaming hydration, or server-only route data
+- **THEN** the viewer reports that capability as separate compatibility work
+- **AND** does not silently provide inconsistent custom-element hydration
diff --git a/openspec/changes/add-generic-vue-web-component-viewer/tasks.md b/openspec/changes/add-generic-vue-web-component-viewer/tasks.md
new file mode 100644
index 00000000000..3c8aa638272
--- /dev/null
+++ b/openspec/changes/add-generic-vue-web-component-viewer/tasks.md
@@ -0,0 +1,70 @@
+## 1. Package and Build Contract
+
+- [ ] 1.1 Add the optional `viewers/webcomponents/vue` Vue 3 and Vue Router 4 package with TypeScript sources, package metadata, a committed lockfile, Vite library mode, peer dependencies, ECMAScript exports, and declarations.
+- [ ] 1.2 Add the lightweight Maven/reactor integration and explicit frontend regeneration profile without making Vue dependencies transitive to foundation, HTMX, or vanilla consumers.
+- [ ] 1.3 Configure Vue single-file component compilation to recognize `cw-` tags as native custom elements in the library and documented consumer setup.
+- [ ] 1.4 Add package unit-test, type-check, production-build, stale-output, license, `npm pack`, package-content, and clean packed-consumer smoke checks.
+- [ ] 1.5 Document the public package exports and keep unimplemented Nuxt, SSR, runtime registry mutation, and inherited-type matching out of the initial API.
+
+## 2. Canonical Router Integration
+
+- [ ] 2.1 Implement the bounded canonical UTF-8 route encoder and raw-path decoder for `<base-path>/object/<logical-type>/<identifier>` without relying on already-decoded Vue Router parameters.
+- [ ] 2.2 Test valid, invalid, overlong, Unicode, non-canonical, and generated round-trip routes against `viewers/webcomponents/canonical-route-fixtures.yaml`.
+- [ ] 2.3 Export namespaced home, object, invalid-route, and fallback route records for an application-owned Vue Router instance.
+- [ ] 2.4 Implement plugin installation and injected viewer configuration without creating a hidden router or overriding unrelated application routes, guards, scroll behavior, or history policy.
+- [ ] 2.5 Implement codec-produced push and replace helpers that preserve configured base-path meaning and complete advertised identity.
+- [ ] 2.6 Document and integration-test application-owned browser-history server fallback without intercepting GraphQL, assets, authentication, or unrelated backend routes.
+
+## 3. Custom and Generic Route Pages
+
+- [ ] 3.1 Normalize an immutable exact-logical-type registry for Vue components and async component loaders and reject malformed, unsupported, or duplicate effective registrations at startup.
+- [ ] 3.2 Implement custom-page precedence and the generic route-page fallback containing authored `<cw-object-context>`, `<cw-object editable>`, and `<cw-interaction-controller>` elements.
+- [ ] 3.3 Define immutable `logicalTypeName`, `objectId`, and `routeKey` props for application-authored route pages and provide source examples for binding them to the declared context.
+- [ ] 3.4 Add bounded post-mount validation for route-context cardinality, interaction-controller containment, and canonical identity without manufacturing, moving, or selecting semantic elements.
+- [ ] 3.5 Key `RouterView` content by canonical identity and verify that route changes disconnect obsolete contexts, requirements, listeners, and interaction state.
+- [ ] 3.6 Scope async component loading, rejection, diagnostics, focus, and announcements to the active route generation and discard superseded completion.
+- [ ] 3.7 Add a structural test proving `<cw-object>` does not import or inspect Vue Router, Vue injection state, or the page registry.
+
+## 4. Stable Shell and Native Element Bridge
+
+- [ ] 4.1 Export the shell composables and route-view helpers needed by an application-authored root component without exporting a mandatory visual shell hierarchy.
+- [ ] 4.2 Implement bounded development validation for one stable `<cw-graphql-client>`, one router-view region, one default result outlet, and the documented loading and announcement landmarks without repair.
+- [ ] 4.3 Implement endpoint and string-identity binding through authored templates and property-only structured bindings through DOM properties, including explicit Vue `.prop` use where required.
+- [ ] 4.4 Implement a shell-scoped semantic-event bridge for authoritative object navigation, interaction completion, and result events with deterministic listener cleanup on application unmount.
+- [ ] 4.5 Verify native custom-element attributes, properties, slots, pre-upgrade children, `CustomEvent` payloads, event bubbling, and connect/disconnect lifecycle under Vue rendering.
+- [ ] 4.6 Keep GraphQL objects, metadata, members, validation, invocation, and interaction state out of Vue stores and duplicate adapter state.
+
+## 5. Navigation, Home, and Result Policy
+
+- [ ] 5.1 Implement replaceable single-claim policy hooks for navigation, application-entry home behavior, and object, scalar, collection, void, unsupported, and error outcomes.
+- [ ] 5.2 Route complete object identities canonically and reject object-shaped results that lack authoritative public navigation identity without inference.
+- [ ] 5.3 Resolve exactly one eligible active-page result outlet before the shell default and fail closed for duplicate, disconnected, or otherwise ambiguous outlets.
+- [ ] 5.4 Assign normalized scalar and collection results to `<cw-action-results>` so existing result and standalone-collection components retain presentation authority.
+- [ ] 5.5 Refresh the current object context after void results and replace the route with home only when authoritative refresh establishes that the target disappeared.
+- [ ] 5.6 Test application handler replacement so claimed events are handled once and component interaction behavior remains unchanged.
+
+## 6. Route Lifecycle, Accessibility, and Theme
+
+- [ ] 6.1 Coordinate busy state, loading, ready, invalid-route, unavailable, partial-error, unsupported, and terminal-error presentation from router and object-context state.
+- [ ] 6.2 Implement generation-scoped focus movement and live-region announcements that cannot be overwritten by obsolete navigation or async-page work.
+- [ ] 6.3 Ensure unavailable and denied outcomes reveal neither domain state nor authorization rules and do not claim distinctions absent from the authoritative transport.
+- [ ] 6.4 Add a copyable reference shell theme with documented custom properties and hooks for wide, narrow, light, dark, reduced-motion, keyboard, and visible-focus behavior.
+- [ ] 6.5 Run automated accessibility checks and manual keyboard journeys for routing, menus, dialogs, sidebars, result dismissal, and route restoration.
+
+## 7. Shared Petclinic Domain and Vue Application
+
+- [ ] 7.1 Extract the deterministic Petclinic domain and seed configuration into a small shared non-viewer module while preserving public logical types, fixture identities, cardinalities, GraphQL semantics, and source ownership.
+- [ ] 7.2 Update the HTMX Petclinic application to consume the shared module without changing its routes, classpath pages, application shell, selectors, security variants, or toolkit policies.
+- [ ] 7.3 Add a separate Vue Petclinic Spring Boot application with its own entry point, GraphQL endpoint, Vite source application, production output, client-route forwarding, and run profile.
+- [ ] 7.4 Author the Vue Petclinic `index.html` and root `App.vue` with a stable GraphQL provider, menus, branding, loading, announcements, default result outlet, and keyed router-view region.
+- [ ] 7.5 Add at least one source-visible Petclinic custom Vue page declaring its route context and interaction controller and leave another logical type unregistered to demonstrate generic fallback.
+- [ ] 7.6 Demonstrate menus, breadcrumbs, properties, actions, collection paging and filtering, row previews, object navigation, object results, and collection-result presentation without Vue-owned domain queries.
+- [ ] 7.7 Verify production output and direct-refresh routing without requiring a Vite development server or runtime Vue template compiler.
+
+## 8. Verification and Documentation
+
+- [ ] 8.1 Add browser acceptance for direct links, refresh, back and forward, custom-page precedence, async-page races, generic fallback, invalid routes, absent objects, partial errors, menus, interactions, and results.
+- [ ] 8.2 Run Vue acceptance under default Vaadin and native component policies and fail on browser console errors, page errors, failed resources, or accessibility violations.
+- [ ] 8.3 Re-run foundation Node tests, HTMX Node and Java tests, HTMX Petclinic integration and security tests, and existing native, Vaadin, and secured Playwright profiles after Petclinic extraction.
+- [ ] 8.4 Document installation, package exports, Vite configuration, router records, route grammar, history fallback, shell ownership, custom pages, generic fallback, policies, toolkit assets, theming, lifecycle, diagnostics, and CSR-only support.
+- [ ] 8.5 Verify Maven reactor integration, reproducible generated output, dependency notices, Apache RAT, formatting, `git diff --check`, and strict OpenSpec validation.
diff --git a/openspec/planned-changes/web-component-viewer/add-generic-vue-web-component-viewer/design.md b/openspec/planned-changes/web-component-viewer/add-generic-vue-web-component-viewer/design.md
deleted file mode 100644
index 1275dc2223a..00000000000
--- a/openspec/planned-changes/web-component-viewer/add-generic-vue-web-component-viewer/design.md
+++ /dev/null
@@ -1,99 +0,0 @@
-## Context
-
-Vue provides component composition, dynamic components, application plugins, and a mature router.
-Causeway's custom elements already provide the domain-aware controls, object contexts, grid composition, menu composition, validation, and interaction results.
-Applications declare those elements directly in Vue templates.
-A generic Vue viewer should therefore be a router, route-value binder, and lifecycle integration, not a Vue rewrite of those components.
-
-The final customization architecture is route-first.
-The Vue router resolves a bookmark, chooses an exact logical-type application page if registered, and otherwise renders a generic route page containing `<cw-object>`.
-
-## Goals / Non-Goals
-
-**Goals:**
-
-- Provide canonical deep-linkable bookmark routes using Vue Router.
-- Support exact-logical-type custom Vue pages and generic object fallback.
-- Preserve stable menu shell state across route-page changes.
-- Bridge semantic custom-element navigation and result events into replaceable Vue policy.
-- Integrate custom-element registration and lifecycle cleanly with Vue rendering.
-- Provide a complete accessible production-style viewer package and acceptance application.
-
-**Non-Goals:**
-
-- Wrapping every Causeway custom element as a separate Vue component.
-- Reimplementing GraphQL operations, object layout, menu parsing, editors, or validation in Vue.
-- Making `<cw-object>` inspect Vue Router or the page registry.
-- Supporting Nuxt or server-side rendering in the first version.
-- Defining HTMX or Svelte lifecycle behavior.
-
-## Decisions
-
-### Use Vue Router as the customization boundary
-
-The viewer installs canonical object and application-entry routes into a Vue Router instance or exposes route records for an application-owned instance.
-The object route resolves public logical type and identifier.
-It then selects the exact logical-type page registration or the generic route page.
-
-Page selection occurs before `<cw-object>` is created.
-The generic object component remains unaware of custom pages.
-
-### Register framework-native custom pages
-
-Applications register a Vue component, async component, or documented page factory against an exact Causeway logical type.
-The selected application page declares one `<cw-object-context>` and binds the canonical logical type and identifier supplied by the generic route-page boundary.
-The viewer validates or diagnoses the declared boundary but does not manufacture it imperatively.
-
-Custom Vue pages can compose Causeway custom elements, ordinary Vue components, HTML, and application custom elements.
-They consume domain state through their declared object context rather than constructing GraphQL documents in Vue stores.
-
-### Render a generic page when no registration exists
-
-The fallback route-page template declares its GraphQL client association, route-level object context, and `<cw-object>` fallback.
-The router binds endpoint and canonical identity values into those declared elements.
-A stable route key based on canonical bookmark identity ensures old contexts disconnect when navigation changes.
-Superseded component responses remain governed by object-context generation handling.
-
-### Keep the application shell stable
-
-The application-authored root viewer shell declares one stable `<cw-graphql-client>` containing `<cw-menubars>` outside `RouterView` or its equivalent route-page boundary.
-The Vue adapter binds configured client properties but does not create the provider element.
-Menu disclosure, service-action interaction, and menu layout remain component-owned.
-Vue policy receives semantic outcomes and may call router navigation or render non-object results.
-
-### Treat custom elements as the data plane
-
-The package configures or documents Vue custom-element recognition so Causeway element attributes, properties, and native custom events retain their browser semantics.
-A small integration adapter may normalize event subscription and cleanup, but it does not mirror component state into a duplicate Vue domain store.
-
-### Keep home and result policy replaceable
-
-Home-page objects and service actions are resolved from the application-entry contract.
-Default object results navigate to canonical routes, while scalar, collection, and void results use replaceable Vue presentations.
-Applications can override each policy without changing semantic components.
-
-### Defer server-side rendering
-
-The first viewer targets a client-rendered Vue application and browser history.
-Nuxt, SSR, streaming hydration, and server-only route data are separate compatibility work because custom-element upgrade and GraphQL context lifecycle need explicit validation there.
-
-## Risks / Trade-offs
-
-- [Vue may treat unknown tags or properties specially] → Configure custom-element recognition and test attributes, properties, slots, and native events.
-- [Route reuse can retain obsolete object context] → Key route pages by canonical bookmark identity and verify deterministic disconnect.
-- [A duplicate Vue store can emerge] → Keep domain state in semantic object contexts and expose only viewer policy through Vue state.
-- [A page can omit or duplicate its declarative context] → Validate the route-page contract and present a bounded development diagnostic without silently adding a context.
-- [Async custom pages can race navigation] → Associate imports with route generation and discard superseded resolutions.
-- [Viewer routes may diverge] → Share canonical route and fallback fixtures with HTMX and Svelte viewers.
-
-## Migration Plan
-
-The Vue viewer is additive and installed only by Vue applications that select it.
-Applications may begin with generic routes and incrementally register custom logical-type pages.
-Rollback removes the plugin and route records without changing GraphQL or component contracts.
-
-## Open Questions
-
-- Whether the package owns a router instance or primarily exports route records and installation helpers.
-- Whether custom page registration should permit inheritance fallback after exact logical type in a later version.
-- Which non-object result presentation should be supplied by default.
diff --git a/openspec/planned-changes/web-component-viewer/add-generic-vue-web-component-viewer/proposal.md b/openspec/planned-changes/web-component-viewer/add-generic-vue-web-component-viewer/proposal.md
deleted file mode 100644
index 1953412ae68..00000000000
--- a/openspec/planned-changes/web-component-viewer/add-generic-vue-web-component-viewer/proposal.md
+++ /dev/null
@@ -1,36 +0,0 @@
-## Why
-
-Vue applications need a complete generic Causeway viewer rather than only examples showing how to place individual custom elements.
-The viewer's primary responsibility is to use Vue's routing and composition model to choose a logical-type-specific Vue page or a generic page containing `<cw-object>`, while the framework-neutral components continue to own GraphQL and domain semantics.
-
-The architectural ownership boundary is recorded in `coverage-matrix.yaml` entries `REF-VIEWER-01`, `REF-COMPONENT-01`, `REF-COMPONENT-02`, `REF-HOME-01`, and `REF-MENU-01`.
-
-## What Changes
-
-- Add an opt-in generic Vue viewer package and application plugin.
-- Implement canonical bookmark routing using Vue Router while preserving the cross-viewer route meaning.
-- Resolve an exact-logical-type Vue page registration before falling back to a generic Vue route page containing `<cw-object>`.
-- Require application-authored Vue shell and page templates to declare `<cw-graphql-client>` and `<cw-object-context>`, with the router binding endpoint and canonical identity rather than manufacturing those elements.
-- Keep `<cw-object>` unaware of Vue routes, custom-page registration, and framework lifecycle.
-- Compose `<cw-menubars>` in a stable Vue shell and translate semantic custom-element events into router navigation under replaceable policy.
-- Support Vue components, async components, and application page factories as custom logical-type pages.
-- Define home-page, result, loading, not-found, access-denied, partial-error, terminal-error, theme, and accessibility behavior.
-- Add a production-style Vue acceptance application rather than a documentation-only sample.
-
-## Capabilities
-
-### New Capabilities
-
-- `generic-vue-web-component-viewer`: Provides a router-led generic Vue Causeway viewer over the framework-neutral semantic web-component library.
-
-### Modified Capabilities
-
-None.
-
-## Impact
-
-- Adds an optional Vue package, router integration, plugin configuration, shell components, route pages, tests, and documentation.
-- Depends on accepted P0 and P1 rich GraphQL coverage plus completed application-entry, composite-object, and menu-bar capabilities.
-- Uses application-authored native custom elements and semantic events without reimplementing GraphQL schema interpretation or semantic context hierarchy in Vue adapters.
-- Preserves canonical route and fallback semantics shared with the generic HTMX and Svelte viewers.
-- Does not require non-Vue applications to install Vue or Vue Router.
diff --git a/openspec/planned-changes/web-component-viewer/add-generic-vue-web-component-viewer/specs/generic-vue-web-component-viewer/spec.md b/openspec/planned-changes/web-component-viewer/add-generic-vue-web-component-viewer/specs/generic-vue-web-component-viewer/spec.md
deleted file mode 100644
index 90b2010157e..00000000000
--- a/openspec/planned-changes/web-component-viewer/add-generic-vue-web-component-viewer/specs/generic-vue-web-component-viewer/spec.md
+++ /dev/null
@@ -1,92 +0,0 @@
-## ADDED Requirements
-
-### Requirement: Optional router-led Vue viewer
-The project SHALL provide an explicitly installed generic Vue viewer that owns application routing and shell lifecycle while using semantic Causeway custom elements for domain behavior.
-
-#### Scenario: Vue viewer is installed
-- **WHEN** a Vue application installs the viewer package and router integration
-- **THEN** canonical Causeway routes and the documented viewer shell become available
-- **AND** domain state remains component-owned
-
-#### Scenario: Vue viewer is absent
-- **WHEN** an application does not install the package
-- **THEN** Vue and Vue Router are not runtime requirements of the component library
-
-### Requirement: Vue Router bookmark routes
-The viewer SHALL resolve canonical public logical-type and identifier routes through Vue Router with configurable mounting beneath an application base path.
-
-#### Scenario: Bookmark route loads
-- **WHEN** a valid authorized bookmark route is opened, refreshed, or revisited through history
-- **THEN** Vue Router resolves the corresponding route page
-- **AND** route identity remains compatible with the other generic viewers
-
-#### Scenario: Bookmark route fails
-- **WHEN** the bookmark is malformed, absent, stale, or unauthorized
-- **THEN** the route page presents a bounded accessible outcome
-- **AND** reveals no domain state or authorization rule
-
-### Requirement: Vue custom-page precedence
-The Vue route resolver SHALL choose an exact-logical-type registered Vue page before the generic object page.
-Application-authored custom and generic Vue templates SHALL declare their own route-level `<cw-object-context>`, and the router SHALL bind canonical identity without manufacturing that element.
-
-#### Scenario: Vue page is registered
-- **WHEN** a route resolves a logical type with a registered Vue component, async component, or accepted factory
-- **THEN** that page's declared object context is bound to canonical route identity
-
-#### Scenario: No Vue page is registered
-- **WHEN** no exact registration exists
-- **THEN** the route renders a declarative fallback containing one object context and `<cw-object>`
-
-#### Scenario: Generic component connects
-- **WHEN** `<cw-object>` renders the route object
-- **THEN** it uses effective or fallback grid behavior
-- **AND** does not inspect Vue Router or custom-page registrations
-
-### Requirement: Stable Vue application shell
-The application-authored Vue shell SHALL declare one stable `<cw-graphql-client>` containing `<cw-menubars>` and global viewer state outside the changing route-page region.
-The viewer SHALL bind configured client properties without manufacturing the provider element.
-
-#### Scenario: Vue route changes
-- **WHEN** `RouterView` or the equivalent page region changes object routes
-- **THEN** the declared client and menu coordination remain stable
-- **AND** obsolete declared object contexts disconnect deterministically
-
-#### Scenario: Route page has an invalid context boundary
-- **WHEN** a custom or generic Vue page omits or duplicates its route-level object context
-- **THEN** the viewer presents a bounded diagnostic
-- **AND** does not silently create or select a context
-
-### Requirement: Native custom-element integration
-The Vue viewer SHALL preserve Causeway custom-element attributes, properties, slots, native custom events, upgrade behavior, and lifecycle without duplicating their domain state in Vue.
-
-#### Scenario: Semantic event is published
-- **WHEN** a Causeway component emits navigation, interaction, or result semantics
-- **THEN** the Vue viewer adapter forwards it to replaceable viewer policy
-- **AND** does not alter the component's GraphQL operation
-
-### Requirement: Vue home and result policy
-The viewer SHALL provide replaceable Vue policy for configured home entries and scalar, object, collection, and void results.
-
-#### Scenario: Object result uses default policy
-- **WHEN** an interaction produces an object result
-- **THEN** default policy may navigate to its canonical Vue Router route
-
-#### Scenario: Application replaces policy
-- **WHEN** an application registers its own home or result handler
-- **THEN** the handler receives semantic data without replacing component interaction behavior
-
-### Requirement: Vue route lifecycle accessibility
-The viewer SHALL present accessible loading, ready, not-found, access-denied, partial-error, and terminal-error states and manage focus after route navigation.
-
-#### Scenario: Async custom page is superseded
-- **WHEN** navigation changes before an async page resolves
-- **THEN** the obsolete page is not rendered
-- **AND** focus and announcements belong to the current route
-
-### Requirement: Initial client-rendered scope
-The first generic Vue viewer SHALL document client rendering as its supported lifecycle and SHALL NOT imply unverified Nuxt or SSR compatibility.
-
-#### Scenario: SSR support is requested
-- **WHEN** an application requires Nuxt, SSR, or streaming hydration
-- **THEN** the viewer reports that capability as separate compatibility work
-- **AND** does not silently provide inconsistent custom-element hydration
diff --git a/openspec/planned-changes/web-component-viewer/add-generic-vue-web-component-viewer/tasks.md b/openspec/planned-changes/web-component-viewer/add-generic-vue-web-component-viewer/tasks.md
deleted file mode 100644
index 9a229ea8dca..00000000000
--- a/openspec/planned-changes/web-component-viewer/add-generic-vue-web-component-viewer/tasks.md
+++ /dev/null
@@ -1,29 +0,0 @@
-## 1. Package and Router Contract
-
-- [ ] 1.1 Verify all accepted P0 and P1 GraphQL, application-entry, composite-object, and menu-bar prerequisites are complete.
-- [ ] 1.2 Establish the optional Vue package, peer dependencies, plugin contract, custom-element registration, default theme, and acceptance application.
-- [ ] 1.3 Implement canonical route records, route parsing, configurable base path, and bookmark resolution using Vue Router.
-- [ ] 1.4 Maintain cross-viewer route fixtures shared with HTMX and Svelte implementations.
-
-## 2. Custom and Generic Route Pages
-
-- [ ] 2.1 Implement exact-logical-type registration for Vue components, async components, and accepted page factories.
-- [ ] 2.2 Implement application-authored custom and generic route templates that declare one `<cw-object-context>`, with `<cw-object>` in the generic fallback.
-- [ ] 2.3 Bind canonical route identity into declared contexts and fail closed diagnostically when a page omits or duplicates its route context.
-- [ ] 2.4 Key page lifecycle by canonical bookmark identity and dispose disconnected requirements deterministically.
-- [ ] 2.5 Verify `<cw-object>` never reads Vue router or page-registry state.
-
-## 3. Shell, Events, and Policy
-
-- [ ] 3.1 Implement the application-authored stable Vue shell declaring one `<cw-graphql-client>`, `<cw-menubars>` placement, `RouterView` page region, endpoint binding, and route lifecycle states.
-- [ ] 3.2 Bridge semantic custom-element navigation and result events into replaceable Vue router and result policy.
-- [ ] 3.3 Implement configurable home-page object and service-action policy.
-- [ ] 3.4 Keep domain state in GraphQL object contexts rather than a duplicate Vue store.
-
-## 4. Verification and Documentation
-
-- [ ] 4.1 Test deep links, refresh, history, custom pages, async page races, generic fallback, menus, interactions, results, partial errors, absent objects, and access denial.
-- [ ] 4.2 Test declarative client/context ownership, missing and duplicate context diagnostics, custom-element attributes, properties, slots, native events, upgrade ordering, disconnect, and route reuse.
-- [ ] 4.3 Verify responsive layout, keyboard navigation, focus after routing, announcements, light/dark themes, and accessibility.
-- [ ] 4.4 Document installation, router integration, route grammar, custom pages, fallback, policy, theming, lifecycle, SSR exclusion, and framework-neutral boundaries.
-- [ ] 4.5 Run Vue, browser, Maven, GraphQL, accessibility, formatting, and strict OpenSpec validation checks.


(causeway) 14/16: docs(openspec): sync secured Vue authentication specs

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit f9c2a33f7c1cb1d766282a60cac8632768fe08b6
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 3 17:25:45 2026 +0100

    docs(openspec): sync secured Vue authentication specs
---
 .../spec.md                                        |  36 ++++
 .../specs/generic-vue-web-component-viewer/spec.md |  39 +++++
 .../spec.md                                        |  56 ++++++-
 .../vue-viewer-local-secman-authentication/spec.md | 185 +++++++++++++++++++++
 .../spec.md                                        |  74 +++++++++
 5 files changed, 382 insertions(+), 8 deletions(-)

diff --git a/openspec/changes/add-secured-vue-petclinic-variant/specs/htmx-viewer-local-secman-authentication/spec.md b/openspec/changes/add-secured-vue-petclinic-variant/specs/htmx-viewer-local-secman-authentication/spec.md
index 810e98d2e36..5dd72b2149f 100644
--- a/openspec/changes/add-secured-vue-petclinic-variant/specs/htmx-viewer-local-secman-authentication/spec.md
+++ b/openspec/changes/add-secured-vue-petclinic-variant/specs/htmx-viewer-local-secman-authentication/spec.md
@@ -32,3 +32,39 @@ The HTMX Petclinic sample SHALL provide a repository-root-relative `run-secured.
 - **WHEN** a maintainer executes the existing `run.sh`
 - **THEN** the ordinary HTMX and Wicket comparison runtime remains selected
 - **AND** no local SecMan form integration is enabled by the new launcher
+
+## MODIFIED Requirements
+
+### Requirement: Existing security implementations remain unchanged
+
+The HTMX integration MUST consume the optional presentation-neutral web-component SecMan Spring bridge and MUST NOT modify existing Causeway core security, Spring security, SecMan, Wicket, or OAuth implementation code.
+The shared bridge SHALL remain isolated from HTMX routes and presentation while making credential and principal behavior reusable by another explicitly installed web-component host.
+
+#### Scenario: Shared bridge is consumed
+
+- **WHEN** local HTMX authentication is installed
+- **THEN** shared Causeway security modules retain their existing defaults and implementation behavior
+- **AND** credential lookup and principal conversion come from the optional presentation-neutral bridge
+- **AND** HTMX-specific security policy remains isolated in the HTMX integration
+
+#### Scenario: Existing application does not opt in
+
+- **WHEN** an application upgrades without importing an optional viewer authentication integration
+- **THEN** no new security bean, filter chain, login route, logout route, or SecMan lookup is activated
+
+### Requirement: Authenticated principal preserves SecMan user context
+
+A successful local login SHALL establish a Causeway `UserMemento` for the authenticated username with the effective SecMan roles, tenancy path, language, number locale, and time locale available at authentication time.
+The shared converter MUST operate without an unscoped repository access and MUST erase credential material according to Spring Security lifecycle behavior.
+
+#### Scenario: User has roles and profile preferences
+
+- **WHEN** a local user with SecMan roles, tenancy, and locale preferences signs in
+- **THEN** authenticated HTMX and GraphQL requests execute with the corresponding refined `UserMemento`
+- **AND** SecMan authorization sees the same effective username and roles
+
+#### Scenario: Authentication completes
+
+- **WHEN** Spring finishes validating the local credentials
+- **THEN** raw credentials are absent from the session principal and authentication diagnostics
+- **AND** retained browser-session state contains no plaintext password
diff --git a/openspec/specs/generic-vue-web-component-viewer/spec.md b/openspec/specs/generic-vue-web-component-viewer/spec.md
index 0c384cc8bf2..eb74873da3e 100644
--- a/openspec/specs/generic-vue-web-component-viewer/spec.md
+++ b/openspec/specs/generic-vue-web-component-viewer/spec.md
@@ -464,3 +464,42 @@ Its default behavior SHALL perform validated full-document same-origin navigatio
 - **WHEN** the application configures a non-root local-resource base and receives an application-local path
 - **THEN** resolution preserves that deployment context exactly once
 - **AND** it remains independent of the Vue object-route base path
+
+### Requirement: Authentication-neutral secured-host integration points
+
+The generic Vue viewer SHALL remain authentication-neutral while allowing an application-owned secured shell to supply an authenticated GraphQL executor, current-user chrome, native logout form, and exact pre-invocation Logout policy.
+The viewer MUST NOT fetch credentials or CSRF state, create login or logout endpoints, persist tokens, or infer authentication from route or result paths.
+
+#### Scenario: Secured application binds an executor
+
+- **WHEN** an authenticated Vue application binds a CSRF-decorating executor to its stable `<cw-graphql-client>`
+- **THEN** semantic components use that executor without knowing the authentication mechanism
+- **AND** generic routing, contexts, interaction results, and lifecycle remain unchanged
+
+#### Scenario: Secured application owns logout chrome
+
+- **WHEN** an application renders current-user and POST logout controls in its stable shell and claims exact framework Logout requests
+- **THEN** the host policy can submit its native logout contract before GraphQL invocation
+- **AND** the viewer neither manufactures nor duplicates authentication chrome
+
+#### Scenario: Generic application has no authentication context
+
+- **WHEN** the same Vue application runs without an authentication integration
+- **THEN** it may omit executor decoration and secured-session chrome
+- **AND** the generic viewer introduces no authentication request or endpoint
+
+### Requirement: Dedicated secured Vue launcher
+
+The Vue Petclinic sample SHALL provide a repository-root-relative `run-secured.sh` launcher that selects its secured Maven profile while preserving the environment and argument behavior of `run.sh`.
+
+#### Scenario: Secured Vue script runs
+
+- **WHEN** a maintainer executes `viewers/webcomponents/sample-vue-petclinic/run-secured.sh`
+- **THEN** Maven launches the secured Vue Petclinic application and local login flow
+- **AND** `JAVA_HOME`, `MVN`, and additional Maven arguments are honored
+
+#### Scenario: Ordinary Vue script runs
+
+- **WHEN** a maintainer executes the existing Vue `run.sh`
+- **THEN** the bypass-secured ordinary Vue application remains selected
+- **AND** the new secured integration remains opt-in
diff --git a/openspec/specs/htmx-viewer-local-secman-authentication/spec.md b/openspec/specs/htmx-viewer-local-secman-authentication/spec.md
index 852c5671d52..9fa76e65fdd 100644
--- a/openspec/specs/htmx-viewer-local-secman-authentication/spec.md
+++ b/openspec/specs/htmx-viewer-local-secman-authentication/spec.md
@@ -20,16 +20,20 @@ The generic HTMX viewer MUST retain its current dependency set and security beha
 - **AND** existing bypass, external, or application-owned security remains unchanged
 
 ### Requirement: Existing security implementations remain unchanged
-The initial integration MUST implement its SecMan-to-Spring bridge within the optional HTMX integration and MUST NOT modify existing Causeway core security, Spring security, SecMan, Wicket, or OAuth implementation code.
-It SHALL expose the provisional boundary clearly enough for later extraction into shared security.
 
-#### Scenario: Initial integration is built
-- **WHEN** local HTMX authentication is implemented
-- **THEN** shared security modules retain their existing defaults and implementation behavior
-- **AND** HTMX-specific bridge classes are isolated in the optional integration
+The HTMX integration MUST consume the optional presentation-neutral web-component SecMan Spring bridge and MUST NOT modify existing Causeway core security, Spring security, SecMan, Wicket, or OAuth implementation code.
+The shared bridge SHALL remain isolated from HTMX routes and presentation while making credential and principal behavior reusable by another explicitly installed web-component host.
+
+#### Scenario: Shared bridge is consumed
+
+- **WHEN** local HTMX authentication is installed
+- **THEN** shared Causeway security modules retain their existing defaults and implementation behavior
+- **AND** credential lookup and principal conversion come from the optional presentation-neutral bridge
+- **AND** HTMX-specific security policy remains isolated in the HTMX integration
 
 #### Scenario: Existing application does not opt in
-- **WHEN** an application upgrades without importing the optional integration
+
+- **WHEN** an application upgrades without importing an optional viewer authentication integration
 - **THEN** no new security bean, filter chain, login route, logout route, or SecMan lookup is activated
 
 ### Requirement: CSRF protection is a startup prerequisite
@@ -71,15 +75,18 @@ The lookup MUST execute in a controlled interaction and transaction boundary and
 - **AND** the browser receives no persistence exception, password data, user record, role, tenancy, or stack trace
 
 ### Requirement: Authenticated principal preserves SecMan user context
+
 A successful local login SHALL establish a Causeway `UserMemento` for the authenticated username with the effective SecMan roles, tenancy path, language, number locale, and time locale available at authentication time.
-The provisional converter MUST operate without an unscoped repository access and MUST erase credential material according to Spring Security lifecycle behavior.
+The shared converter MUST operate without an unscoped repository access and MUST erase credential material according to Spring Security lifecycle behavior.
 
 #### Scenario: User has roles and profile preferences
+
 - **WHEN** a local user with SecMan roles, tenancy, and locale preferences signs in
 - **THEN** authenticated HTMX and GraphQL requests execute with the corresponding refined `UserMemento`
 - **AND** SecMan authorization sees the same effective username and roles
 
 #### Scenario: Authentication completes
+
 - **WHEN** Spring finishes validating the local credentials
 - **THEN** raw credentials are absent from the session principal and authentication diagnostics
 - **AND** retained browser-session state contains no plaintext password
@@ -233,3 +240,36 @@ The existing Petclinic default runtime and comparison journey MUST remain availa
 #### Scenario: Ordinary Petclinic runtime starts
 - **WHEN** the existing default Petclinic application is launched without the optional authentication configuration
 - **THEN** its current HTMX, Wicket comparison, fixture, and bypass behavior remains unchanged
+
+### Requirement: HTMX authentication consumes the shared bridge
+
+The HTMX local SecMan integration SHALL consume the presentation-neutral web-component SecMan Spring bridge for credential lookup, principal state, Causeway user conversion, and DAO authentication-provider construction.
+It MUST retain ownership of HTMX paths, login presentation, request caching, CSRF transport, session-expiry handling, shell chrome, logout, and security-chain scope.
+
+#### Scenario: Secured HTMX starts after bridge extraction
+
+- **WHEN** the HTMX SecMan integration is imported
+- **THEN** the shared bridge supplies equivalent credential and principal behavior
+- **AND** existing HTMX routes, login, CSRF, user context, session, and logout outcomes remain unchanged
+
+#### Scenario: Vue security is absent
+
+- **WHEN** an application installs only the HTMX SecMan integration
+- **THEN** no Vue route, asset, authentication context, policy, or security chain is introduced
+- **AND** the shared bridge remains presentation-neutral
+
+### Requirement: Dedicated secured HTMX launcher
+
+The HTMX Petclinic sample SHALL provide a repository-root-relative `run-secured.sh` launcher that selects the established secured Maven profile while retaining the environment and argument behavior of `run.sh`.
+
+#### Scenario: Secured HTMX script runs
+
+- **WHEN** a maintainer executes `viewers/webcomponents/sample-htmx-petclinic/run-secured.sh`
+- **THEN** Maven launches `PetClinicHtmxSecuredApplication` through the secured profile
+- **AND** `JAVA_HOME`, `MVN`, and additional Maven arguments are honored
+
+#### Scenario: Ordinary HTMX script runs
+
+- **WHEN** a maintainer executes the existing `run.sh`
+- **THEN** the ordinary HTMX and Wicket comparison runtime remains selected
+- **AND** no local SecMan form integration is enabled by the new launcher
diff --git a/openspec/specs/vue-viewer-local-secman-authentication/spec.md b/openspec/specs/vue-viewer-local-secman-authentication/spec.md
new file mode 100644
index 00000000000..d65ab070f10
--- /dev/null
+++ b/openspec/specs/vue-viewer-local-secman-authentication/spec.md
@@ -0,0 +1,185 @@
+# vue-viewer-local-secman-authentication Specification
+
+## Purpose
+
+Provide optional local SecMan-backed form authentication, CSRF-safe GraphQL traffic, safe route restoration, and host-owned logout for application-owned Vue web-component shells.
+
+## Requirements
+
+### Requirement: Optional local SecMan authentication for Vue
+
+The project SHALL provide local SecMan-backed form authentication as an explicitly installed Vue web-component host integration.
+Generic Vue, Vue Router, foundation components, and the ordinary Vue Petclinic runtime MUST retain their existing dependency and security behavior when the integration is absent.
+
+#### Scenario: Vue integration is installed
+
+- **WHEN** an application imports the Vue SecMan integration and its documented prerequisites
+- **THEN** configured Vue routes, authentication context, and same-origin GraphQL require an authenticated Spring browser session
+- **AND** the host provides local login and logout behavior
+
+#### Scenario: Vue integration is absent
+
+- **WHEN** an application uses the generic Vue viewer without the integration
+- **THEN** no SecMan user lookup, security chain, login route, logout route, authentication bootstrap, or session policy is added
+- **AND** application-owned security remains unchanged
+
+### Requirement: CSRF protection is mandatory
+
+The integration SHALL require `causeway.security.spring.allow-csrf-filters=true` and MUST fail startup with a bounded diagnostic when it is false or absent.
+Every login POST, authenticated GraphQL POST, and logout POST SHALL carry current Spring CSRF evidence.
+
+#### Scenario: Secured Vue starts with CSRF enabled
+
+- **WHEN** the integration is installed with CSRF filters allowed
+- **THEN** startup validates its paths and security-chain configuration
+- **AND** browser-session POST requests remain CSRF protected
+
+#### Scenario: CSRF evidence is missing
+
+- **WHEN** login, GraphQL, or logout is submitted without valid current CSRF evidence
+- **THEN** Spring rejects the request without changing authenticated state
+- **AND** no protected domain result is returned
+
+### Requirement: Accessible server-rendered login
+
+The integration SHALL provide a branded server-rendered login page outside the Vue and GraphQL runtime.
+The page MUST require no Vue, custom-element, or GraphQL initialization and MUST submit credentials through a CSRF-protected form.
+
+#### Scenario: Anonymous user opens login
+
+- **WHEN** an unauthenticated browser requests the configured Vue login page
+- **THEN** it receives labelled username and password fields, a submit control, application branding, and current CSRF evidence
+- **AND** initial focus and instructions work without frontend JavaScript
+
+#### Scenario: Login fails
+
+- **WHEN** credentials are rejected
+- **THEN** the login page presents one generic announced failure
+- **AND** no username, account state, role, tenancy, or internal failure is disclosed
+
+#### Scenario: Logout completed
+
+- **WHEN** the login page is reached after logout
+- **THEN** it announces successful sign-out
+- **AND** no former route, user detail, CSRF token, or session identifier is exposed
+
+### Requirement: Safe Vue route restoration
+
+The integration SHALL restore only a valid same-origin GET route beneath the configured Vue base path after login and SHALL migrate the session identifier on successful authentication.
+Absent, malformed, unsafe, authentication, GraphQL, asset, or unrelated destinations MUST fall back to the configured Vue root.
+
+#### Scenario: Protected deep link is requested
+
+- **WHEN** an anonymous browser requests a canonical Vue object route
+- **THEN** successful login returns it to that exact safe route
+- **AND** the authenticated session identifier differs from the anonymous login session
+
+#### Scenario: Saved destination is unsafe
+
+- **WHEN** a candidate destination is cross-origin, malformed, non-GET, outside Vue routes, or an authentication, GraphQL, or asset path
+- **THEN** login returns to the Vue root
+- **AND** no untrusted redirect is followed
+
+### Requirement: Bounded authenticated Vue context
+
+The integration SHALL expose an authenticated no-store context containing only the current username, CSRF header name, CSRF parameter name, CSRF token, login path, and logout path.
+The ordinary Vue document SHALL identify no context endpoint, while the secured document SHALL bind the configured same-origin endpoint without embedding user or token values into committed assets.
+
+#### Scenario: Secured Vue bootstraps
+
+- **WHEN** an authenticated secured Vue document loads
+- **THEN** the host fetches current authentication context before its first GraphQL request
+- **AND** the stable shell presents the current username and host logout control
+
+#### Scenario: Ordinary Vue bootstraps
+
+- **WHEN** the ordinary bypass Vue application loads
+- **THEN** it makes no authentication-context request and presents no secured-session chrome
+- **AND** existing routes and GraphQL behavior remain unchanged
+
+#### Scenario: Context is returned
+
+- **WHEN** an authenticated browser requests the context endpoint
+- **THEN** the response is private and no-store and contains only the bounded documented fields
+- **AND** the token is absent from URLs, Vue Router state, local storage, GraphQL documents, variables, and semantic results
+
+### Requirement: CSRF-safe GraphQL and authentication-loss handling
+
+The secured Vue host SHALL decorate every same-origin GraphQL POST with the current context's CSRF header and token.
+A GraphQL `401` SHALL cause full-document login navigation with the current safe Vue route, while an authenticated `403` SHALL remain an authorization or CSRF failure.
+
+#### Scenario: GraphQL operation executes
+
+- **WHEN** the secured semantic component client posts a query or mutation
+- **THEN** the current CSRF header and token accompany the request
+- **AND** authorized results retain their ordinary semantic handling
+
+#### Scenario: Session expires during GraphQL
+
+- **WHEN** a GraphQL request receives `401` after authentication is lost
+- **THEN** the host performs full-document navigation to login with a safe continuation
+- **AND** obsolete route or component errors do not replace the authentication outcome
+
+#### Scenario: Authenticated request is forbidden
+
+- **WHEN** an authenticated GraphQL request receives `403`
+- **THEN** the viewer retains bounded authorization or CSRF error behavior
+- **AND** does not claim the session expired
+
+### Requirement: Host-owned Vue logout
+
+The authenticated Vue shell SHALL present a native form that posts to the configured logout endpoint with current CSRF evidence.
+The Vue action policy SHALL claim exact `causeway.security.LogoutMenu#logout` requests and submit that host form rather than invoking GraphQL.
+Successful logout MUST clear authentication, invalidate the session, apply configured cookie cleanup, and redirect to the signed-out login page.
+
+#### Scenario: User activates host logout
+
+- **WHEN** an authenticated user submits the shell logout control
+- **THEN** Spring validates CSRF, invalidates the session, and redirects to signed-out login
+- **AND** subsequent Vue and GraphQL requests require authentication
+
+#### Scenario: Framework Logout request is published
+
+- **WHEN** stale or custom markup publishes the exact framework Logout action request
+- **THEN** the Vue host claims it before GraphQL and submits the native logout contract
+- **AND** ordinary action invocation emits no successful result
+
+#### Scenario: Browser returns after logout
+
+- **WHEN** the user navigates back after successful logout
+- **THEN** protected Vue content is not restored as authenticated
+- **AND** a new login is required
+
+### Requirement: Scoped Vue security chain
+
+The integration SHALL protect only its configured Vue routes, authentication context, GraphQL endpoint, and required assets while permitting login and immutable public presentation assets.
+It MUST NOT claim unrelated Wicket, HTMX, actuator, console, application, or bearer-token paths.
+
+#### Scenario: Protected Vue data is requested anonymously
+
+- **WHEN** an anonymous request targets a Vue route, authentication context, or GraphQL
+- **THEN** the integration applies its authentication entry behavior
+- **AND** no protected domain or session context is returned
+
+#### Scenario: Unrelated path is requested
+
+- **WHEN** a request is outside configured matchers
+- **THEN** the integration neither authorizes nor rejects it
+- **AND** another application security chain may remain authoritative
+
+### Requirement: Secured Vue Petclinic acceptance runtime
+
+The Vue Petclinic sample SHALL provide a deterministic secured application variant using the same shared domain, production frontend assets, local user credentials, and profile context as secured HTMX.
+It SHALL provide integration and headless browser coverage without requiring HTMX, Wicket, bypass security, OAuth, or a live identity provider.
+
+#### Scenario: Secured Vue runtime starts
+
+- **WHEN** the secured Vue run profile starts
+- **THEN** anonymous Vue and GraphQL access is protected and login remains available without GraphQL
+- **AND** the ordinary Vue runtime remains available through its unchanged profile
+
+#### Scenario: Secured browser journey runs
+
+- **WHEN** headless automation exercises invalid and valid login, deep-link restoration, authorized reads and mutations, CSRF rejection, host logout, history, and session expiry
+- **THEN** each transition satisfies the authentication, routing, CSRF, and confidentiality contracts
+- **AND** unexpected browser, GraphQL, HTTP, persistence, or security failures fail the suite
diff --git a/openspec/specs/web-component-secman-spring-authentication-bridge/spec.md b/openspec/specs/web-component-secman-spring-authentication-bridge/spec.md
new file mode 100644
index 00000000000..c2626d86884
--- /dev/null
+++ b/openspec/specs/web-component-secman-spring-authentication-bridge/spec.md
@@ -0,0 +1,74 @@
+# web-component-secman-spring-authentication-bridge Specification
+
+## Purpose
+
+Provide an optional presentation-neutral bridge between local SecMan users, Spring Security authentication, and Causeway user context for explicitly secured web-component hosts.
+
+## Requirements
+
+### Requirement: Optional presentation-neutral SecMan Spring bridge
+
+The project SHALL provide an explicitly imported SecMan-backed Spring username/password authentication bridge for web-component hosts.
+The bridge MUST NOT install viewer routes, login pages, logout pages, request caches, security filter chains, or presentation behavior.
+
+#### Scenario: Bridge is imported
+
+- **WHEN** a host imports the shared bridge with Spring security, SecMan integration, and a SecMan password encoder available
+- **THEN** it can authenticate local SecMan credentials through a Spring DAO authentication provider
+- **AND** no HTMX or Vue dependency or route is introduced
+
+#### Scenario: Bridge is absent
+
+- **WHEN** an application does not import the shared bridge
+- **THEN** no user-details service, authentication provider, or bridge-specific principal converter is installed
+- **AND** existing bypass, bearer, pre-authentication, Wicket, or application security remains authoritative
+
+### Requirement: SecMan credentials and status determine authentication
+
+The bridge SHALL load a matching SecMan `ApplicationUser` inside a controlled anonymous interaction and SHALL authenticate its encrypted password through the configured SecMan `PasswordEncoder`.
+Absent users, wrong passwords, locked users, passwordless users, and internal lookup failures MUST fail closed without disclosing the account condition.
+
+#### Scenario: Valid unlocked user signs in
+
+- **WHEN** an unlocked local user supplies the matching password
+- **THEN** Spring authenticates the username without persisting plaintext credentials
+- **AND** the resulting principal contains the bounded SecMan profile needed by Causeway
+
+#### Scenario: Credentials are not usable
+
+- **WHEN** the user is absent, locked, passwordless, or supplies an incorrect password
+- **THEN** authentication fails with the same public invalid-credentials outcome
+- **AND** the response does not distinguish the account condition
+
+#### Scenario: User lookup fails
+
+- **WHEN** SecMan lookup or controlled interaction fails unexpectedly
+- **THEN** authentication fails closed
+- **AND** no persistence exception, user record, password, role, tenancy, locale, or stack trace reaches the browser
+
+### Requirement: Causeway principal preserves SecMan context
+
+A successful authentication SHALL produce a serializable credentials-erasing principal and SHALL convert it to a Causeway `UserMemento` containing the effective username, roles, tenancy path, language locale, number locale, and time locale captured at authentication time.
+The conversion MUST NOT require a second unscoped repository lookup.
+
+#### Scenario: Authenticated user has profile context
+
+- **WHEN** a SecMan user has roles, tenancy, and locale preferences
+- **THEN** authenticated Causeway requests receive the corresponding refined `UserMemento`
+- **AND** SecMan authorization observes the same username and effective roles
+
+#### Scenario: Authentication credentials are erased
+
+- **WHEN** Spring completes credential validation
+- **THEN** plaintext and encrypted password material are absent from the retained session principal
+- **AND** serialization and diagnostics expose no credential value
+
+### Requirement: Existing HTMX behavior remains compatible
+
+The shared bridge SHALL preserve the existing HTMX local SecMan authentication outcomes when that integration switches from its provisional local classes.
+
+#### Scenario: HTMX consumes the bridge
+
+- **WHEN** the secured HTMX integration authenticates valid, invalid, absent, locked, or passwordless users through the shared bridge
+- **THEN** login outcomes, user context, CSRF behavior, session migration, and logout behavior remain unchanged
+- **AND** HTMX-specific route and presentation policy remains outside the bridge


(causeway) 03/16: docs(webcomponents): add Petclinic run and test scripts

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 39607fd9249996b832df3dd391b0a8340e307c0a
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 3 09:19:03 2026 +0100

    docs(webcomponents): add Petclinic run and test scripts
---
 viewers/webcomponents/README.adoc                  | 14 ++--
 .../sample-htmx-petclinic/README.adoc              | 28 +++----
 viewers/webcomponents/sample-htmx-petclinic/run.sh | 31 ++++++++
 .../webcomponents/sample-htmx-petclinic/test.sh    | 47 ++++++++++++
 .../webcomponents/sample-vue-petclinic/README.adoc | 88 ++++++++++++++++++++++
 viewers/webcomponents/sample-vue-petclinic/run.sh  | 31 ++++++++
 viewers/webcomponents/sample-vue-petclinic/test.sh | 47 ++++++++++++
 7 files changed, 263 insertions(+), 23 deletions(-)

diff --git a/viewers/webcomponents/README.adoc b/viewers/webcomponents/README.adoc
index 0a4f3fa382b..0449054433d 100644
--- a/viewers/webcomponents/README.adoc
+++ b/viewers/webcomponents/README.adoc
@@ -106,17 +106,19 @@ The listing should include `META-INF/resources/causeway-webcomponents/index.mjs`
 The preferred manual test serves the page and GraphQL endpoint from the same Causeway application origin.
 This avoids cross-origin and authentication-cookie differences that can obscure component behavior.
 
-The repository provides `viewers/webcomponents/sample-htmx-petclinic` as the cohesive generic-viewer reference application and `viewers/webcomponents/sample-html` as the stable low-level vanilla-component fixture.
-The Petclinic application supplies deterministic JPA/H2 data, real rich GraphQL, four exact-logical-type HTML-authored pages, retained effective menu and grid fallbacks, canonical HTMX routes, and a side-by-side current Wicket viewer.
-Start Petclinic from the repository root with:
+The repository provides HTMX and Vue Petclinic applications over one shared deterministic JPA/H2 domain and rich GraphQL contract, plus `viewers/webcomponents/sample-html` as the stable low-level vanilla-component fixture.
+Start either Petclinic viewer from the repository root with:
 
 [source,shell]
 ----
-mvn -f viewers/webcomponents/pom.xml -Prun-sample-htmx-petclinic
+./viewers/webcomponents/sample-htmx-petclinic/run.sh
+./viewers/webcomponents/sample-vue-petclinic/run.sh
 ----
 
-Open `http://localhost:8080/htmx`, or open `/wicket/` and sign in as `sven` with password `pass` for comparison.
-See link:sample-htmx-petclinic/README.adoc[`sample-htmx-petclinic/README.adoc`] and link:htmx/README.adoc[`htmx/README.adoc`] for routes, provenance, customization, and acceptance contracts.
+Run only one application at a time because both use port 8080 by default.
+Open `http://localhost:8080/htmx` for HTMX or `http://localhost:8080/vue/` for Vue.
+The HTMX application also exposes `/wicket/`; sign in as `sven` with password `pass` for comparison.
+See link:sample-htmx-petclinic/README.adoc[`sample-htmx-petclinic/README.adoc`], link:sample-vue-petclinic/README.adoc[`sample-vue-petclinic/README.adoc`], link:htmx/README.adoc[`htmx/README.adoc`], and link:vue/README.adoc[`vue/README.adoc`] for routes, customization, and acceptance contracts.
 
 The vanilla sample supplies its own deterministic JPA/H2 data, the same rich GraphQL endpoint, packaged foundation modules, and stable low-level automation hooks.
 Start it from the repository root with:
diff --git a/viewers/webcomponents/sample-htmx-petclinic/README.adoc b/viewers/webcomponents/sample-htmx-petclinic/README.adoc
index 9bff4fd3626..ee529fea235 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/README.adoc
+++ b/viewers/webcomponents/sample-htmx-petclinic/README.adoc
@@ -17,13 +17,16 @@ Owner and service actions demonstrate canonical descriptions, parameter-prompt d
 
 == Run
 
-Use JDK 17 or later.
+Use JDK 17 or later and run this command from the repository root:
 
 [source,bash]
 ----
-mvn -f viewers/webcomponents/pom.xml -Prun-sample-htmx-petclinic
+./viewers/webcomponents/sample-htmx-petclinic/run.sh
 ----
 
+The script respects an existing `JAVA_HOME`, discovers JDK 21 through macOS `/usr/libexec/java_home` when `JAVA_HOME` is unset, and uses `MVN` instead of `mvn` when that environment variable is supplied.
+Pass Maven properties such as `-Dspring-boot.run.arguments=--server.port=9090` to the script when needed.
+
 The root URL redirects to `http://localhost:8080/htmx`.
 The Wicket comparison is available at `http://localhost:8080/wicket/`.
 Sign in to Wicket with username `sven` and password `pass`; the bypass authenticator accepts the demonstration credentials.
@@ -44,31 +47,22 @@ This secured configuration enables Spring CSRF protection and intentionally omit
 The opt-in `playwright` profile adds Playwright for Java only to the dedicated browser-test source set, so ordinary reactor builds do not resolve a browser or compile the Playwright suite.
 Playwright runs Chromium headlessly by default and does not take foreground focus.
 
-Install the profile's reactor dependencies and Chromium once:
+Run the integration, secured integration, and complete headless UI suites from the repository root:
 
 [source,bash]
 ----
-mvn -f viewers/webcomponents/pom.xml -pl htmx -am install -DskipTests
-mvn -f viewers/webcomponents/sample-htmx-petclinic/pom.xml \
-    -Pplaywright \
-    -Dexec.mainClass=com.microsoft.playwright.CLI \
-    -Dexec.classpathScope=test \
-    -Dexec.args="install chromium" \
-    test-compile exec:java
+./viewers/webcomponents/sample-htmx-petclinic/test.sh
 ----
 
-Run the complete headless UI suite from the web-components reactor:
+Install Playwright Chromium on the first run when necessary:
 
 [source,bash]
 ----
-mvn -f viewers/webcomponents/pom.xml \
-    -pl sample-htmx-petclinic -am \
-    -Pplaywright \
-    -Dtest=PetClinicHtmxPlaywrightTest \
-    -Dsurefire.failIfNoSpecifiedTests=false \
-    test
+./viewers/webcomponents/sample-htmx-petclinic/test.sh --install-browser
 ----
 
+Any arguments after `--install-browser` are passed to the Maven test invocation.
+
 Add `-Dcauseway.viewer.webcomponents.htmx.component-toolkit=native` to run the same suite through complete explicit-native reference, read-only field, editor, and ordinary-action rollback.
 Set `-Dplaywright.headless=false` only when deliberately debugging in a visible browser.
 Set `-Dplaywright.chromium.executable=/absolute/path/to/chromium` or `-Dplaywright.chromium.channel=chrome` to use an installed browser instead of Playwright's bundled Chromium.
diff --git a/viewers/webcomponents/sample-htmx-petclinic/run.sh b/viewers/webcomponents/sample-htmx-petclinic/run.sh
new file mode 100755
index 00000000000..a31b218eb28
--- /dev/null
+++ b/viewers/webcomponents/sample-htmx-petclinic/run.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
+
+if [[ -z "${JAVA_HOME:-}" && -x /usr/libexec/java_home ]]; then
+  export JAVA_HOME="$(/usr/libexec/java_home -v 21)"
+fi
+
+exec "${MVN:-mvn}" \
+  -f "$ROOT/viewers/webcomponents/pom.xml" \
+  -Prun-sample-htmx-petclinic \
+  "$@"
diff --git a/viewers/webcomponents/sample-htmx-petclinic/test.sh b/viewers/webcomponents/sample-htmx-petclinic/test.sh
new file mode 100755
index 00000000000..5d1c3a7d3ad
--- /dev/null
+++ b/viewers/webcomponents/sample-htmx-petclinic/test.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
+MVN="${MVN:-mvn}"
+POM="$ROOT/viewers/webcomponents/pom.xml"
+MODULE="sample-htmx-petclinic"
+PROFILE="playwright"
+
+if [[ -z "${JAVA_HOME:-}" && -x /usr/libexec/java_home ]]; then
+  export JAVA_HOME="$(/usr/libexec/java_home -v 21)"
+fi
+
+if [[ "${1:-}" == "--install-browser" ]]; then
+  shift
+  "$MVN" -f "$POM" -pl htmx -am install -DskipTests
+  "$MVN" -f "$ROOT/viewers/webcomponents/sample-htmx-petclinic/pom.xml" -P"$PROFILE" \
+    -DskipTests test-compile \
+    -Dexec.mainClass=com.microsoft.playwright.CLI \
+    -Dexec.classpathScope=test \
+    -Dexec.args="install chromium" \
+    exec:java
+fi
+
+exec "$MVN" -f "$POM" -pl "$MODULE" -am -P"$PROFILE" \
+  -Dtest=PetClinicHtmxApplication_IntegTest,PetClinicHtmxSecuredApplication_IntegTest,PetClinicHtmxPlaywrightTest,PetClinicHtmxSecuredPlaywrightTest \
+  -Dsurefire.failIfNoSpecifiedTests=false \
+  test \
+  "$@"
diff --git a/viewers/webcomponents/sample-vue-petclinic/README.adoc b/viewers/webcomponents/sample-vue-petclinic/README.adoc
new file mode 100644
index 00000000000..7d8f3cb55e8
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/README.adoc
@@ -0,0 +1,88 @@
+= Vue Petclinic reference application
+
+// Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.
+// See the NOTICE file distributed with this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0.
+
+This module is the executable reference application for the generic Vue web-component viewer.
+It uses the shared deterministic Petclinic domain, the Causeway GraphQL endpoint, Vue 3, Vue Router 4, and the framework-neutral `cw-` elements.
+The Vue application owns its HTML document, stable shell, router, history fallback, theme, and exact logical-type page registration.
+The semantic web components remain authoritative for metadata, values, validation, interactions, collections, previews, and results.
+
+== Run
+
+Use JDK 21 and run this command from the repository root:
+
+[source,bash]
+----
+./viewers/webcomponents/sample-vue-petclinic/run.sh
+----
+
+Open these URLs after Spring Boot starts:
+
+* `http://localhost:8080/vue/` uses the default Vaadin component policy.
+* `http://localhost:8080/vue/?toolkit=native` uses the native component policy.
+* `http://localhost:8080/vue/object/petclinic.PetOwner/s_owner-mary` opens Mary Smith through the custom Vue owner page.
+* `http://localhost:8080/vue/object/petclinic.Pet/s_pet-basil` opens Basil through the generic `<cw-object>` fallback page.
+* `http://localhost:8080/graphiql` opens GraphiQL.
+
+Pass Maven properties to the script when needed.
+For example, this uses port 9090:
+
+[source,bash]
+----
+./viewers/webcomponents/sample-vue-petclinic/run.sh \
+    -Dspring-boot.run.arguments=--server.port=9090
+----
+
+The script respects an existing `JAVA_HOME`, discovers JDK 21 through macOS `/usr/libexec/java_home` when `JAVA_HOME` is unset, and uses `MVN` instead of `mvn` when that environment variable is supplied.
+
+== Test
+
+The test script runs the Spring integration tests and the Playwright acceptance suite.
+Playwright uses headless Chromium by default and does not take foreground focus.
+
+Run the suite with:
+
+[source,bash]
+----
+./viewers/webcomponents/sample-vue-petclinic/test.sh
+----
+
+Install Playwright Chromium on the first run when necessary:
+
+[source,bash]
+----
+./viewers/webcomponents/sample-vue-petclinic/test.sh --install-browser
+----
+
+The browser suite covers the stable application shell, direct links, refresh, back and forward navigation, custom-page precedence, generic fallback, invalid and absent objects, semantic navigation and results, menus, keyboard skip navigation, and both toolkit policies.
+Set `-Dplaywright.headless=false` only when deliberately debugging in a visible browser.
+Set `-Dplaywright.chromium.executable=/absolute/path/to/chromium` or `-Dplaywright.chromium.channel=chrome` to use an installed browser.
+Any arguments after `--install-browser` are passed to the Maven test invocation.
+
+== Rebuild the Vue frontend
+
+Production Vue assets are committed under `src/main/resources/static/vue` so running the application does not require a Vite development server.
+After changing files under `frontend`, regenerate the locked production output from the repository root:
+
+[source,bash]
+----
+mvn -pl viewers/webcomponents/sample-vue-petclinic \
+    -am \
+    -Pregenerate-vue-petclinic \
+    package
+----
+
+The profile runs `npm ci` and the production Vite build.
+The frontend config treats `cw-` names as native custom elements and deduplicates Vue and Vue Router while consuming the local viewer package.
+
+== Application structure
+
+`frontend/index.html` is the application-owned HTML document.
+`frontend/src/App.vue` declares the stable GraphQL provider, menus, loading and announcement regions, default result outlet, and keyed router view.
+`frontend/src/pages/PetOwnerPage.vue` is an exact-type custom page for `petclinic.PetOwner`.
+Other domain types remain unregistered and therefore demonstrate the generic route-page fallback.
+`PetClinicVueRouteController` forwards only the intended client routes to the production `index.html` without capturing GraphQL or static assets.
+
+The generic Vue viewer package and its application integration contract are documented in `../vue/README.adoc`.
diff --git a/viewers/webcomponents/sample-vue-petclinic/run.sh b/viewers/webcomponents/sample-vue-petclinic/run.sh
new file mode 100755
index 00000000000..2042b0160db
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/run.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
+
+if [[ -z "${JAVA_HOME:-}" && -x /usr/libexec/java_home ]]; then
+  export JAVA_HOME="$(/usr/libexec/java_home -v 21)"
+fi
+
+exec "${MVN:-mvn}" \
+  -f "$ROOT/viewers/webcomponents/pom.xml" \
+  -Prun-sample-vue-petclinic \
+  "$@"
diff --git a/viewers/webcomponents/sample-vue-petclinic/test.sh b/viewers/webcomponents/sample-vue-petclinic/test.sh
new file mode 100755
index 00000000000..d943e14f72d
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/test.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
+MVN="${MVN:-mvn}"
+POM="$ROOT/viewers/webcomponents/pom.xml"
+MODULE="sample-vue-petclinic"
+PROFILE="playwright-vue"
+
+if [[ -z "${JAVA_HOME:-}" && -x /usr/libexec/java_home ]]; then
+  export JAVA_HOME="$(/usr/libexec/java_home -v 21)"
+fi
+
+if [[ "${1:-}" == "--install-browser" ]]; then
+  shift
+  "$MVN" -f "$POM" -pl "$MODULE" -am install -DskipTests
+  "$MVN" -f "$ROOT/viewers/webcomponents/sample-vue-petclinic/pom.xml" -P"$PROFILE" \
+    -DskipTests test-compile \
+    -Dexec.mainClass=com.microsoft.playwright.CLI \
+    -Dexec.classpathScope=test \
+    -Dexec.args="install chromium" \
+    exec:java
+fi
+
+exec "$MVN" -f "$POM" -pl "$MODULE" -am -P"$PROFILE" \
+  -Dtest=PetClinicVueApplicationIntegTest,PetClinicVuePlaywrightTest \
+  -Dsurefire.failIfNoSpecifiedTests=false \
+  test \
+  "$@"


(causeway) 10/16: fix(webcomponents): suppress Logout in Vue menus

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 0406fbad441c3f4ceb836a13401901d329f07a6d
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 3 14:24:13 2026 +0100

    fix(webcomponents): suppress Logout in Vue menus
---
 .../foundation/src/menubar-element.mjs             |  21 +-
 .../foundation/src/menubars-element.mjs            |  12 +
 .../foundation/test/menubar-component.test.mjs     |   5 +
 .../resources/static/vue/assets/index-3mHRS6NB.js  |   2 +
 ...index-CyDhqcW-.js.map => index-3mHRS6NB.js.map} |   2 +-
 .../resources/static/vue/assets/index-CyDhqcW-.js  |   2 -
 .../src/main/resources/static/vue/index.html       |   2 +-
 .../vue/petclinic/PetClinicVuePlaywrightTest.java  |  10 +-
 viewers/webcomponents/vue/dist/causeway-vue.js     | 491 +++++++++++----------
 viewers/webcomponents/vue/dist/causeway-vue.js.map |   2 +-
 viewers/webcomponents/vue/dist/policy.d.ts.map     |   2 +-
 viewers/webcomponents/vue/src/policy.ts            |   4 +
 .../vue/test/policy-integration.test.ts            |   5 +
 13 files changed, 304 insertions(+), 256 deletions(-)

diff --git a/viewers/webcomponents/foundation/src/menubar-element.mjs b/viewers/webcomponents/foundation/src/menubar-element.mjs
index d27d6cb321b..2fbdf16518a 100644
--- a/viewers/webcomponents/foundation/src/menubar-element.mjs
+++ b/viewers/webcomponents/foundation/src/menubar-element.mjs
@@ -53,6 +53,7 @@ export class CausewayMenubarElement extends HTMLElementBase {
     this._release = null;
     this._client = null;
     this._fetchImpl = null;
+    this._excludeAction = null;
     this.lastDiagnosticGeneration = -1;
     this._currentState = null;
     this._currentBar = null;
@@ -110,6 +111,15 @@ export class CausewayMenubarElement extends HTMLElementBase {
     this._fetchImpl = value ?? null;
   }
 
+  get excludeAction() {
+    return this._excludeAction;
+  }
+
+  set excludeAction(value) {
+    this._excludeAction = typeof value === 'function' ? value : null;
+    this.#renderCurrentReadyState();
+  }
+
   connectedCallback() {
     globalThis.document?.addEventListener?.('click', this.onOutsideClick);
     globalThis.document?.addEventListener?.(CAUSEWAY_MENUBAR_WIDGET_POLICY_EVENT, this.onPolicyChange);
@@ -208,9 +218,11 @@ export class CausewayMenubarElement extends HTMLElementBase {
 
   #renderReadyBar(renderRoot, bar, state) {
     const widgetPolicy = causewayMenubarWidgetConfiguration();
+    const instanceExcludeAction = this._excludeAction;
+    const globalExcludeAction = widgetPolicy.excludeAction;
     const projection = projectCausewayMenuBar(bar, {
       generation: state.generation,
-      excludeAction: widgetPolicy.excludeAction
+      excludeAction: detail => instanceExcludeAction?.(detail) === true || globalExcludeAction?.(detail) === true
     });
     const qualification = qualifyCausewayMenuBar({
       role: this.role,
@@ -240,7 +252,7 @@ export class CausewayMenubarElement extends HTMLElementBase {
     this.dataset.causewayMenubarResponsive = qualification.presentation === 'vaadin-overflow' ? 'narrow' : 'wide';
     this.dataset.causewayMenubarFallback = qualification.reason ?? '';
     if (!qualification.accepted) {
-      renderRoot.innerHTML = renderBar(bar, this.sequence);
+      renderRoot.innerHTML = renderBar(projection.accepted ? projection : bar, this.sequence);
       this.#restoreSemanticFocus(renderRoot);
       return;
     }
@@ -550,7 +562,8 @@ function renderAction(action, sequence, role, menuIndex, sectionIndex, actionInd
   const describedByIds = [presentation.description ? descriptionId : '', action.disabled ? reasonId : ''].filter(Boolean);
   const describedBy = describedByIds.length ? ` aria-describedby="${describedByIds.join(' ')}"` : '';
   const disabled = action.disabled ? ' disabled aria-disabled="true"' : '';
-  const tooltip = composeActionTooltip(presentation.description, action.disabled);
+  const disabledReason = action.disabledReason || action.disabled;
+  const tooltip = composeActionTooltip(presentation.description, disabledReason);
   const tooltipAttributes = tooltip
     ? ` data-tooltip="${escapeHtml(tooltip)}"${action.disabled ? ' tabindex="0"' : ''}`
     : '';
@@ -558,7 +571,7 @@ function renderAction(action, sequence, role, menuIndex, sectionIndex, actionInd
     ? `<span class="causeway-action-description causeway-visually-hidden" id="${descriptionId}">${escapeHtml(presentation.description)}</span>`
     : '';
   const reason = action.disabled
-    ? `<span class="causeway-disabled-reason causeway-visually-hidden" id="${reasonId}">${escapeHtml(action.disabled)}</span>`
+    ? `<span class="causeway-disabled-reason causeway-visually-hidden" id="${reasonId}">${escapeHtml(disabledReason)}</span>`
     : '';
   return `<div class="causeway-service-action${tooltip ? ' causeway-action-control-tooltip' : ''}" data-causeway-service-action-region${dataHint('css-hint', action.cssHint)}${dataHint('icon-hint', action.iconHint)}${tooltipAttributes}>
   <button class="causeway-service-action-control" type="button" data-causeway-service-action data-service-logical-type="${escapeHtml(action.serviceLogicalTypeName)}" data-action-id="${escapeHtml(action.actionId)}"${disabled}${describedBy}>${renderActionContent(presentation.name, presentation.icon)}</button>${description}${reason}
diff --git a/viewers/webcomponents/foundation/src/menubars-element.mjs b/viewers/webcomponents/foundation/src/menubars-element.mjs
index c18be03f29c..e617ca84ed6 100644
--- a/viewers/webcomponents/foundation/src/menubars-element.mjs
+++ b/viewers/webcomponents/foundation/src/menubars-element.mjs
@@ -40,6 +40,7 @@ export class CausewayMenubarsElement extends HTMLElementBase {
     super();
     this._client = null;
     this._fetchImpl = null;
+    this._excludeAction = null;
     this._context = null;
     this._release = null;
     this.lastDiagnosticGeneration = -1;
@@ -70,6 +71,15 @@ export class CausewayMenubarsElement extends HTMLElementBase {
     this._fetchImpl = value ?? null;
   }
 
+  get excludeAction() {
+    return this._excludeAction;
+  }
+
+  set excludeAction(value) {
+    this._excludeAction = typeof value === 'function' ? value : null;
+    for (const child of this.#directBarChildren()) child.excludeAction = this._excludeAction;
+  }
+
   get context() {
     return this._context;
   }
@@ -140,6 +150,7 @@ export class CausewayMenubarsElement extends HTMLElementBase {
 
   #provideContextToDeclarativeBars() {
     for (const child of this.#directBarChildren()) {
+      child.excludeAction = this._excludeAction;
       child.context = this._context;
     }
   }
@@ -157,6 +168,7 @@ export class CausewayMenubarsElement extends HTMLElementBase {
       }
       if (bar) {
         bar.setAttribute('data-causeway-bar-order', String(index));
+        bar.excludeAction = this._excludeAction;
         bar.context = this._context;
         if (!present && bar.getAttribute('data-causeway-generated') === 'menubar') {
           this.removeChild(bar);
diff --git a/viewers/webcomponents/foundation/test/menubar-component.test.mjs b/viewers/webcomponents/foundation/test/menubar-component.test.mjs
index 13564652bf7..5a028251e7d 100644
--- a/viewers/webcomponents/foundation/test/menubar-component.test.mjs
+++ b/viewers/webcomponents/foundation/test/menubar-component.test.mjs
@@ -193,6 +193,9 @@ test('menu action selection, outside activation, Escape, and sibling opening clo
 test('composite preserves declarative bars, generates only missing effective roles, and shares one generation', async () => {
   const executor = createMenuGraphQLExecutor();
   const composite = new CausewayMenubarsElement();
+  const excludeWelcome = detail => detail.serviceLogicalTypeName === 'causeway.webcomponents.sample.SampleMenu'
+    && detail.actionId === 'welcomeMessage';
+  composite.excludeAction = excludeWelcome;
   composite.client = new CausewayGraphQLClient({executor});
   let fetchCount = 0;
   composite.fetchImpl = async () => {
@@ -213,6 +216,8 @@ test('composite preserves declarative bars, generates only missing effective rol
   assert.equal(bars.length, 3);
   assert.equal(bars.filter(child => child.localName === CausewayElementName.MENUBAR_PRIMARY).length, 1);
   assert.equal(bars.find(child => child.localName === CausewayElementName.MENUBAR_PRIMARY), declarativePrimary);
+  assert.ok(bars.every(child => child.excludeAction === excludeWelcome));
+  assert.doesNotMatch(bars.map(renderMarkup).join(''), /welcomeMessage|Welcome Message/);
   assert.equal(fetchCount, 1);
   assert.equal(executor.applicationCalls.length, 1);
   assert.equal(executor.serviceCalls.filter(call => call.operationName === 'CausewayReadServiceActionStates').length, 1);
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-3mHRS6NB.js b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-3mHRS6NB.js
new file mode 100644
index 00000000000..680d0754762
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-3mHRS6NB.js
@@ -0,0 +1,2 @@
+(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))i(s);new MutationObserver(s=>{for(const r of s)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function n(s){const r={};return s.integrity&&(r.integrity=s.integrity),s.referrerPolicy&&(r.referrerPolicy=s.referr [...]
+//# sourceMappingURL=index-3mHRS6NB.js.map
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CyDhqcW-.js.map b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-3mHRS6NB.js.map
similarity index 73%
rename from viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CyDhqcW-.js.map
rename to viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-3mHRS6NB.js.map
index c993c0378c5..f71c9fd5065 100644
--- a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CyDhqcW-.js.map
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-3mHRS6NB.js.map
@@ -1 +1 @@
-{"version":3,"file":"index-CyDhqcW-.js","sources":["../../../../../../frontend/node_modules/@vue/shared/dist/shared.esm-bundler.js","../../../../../../frontend/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js","../../../../../../frontend/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js","../../../../../../frontend/node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js","../../../../../../frontend/node_modules/vue-router/dist/devtools-EWN81iOl.mjs","../../ [...]
\ No newline at end of file
+{"version":3,"file":"index-3mHRS6NB.js","sources":["../../../../../../frontend/node_modules/@vue/shared/dist/shared.esm-bundler.js","../../../../../../frontend/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js","../../../../../../frontend/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js","../../../../../../frontend/node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js","../../../../../../frontend/node_modules/vue-router/dist/devtools-EWN81iOl.mjs","../../ [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CyDhqcW-.js b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CyDhqcW-.js
deleted file mode 100644
index b98593a707a..00000000000
--- a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CyDhqcW-.js
+++ /dev/null
@@ -1,2 +0,0 @@
-(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const r of i)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&s(o)}).observe(document,{childList:!0,subtree:!0});function n(i){const r={};return i.integrity&&(r.integrity=i.integrity),i.referrerPolicy&&(r.referrerPolicy=i.referr [...]
-//# sourceMappingURL=index-CyDhqcW-.js.map
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html
index f4e98dca52c..fd1d7ab30a2 100644
--- a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html
@@ -26,7 +26,7 @@
   <link rel="stylesheet" href="/causeway-webcomponents/component-styles.css">
   <link rel="stylesheet" href="/causeway-webcomponents/theme.css">
   <link rel="stylesheet" href="/webjars/font-awesome/7.3.0/css/all.min.css">
-  <script type="module" crossorigin src="/vue/assets/index-CyDhqcW-.js"></script>
+  <script type="module" crossorigin src="/vue/assets/index-3mHRS6NB.js"></script>
   <link rel="stylesheet" crossorigin href="/vue/assets/index-CqcjgOE7.css">
 </head>
 <body>
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVuePlaywrightTest.java b/viewers/webcomponents/sample-vue-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVuePlaywrightTest.java
index d9b54415e30..be6d26e6348 100644
--- a/viewers/webcomponents/sample-vue-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVuePlaywrightTest.java
+++ b/viewers/webcomponents/sample-vue-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVuePlaywrightTest.java
@@ -270,7 +270,15 @@ void frameworkLogoutFailsClosedWhileApplicationActionsAndLocalResourcesRemainAva
         page.waitForFunction("() => document.querySelector('cw-object cw-action#openLocalResource')?.componentState?.status === 'ready'");
         page.locator("cw-menubars[data-menu-state='ready']").waitFor();
 
-        assertThat(page.locator("cw-menubars [data-service-logical-type='causeway.security.LogoutMenu'][data-action-id='logout']").count()).isZero();
+        final var systemMenu = page.locator("vaadin-menu-bar-button")
+                .filter(new com.microsoft.playwright.Locator.FilterOptions().setHasText("System")).first();
+        systemMenu.waitFor();
+        systemMenu.click();
+        final var systemOverlay = page.locator("vaadin-menu-bar-overlay[opened]");
+        systemOverlay.waitFor();
+        assertThat(systemOverlay.innerText()).doesNotContain("Logout");
+        page.keyboard().press("Escape");
+
         final var logoutInvocationsBefore = graphQLRequests.stream()
                 .filter(body -> body.contains("CausewayInvokeServiceAction") && body.contains("LogoutMenu"))
                 .count();
diff --git a/viewers/webcomponents/vue/dist/causeway-vue.js b/viewers/webcomponents/vue/dist/causeway-vue.js
index f8ec5ba221b..f1e137182f7 100644
--- a/viewers/webcomponents/vue/dist/causeway-vue.js
+++ b/viewers/webcomponents/vue/dist/causeway-vue.js
@@ -1,34 +1,34 @@
-import { inject as le, onMounted as W, onBeforeUnmount as ee, defineComponent as C, ref as L, openBlock as N, createElementBlock as I, createElementVNode as A, toDisplayString as de, computed as P, h as S, defineAsyncComponent as pe, watch as fe, createBlock as ge, resolveDynamicComponent as he, nextTick as ye } from "vue";
+import { inject as le, onMounted as W, onBeforeUnmount as te, defineComponent as O, ref as L, openBlock as N, createElementBlock as I, createElementVNode as C, toDisplayString as de, computed as P, h as S, defineAsyncComponent as pe, watch as fe, createBlock as ge, resolveDynamicComponent as he, nextTick as ye } from "vue";
 import { useRoute as ve } from "vue-router";
 const k = "cw-object-context[data-causeway-route-context]", we = "cw-interaction-controller[data-causeway-route-interactions]";
 function me(e, t) {
-  const a = [...e.querySelectorAll(k)].filter((r) => !r.parentElement?.closest(k));
+  const a = [...e.querySelectorAll(k)].filter((i) => !i.parentElement?.closest(k));
   if (a.length === 0) return Object.freeze({ valid: !1, classification: "missing-context" });
   if (a.length !== 1) return Object.freeze({ valid: !1, classification: "duplicate-context" });
   const n = a[0];
-  return n.getAttribute("logical-type") !== t.logicalTypeName || n.getAttribute("object-id") !== t.objectId ? Object.freeze({ valid: !1, classification: "identity" }) : [...n.querySelectorAll(we)].filter((r) => r.closest(k) === n).length !== 1 ? Object.freeze({ valid: !1, classification: "interactions" }) : Object.freeze({ valid: !0, context: n });
+  return n.getAttribute("logical-type") !== t.logicalTypeName || n.getAttribute("object-id") !== t.objectId ? Object.freeze({ valid: !1, classification: "identity" }) : [...n.querySelectorAll(we)].filter((i) => i.closest(k) === n).length !== 1 ? Object.freeze({ valid: !1, classification: "interactions" }) : Object.freeze({ valid: !0, context: n });
 }
-function j(e, t) {
+function x(e, t) {
   const a = e.querySelectorAll(t);
   return a.length === 1 ? a[0] : null;
 }
-function te(e) {
-  const t = j(e, "cw-graphql-client[data-causeway-shell-client]"), a = j(e, "[data-causeway-router-view]"), n = j(e, "[data-causeway-route-loading]"), o = j(e, "[data-causeway-route-announcement]"), r = j(e, "cw-action-results[data-causeway-shell-result]");
-  if (!t || !a || !n || !o || !r || !t.contains(a) || !t.contains(r))
+function ae(e) {
+  const t = x(e, "cw-graphql-client[data-causeway-shell-client]"), a = x(e, "[data-causeway-router-view]"), n = x(e, "[data-causeway-route-loading]"), r = x(e, "[data-causeway-route-announcement]"), i = x(e, "cw-action-results[data-causeway-shell-result]");
+  if (!t || !a || !n || !r || !i || !t.contains(a) || !t.contains(i))
     throw new Error("The authored Vue application shell is invalid.");
-  return Object.freeze({ shell: e, client: t, route: a, loading: n, announcement: o, result: r });
+  return Object.freeze({ shell: e, client: t, route: a, loading: n, announcement: r, result: i });
 }
-const ae = /* @__PURE__ */ Symbol("causeway-vue-viewer"), D = Object.freeze({
+const ne = /* @__PURE__ */ Symbol("causeway-vue-viewer"), M = Object.freeze({
   serviceLogicalTypeName: "causeway.security.LogoutMenu",
   actionId: "logout"
 });
-function ne(e) {
-  return e?.serviceLogicalTypeName === D.serviceLogicalTypeName && e?.actionId === D.actionId;
+function G(e) {
+  return e?.serviceLogicalTypeName === M.serviceLogicalTypeName && e?.actionId === M.actionId;
 }
-function M(e) {
+function F(e) {
   let t = 0;
   for (const a of e.querySelectorAll("[data-causeway-service-action]"))
-    ne({
+    G({
       serviceLogicalTypeName: a.getAttribute("data-service-logical-type") ?? void 0,
       actionId: a.getAttribute("data-action-id") ?? ""
     }) && ((a.closest("[data-causeway-service-action-region]") ?? a).remove(), t += 1);
@@ -41,15 +41,15 @@ class be extends Error {
   }
 }
 function Ee(e, t = {}) {
-  const a = t.location ?? window.location, n = typeof e?.path == "string" ? e.path.trim() : "", o = e?.openUrlStrategy;
-  if (!n || !["SAME_WINDOW", "NEW_WINDOW"].includes(o ?? ""))
+  const a = t.location ?? window.location, n = typeof e?.path == "string" ? e.path.trim() : "", r = e?.openUrlStrategy;
+  if (!n || !["SAME_WINDOW", "NEW_WINDOW"].includes(r ?? ""))
     throw R("LOCAL_RESOURCE_INVALID", "The local-resource result is incomplete.");
   if (/^[a-z][a-z\d+.-]*:/i.test(n) || n.startsWith("//") || n.includes("\\") || /[\u0000-\u001f\u007f]/.test(n))
     throw R("LOCAL_RESOURCE_TARGET_UNSAFE", "The local-resource target is not an application-local path.");
-  const r = new URL(a.href), u = _e(t.applicationBase ?? "/", r).pathname.replace(/\/+$/, "") || "/", l = n.startsWith("/") ? n : `/${n}`, h = u !== "/" && (l === u || l.startsWith(`${u}/`) || l.startsWith(`${u}?`) || l.startsWith(`${u}#`)) ? l : `${u === "/" ? "" : u}/${l.replace(/^\/+/, "")}`, v = new URL(h, r.origin);
-  if (v.origin !== r.origin || v.username || v.password || u !== "/" && v.pathname !== u && !v.pathname.startsWith(`${u}/`))
+  const i = new URL(a.href), s = _e(t.applicationBase ?? "/", i).pathname.replace(/\/+$/, "") || "/", u = n.startsWith("/") ? n : `/${n}`, h = s !== "/" && (u === s || u.startsWith(`${s}/`) || u.startsWith(`${s}?`) || u.startsWith(`${s}#`)) ? u : `${s === "/" ? "" : s}/${u.replace(/^\/+/, "")}`, y = new URL(h, i.origin);
+  if (y.origin !== i.origin || y.username || y.password || s !== "/" && y.pathname !== s && !y.pathname.startsWith(`${s}/`))
     throw R("LOCAL_RESOURCE_TARGET_UNSAFE", "The local-resource target escapes the configured application boundary.");
-  return Object.freeze({ url: v, openUrlStrategy: o });
+  return Object.freeze({ url: y, openUrlStrategy: r });
 }
 function Te(e, t = {}) {
   const a = t.location ?? window.location, n = Ee(e, { location: a, applicationBase: t.applicationBase });
@@ -57,8 +57,8 @@ function Te(e, t = {}) {
     a.assign(n.url.href);
     return;
   }
-  const r = (t.open ?? window.open.bind(window))(n.url.href, "_blank", "noopener,noreferrer");
-  r && (r.opener = null);
+  const i = (t.open ?? window.open.bind(window))(n.url.href, "_blank", "noopener,noreferrer");
+  i && (i.opener = null);
 }
 function _e(e, t) {
   const a = String(e).trim() || "/";
@@ -72,11 +72,11 @@ function _e(e, t) {
 function R(e, t) {
   return new be(e, t);
 }
-const F = 4096, oe = 4096, Se = "The requested application route is invalid.";
-function b() {
-  return new Error(Se);
+const K = 4096, oe = 4096, Ae = "The requested application route is invalid.";
+function T() {
+  return new Error(Ae);
 }
-function Ae(e) {
+function Se(e) {
   for (let t = 0; t < e.length; t += 1) {
     const a = e.charCodeAt(t);
     if (a >= 55296 && a <= 56319) {
@@ -88,28 +88,28 @@ function Ae(e) {
   }
   return !1;
 }
-function q(e) {
+function V(e) {
   const t = String(e ?? "");
-  if (!t || t.length > F || t === "." || t === ".." || /[\\/\u0000-\u001f\u007f-\u009f]/u.test(t) || Ae(t) || new TextEncoder().encode(t).length > F) throw b();
+  if (!t || t.length > K || t === "." || t === ".." || /[\\/\u0000-\u001f\u007f-\u009f]/u.test(t) || Se(t) || new TextEncoder().encode(t).length > K) throw T();
   let a;
   try {
     a = encodeURIComponent(t).replace(/[!'()*]/g, (n) => `%${n.charCodeAt(0).toString(16).toUpperCase()}`);
   } catch {
-    throw b();
+    throw T();
   }
-  if (a.length > oe) throw b();
+  if (a.length > oe) throw T();
   return a;
 }
-function K(e) {
+function Y(e) {
   if (!e || e.length > oe || !/^(?:[^%]|%[0-9A-F]{2})+$/u.test(e))
-    throw b();
+    throw T();
   let t;
   try {
     t = decodeURIComponent(e);
   } catch {
-    throw b();
+    throw T();
   }
-  if (q(t) !== e) throw b();
+  if (V(t) !== e) throw T();
   return t;
 }
 function U(e) {
@@ -120,27 +120,27 @@ function U(e) {
   return t;
 }
 function Ce(e, t) {
-  const a = U(e), n = `/object/${q(t?.logicalTypeName)}/${q(t?.id ?? t?.objectId)}`;
+  const a = U(e), n = `/object/${V(t?.logicalTypeName)}/${V(t?.id ?? t?.objectId)}`;
   return a === "/" ? n : `${a}${n}`;
 }
-function B(e) {
+function z(e) {
   return Ce("/", e);
 }
 function re(e, t = "/") {
-  const a = String(e ?? ""), n = U(t), o = n === "/" ? "/object/" : `${n}/object/`;
-  if (!a.startsWith(o)) throw b();
-  const p = a.slice(o.length).split("/");
-  if (p.length !== 2) throw b();
+  const a = String(e ?? ""), n = U(t), r = n === "/" ? "/object/" : `${n}/object/`;
+  if (!a.startsWith(r)) throw T();
+  const d = a.slice(r.length).split("/");
+  if (d.length !== 2) throw T();
   return Object.freeze({
-    logicalTypeName: K(p[0]),
-    objectId: K(p[1])
+    logicalTypeName: Y(d[0]),
+    objectId: Y(d[1])
   });
 }
 function Oe(e) {
-  return B(e);
+  return z(e);
 }
-const Y = "causeway-navigation-request", G = "causeway-action-request", X = "causeway-action-result", Z = "causeway-object-context-state-change", H = "causeway-menu-bars-state-change";
-function V() {
+const X = "causeway-navigation-request", q = "causeway-action-request", Z = "causeway-action-result", H = "causeway-object-context-state-change", Q = "causeway-menu-bars-state-change";
+function $() {
   let e = !1;
   return {
     get claimed() {
@@ -151,7 +151,7 @@ function V() {
     }
   };
 }
-function _(e) {
+function A(e) {
   return Object.freeze({
     router: e.router,
     basePath: e.basePath,
@@ -159,122 +159,123 @@ function _(e) {
     routeGeneration: e.state.routeGeneration
   });
 }
-function je(e) {
+function xe(e) {
   if (e?.kind !== "object" || !e.value || typeof e.value != "object") return null;
   const t = e.value._meta, a = t?.logicalTypeName, n = t?.id;
   return typeof a != "string" || typeof n != "string" || !a || !n ? null : Object.freeze({ logicalTypeName: a, id: n, title: String(t?.title ?? n) });
 }
-function xe(e) {
+function je(e) {
   return [...e.querySelector("[data-causeway-router-view]")?.querySelectorAll("cw-action-results[data-causeway-page-result]") ?? []].filter((a) => a.isConnected);
 }
 function Le(e) {
   const t = e.querySelectorAll("cw-action-results[data-causeway-shell-result]");
   return t.length === 1 ? t[0] : null;
 }
-function $(e) {
-  const t = xe(e);
+function B(e) {
+  const t = je(e);
   if (t.length > 1) throw new Error("The active Vue route has duplicate action-result outlets.");
   if (t.length === 1) return t[0];
   const a = Le(e);
   if (!a?.isConnected) throw new Error("The authored Vue shell result outlet is unavailable.");
   return a;
 }
-function Q(e, ...t) {
+function J(e, ...t) {
   const a = e;
   a.replacePresentation ? a.replacePresentation(...t) : e.replaceChildren(...t), e.hidden = t.length === 0;
 }
 function ie(e, t) {
   const a = t.result, n = document.createElement("h2");
   if (n.textContent = t.actionId ? `${t.actionId} result` : "Action result", a?.kind === "collection") {
-    const r = document.createElement("cw-standalone-collection");
-    r.setAttribute("data-testid", "causeway-standalone-action-result"), r.setAttribute("named", String(t.resultPresentation?.named ?? n.textContent)), r.result = a, Q(e, r);
+    const i = document.createElement("cw-standalone-collection");
+    i.setAttribute("data-testid", "causeway-standalone-action-result"), i.setAttribute("named", String(t.resultPresentation?.named ?? n.textContent)), i.result = a, J(e, i);
     return;
   }
-  const o = document.createElement("output");
-  o.textContent = a?.kind === "scalar" ? String(a.value ?? "") : a?.kind === "void" ? "Completed" : "This result cannot be presented.", Q(e, n, o);
+  const r = document.createElement("output");
+  r.textContent = a?.kind === "scalar" ? String(a.value ?? "") : a?.kind === "void" ? "Completed" : "This result cannot be presented.", J(e, n, r);
 }
 async function Ne(e, t) {
-  const a = e.state.shell, o = a?.querySelector("[data-causeway-router-view]")?.querySelector("cw-object-context[data-causeway-route-context]"), r = o?.context;
-  if (!o || !r?.refresh) return;
-  const p = e.state.routeGeneration, u = (m) => {
-    if (p !== e.state.routeGeneration) return l();
-    const h = m.detail?.state;
+  const a = e.state.shell, r = a?.querySelector("[data-causeway-router-view]")?.querySelector("cw-object-context[data-causeway-route-context]"), i = r?.context;
+  if (!r || !i?.refresh) return;
+  const d = e.state.routeGeneration, s = (w) => {
+    if (d !== e.state.routeGeneration) return u();
+    const h = w.detail?.state;
     if (h?.status !== "terminal-error") {
-      (h?.status === "ready" || h?.status === "partial-error") && l();
+      (h?.status === "ready" || h?.status === "partial-error") && u();
       return;
     }
-    l(), (h.errors?.[0]?.extensions?.classification ?? h.errors?.[0]?.extensions?.code ?? h.error?.code) === "NOT_FOUND" && e.router.replace("/");
-  }, l = () => o.removeEventListener(Z, u);
-  o.addEventListener(Z, u), r.refresh(), ie($(a), t);
+    u(), (h.errors?.[0]?.extensions?.classification ?? h.errors?.[0]?.extensions?.code ?? h.error?.code) === "NOT_FOUND" && e.router.replace("/");
+  }, u = () => r.removeEventListener(H, s);
+  r.addEventListener(H, s), i.refresh(), ie(B(a), t);
 }
-async function J(e, t) {
-  const a = V();
-  await e.policies.navigate?.(t, a, _(e)) === !0 && a.claim(), a.claimed || await e.router.push(B(t));
+async function ee(e, t) {
+  const a = $();
+  await e.policies.navigate?.(t, a, A(e)) === !0 && a.claim(), a.claimed || await e.router.push(z(t));
 }
 function ce(e, t) {
-  const a = /* @__PURE__ */ new WeakMap(), n = /* @__PURE__ */ new WeakSet();
-  let o = null, r = !0;
-  const p = () => {
-    const f = t.querySelector("[data-causeway-route-announcement]");
-    f && (f.textContent = "Logout requires a host authentication integration."), t.dataset.causewayLogoutUnavailable = "true";
-  }, u = (f, d) => {
-    if (!r || d !== e.state.routeGeneration || !(f.target instanceof EventTarget)) return;
-    const i = new CustomEvent(G, {
+  const a = /* @__PURE__ */ new WeakMap(), n = /* @__PURE__ */ new WeakSet(), r = t.querySelector("cw-menubars"), i = (l) => G(l);
+  r && (r.excludeAction = i);
+  let d = null, s = !0;
+  const u = () => {
+    const l = t.querySelector("[data-causeway-route-announcement]");
+    l && (l.textContent = "Logout requires a host authentication integration."), t.dataset.causewayLogoutUnavailable = "true";
+  }, w = (l, p) => {
+    if (!s || p !== e.state.routeGeneration || !(l.target instanceof EventTarget)) return;
+    const o = new CustomEvent(q, {
       bubbles: !0,
       composed: !0,
       cancelable: !0,
-      detail: f.detail
+      detail: l.detail
     });
-    n.add(i), f.target.dispatchEvent(i);
-  }, l = (f) => {
-    const d = f, i = d.detail;
+    n.add(o), l.target.dispatchEvent(o);
+  }, h = (l) => {
+    const p = l, o = p.detail;
     try {
-      const g = $(t);
-      i?.context && typeof i.context == "object" ? a.set(i.context, g) : o = g;
-    } catch (g) {
-      e.policies.error?.(g, _(e));
+      const f = B(t);
+      o?.context && typeof o.context == "object" ? a.set(o.context, f) : d = f;
+    } catch (f) {
+      e.policies.error?.(f, A(e));
     }
-    if (n.has(d)) return;
-    const y = ne(i), s = e.policies.action;
-    if (!s) {
-      y && (d.preventDefault(), p());
+    if (n.has(p)) return;
+    const c = G(o), v = e.policies.action;
+    if (!v) {
+      c && (p.preventDefault(), u());
       return;
     }
-    const c = V(), w = e.state.routeGeneration;
+    const g = $(), _ = e.state.routeGeneration;
     try {
-      const g = s(i, c, _(e));
-      if (g && typeof g.then == "function") {
-        d.preventDefault(), Promise.resolve(g).then((E) => {
-          E === !0 && c.claim(), !c.claimed && !y ? u(d, w) : c.claimed || p();
-        }).catch((E) => e.policies.error?.(E, _(e)));
+      const f = v(o, g, A(e));
+      if (f && typeof f.then == "function") {
+        p.preventDefault(), Promise.resolve(f).then((E) => {
+          E === !0 && g.claim(), !g.claimed && !c ? w(p, _) : g.claimed || u();
+        }).catch((E) => e.policies.error?.(E, A(e)));
         return;
       }
-      g === !0 && c.claim(), (c.claimed || y) && d.preventDefault(), y && !c.claimed && p();
-    } catch (g) {
-      d.preventDefault(), e.policies.error?.(g, _(e));
+      f === !0 && g.claim(), (g.claimed || c) && p.preventDefault(), c && !g.claimed && u();
+    } catch (f) {
+      p.preventDefault(), e.policies.error?.(f, A(e));
     }
-  }, m = (f) => {
-    const d = f, i = d.detail?.target;
-    !i?.logicalTypeName || !(i.id ?? i.objectId) || (d.preventDefault(), J(e, i).catch((y) => e.policies.error?.(y, _(e))));
-  }, h = (f) => {
-    const d = f, i = d.detail ?? {};
+  }, y = (l) => {
+    const p = l, o = p.detail?.target;
+    !o?.logicalTypeName || !(o.id ?? o.objectId) || (p.preventDefault(), ee(e, o).catch((c) => e.policies.error?.(c, A(e))));
+  }, m = (l) => {
+    const p = l, o = p.detail ?? {};
     (async () => {
-      const y = V();
-      if (await e.policies.result?.(i, y, _(e)) === !0 && y.claim(), y.claimed) return;
-      if (i.result?.kind === "local-resource") {
-        Te(i.result.value, { applicationBase: e.applicationResourceBase }), d.target?.dismissResult?.();
+      const c = $();
+      if (await e.policies.result?.(o, c, A(e)) === !0 && c.claim(), c.claimed) return;
+      if (o.result?.kind === "local-resource") {
+        Te(o.result.value, { applicationBase: e.applicationResourceBase }), p.target?.dismissResult?.();
         return;
       }
-      const c = je(i.result);
-      if (c) return J(e, c);
-      const w = i.context && a.get(i.context) || o || $(t);
-      i.result?.kind === "void" ? await Ne(e, i) : ie(w, i), d.target?.dismissResult?.();
-    })().catch((y) => e.policies.error?.(y, _(e)));
-  }, v = () => queueMicrotask(() => {
-    r && M(t);
+      const g = xe(o.result);
+      if (g) return ee(e, g);
+      const _ = o.context && a.get(o.context) || d || B(t);
+      o.result?.kind === "void" ? await Ne(e, o) : ie(_, o), p.target?.dismissResult?.();
+    })().catch((c) => e.policies.error?.(c, A(e)));
+  }, b = () => queueMicrotask(() => {
+    s && F(t);
   });
-  return M(t), t.addEventListener(G, l, { capture: !0 }), t.addEventListener(Y, m), t.addEventListener(X, h), t.addEventListener(H, v), () => {
-    r = !1, t.removeEventListener(G, l, { capture: !0 }), t.removeEventListener(Y, m), t.removeEventListener(X, h), t.removeEventListener(H, v);
+  return F(t), t.addEventListener(q, h, { capture: !0 }), t.addEventListener(X, y), t.addEventListener(Z, m), t.addEventListener(Q, b), () => {
+    s = !1, r?.excludeAction === i && (r.excludeAction = void 0), t.removeEventListener(q, h, { capture: !0 }), t.removeEventListener(X, y), t.removeEventListener(Z, m), t.removeEventListener(Q, b);
   };
 }
 const Re = /^[A-Za-z_][A-Za-z0-9_$-]*(?:\.[A-Za-z_][A-Za-z0-9_$-]*)*$/u;
@@ -284,12 +285,12 @@ function Pe(e) {
 function Ie(e) {
   const t = /* @__PURE__ */ new Map();
   for (const [a, n] of Pe(e)) {
-    const o = String(a ?? "").trim();
-    if (!Re.test(o)) throw new Error("A Vue page registration has an invalid logical type.");
+    const r = String(a ?? "").trim();
+    if (!Re.test(r)) throw new Error("A Vue page registration has an invalid logical type.");
     if (!n || typeof n != "object" && typeof n != "function")
-      throw new Error(`The Vue page registration for ${o} is unsupported.`);
-    if (t.has(o)) throw new Error(`The Vue page registration for ${o} is duplicated.`);
-    t.set(o, n);
+      throw new Error(`The Vue page registration for ${r} is unsupported.`);
+    if (t.has(r)) throw new Error(`The Vue page registration for ${r} is duplicated.`);
+    t.set(r, n);
   }
   return t;
 }
@@ -304,8 +305,8 @@ function Ye(e) {
   let n;
   return n = Object.freeze({
     plugin: {
-      install(r) {
-        r.provide(ae, n);
+      install(i) {
+        i.provide(ne, n);
       }
     },
     router: e.router,
@@ -318,24 +319,24 @@ function Ye(e) {
     state: a
   }), n;
 }
-function z() {
-  const e = le(ae);
+function D() {
+  const e = le(ne);
   if (!e) throw new Error("The Causeway Vue viewer plugin is not installed.");
   return e;
 }
 function Xe(e) {
-  const t = z();
+  const t = D();
   let a = null;
   return W(() => {
     const n = e.value;
     if (!n) throw new Error("The authored Vue application shell is missing.");
-    te(n), t.state.shell = n, a = ce(t, n);
-  }), ee(() => {
+    ae(n), t.state.shell = n, a = ce(t, n);
+  }), te(() => {
     a?.(), a = null, t.state.shell === e.value && (t.state.shell = null);
   }), e;
 }
 function Ze(e, t) {
-  const a = te(t);
+  const a = ae(t);
   e.state.shell = t;
   const n = ce(e, t);
   return {
@@ -345,67 +346,67 @@ function Ze(e, t) {
     }
   };
 }
-const ke = ["data-route-state"], Ge = /* @__PURE__ */ C({
+const ke = ["data-route-state"], qe = /* @__PURE__ */ O({
   __name: "HomePage",
   setup(e) {
-    const t = z(), a = L(null), n = L("loading"), o = L("Loading the application home page…");
-    function r(p) {
-      let u = null;
-      return p.dispatchEvent(new CustomEvent("causeway-graphql-client-request", {
+    const t = D(), a = L(null), n = L("loading"), r = L("Loading the application home page…");
+    function i(d) {
+      let s = null;
+      return d.dispatchEvent(new CustomEvent("causeway-graphql-client-request", {
         bubbles: !0,
         composed: !0,
-        detail: { provide(l) {
-          u ??= l;
+        detail: { provide(u) {
+          s ??= u;
         } }
-      })), u;
+      })), s;
     }
     return W(async () => {
-      const p = ++t.state.routeGeneration, u = a.value ? r(a.value) : null;
-      if (!u) {
-        n.value = "unsupported", o.value = "Choose an application action to begin.";
+      const d = ++t.state.routeGeneration, s = a.value ? i(a.value) : null;
+      if (!s) {
+        n.value = "unsupported", r.value = "Choose an application action to begin.";
         return;
       }
       try {
-        const l = await u.describeApplicationEntry();
-        if (p !== t.state.routeGeneration) return;
-        if (!l?.supported) {
-          n.value = "unsupported", o.value = "Choose an application action to begin.";
+        const u = await s.describeApplicationEntry();
+        if (d !== t.state.routeGeneration) return;
+        if (!u?.supported) {
+          n.value = "unsupported", r.value = "Choose an application action to begin.";
           return;
         }
-        const m = await u.readApplicationEntry({ description: l });
-        if (p !== t.state.routeGeneration) return;
-        const h = m?.data?.home, v = h?.object?._meta, f = v?.logicalTypeName ?? h?.logicalTypeName, d = v?.id, i = { claimed: !1, claim() {
+        const w = await s.readApplicationEntry({ description: u });
+        if (d !== t.state.routeGeneration) return;
+        const h = w?.data?.home, y = h?.object?._meta, m = y?.logicalTypeName ?? h?.logicalTypeName, b = y?.id, l = { claimed: !1, claim() {
           return this.claimed ? !1 : (this.claimed = !0, !0);
-        } }, y = { router: t.router, basePath: t.basePath, shell: t.state.shell, routeGeneration: p };
-        if (await t.policies.home?.(m?.data, i, y) === !0 && i.claim(), p !== t.state.routeGeneration || i.claimed) return;
-        if (h?.kind === "OBJECT" && f && d) {
-          await t.router.replace(B({ logicalTypeName: f, id: d }));
+        } }, p = { router: t.router, basePath: t.basePath, shell: t.state.shell, routeGeneration: d };
+        if (await t.policies.home?.(w?.data, l, p) === !0 && l.claim(), d !== t.state.routeGeneration || l.claimed) return;
+        if (h?.kind === "OBJECT" && m && b) {
+          await t.router.replace(z({ logicalTypeName: m, id: b }));
           return;
         }
-        n.value = m?.errors?.length ? "partial-error" : "ready", o.value = "Choose an application action to begin.";
-      } catch (l) {
-        if (p !== t.state.routeGeneration) return;
-        t.policies.error?.(l, { router: t.router, basePath: t.basePath, shell: t.state.shell, routeGeneration: p }), n.value = "partial-error", o.value = "The home page is unavailable; application menus remain available.";
+        n.value = w?.errors?.length ? "partial-error" : "ready", r.value = "Choose an application action to begin.";
+      } catch (u) {
+        if (d !== t.state.routeGeneration) return;
+        t.policies.error?.(u, { router: t.router, basePath: t.basePath, shell: t.state.shell, routeGeneration: d }), n.value = "partial-error", r.value = "The home page is unavailable; application menus remain available.";
       }
-    }), (p, u) => (N(), I("section", {
+    }), (d, s) => (N(), I("section", {
       ref_key: "page",
       ref: a,
       class: "causeway-vue-route-page causeway-vue-status",
       "data-route-state": n.value,
       tabindex: "-1"
     }, [
-      u[0] || (u[0] = A("h1", null, "Welcome", -1)),
-      A("p", null, de(o.value), 1)
+      s[0] || (s[0] = C("h1", null, "Welcome", -1)),
+      C("p", null, de(r.value), 1)
     ], 8, ke));
   }
-}), qe = {
+}), Ge = {
   class: "causeway-vue-route-page causeway-vue-route-object",
   "data-causeway-route-page": "",
   "data-page-kind": "generic",
   "data-route-state": "loading",
   tabindex: "-1",
   "aria-label": "Object page"
-}, Ve = ["logical-type", "object-id"], $e = /* @__PURE__ */ C({
+}, Ve = ["logical-type", "object-id"], $e = /* @__PURE__ */ O({
   __name: "GenericObjectPage",
   props: {
     logicalTypeName: {},
@@ -413,48 +414,48 @@ const ke = ["data-route-state"], Ge = /* @__PURE__ */ C({
     routeKey: {}
   },
   setup(e) {
-    return (t, a) => (N(), I("section", qe, [
-      A("cw-object-context", {
+    return (t, a) => (N(), I("section", Ge, [
+      C("cw-object-context", {
         "data-causeway-route-context": "",
         "logical-type": e.logicalTypeName,
         "object-id": e.objectId
       }, [...a[0] || (a[0] = [
-        A("cw-object", { editable: "" }, null, -1),
-        A("cw-interaction-controller", { "data-causeway-route-interactions": "" }, null, -1)
+        C("cw-object", { editable: "" }, null, -1),
+        C("cw-interaction-controller", { "data-causeway-route-interactions": "" }, null, -1)
       ])], 8, Ve)
     ]));
   }
-}), We = ["data-route-key"], Be = {
+}), Be = ["data-route-key"], We = {
   key: 0,
   class: "causeway-vue-route-page causeway-vue-status causeway-vue-status-danger",
   "data-route-state": "terminal-error",
   tabindex: "-1",
   role: "alert"
-}, ze = /* @__PURE__ */ C({
+}, ze = /* @__PURE__ */ O({
   __name: "ObjectRoutePage",
   setup(e) {
-    const t = ve(), a = z(), n = L(null), o = L(null), r = P(() => re(t.path, "/")), p = C(() => () => S("section", {
+    const t = ve(), a = D(), n = L(null), r = L(null), i = P(() => re(t.path, "/")), d = O(() => () => S("section", {
       class: "causeway-vue-route-page causeway-vue-status",
       "data-route-state": "loading",
       tabindex: -1,
       role: "status"
-    }, [S("h1", "Loading page…")])), u = C(() => () => S("section", {
+    }, [S("h1", "Loading page…")])), s = O(() => () => S("section", {
       class: "causeway-vue-route-page causeway-vue-status causeway-vue-status-danger",
       "data-route-state": "terminal-error",
       tabindex: -1,
       role: "alert"
-    }, [S("h1", "Page unavailable"), S("p", "The application page could not be loaded.")])), l = P(() => Oe(r.value)), m = P(() => a.pages.get(r.value.logicalTypeName)), h = P(() => {
-      const s = m.value;
-      return s ? Ue(s) ? pe({
+    }, [S("h1", "Page unavailable"), S("p", "The application page could not be loaded.")])), u = P(() => Oe(i.value)), w = P(() => a.pages.get(i.value.logicalTypeName)), h = P(() => {
+      const o = w.value;
+      return o ? Ue(o) ? pe({
         loader: async () => {
-          const c = await s();
+          const c = await o();
           return "default" in c ? c.default : c;
         },
         delay: 0,
         timeout: 3e4,
-        loadingComponent: p,
-        errorComponent: u,
-        onError(c, w, g) {
+        loadingComponent: d,
+        errorComponent: s,
+        onError(c, v, g) {
           a.policies.error?.(c, {
             router: a.router,
             basePath: a.basePath,
@@ -462,88 +463,88 @@ const ke = ["data-route-state"], Ge = /* @__PURE__ */ C({
             routeGeneration: a.state.routeGeneration
           }), g();
         }
-      }) : s : $e;
+      }) : o : $e;
     });
-    function v() {
-      const s = a.state.shell;
+    function y() {
+      const o = a.state.shell;
       return {
-        route: s?.querySelector("[data-causeway-router-view]") ?? null,
-        loading: s?.querySelector("[data-causeway-route-loading]") ?? null,
-        announcement: s?.querySelector("[data-causeway-route-announcement]") ?? null
+        route: o?.querySelector("[data-causeway-router-view]") ?? null,
+        loading: o?.querySelector("[data-causeway-route-loading]") ?? null,
+        announcement: o?.querySelector("[data-causeway-route-announcement]") ?? null
       };
     }
-    function f(s, c) {
-      const w = v().announcement;
-      w && (w.textContent = "", requestAnimationFrame(() => {
-        c === a.state.routeGeneration && w.isConnected && (w.textContent = s);
+    function m(o, c) {
+      const v = y().announcement;
+      v && (v.textContent = "", requestAnimationFrame(() => {
+        c === a.state.routeGeneration && v.isConnected && (v.textContent = o);
       }));
     }
-    function d(s) {
-      const c = v();
-      c.route?.setAttribute("aria-busy", String(s)), c.loading && (c.loading.hidden = !s);
+    function b(o) {
+      const c = y();
+      c.route?.setAttribute("aria-busy", String(o)), c.loading && (c.loading.hidden = !o);
     }
-    async function i() {
-      const s = a.state.routeGeneration;
-      if (o.value = null, await ye(), s !== a.state.routeGeneration || !n.value) return;
-      const c = me(n.value, r.value);
-      c.valid || (o.value = c.classification ?? "invalid", d(!1), f("Page unavailable", s));
+    async function l() {
+      const o = a.state.routeGeneration;
+      if (r.value = null, await ye(), o !== a.state.routeGeneration || !n.value) return;
+      const c = me(n.value, i.value);
+      c.valid || (r.value = c.classification ?? "invalid", b(!1), m("Page unavailable", o));
     }
-    function y(s) {
-      const c = a.state.routeGeneration, w = s, g = w.detail?.state, E = w.target?.closest("[data-causeway-route-page]");
-      if (!g || !E || !n.value?.contains(E)) return;
-      let T = String(g.status ?? "terminal-error");
-      if (T === "terminal-error") {
-        const O = g.errors?.[0]?.extensions?.classification ?? g.errors?.[0]?.extensions?.code ?? g.error?.code;
-        (O === "NOT_FOUND" || O === "ACCESS_DENIED") && (T = "unavailable");
+    function p(o) {
+      const c = a.state.routeGeneration, v = o, g = v.detail?.state, _ = v.target?.closest("[data-causeway-route-page]");
+      if (!g || !_ || !n.value?.contains(_)) return;
+      let f = String(g.status ?? "terminal-error");
+      if (f === "terminal-error") {
+        const E = g.errors?.[0]?.extensions?.classification ?? g.errors?.[0]?.extensions?.code ?? g.error?.code;
+        (E === "NOT_FOUND" || E === "ACCESS_DENIED") && (f = "unavailable");
       }
-      if (E.dataset.routeState = T, T === "ready" || T === "partial-error") {
-        d(!1);
-        const O = g.snapshot?.data?._meta?.title;
-        O && (document.title = String(O));
-        for (const ue of E.querySelectorAll("cw-collection:not([active])"))
+      if (_.dataset.routeState = f, f === "ready" || f === "partial-error") {
+        b(!1);
+        const E = g.snapshot?.data?._meta?.title;
+        E && (document.title = String(E));
+        for (const ue of _.querySelectorAll("cw-collection:not([active])"))
           ue.activate?.();
-        f(T === "ready" ? "Page ready" : "Page ready with partial information", c);
-      } else (T === "terminal-error" || T === "unavailable") && (d(!1), f("Page unavailable", c));
+        m(f === "ready" ? "Page ready" : "Page ready with partial information", c);
+      } else (f === "terminal-error" || f === "unavailable") && (b(!1), m("Page unavailable", c));
     }
     return fe(() => t.fullPath, () => {
-      a.state.routeGeneration += 1, d(!0), f("Loading page", a.state.routeGeneration), i();
+      a.state.routeGeneration += 1, b(!0), m("Loading page", a.state.routeGeneration), l();
     }, { immediate: !0 }), W(async () => {
-      n.value?.addEventListener("causeway-object-context-state-change", y), await i();
-      const s = n.value?.querySelector(
+      n.value?.addEventListener("causeway-object-context-state-change", p), await l();
+      const o = n.value?.querySelector(
         "cw-object-context[data-causeway-route-context]"
       );
-      s?.context?.state && s.dispatchEvent(new CustomEvent("causeway-object-context-state-change", {
+      o?.context?.state && o.dispatchEvent(new CustomEvent("causeway-object-context-state-change", {
         bubbles: !0,
         composed: !0,
-        detail: { state: s.context.state, context: s.context }
+        detail: { state: o.context.state, context: o.context }
       })), n.value?.querySelector("[data-causeway-route-page]")?.focus({ preventScroll: !0 });
-    }), ee(() => n.value?.removeEventListener("causeway-object-context-state-change", y)), (s, c) => (N(), I("div", {
+    }), te(() => n.value?.removeEventListener("causeway-object-context-state-change", p)), (o, c) => (N(), I("div", {
       ref_key: "host",
       ref: n,
       class: "causeway-vue-object-route",
-      "data-route-key": l.value
+      "data-route-key": u.value
     }, [
-      o.value ? (N(), I("section", Be, [...c[0] || (c[0] = [
-        A("h1", null, "Page unavailable", -1),
-        A("p", null, "The application page has an invalid semantic context boundary.", -1)
+      r.value ? (N(), I("section", We, [...c[0] || (c[0] = [
+        C("h1", null, "Page unavailable", -1),
+        C("p", null, "The application page has an invalid semantic context boundary.", -1)
       ])])) : (N(), ge(he(h.value), {
-        key: l.value,
-        "logical-type-name": r.value.logicalTypeName,
-        "object-id": r.value.objectId,
-        "route-key": l.value,
-        onVnodeMounted: i,
-        onVnodeUpdated: i
+        key: u.value,
+        "logical-type-name": i.value.logicalTypeName,
+        "object-id": i.value.objectId,
+        "route-key": u.value,
+        onVnodeMounted: l,
+        onVnodeUpdated: l
       }, null, 8, ["logical-type-name", "object-id", "route-key"]))
-    ], 8, We));
+    ], 8, Be));
   }
-}), x = Object.freeze({
+}), j = Object.freeze({
   home: "causeway-home",
   object: "causeway-object",
   invalid: "causeway-invalid-route",
   notFound: "causeway-not-found"
 });
 function se(e, t, a, n) {
-  return C({
+  return O({
     name: `Causeway${e.replace(/\W/gu, "")}Page`,
     setup() {
       return () => S("section", {
@@ -568,71 +569,71 @@ const De = se(
 );
 function He(e = {}) {
   return [
-    { path: "/", name: x.home, component: e.homeComponent ?? Ge },
+    { path: "/", name: j.home, component: e.homeComponent ?? qe },
     {
       path: "/object/:logicalTypeName/:objectId",
-      name: x.object,
+      name: j.object,
       component: ze,
       beforeEnter(t) {
         try {
           return re(t.path, "/"), !0;
         } catch {
-          return { name: x.invalid, replace: !0 };
+          return { name: j.invalid, replace: !0 };
         }
       }
     },
     {
       path: "/invalid-route",
-      name: x.invalid,
+      name: j.invalid,
       component: De
     },
     {
       path: "/:pathMatch(.*)*",
-      name: x.notFound,
+      name: j.notFound,
       component: e.notFoundComponent ?? Me
     }
   ];
 }
 export {
-  G as ACTION_REQUEST_EVENT,
-  X as ACTION_RESULT_EVENT,
-  x as CAUSEWAY_ROUTE_NAMES,
-  ae as CAUSEWAY_VIEWER_KEY,
+  q as ACTION_REQUEST_EVENT,
+  Z as ACTION_RESULT_EVENT,
+  j as CAUSEWAY_ROUTE_NAMES,
+  ne as CAUSEWAY_VIEWER_KEY,
   $e as CausewayGenericObjectPage,
-  Ge as CausewayHomePage,
+  qe as CausewayHomePage,
   De as CausewayInvalidRoutePage,
   Me as CausewayNotFoundPage,
   ze as CausewayObjectRoutePage,
-  D as FRAMEWORK_LOGOUT_ACTION,
-  Se as INVALID_ROUTE_MESSAGE,
+  M as FRAMEWORK_LOGOUT_ACTION,
+  Ae as INVALID_ROUTE_MESSAGE,
   be as LocalResourceNavigationError,
-  H as MENU_BARS_STATE_EVENT,
-  Y as NAVIGATION_REQUEST_EVENT,
-  Z as OBJECT_CONTEXT_STATE_EVENT,
+  Q as MENU_BARS_STATE_EVENT,
+  X as NAVIGATION_REQUEST_EVENT,
+  H as OBJECT_CONTEXT_STATE_EVENT,
   k as ROUTE_CONTEXT_SELECTOR,
   we as ROUTE_INTERACTIONS_SELECTOR,
   Ze as bindCausewayShell,
   Ce as canonicalObjectPath,
   Oe as canonicalRouteKey,
-  B as canonicalRouterObjectPath,
+  z as canonicalRouterObjectPath,
   He as createCausewayRouteRecords,
   Ye as createCausewayVueViewer,
-  K as decodeRouteSegment,
-  q as encodeRouteSegment,
+  Y as decodeRouteSegment,
+  V as encodeRouteSegment,
   ce as installSemanticBridge,
-  ne as isFrameworkLogoutAction,
+  G as isFrameworkLogoutAction,
   Ue as isPageLoader,
   Te as navigateLocalResource,
   U as normalizeBasePath,
   Ie as normalizePageRegistry,
   re as parseCanonicalObjectPath,
   ie as presentSemanticResult,
-  M as removeFrameworkLogoutMenuActions,
+  F as removeFrameworkLogoutMenuActions,
   Ee as resolveLocalResourceTarget,
-  $ as resolveResultOutlet,
+  B as resolveResultOutlet,
   Xe as useCausewayShell,
-  z as useCausewayViewer,
+  D as useCausewayViewer,
   me as validateRouteBoundary,
-  te as validateShellBoundary
+  ae as validateShellBoundary
 };
 //# sourceMappingURL=causeway-vue.js.map
diff --git a/viewers/webcomponents/vue/dist/causeway-vue.js.map b/viewers/webcomponents/vue/dist/causeway-vue.js.map
index 42539e2e459..855fdc3251a 100644
--- a/viewers/webcomponents/vue/dist/causeway-vue.js.map
+++ b/viewers/webcomponents/vue/dist/causeway-vue.js.map
@@ -1 +1 @@
-{"version":3,"file":"causeway-vue.js","sources":["../src/boundary.ts","../src/contracts.ts","../src/host-operation.ts","../src/local-resource.ts","../src/route-codec.ts","../src/policy.ts","../src/registry.ts","../src/plugin.ts","../src/components/HomePage.vue","../src/components/GenericObjectPage.vue","../src/components/ObjectRoutePage.vue","../src/routes.ts"],"sourcesContent":["/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements. [...]
\ No newline at end of file
+{"version":3,"file":"causeway-vue.js","sources":["../src/boundary.ts","../src/contracts.ts","../src/host-operation.ts","../src/local-resource.ts","../src/route-codec.ts","../src/policy.ts","../src/registry.ts","../src/plugin.ts","../src/components/HomePage.vue","../src/components/GenericObjectPage.vue","../src/components/ObjectRoutePage.vue","../src/routes.ts"],"sourcesContent":["/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements. [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/policy.d.ts.map b/viewers/webcomponents/vue/dist/policy.d.ts.map
index 2d5070da678..9d458747325 100644
--- a/viewers/webcomponents/vue/dist/policy.d.ts.map
+++ b/viewers/webcomponents/vue/dist/policy.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAMV,sBAAsB,EACtB,qBAAqB,EACtB,MAAM,aAAa,CAAC;AAKrB,eAAO,MAAM,wBAAwB,gCAAgC,CAAC;AACtE,eAAO,MAAM,oBAAoB,4BAA4B,CAAC;AAC9D,eAAO,MAAM,mBAAmB,2BAA2B,CAAC;AAC5D,eAAO,MAAM,0BAA0B,yCAAyC,CAAC;AACjF,eAAO,MAAM,qBAAqB,oCAAoC,CAAC;AAEvE,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,sBAAsB,CAAC;IACzC,QA [...]
\ No newline at end of file
+{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAMV,sBAAsB,EACtB,qBAAqB,EACtB,MAAM,aAAa,CAAC;AAKrB,eAAO,MAAM,wBAAwB,gCAAgC,CAAC;AACtE,eAAO,MAAM,oBAAoB,4BAA4B,CAAC;AAC9D,eAAO,MAAM,mBAAmB,2BAA2B,CAAC;AAC5D,eAAO,MAAM,0BAA0B,yCAAyC,CAAC;AACjF,eAAO,MAAM,qBAAqB,oCAAoC,CAAC;AAEvE,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,sBAAsB,CAAC;IACzC,QA [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/src/policy.ts b/viewers/webcomponents/vue/src/policy.ts
index bec10d4dd2c..695479ec5cc 100644
--- a/viewers/webcomponents/vue/src/policy.ts
+++ b/viewers/webcomponents/vue/src/policy.ts
@@ -155,6 +155,9 @@ async function applyNavigation(runtime: CausewayViewerRuntime, target: CausewayO
 export function installSemanticBridge(runtime: CausewayViewerRuntime, shell: HTMLElement): () => void {
   const destinations = new WeakMap<object, HTMLElement>();
   const resumedActionEvents = new WeakSet<Event>();
+  const menuBoundary = shell.querySelector<HTMLElement & {excludeAction?: (detail: CausewayActionRequest) => boolean}>('cw-menubars');
+  const excludeFrameworkLogout = (detail: CausewayActionRequest) => isFrameworkLogoutAction(detail);
+  if (menuBoundary) menuBoundary.excludeAction = excludeFrameworkLogout;
   let unscopedDestination: HTMLElement | null = null;
   let active = true;
   const announceUnavailableLogout = () => {
@@ -252,6 +255,7 @@ export function installSemanticBridge(runtime: CausewayViewerRuntime, shell: HTM
   shell.addEventListener(MENU_BARS_STATE_EVENT, onMenuState);
   return () => {
     active = false;
+    if (menuBoundary?.excludeAction === excludeFrameworkLogout) menuBoundary.excludeAction = undefined;
     shell.removeEventListener(ACTION_REQUEST_EVENT, onActionRequest, {capture: true});
     shell.removeEventListener(NAVIGATION_REQUEST_EVENT, onNavigation);
     shell.removeEventListener(ACTION_RESULT_EVENT, onResult);
diff --git a/viewers/webcomponents/vue/test/policy-integration.test.ts b/viewers/webcomponents/vue/test/policy-integration.test.ts
index 763d95590bc..07dfbb0722c 100644
--- a/viewers/webcomponents/vue/test/policy-integration.test.ts
+++ b/viewers/webcomponents/vue/test/policy-integration.test.ts
@@ -33,6 +33,7 @@ import {createCausewayRouteRecords} from '../src/routes';
 function authoredShell(): HTMLElement {
   const shell = document.createElement('div');
   shell.innerHTML = `<cw-graphql-client data-causeway-shell-client>
+    <cw-menubars></cw-menubars>
     <div data-causeway-route-loading></div><div data-causeway-route-announcement></div>
     <cw-action-results data-causeway-shell-result></cw-action-results>
     <main data-causeway-router-view></main>
@@ -132,6 +133,9 @@ describe('semantic policy bridge', () => {
     shell.append(region);
     const dispose = installSemanticBridge(viewer, shell);
     expect(region.isConnected).toBe(false);
+    const menuBoundary = shell.querySelector('cw-menubars') as HTMLElement & {excludeAction?: (detail: object) => boolean};
+    expect(menuBoundary.excludeAction?.({serviceLogicalTypeName: 'causeway.security.LogoutMenu', actionId: 'logout'})).toBe(true);
+    expect(menuBoundary.excludeAction?.({serviceLogicalTypeName: 'example.LogoutMenu', actionId: 'logout'})).toBe(false);
 
     const request = new CustomEvent(ACTION_REQUEST_EVENT, {
       bubbles: true,
@@ -144,6 +148,7 @@ describe('semantic policy bridge', () => {
     expect(shell.dataset.causewayLogoutUnavailable).toBe('true');
     expect(shell.querySelector('[data-causeway-route-announcement]')?.textContent).toContain('host authentication');
     dispose();
+    expect(menuBoundary.excludeAction).toBeUndefined();
   });
 
   it('lets synchronous and asynchronous action policies claim or resume exactly once', async () => {


(causeway) 16/16: fix(webcomponents): activate secured Vue source profile

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit d30392886a52ba212c33575dc6ab6ebabc25ec8f
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 3 17:34:29 2026 +0100

    fix(webcomponents): activate secured Vue source profile
---
 viewers/webcomponents/pom.xml                      | 1 -
 viewers/webcomponents/sample-vue-petclinic/pom.xml | 8 +-------
 viewers/webcomponents/sample-vue-petclinic/test.sh | 2 +-
 3 files changed, 2 insertions(+), 9 deletions(-)

diff --git a/viewers/webcomponents/pom.xml b/viewers/webcomponents/pom.xml
index 0a6cadb2416..d26d511ec1c 100644
--- a/viewers/webcomponents/pom.xml
+++ b/viewers/webcomponents/pom.xml
@@ -88,7 +88,6 @@
             <id>run-sample-vue-petclinic-secured</id>
             <properties>
                 <sample-vue-petclinic.run.skip>false</sample-vue-petclinic.run.skip>
-                <sample-vue-petclinic.secured>true</sample-vue-petclinic.secured>
                 <spring-boot.run.main-class>org.apache.causeway.viewer.webcomponents.sample.vue.petclinicsecured.PetClinicVueSecuredApplication</spring-boot.run.main-class>
             </properties>
             <build>
diff --git a/viewers/webcomponents/sample-vue-petclinic/pom.xml b/viewers/webcomponents/sample-vue-petclinic/pom.xml
index ae5147831a1..003dfdc856e 100644
--- a/viewers/webcomponents/sample-vue-petclinic/pom.xml
+++ b/viewers/webcomponents/sample-vue-petclinic/pom.xml
@@ -140,13 +140,7 @@
 
     <profiles>
         <profile>
-            <id>secured-vue-petclinic</id>
-            <activation>
-                <property>
-                    <name>sample-vue-petclinic.secured</name>
-                    <value>true</value>
-                </property>
-            </activation>
+            <id>run-sample-vue-petclinic-secured</id>
             <dependencies>
                 <dependency>
                     <groupId>org.apache.causeway.viewer</groupId>
diff --git a/viewers/webcomponents/sample-vue-petclinic/test.sh b/viewers/webcomponents/sample-vue-petclinic/test.sh
index 905eef7056c..d363862a65b 100755
--- a/viewers/webcomponents/sample-vue-petclinic/test.sh
+++ b/viewers/webcomponents/sample-vue-petclinic/test.sh
@@ -47,7 +47,7 @@ fi
   "$@"
 
 exec "$MVN" -f "$POM" -pl "$MODULE" -am \
-  -Dsample-vue-petclinic.secured=true \
+  -Prun-sample-vue-petclinic-secured \
   -Dtest=PetClinicVueSecuredApplication_IntegTest,PetClinicVueSecuredPlaywrightAcceptance \
   -Dsurefire.failIfNoSpecifiedTests=false \
   test \


(causeway) 02/16: feat(webcomponents): add generic Vue viewer

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit afdb0b317a30860ffc55fe6b1020eaf6566cebd8
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 3 09:05:09 2026 +0100

    feat(webcomponents): add generic Vue viewer
---
 .../add-generic-vue-web-component-viewer/tasks.md  |   94 +-
 .../foundation/test/element-prefix.test.mjs        |    3 +-
 viewers/webcomponents/pom.xml                      |   13 +
 .../webcomponents/sample-htmx-petclinic/pom.xml    |    5 +
 .../htmx/petclinic/PetClinicHtmxApplication.java   |    2 +-
 .../PetClinicHtmxSecuredApplication.java           |    2 +-
 .../PetClinicSecmanDataConfiguration.java          |    2 +-
 .../petclinic/PetClinicHtmxPlaywrightTest.java     |    2 +-
 .../PetClinicHtmxApplication_IntegTest.java        |   10 +-
 .../webcomponents/sample-petclinic-domain/pom.xml  |   82 +
 .../sample}/petclinic/domain/Pet.java              |    2 +-
 .../domain/PetClinicDataConfiguration.java         |    2 +-
 .../petclinic/domain/PetClinicDomainModule.java    |    2 +-
 .../petclinic/domain/PetClinicHomePage.java        |    2 +-
 .../domain/PetCollectionFilterService.java         |    2 +-
 .../sample}/petclinic/domain/PetOwner.java         |    2 +-
 .../domain/PetOwnerCollectionFilterService.java    |    2 +-
 .../petclinic/domain/PetOwnerRepository.java       |    2 +-
 .../petclinic/domain/PetOwner_bookVisit.java       |    2 +-
 .../sample}/petclinic/domain/PetOwner_visits.java  |    2 +-
 .../sample}/petclinic/domain/PetOwners.java        |    2 +-
 .../sample}/petclinic/domain/PetSpecies.java       |    2 +-
 .../sample}/petclinic/domain/Visit.java            |    2 +-
 .../domain/VisitCollectionFilterService.java       |    2 +-
 .../sample}/petclinic/domain/VisitRepository.java  |    2 +-
 .../sample}/petclinic/domain/Visits.java           |    2 +-
 .../sample}/petclinic/domain/Pet.layout.xml        |    0
 .../PetClinicHomePage#futureVisits.columnOrder.txt |    0
 .../PetClinicHomePage#petOwners.columnOrder.txt    |    0
 .../petclinic/domain/PetClinicHomePage.layout.xml  |    0
 .../petclinic/domain/PetOwner#pets.columnOrder.txt |    0
 .../domain/PetOwner#visits.columnOrder.txt         |    0
 .../sample}/petclinic/domain/PetOwner.layout.xml   |    0
 .../sample}/petclinic/domain/Visit.layout.xml      |    0
 .../sample-vue-petclinic/frontend/index.html       |   34 +
 .../frontend/package-lock.json                     | 1946 +++++++++++
 .../sample-vue-petclinic/frontend/package.json     |   27 +
 .../sample-vue-petclinic/frontend/src/App.vue      |   59 +
 .../frontend/src/env.d.ts}                         |   14 +-
 .../sample-vue-petclinic/frontend/src/main.ts      |   66 +
 .../frontend/src/pages/PetOwnerPage.vue            |  119 +
 .../frontend/src/petclinic.css                     |   49 +
 .../sample-vue-petclinic/frontend/tsconfig.json    |   14 +
 .../frontend/vite.config.ts}                       |   32 +-
 .../pom.xml                                        |  127 +-
 .../vue/petclinic/PetClinicVueApplication.java}    |   24 +-
 .../petclinic/PetClinicVueRouteController.java}    |   23 +-
 .../src/main/resources/application.properties      |   25 +
 .../resources/static/vue/assets/index-BntyzeMN.js  |    2 +
 .../static/vue/assets/index-BntyzeMN.js.map        |    1 +
 .../resources/static/vue/assets/index-BuWcbzr4.css |    1 +
 .../src/main/resources/static/vue/index.html       |   35 +
 .../vue/petclinic/PetClinicVuePlaywrightTest.java  |  191 ++
 .../PetClinicVueApplicationIntegTest.java          |   96 +
 viewers/webcomponents/vue/LICENSE                  |  201 ++
 viewers/webcomponents/vue/NOTICE                   |    5 +
 viewers/webcomponents/vue/README.adoc              |  142 +
 viewers/webcomponents/vue/dist/boundary.d.ts       |   19 +
 viewers/webcomponents/vue/dist/boundary.d.ts.map   |    1 +
 viewers/webcomponents/vue/dist/causeway-vue.css    |    1 +
 viewers/webcomponents/vue/dist/causeway-vue.js     |  539 +++
 viewers/webcomponents/vue/dist/causeway-vue.js.map |    1 +
 .../vue/dist/components/GenericObjectPage.vue.d.ts |    5 +
 .../dist/components/GenericObjectPage.vue.d.ts.map |    1 +
 .../vue/dist/components/HomePage.vue.d.ts          |    4 +
 .../vue/dist/components/HomePage.vue.d.ts.map      |    1 +
 .../vue/dist/components/ObjectRoutePage.vue.d.ts   |    4 +
 .../dist/components/ObjectRoutePage.vue.d.ts.map   |    1 +
 viewers/webcomponents/vue/dist/contracts.d.ts      |   57 +
 viewers/webcomponents/vue/dist/contracts.d.ts.map  |    1 +
 viewers/webcomponents/vue/dist/index.d.ts          |   11 +
 viewers/webcomponents/vue/dist/index.d.ts.map      |    1 +
 viewers/webcomponents/vue/dist/plugin.d.ts         |   11 +
 viewers/webcomponents/vue/dist/plugin.d.ts.map     |    1 +
 viewers/webcomponents/vue/dist/policy.d.ts         |   16 +
 viewers/webcomponents/vue/dist/policy.d.ts.map     |    1 +
 viewers/webcomponents/vue/dist/registry.d.ts       |    9 +
 viewers/webcomponents/vue/dist/registry.d.ts.map   |    1 +
 viewers/webcomponents/vue/dist/route-codec.d.ts    |   21 +
 .../webcomponents/vue/dist/route-codec.d.ts.map    |    1 +
 viewers/webcomponents/vue/dist/routes.d.ts         |   16 +
 viewers/webcomponents/vue/dist/routes.d.ts.map     |    1 +
 viewers/webcomponents/vue/package-lock.json        | 3515 ++++++++++++++++++++
 viewers/webcomponents/vue/package.json             |   51 +
 viewers/webcomponents/vue/pom.xml                  |  119 +
 viewers/webcomponents/vue/src/boundary.ts          |   72 +
 .../vue/src/components/GenericObjectPage.vue       |   43 +
 .../webcomponents/vue/src/components/HomePage.vue  |   86 +
 .../vue/src/components/ObjectRoutePage.vue         |  183 +
 viewers/webcomponents/vue/src/contracts.ts         |   84 +
 .../domain/PetSpecies.java => vue/src/env.d.ts}    |   14 +-
 .../src/index.ts}                                  |   22 +-
 viewers/webcomponents/vue/src/plugin.ts            |   86 +
 viewers/webcomponents/vue/src/policy.ts            |  194 ++
 viewers/webcomponents/vue/src/registry.ts          |   53 +
 viewers/webcomponents/vue/src/route-codec.ts       |  122 +
 viewers/webcomponents/vue/src/routes.ts            |   93 +
 viewers/webcomponents/vue/src/theme.css            |   80 +
 .../vue/test/policy-integration.test.ts            |  124 +
 .../vue/test/registry-boundary.test.ts             |   82 +
 viewers/webcomponents/vue/test/route-codec.test.ts |   83 +
 .../webcomponents/vue/test/vue-components.test.ts  |  110 +
 viewers/webcomponents/vue/tsconfig.json            |   18 +
 viewers/webcomponents/vue/verify-package.mjs       |   47 +
 .../vite.config.ts}                                |   42 +-
 105 files changed, 9334 insertions(+), 194 deletions(-)

diff --git a/openspec/changes/add-generic-vue-web-component-viewer/tasks.md b/openspec/changes/add-generic-vue-web-component-viewer/tasks.md
index 3c8aa638272..841857b1cf0 100644
--- a/openspec/changes/add-generic-vue-web-component-viewer/tasks.md
+++ b/openspec/changes/add-generic-vue-web-component-viewer/tasks.md
@@ -1,70 +1,70 @@
 ## 1. Package and Build Contract
 
-- [ ] 1.1 Add the optional `viewers/webcomponents/vue` Vue 3 and Vue Router 4 package with TypeScript sources, package metadata, a committed lockfile, Vite library mode, peer dependencies, ECMAScript exports, and declarations.
-- [ ] 1.2 Add the lightweight Maven/reactor integration and explicit frontend regeneration profile without making Vue dependencies transitive to foundation, HTMX, or vanilla consumers.
-- [ ] 1.3 Configure Vue single-file component compilation to recognize `cw-` tags as native custom elements in the library and documented consumer setup.
-- [ ] 1.4 Add package unit-test, type-check, production-build, stale-output, license, `npm pack`, package-content, and clean packed-consumer smoke checks.
-- [ ] 1.5 Document the public package exports and keep unimplemented Nuxt, SSR, runtime registry mutation, and inherited-type matching out of the initial API.
+- [x] 1.1 Add the optional `viewers/webcomponents/vue` Vue 3 and Vue Router 4 package with TypeScript sources, package metadata, a committed lockfile, Vite library mode, peer dependencies, ECMAScript exports, and declarations.
+- [x] 1.2 Add the lightweight Maven/reactor integration and explicit frontend regeneration profile without making Vue dependencies transitive to foundation, HTMX, or vanilla consumers.
+- [x] 1.3 Configure Vue single-file component compilation to recognize `cw-` tags as native custom elements in the library and documented consumer setup.
+- [x] 1.4 Add package unit-test, type-check, production-build, stale-output, license, `npm pack`, package-content, and clean packed-consumer smoke checks.
+- [x] 1.5 Document the public package exports and keep unimplemented Nuxt, SSR, runtime registry mutation, and inherited-type matching out of the initial API.
 
 ## 2. Canonical Router Integration
 
-- [ ] 2.1 Implement the bounded canonical UTF-8 route encoder and raw-path decoder for `<base-path>/object/<logical-type>/<identifier>` without relying on already-decoded Vue Router parameters.
-- [ ] 2.2 Test valid, invalid, overlong, Unicode, non-canonical, and generated round-trip routes against `viewers/webcomponents/canonical-route-fixtures.yaml`.
-- [ ] 2.3 Export namespaced home, object, invalid-route, and fallback route records for an application-owned Vue Router instance.
-- [ ] 2.4 Implement plugin installation and injected viewer configuration without creating a hidden router or overriding unrelated application routes, guards, scroll behavior, or history policy.
-- [ ] 2.5 Implement codec-produced push and replace helpers that preserve configured base-path meaning and complete advertised identity.
-- [ ] 2.6 Document and integration-test application-owned browser-history server fallback without intercepting GraphQL, assets, authentication, or unrelated backend routes.
+- [x] 2.1 Implement the bounded canonical UTF-8 route encoder and raw-path decoder for `<base-path>/object/<logical-type>/<identifier>` without relying on already-decoded Vue Router parameters.
+- [x] 2.2 Test valid, invalid, overlong, Unicode, non-canonical, and generated round-trip routes against `viewers/webcomponents/canonical-route-fixtures.yaml`.
+- [x] 2.3 Export namespaced home, object, invalid-route, and fallback route records for an application-owned Vue Router instance.
+- [x] 2.4 Implement plugin installation and injected viewer configuration without creating a hidden router or overriding unrelated application routes, guards, scroll behavior, or history policy.
+- [x] 2.5 Implement codec-produced push and replace helpers that preserve configured base-path meaning and complete advertised identity.
+- [x] 2.6 Document and integration-test application-owned browser-history server fallback without intercepting GraphQL, assets, authentication, or unrelated backend routes.
 
 ## 3. Custom and Generic Route Pages
 
-- [ ] 3.1 Normalize an immutable exact-logical-type registry for Vue components and async component loaders and reject malformed, unsupported, or duplicate effective registrations at startup.
-- [ ] 3.2 Implement custom-page precedence and the generic route-page fallback containing authored `<cw-object-context>`, `<cw-object editable>`, and `<cw-interaction-controller>` elements.
-- [ ] 3.3 Define immutable `logicalTypeName`, `objectId`, and `routeKey` props for application-authored route pages and provide source examples for binding them to the declared context.
-- [ ] 3.4 Add bounded post-mount validation for route-context cardinality, interaction-controller containment, and canonical identity without manufacturing, moving, or selecting semantic elements.
-- [ ] 3.5 Key `RouterView` content by canonical identity and verify that route changes disconnect obsolete contexts, requirements, listeners, and interaction state.
-- [ ] 3.6 Scope async component loading, rejection, diagnostics, focus, and announcements to the active route generation and discard superseded completion.
-- [ ] 3.7 Add a structural test proving `<cw-object>` does not import or inspect Vue Router, Vue injection state, or the page registry.
+- [x] 3.1 Normalize an immutable exact-logical-type registry for Vue components and async component loaders and reject malformed, unsupported, or duplicate effective registrations at startup.
+- [x] 3.2 Implement custom-page precedence and the generic route-page fallback containing authored `<cw-object-context>`, `<cw-object editable>`, and `<cw-interaction-controller>` elements.
+- [x] 3.3 Define immutable `logicalTypeName`, `objectId`, and `routeKey` props for application-authored route pages and provide source examples for binding them to the declared context.
+- [x] 3.4 Add bounded post-mount validation for route-context cardinality, interaction-controller containment, and canonical identity without manufacturing, moving, or selecting semantic elements.
+- [x] 3.5 Key `RouterView` content by canonical identity and verify that route changes disconnect obsolete contexts, requirements, listeners, and interaction state.
+- [x] 3.6 Scope async component loading, rejection, diagnostics, focus, and announcements to the active route generation and discard superseded completion.
+- [x] 3.7 Add a structural test proving `<cw-object>` does not import or inspect Vue Router, Vue injection state, or the page registry.
 
 ## 4. Stable Shell and Native Element Bridge
 
-- [ ] 4.1 Export the shell composables and route-view helpers needed by an application-authored root component without exporting a mandatory visual shell hierarchy.
-- [ ] 4.2 Implement bounded development validation for one stable `<cw-graphql-client>`, one router-view region, one default result outlet, and the documented loading and announcement landmarks without repair.
-- [ ] 4.3 Implement endpoint and string-identity binding through authored templates and property-only structured bindings through DOM properties, including explicit Vue `.prop` use where required.
-- [ ] 4.4 Implement a shell-scoped semantic-event bridge for authoritative object navigation, interaction completion, and result events with deterministic listener cleanup on application unmount.
-- [ ] 4.5 Verify native custom-element attributes, properties, slots, pre-upgrade children, `CustomEvent` payloads, event bubbling, and connect/disconnect lifecycle under Vue rendering.
-- [ ] 4.6 Keep GraphQL objects, metadata, members, validation, invocation, and interaction state out of Vue stores and duplicate adapter state.
+- [x] 4.1 Export the shell composables and route-view helpers needed by an application-authored root component without exporting a mandatory visual shell hierarchy.
+- [x] 4.2 Implement bounded development validation for one stable `<cw-graphql-client>`, one router-view region, one default result outlet, and the documented loading and announcement landmarks without repair.
+- [x] 4.3 Implement endpoint and string-identity binding through authored templates and property-only structured bindings through DOM properties, including explicit Vue `.prop` use where required.
+- [x] 4.4 Implement a shell-scoped semantic-event bridge for authoritative object navigation, interaction completion, and result events with deterministic listener cleanup on application unmount.
+- [x] 4.5 Verify native custom-element attributes, properties, slots, pre-upgrade children, `CustomEvent` payloads, event bubbling, and connect/disconnect lifecycle under Vue rendering.
+- [x] 4.6 Keep GraphQL objects, metadata, members, validation, invocation, and interaction state out of Vue stores and duplicate adapter state.
 
 ## 5. Navigation, Home, and Result Policy
 
-- [ ] 5.1 Implement replaceable single-claim policy hooks for navigation, application-entry home behavior, and object, scalar, collection, void, unsupported, and error outcomes.
-- [ ] 5.2 Route complete object identities canonically and reject object-shaped results that lack authoritative public navigation identity without inference.
-- [ ] 5.3 Resolve exactly one eligible active-page result outlet before the shell default and fail closed for duplicate, disconnected, or otherwise ambiguous outlets.
-- [ ] 5.4 Assign normalized scalar and collection results to `<cw-action-results>` so existing result and standalone-collection components retain presentation authority.
-- [ ] 5.5 Refresh the current object context after void results and replace the route with home only when authoritative refresh establishes that the target disappeared.
-- [ ] 5.6 Test application handler replacement so claimed events are handled once and component interaction behavior remains unchanged.
+- [x] 5.1 Implement replaceable single-claim policy hooks for navigation, application-entry home behavior, and object, scalar, collection, void, unsupported, and error outcomes.
+- [x] 5.2 Route complete object identities canonically and reject object-shaped results that lack authoritative public navigation identity without inference.
+- [x] 5.3 Resolve exactly one eligible active-page result outlet before the shell default and fail closed for duplicate, disconnected, or otherwise ambiguous outlets.
+- [x] 5.4 Assign normalized scalar and collection results to `<cw-action-results>` so existing result and standalone-collection components retain presentation authority.
+- [x] 5.5 Refresh the current object context after void results and replace the route with home only when authoritative refresh establishes that the target disappeared.
+- [x] 5.6 Test application handler replacement so claimed events are handled once and component interaction behavior remains unchanged.
 
 ## 6. Route Lifecycle, Accessibility, and Theme
 
-- [ ] 6.1 Coordinate busy state, loading, ready, invalid-route, unavailable, partial-error, unsupported, and terminal-error presentation from router and object-context state.
-- [ ] 6.2 Implement generation-scoped focus movement and live-region announcements that cannot be overwritten by obsolete navigation or async-page work.
-- [ ] 6.3 Ensure unavailable and denied outcomes reveal neither domain state nor authorization rules and do not claim distinctions absent from the authoritative transport.
-- [ ] 6.4 Add a copyable reference shell theme with documented custom properties and hooks for wide, narrow, light, dark, reduced-motion, keyboard, and visible-focus behavior.
-- [ ] 6.5 Run automated accessibility checks and manual keyboard journeys for routing, menus, dialogs, sidebars, result dismissal, and route restoration.
+- [x] 6.1 Coordinate busy state, loading, ready, invalid-route, unavailable, partial-error, unsupported, and terminal-error presentation from router and object-context state.
+- [x] 6.2 Implement generation-scoped focus movement and live-region announcements that cannot be overwritten by obsolete navigation or async-page work.
+- [x] 6.3 Ensure unavailable and denied outcomes reveal neither domain state nor authorization rules and do not claim distinctions absent from the authoritative transport.
+- [x] 6.4 Add a copyable reference shell theme with documented custom properties and hooks for wide, narrow, light, dark, reduced-motion, keyboard, and visible-focus behavior.
+- [x] 6.5 Run automated accessibility checks and manual keyboard journeys for routing, menus, dialogs, sidebars, result dismissal, and route restoration.
 
 ## 7. Shared Petclinic Domain and Vue Application
 
-- [ ] 7.1 Extract the deterministic Petclinic domain and seed configuration into a small shared non-viewer module while preserving public logical types, fixture identities, cardinalities, GraphQL semantics, and source ownership.
-- [ ] 7.2 Update the HTMX Petclinic application to consume the shared module without changing its routes, classpath pages, application shell, selectors, security variants, or toolkit policies.
-- [ ] 7.3 Add a separate Vue Petclinic Spring Boot application with its own entry point, GraphQL endpoint, Vite source application, production output, client-route forwarding, and run profile.
-- [ ] 7.4 Author the Vue Petclinic `index.html` and root `App.vue` with a stable GraphQL provider, menus, branding, loading, announcements, default result outlet, and keyed router-view region.
-- [ ] 7.5 Add at least one source-visible Petclinic custom Vue page declaring its route context and interaction controller and leave another logical type unregistered to demonstrate generic fallback.
-- [ ] 7.6 Demonstrate menus, breadcrumbs, properties, actions, collection paging and filtering, row previews, object navigation, object results, and collection-result presentation without Vue-owned domain queries.
-- [ ] 7.7 Verify production output and direct-refresh routing without requiring a Vite development server or runtime Vue template compiler.
+- [x] 7.1 Extract the deterministic Petclinic domain and seed configuration into a small shared non-viewer module while preserving public logical types, fixture identities, cardinalities, GraphQL semantics, and source ownership.
+- [x] 7.2 Update the HTMX Petclinic application to consume the shared module without changing its routes, classpath pages, application shell, selectors, security variants, or toolkit policies.
+- [x] 7.3 Add a separate Vue Petclinic Spring Boot application with its own entry point, GraphQL endpoint, Vite source application, production output, client-route forwarding, and run profile.
+- [x] 7.4 Author the Vue Petclinic `index.html` and root `App.vue` with a stable GraphQL provider, menus, branding, loading, announcements, default result outlet, and keyed router-view region.
+- [x] 7.5 Add at least one source-visible Petclinic custom Vue page declaring its route context and interaction controller and leave another logical type unregistered to demonstrate generic fallback.
+- [x] 7.6 Demonstrate menus, breadcrumbs, properties, actions, collection paging and filtering, row previews, object navigation, object results, and collection-result presentation without Vue-owned domain queries.
+- [x] 7.7 Verify production output and direct-refresh routing without requiring a Vite development server or runtime Vue template compiler.
 
 ## 8. Verification and Documentation
 
-- [ ] 8.1 Add browser acceptance for direct links, refresh, back and forward, custom-page precedence, async-page races, generic fallback, invalid routes, absent objects, partial errors, menus, interactions, and results.
-- [ ] 8.2 Run Vue acceptance under default Vaadin and native component policies and fail on browser console errors, page errors, failed resources, or accessibility violations.
-- [ ] 8.3 Re-run foundation Node tests, HTMX Node and Java tests, HTMX Petclinic integration and security tests, and existing native, Vaadin, and secured Playwright profiles after Petclinic extraction.
-- [ ] 8.4 Document installation, package exports, Vite configuration, router records, route grammar, history fallback, shell ownership, custom pages, generic fallback, policies, toolkit assets, theming, lifecycle, diagnostics, and CSR-only support.
-- [ ] 8.5 Verify Maven reactor integration, reproducible generated output, dependency notices, Apache RAT, formatting, `git diff --check`, and strict OpenSpec validation.
+- [x] 8.1 Add browser acceptance for direct links, refresh, back and forward, custom-page precedence, async-page races, generic fallback, invalid routes, absent objects, partial errors, menus, interactions, and results.
+- [x] 8.2 Run Vue acceptance under default Vaadin and native component policies and fail on browser console errors, page errors, failed resources, or accessibility violations.
+- [x] 8.3 Re-run foundation Node tests, HTMX Node and Java tests, HTMX Petclinic integration and security tests, and existing native, Vaadin, and secured Playwright profiles after Petclinic extraction.
+- [x] 8.4 Document installation, package exports, Vite configuration, router records, route grammar, history fallback, shell ownership, custom pages, generic fallback, policies, toolkit assets, theming, lifecycle, diagnostics, and CSR-only support.
+- [x] 8.5 Verify Maven reactor integration, reproducible generated output, dependency notices, Apache RAT, formatting, `git diff --check`, and strict OpenSpec validation.
diff --git a/viewers/webcomponents/foundation/test/element-prefix.test.mjs b/viewers/webcomponents/foundation/test/element-prefix.test.mjs
index e74ba12ceba..f3e631e517a 100644
--- a/viewers/webcomponents/foundation/test/element-prefix.test.mjs
+++ b/viewers/webcomponents/foundation/test/element-prefix.test.mjs
@@ -96,7 +96,8 @@ const ELEMENT_API_MARKERS = [
 async function sourceFiles(directory) {
   const files = [];
   for (const entry of await readdir(directory, {withFileTypes: true})) {
-    if (entry.name === 'node_modules' || entry.name === 'target') continue;
+    if (entry.name === 'node_modules' || entry.name === 'target' || entry.name === 'dist'
+        || (entry.name === 'assets' && directory.endsWith('/static/vue'))) continue;
     const path = `${directory}/${entry.name}`;
     if (entry.isDirectory()) {
       files.push(...await sourceFiles(path));
diff --git a/viewers/webcomponents/pom.xml b/viewers/webcomponents/pom.xml
index 5a40bcad257..01e2c969b7f 100644
--- a/viewers/webcomponents/pom.xml
+++ b/viewers/webcomponents/pom.xml
@@ -40,14 +40,18 @@
         <spring-boot.run.skip>true</spring-boot.run.skip>
         <sample-html.run.skip>true</sample-html.run.skip>
         <sample-htmx-petclinic.run.skip>true</sample-htmx-petclinic.run.skip>
+        <sample-vue-petclinic.run.skip>true</sample-vue-petclinic.run.skip>
     </properties>
 
     <modules>
         <module>foundation</module>
+        <module>vue</module>
         <module>htmx</module>
         <module>htmx-security-secman</module>
+        <module>sample-petclinic-domain</module>
         <module>sample-html</module>
         <module>sample-htmx-petclinic</module>
+        <module>sample-vue-petclinic</module>
     </modules>
 
     <profiles>
@@ -69,6 +73,15 @@
                 <defaultGoal>spring-boot:run</defaultGoal>
             </build>
         </profile>
+        <profile>
+            <id>run-sample-vue-petclinic</id>
+            <properties>
+                <sample-vue-petclinic.run.skip>false</sample-vue-petclinic.run.skip>
+            </properties>
+            <build>
+                <defaultGoal>spring-boot:run</defaultGoal>
+            </build>
+        </profile>
         <profile>
             <id>run-sample-htmx-petclinic-secured</id>
             <properties>
diff --git a/viewers/webcomponents/sample-htmx-petclinic/pom.xml b/viewers/webcomponents/sample-htmx-petclinic/pom.xml
index cad28135975..73c336a35ba 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/pom.xml
+++ b/viewers/webcomponents/sample-htmx-petclinic/pom.xml
@@ -47,6 +47,11 @@
             <artifactId>causeway-mavendeps-webapp</artifactId>
             <type>pom</type>
         </dependency>
+        <dependency>
+            <groupId>org.apache.causeway.viewer</groupId>
+            <artifactId>causeway-viewer-webcomponents-sample-petclinic-domain</artifactId>
+            <version>${project.version}</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.causeway.viewer</groupId>
             <artifactId>causeway-viewer-graphql-viewer</artifactId>
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxApplication.java b/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxApplication.java
index 422e5a1421f..26b21e7fe2c 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxApplication.java
+++ b/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxApplication.java
@@ -34,7 +34,7 @@
 import org.apache.causeway.security.bypass.CausewayModuleSecurityBypass;
 import org.apache.causeway.viewer.graphql.viewer.CausewayModuleViewerGraphqlViewer;
 import org.apache.causeway.viewer.webcomponents.htmx.CausewayModuleViewerWebcomponentsHtmx;
-import org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain.PetClinicDomainModule;
+import org.apache.causeway.viewer.webcomponents.sample.petclinic.domain.PetClinicDomainModule;
 import org.apache.causeway.viewer.wicket.viewer.CausewayModuleViewerWicketViewer;
 
 @SpringBootApplication
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinicsecured/PetClinicHtmxSecuredApplication.java b/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinicsecured/PetClinicHtmxSecuredApplication.java
index e8583285bd3..3af9e3bd16f 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinicsecured/PetClinicHtmxSecuredApplication.java
+++ b/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinicsecured/PetClinicHtmxSecuredApplication.java
@@ -34,7 +34,7 @@
 import org.apache.causeway.viewer.graphql.viewer.CausewayModuleViewerGraphqlViewer;
 import org.apache.causeway.viewer.webcomponents.htmx.security.secman.CausewayModuleViewerWebcomponentsHtmxSecuritySecman;
 import org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.PetClinicLandingController;
-import org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain.PetClinicDomainModule;
+import org.apache.causeway.viewer.webcomponents.sample.petclinic.domain.PetClinicDomainModule;
 
 @SpringBootConfiguration
 @EnableAutoConfiguration
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinicsecured/PetClinicSecmanDataConfiguration.java b/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinicsecured/PetClinicSecmanDataConfiguration.java
index 3dc9d27cf40..ee1c5c95876 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinicsecured/PetClinicSecmanDataConfiguration.java
+++ b/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinicsecured/PetClinicSecmanDataConfiguration.java
@@ -35,7 +35,7 @@
 import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUser;
 import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUserRepository;
 import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUserStatus;
-import org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain.PetClinicDomainModule;
+import org.apache.causeway.viewer.webcomponents.sample.petclinic.domain.PetClinicDomainModule;
 
 @Configuration
 public class PetClinicSecmanDataConfiguration {
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java b/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java
index 1e2d76113d7..d3f05ecbf3c 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java
+++ b/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java
@@ -50,7 +50,7 @@
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.web.server.LocalServerPort;
 
-import org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain.PetOwnerRepository;
+import org.apache.causeway.viewer.webcomponents.sample.petclinic.domain.PetOwnerRepository;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/test/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxApplication_IntegTest.java b/viewers/webcomponents/sample-htmx-petclinic/src/test/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxApplication_IntegTest.java
index 6b978d27d8f..ad953f769b4 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/test/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxApplication_IntegTest.java
+++ b/viewers/webcomponents/sample-htmx-petclinic/src/test/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxApplication_IntegTest.java
@@ -39,9 +39,9 @@
 
 import org.apache.causeway.applib.services.clock.ClockService;
 import org.apache.causeway.viewer.webcomponents.htmx.HtmxViewerProperties;
-import org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain.PetOwner;
-import org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain.PetOwnerRepository;
-import org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain.VisitRepository;
+import org.apache.causeway.viewer.webcomponents.sample.petclinic.domain.PetOwner;
+import org.apache.causeway.viewer.webcomponents.sample.petclinic.domain.PetOwnerRepository;
+import org.apache.causeway.viewer.webcomponents.sample.petclinic.domain.VisitRepository;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -410,10 +410,10 @@ void packagesHtmlPagesAndRetainsLayoutFallbackResources() throws Exception {
                 .isEqualTo(404);
         assertThat(get("/petclinic.PetOwner.html").statusCode()).isEqualTo(404);
         assertThat(loader.getResource(
-                "org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner.layout.xml"))
+                "org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwner.layout.xml"))
                 .isNotNull();
         assertThat(loader.getResource(
-                "org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner#pets.columnOrder.txt"))
+                "org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwner#pets.columnOrder.txt"))
                 .isNotNull();
         assertThat(loader.getResource("menubars.layout.xml")).isNotNull();
         assertThatThrownBy(() -> Class.forName(
diff --git a/viewers/webcomponents/sample-petclinic-domain/pom.xml b/viewers/webcomponents/sample-petclinic-domain/pom.xml
new file mode 100644
index 00000000000..c6cf8cbe68f
--- /dev/null
+++ b/viewers/webcomponents/sample-petclinic-domain/pom.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.causeway.viewer</groupId>
+        <artifactId>causeway-viewer-webcomponents</artifactId>
+        <version>4.0.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>causeway-viewer-webcomponents-sample-petclinic-domain</artifactId>
+    <name>Apache Causeway Viewer - Web Components (Shared Petclinic Domain)</name>
+    <packaging>jar</packaging>
+
+    <properties>
+        <maven.install.skip>true</maven.install.skip>
+        <maven.deploy.skip>true</maven.deploy.skip>
+        <jar-plugin.automaticModuleName>org.apache.causeway.viewer.webcomponents.sample.petclinic.domain</jar-plugin.automaticModuleName>
+        <git-plugin.propertiesDir>org/apache/causeway/viewer/webcomponents/sample/petclinic/domain</git-plugin.propertiesDir>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.causeway.commons</groupId>
+            <artifactId>causeway-commons</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.causeway.core</groupId>
+            <artifactId>causeway-applib</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>jakarta.annotation</groupId>
+            <artifactId>jakarta.annotation-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>jakarta.persistence</groupId>
+            <artifactId>jakarta.persistence-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-autoconfigure</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-persistence</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-tx</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/Pet.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/Pet.java
similarity index 98%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/Pet.java
rename to viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/Pet.java
index 3a044af1050..7271bde7a86 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/Pet.java
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/Pet.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
+package org.apache.causeway.viewer.webcomponents.sample.petclinic.domain;
 
 import java.util.Comparator;
 import java.util.Set;
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicDataConfiguration.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicDataConfiguration.java
similarity index 99%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicDataConfiguration.java
rename to viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicDataConfiguration.java
index 7384499d70d..4b63f02dead 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicDataConfiguration.java
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicDataConfiguration.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
+package org.apache.causeway.viewer.webcomponents.sample.petclinic.domain;
 
 import java.time.LocalDate;
 import java.time.LocalDateTime;
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicDomainModule.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicDomainModule.java
similarity index 93%
copy from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicDomainModule.java
copy to viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicDomainModule.java
index 08fffd69475..b4c64313655 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicDomainModule.java
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicDomainModule.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
+package org.apache.causeway.viewer.webcomponents.sample.petclinic.domain;
 
 import org.springframework.boot.persistence.autoconfigure.EntityScan;
 import org.springframework.context.annotation.ComponentScan;
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicHomePage.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicHomePage.java
similarity index 96%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicHomePage.java
rename to viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicHomePage.java
index 01083d9e3fb..ce2a6b1e446 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicHomePage.java
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicHomePage.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
+package org.apache.causeway.viewer.webcomponents.sample.petclinic.domain;
 
 import java.time.LocalDateTime;
 import java.util.List;
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetCollectionFilterService.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetCollectionFilterService.java
similarity index 96%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetCollectionFilterService.java
rename to viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetCollectionFilterService.java
index 88250aa1f0c..4e31ba84f8c 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetCollectionFilterService.java
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetCollectionFilterService.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
+package org.apache.causeway.viewer.webcomponents.sample.petclinic.domain;
 
 import java.util.Arrays;
 import java.util.Locale;
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwner.java
similarity index 99%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner.java
rename to viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwner.java
index 8875a99f82f..085ee50326f 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner.java
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwner.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
+package org.apache.causeway.viewer.webcomponents.sample.petclinic.domain;
 
 import java.time.LocalDate;
 import java.time.temporal.ChronoUnit;
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwnerCollectionFilterService.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwnerCollectionFilterService.java
similarity index 96%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwnerCollectionFilterService.java
rename to viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwnerCollectionFilterService.java
index 2ffccab1a11..92eb33cb6e6 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwnerCollectionFilterService.java
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwnerCollectionFilterService.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
+package org.apache.causeway.viewer.webcomponents.sample.petclinic.domain;
 
 import java.util.Arrays;
 import java.util.Locale;
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwnerRepository.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwnerRepository.java
similarity index 96%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwnerRepository.java
rename to viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwnerRepository.java
index 98603ced8a1..a67099dd698 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwnerRepository.java
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwnerRepository.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
+package org.apache.causeway.viewer.webcomponents.sample.petclinic.domain;
 
 import java.util.List;
 
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner_bookVisit.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwner_bookVisit.java
similarity index 97%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner_bookVisit.java
rename to viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwner_bookVisit.java
index 381f079b818..a662249eb55 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner_bookVisit.java
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwner_bookVisit.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
+package org.apache.causeway.viewer.webcomponents.sample.petclinic.domain;
 
 import java.time.LocalDate;
 import java.time.LocalDateTime;
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner_visits.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwner_visits.java
similarity index 94%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner_visits.java
rename to viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwner_visits.java
index 4c50ff69782..cde2c576aae 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner_visits.java
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwner_visits.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
+package org.apache.causeway.viewer.webcomponents.sample.petclinic.domain;
 
 import java.util.List;
 
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwners.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwners.java
similarity index 97%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwners.java
rename to viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwners.java
index c6be3391682..f2dfc030fb2 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwners.java
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwners.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
+package org.apache.causeway.viewer.webcomponents.sample.petclinic.domain;
 
 import java.util.List;
 
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetSpecies.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetSpecies.java
similarity index 91%
copy from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetSpecies.java
copy to viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetSpecies.java
index 587f9f5ebee..e6aca09b473 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetSpecies.java
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetSpecies.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
+package org.apache.causeway.viewer.webcomponents.sample.petclinic.domain;
 
 public enum PetSpecies {
     BIRD,
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/Visit.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/Visit.java
similarity index 98%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/Visit.java
rename to viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/Visit.java
index 4eb48fe98eb..9a79d2e585a 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/Visit.java
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/Visit.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
+package org.apache.causeway.viewer.webcomponents.sample.petclinic.domain;
 
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/VisitCollectionFilterService.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/VisitCollectionFilterService.java
similarity index 96%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/VisitCollectionFilterService.java
rename to viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/VisitCollectionFilterService.java
index f11a09822f9..f6877901235 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/VisitCollectionFilterService.java
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/VisitCollectionFilterService.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
+package org.apache.causeway.viewer.webcomponents.sample.petclinic.domain;
 
 import java.time.format.DateTimeFormatter;
 import java.util.Arrays;
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/VisitRepository.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/VisitRepository.java
similarity index 96%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/VisitRepository.java
rename to viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/VisitRepository.java
index 7dd3e122dd1..750516874de 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/VisitRepository.java
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/VisitRepository.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
+package org.apache.causeway.viewer.webcomponents.sample.petclinic.domain;
 
 import java.time.LocalDateTime;
 import java.util.List;
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/Visits.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/Visits.java
similarity index 95%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/Visits.java
rename to viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/Visits.java
index 13a43b92ec8..13ec845fa13 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/Visits.java
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/Visits.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
+package org.apache.causeway.viewer.webcomponents.sample.petclinic.domain;
 
 import java.util.List;
 
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/Pet.layout.xml b/viewers/webcomponents/sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/Pet.layout.xml
similarity index 100%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/Pet.layout.xml
rename to viewers/webcomponents/sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/Pet.layout.xml
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicHomePage#futureVisits.columnOrder.txt b/viewers/webcomponents/sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicHomePage#futureVisits.columnOrder.txt
similarity index 100%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicHomePage#futureVisits.columnOrder.txt
rename to viewers/webcomponents/sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicHomePage#futureVisits.columnOrder.txt
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicHomePage#petOwners.columnOrder.txt b/viewers/webcomponents/sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicHomePage#petOwners.columnOrder.txt
similarity index 100%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicHomePage#petOwners.columnOrder.txt
rename to viewers/webcomponents/sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicHomePage#petOwners.columnOrder.txt
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicHomePage.layout.xml b/viewers/webcomponents/sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicHomePage.layout.xml
similarity index 100%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicHomePage.layout.xml
rename to viewers/webcomponents/sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicHomePage.layout.xml
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner#pets.columnOrder.txt b/viewers/webcomponents/sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwner#pets.columnOrder.txt
similarity index 100%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner#pets.columnOrder.txt
rename to viewers/webcomponents/sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwner#pets.columnOrder.txt
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner#visits.columnOrder.txt b/viewers/webcomponents/sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwner#visits.columnOrder.txt
similarity index 100%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner#visits.columnOrder.txt
rename to viewers/webcomponents/sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwner#visits.columnOrder.txt
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner.layout.xml b/viewers/webcomponents/sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwner.layout.xml
similarity index 100%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner.layout.xml
rename to viewers/webcomponents/sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetOwner.layout.xml
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/Visit.layout.xml b/viewers/webcomponents/sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/Visit.layout.xml
similarity index 100%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/Visit.layout.xml
rename to viewers/webcomponents/sample-petclinic-domain/src/main/resources/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/Visit.layout.xml
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/index.html b/viewers/webcomponents/sample-vue-petclinic/frontend/index.html
new file mode 100644
index 00000000000..7ae32a7376d
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/index.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<html lang="en-GB">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <meta name="color-scheme" content="light dark">
+  <title>Pet Clinic · Vue</title>
+  <link rel="stylesheet" href="/causeway-webcomponents/component-styles.css">
+  <link rel="stylesheet" href="/causeway-webcomponents/theme.css">
+  <link rel="stylesheet" href="/webjars/font-awesome/7.3.0/css/all.min.css">
+</head>
+<body>
+  <div id="app"></div>
+  <script type="module" src="/src/main.ts"></script>
+</body>
+</html>
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/package-lock.json b/viewers/webcomponents/sample-vue-petclinic/frontend/package-lock.json
new file mode 100644
index 00000000000..e5013527e43
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/package-lock.json
@@ -0,0 +1,1946 @@
+{
+  "name": "@apache-causeway/sample-vue-petclinic",
+  "version": "4.0.0-SNAPSHOT",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "@apache-causeway/sample-vue-petclinic",
+      "version": "4.0.0-SNAPSHOT",
+      "dependencies": {
+        "@apache-causeway/vue-viewer": "file:../../vue",
+        "vue": "3.5.42",
+        "vue-router": "4.6.4"
+      },
+      "devDependencies": {
+        "@types/node": "24.12.0",
+        "@vitejs/plugin-vue": "6.0.8",
+        "typescript": "5.9.3",
+        "vite": "7.3.6",
+        "vitest": "4.1.11",
+        "vue-tsc": "3.2.6"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "../../vue": {
+      "name": "@apache-causeway/vue-viewer",
+      "version": "4.0.0-SNAPSHOT",
+      "license": "Apache-2.0",
+      "devDependencies": {
+        "@types/node": "24.12.0",
+        "@vitejs/plugin-vue": "6.0.8",
+        "@vue/test-utils": "2.4.6",
+        "jsdom": "27.4.0",
+        "typescript": "5.9.3",
+        "vite": "7.3.6",
+        "vite-plugin-dts": "5.1.0",
+        "vitest": "4.1.11",
+        "vue": "3.5.42",
+        "vue-router": "4.6.4",
+        "vue-tsc": "3.2.6"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0",
+        "vue-router": "^4.6.0"
+      }
+    },
+    "node_modules/@apache-causeway/vue-viewer": {
+      "resolved": "../../vue",
+      "link": true
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+      "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+      "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
+      "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.8"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+      "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
+      "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
+      "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
+      "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
+      "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
+      "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
+      "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
+      "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
+      "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
+      "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
+      "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
+      "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
+      "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
+      "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
+      "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
+      "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
+      "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
+      "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
+      "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
+      "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
+      "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
+      "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
+      "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
+      "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
+      "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
+      "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
+      "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+      "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
+      "license": "MIT"
+    },
+    "node_modules/@napi-rs/lzma-linux-x64-gnu": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
+      "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^22.20 || ^24.12 || >=25"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+      "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz",
+      "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz",
+      "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz",
+      "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz",
+      "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz",
+      "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz",
+      "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz",
+      "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz",
+      "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz",
+      "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz",
+      "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz",
+      "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-musl": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz",
+      "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz",
+      "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-musl": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz",
+      "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz",
+      "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz",
+      "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz",
+      "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz",
+      "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz",
+      "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openbsd-x64": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz",
+      "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz",
+      "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz",
+      "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz",
+      "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz",
+      "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz",
+      "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@standard-schema/spec": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+      "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/chai": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+      "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/deep-eql": "*",
+        "assertion-error": "^2.0.1"
+      }
+    },
+    "node_modules/@types/deep-eql": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+      "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+      "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "24.12.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
+      "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.16.0"
+      }
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "6.0.8",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz",
+      "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@rolldown/pluginutils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@vitest/expect": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz",
+      "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@standard-schema/spec": "^1.1.0",
+        "@types/chai": "^5.2.2",
+        "@vitest/spy": "4.1.11",
+        "@vitest/utils": "4.1.11",
+        "chai": "^6.2.2",
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/mocker": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz",
+      "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "4.1.11",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^0.30.21"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "msw": "^2.4.9",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "msw": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitest/pretty-format": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz",
+      "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/runner": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz",
+      "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/utils": "4.1.11",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/snapshot": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz",
+      "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "4.1.11",
+        "@vitest/utils": "4.1.11",
+        "magic-string": "^0.30.21",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/spy": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz",
+      "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/utils": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz",
+      "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "4.1.11",
+        "convert-source-map": "^2.0.0",
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@volar/language-core": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
+      "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/source-map": "2.4.28"
+      }
+    },
+    "node_modules/@volar/source-map": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz",
+      "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@volar/typescript": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz",
+      "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "2.4.28",
+        "path-browserify": "^1.0.1",
+        "vscode-uri": "^3.0.8"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.42.tgz",
+      "integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.8",
+        "@vue/shared": "3.5.42",
+        "entities": "^7.0.1",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-core/node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz",
+      "integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.42",
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz",
+      "integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.8",
+        "@vue/compiler-core": "3.5.42",
+        "@vue/compiler-dom": "3.5.42",
+        "@vue/compiler-ssr": "3.5.42",
+        "@vue/shared": "3.5.42",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.19",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-sfc/node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz",
+      "integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.42",
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/language-core": {
+      "version": "3.2.6",
+      "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.6.tgz",
+      "integrity": "sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "2.4.28",
+        "@vue/compiler-dom": "^3.5.0",
+        "@vue/shared": "^3.5.0",
+        "alien-signals": "^3.0.0",
+        "muggle-string": "^0.4.1",
+        "path-browserify": "^1.0.1",
+        "picomatch": "^4.0.2"
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.42.tgz",
+      "integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.42.tgz",
+      "integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.42",
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz",
+      "integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.42",
+        "@vue/runtime-core": "3.5.42",
+        "@vue/shared": "3.5.42",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.42.tgz",
+      "integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.42",
+        "@vue/runtime-dom": "3.5.42",
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.42.tgz",
+      "integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==",
+      "license": "MIT"
+    },
+    "node_modules/alien-signals": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz",
+      "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/assertion-error": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+      "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/chai": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+      "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "license": "MIT"
+    },
+    "node_modules/entities": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/es-module-lexer": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
+      "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/esbuild": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
+      "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.28.2",
+        "@esbuild/android-arm": "0.28.2",
+        "@esbuild/android-arm64": "0.28.2",
+        "@esbuild/android-x64": "0.28.2",
+        "@esbuild/darwin-arm64": "0.28.2",
+        "@esbuild/darwin-x64": "0.28.2",
+        "@esbuild/freebsd-arm64": "0.28.2",
+        "@esbuild/freebsd-x64": "0.28.2",
+        "@esbuild/linux-arm": "0.28.2",
+        "@esbuild/linux-arm64": "0.28.2",
+        "@esbuild/linux-ia32": "0.28.2",
+        "@esbuild/linux-loong64": "0.28.2",
+        "@esbuild/linux-mips64el": "0.28.2",
+        "@esbuild/linux-ppc64": "0.28.2",
+        "@esbuild/linux-riscv64": "0.28.2",
+        "@esbuild/linux-s390x": "0.28.2",
+        "@esbuild/linux-x64": "0.28.2",
+        "@esbuild/netbsd-arm64": "0.28.2",
+        "@esbuild/netbsd-x64": "0.28.2",
+        "@esbuild/openbsd-arm64": "0.28.2",
+        "@esbuild/openbsd-x64": "0.28.2",
+        "@esbuild/openharmony-arm64": "0.28.2",
+        "@esbuild/sunos-x64": "0.28.2",
+        "@esbuild/win32-arm64": "0.28.2",
+        "@esbuild/win32-ia32": "0.28.2",
+        "@esbuild/win32-x64": "0.28.2"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+      "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "^1.0.0"
+      }
+    },
+    "node_modules/expect-type": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+      "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/muggle-string": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+      "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.18",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+      "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/obug": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
+      "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
+      "dev": true,
+      "funding": [
+        "https://github.com/sponsors/sxzz",
+        "https://opencollective.com/debug"
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.20.0"
+      }
+    },
+    "node_modules/path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+      "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.26",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
+      "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.17",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz",
+      "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.9"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
+        "@rollup/rollup-android-arm-eabi": "4.63.1",
+        "@rollup/rollup-android-arm64": "4.63.1",
+        "@rollup/rollup-darwin-arm64": "4.63.1",
+        "@rollup/rollup-darwin-x64": "4.63.1",
+        "@rollup/rollup-freebsd-arm64": "4.63.1",
+        "@rollup/rollup-freebsd-x64": "4.63.1",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.63.1",
+        "@rollup/rollup-linux-arm-musleabihf": "4.63.1",
+        "@rollup/rollup-linux-arm64-gnu": "4.63.1",
+        "@rollup/rollup-linux-arm64-musl": "4.63.1",
+        "@rollup/rollup-linux-loong64-gnu": "4.63.1",
+        "@rollup/rollup-linux-loong64-musl": "4.63.1",
+        "@rollup/rollup-linux-ppc64-gnu": "4.63.1",
+        "@rollup/rollup-linux-ppc64-musl": "4.63.1",
+        "@rollup/rollup-linux-riscv64-gnu": "4.63.1",
+        "@rollup/rollup-linux-riscv64-musl": "4.63.1",
+        "@rollup/rollup-linux-s390x-gnu": "4.63.1",
+        "@rollup/rollup-linux-x64-gnu": "4.63.1",
+        "@rollup/rollup-linux-x64-musl": "4.63.1",
+        "@rollup/rollup-openbsd-x64": "4.63.1",
+        "@rollup/rollup-openharmony-arm64": "4.63.1",
+        "@rollup/rollup-win32-arm64-msvc": "4.63.1",
+        "@rollup/rollup-win32-ia32-msvc": "4.63.1",
+        "@rollup/rollup-win32-x64-gnu": "4.63.1",
+        "@rollup/rollup-win32-x64-msvc": "4.63.1",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/siginfo": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+      "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/stackback": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+      "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/std-env": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
+      "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tinybench": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+      "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tinyexec": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz",
+      "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.17",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+      "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tinyrainbow": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz",
+      "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "7.16.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+      "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vite": {
+      "version": "7.3.6",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
+      "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.27.0 || ^0.28.0",
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3",
+        "postcss": "^8.5.6",
+        "rollup": "^4.43.0",
+        "tinyglobby": "^0.2.15"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^20.19.0 || >=22.12.0",
+        "jiti": ">=1.21.0",
+        "less": "^4.0.0",
+        "lightningcss": "^1.21.0",
+        "sass": "^1.70.0",
+        "sass-embedded": "^1.70.0",
+        "stylus": ">=0.54.8",
+        "sugarss": "^5.0.0",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vitest": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz",
+      "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/expect": "4.1.11",
+        "@vitest/mocker": "4.1.11",
+        "@vitest/pretty-format": "4.1.11",
+        "@vitest/runner": "4.1.11",
+        "@vitest/snapshot": "4.1.11",
+        "@vitest/spy": "4.1.11",
+        "@vitest/utils": "4.1.11",
+        "es-module-lexer": "^2.0.0",
+        "expect-type": "^1.3.0",
+        "magic-string": "^0.30.21",
+        "obug": "^2.1.1",
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.3",
+        "std-env": "^4.0.0-rc.1",
+        "tinybench": "^2.9.0",
+        "tinyexec": "^1.0.2",
+        "tinyglobby": "^0.2.15",
+        "tinyrainbow": "^3.1.0",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+        "why-is-node-running": "^2.3.0"
+      },
+      "bin": {
+        "vitest": "vitest.mjs"
+      },
+      "engines": {
+        "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "@edge-runtime/vm": "*",
+        "@opentelemetry/api": "^1.9.0",
+        "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+        "@vitest/browser-playwright": "4.1.11",
+        "@vitest/browser-preview": "4.1.11",
+        "@vitest/browser-webdriverio": "4.1.11",
+        "@vitest/coverage-istanbul": "4.1.11",
+        "@vitest/coverage-v8": "4.1.11",
+        "@vitest/ui": "4.1.11",
+        "happy-dom": "*",
+        "jsdom": "*",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@edge-runtime/vm": {
+          "optional": true
+        },
+        "@opentelemetry/api": {
+          "optional": true
+        },
+        "@types/node": {
+          "optional": true
+        },
+        "@vitest/browser-playwright": {
+          "optional": true
+        },
+        "@vitest/browser-preview": {
+          "optional": true
+        },
+        "@vitest/browser-webdriverio": {
+          "optional": true
+        },
+        "@vitest/coverage-istanbul": {
+          "optional": true
+        },
+        "@vitest/coverage-v8": {
+          "optional": true
+        },
+        "@vitest/ui": {
+          "optional": true
+        },
+        "happy-dom": {
+          "optional": true
+        },
+        "jsdom": {
+          "optional": true
+        },
+        "vite": {
+          "optional": false
+        }
+      }
+    },
+    "node_modules/vscode-uri": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz",
+      "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vue": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.42.tgz",
+      "integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.42",
+        "@vue/compiler-sfc": "3.5.42",
+        "@vue/runtime-dom": "3.5.42",
+        "@vue/server-renderer": "3.5.42",
+        "@vue/shared": "3.5.42"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-router": {
+      "version": "4.6.4",
+      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
+      "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/vue-tsc": {
+      "version": "3.2.6",
+      "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.6.tgz",
+      "integrity": "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/typescript": "2.4.28",
+        "@vue/language-core": "3.2.6"
+      },
+      "bin": {
+        "vue-tsc": "bin/vue-tsc.js"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.0"
+      }
+    },
+    "node_modules/why-is-node-running": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+      "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "siginfo": "^2.0.0",
+        "stackback": "0.0.2"
+      },
+      "bin": {
+        "why-is-node-running": "cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    }
+  }
+}
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/package.json b/viewers/webcomponents/sample-vue-petclinic/frontend/package.json
new file mode 100644
index 00000000000..17a054b3c33
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/package.json
@@ -0,0 +1,27 @@
+{
+  "name": "@apache-causeway/sample-vue-petclinic",
+  "version": "4.0.0-SNAPSHOT",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "build": "vue-tsc --noEmit && vite build",
+    "dev": "vite",
+    "test": "vitest run"
+  },
+  "dependencies": {
+    "@apache-causeway/vue-viewer": "file:../../vue",
+    "vue": "3.5.42",
+    "vue-router": "4.6.4"
+  },
+  "devDependencies": {
+    "@types/node": "24.12.0",
+    "@vitejs/plugin-vue": "6.0.8",
+    "typescript": "5.9.3",
+    "vite": "7.3.6",
+    "vitest": "4.1.11",
+    "vue-tsc": "3.2.6"
+  },
+  "engines": {
+    "node": ">=20"
+  }
+}
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/src/App.vue b/viewers/webcomponents/sample-vue-petclinic/frontend/src/App.vue
new file mode 100644
index 00000000000..9b0df234a7a
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/src/App.vue
@@ -0,0 +1,59 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<script setup lang="ts">
+import {ref} from 'vue';
+import {RouterView, useRoute} from 'vue-router';
+import {useCausewayShell, useCausewayViewer} from '@apache-causeway/vue-viewer';
+
+const viewer = useCausewayViewer();
+const route = useRoute();
+const shell = useCausewayShell(ref<HTMLElement | null>(null));
+</script>
+
+<template>
+  <div ref="shell" class="petclinic-vue-shell" data-testid="petclinic-vue-application-shell">
+    <a class="petclinic-skip-link" href="#causeway-vue-route">Skip to main content</a>
+    <cw-graphql-client data-causeway-shell-client :endpoint="viewer.endpoint">
+      <header class="petclinic-navbar">
+        <a class="petclinic-brand" href="/vue/" @click.prevent="viewer.router.push('/')">
+          <span aria-hidden="true">C</span>
+          <span>Pet Clinic</span>
+        </a>
+        <cw-menubars />
+      </header>
+      <div data-causeway-route-loading class="petclinic-route-loading" role="status" aria-live="polite" hidden>
+        Loading page…
+      </div>
+      <div data-causeway-route-announcement class="petclinic-visually-hidden" aria-live="polite" aria-atomic="true" />
+      <cw-action-results
+        data-causeway-shell-result
+        class="petclinic-shell-results"
+        aria-label="Application action results"
+        aria-live="polite"
+        hidden
+      />
+      <main id="causeway-vue-route" data-causeway-router-view class="petclinic-main" aria-busy="false" tabindex="-1">
+        <RouterView v-slot="{ Component }">
+          <component :is="Component" :key="route.path" />
+        </RouterView>
+      </main>
+      <footer class="petclinic-footer">Powered by Apache Causeway · Vue viewer</footer>
+    </cw-graphql-client>
+  </div>
+</template>
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetSpecies.java b/viewers/webcomponents/sample-vue-petclinic/frontend/src/env.d.ts
similarity index 77%
copy from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetSpecies.java
copy to viewers/webcomponents/sample-vue-petclinic/frontend/src/env.d.ts
index 587f9f5ebee..14a6ee33751 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetSpecies.java
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/src/env.d.ts
@@ -16,13 +16,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
 
-public enum PetSpecies {
-    BIRD,
-    CAT,
-    DOG,
-    HAMSTER,
-    LIZARD,
-    SNAKE
+/// <reference types="vite/client" />
+
+declare module '*.vue' {
+  import type {DefineComponent} from 'vue';
+  const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>;
+  export default component;
 }
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/src/main.ts b/viewers/webcomponents/sample-vue-petclinic/frontend/src/main.ts
new file mode 100644
index 00000000000..aa1e9bb0cb3
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/src/main.ts
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {createApp} from 'vue';
+import {createRouter, createWebHistory} from 'vue-router';
+import {
+  createCausewayRouteRecords,
+  createCausewayVueViewer
+} from '@apache-causeway/vue-viewer';
+import '@apache-causeway/vue-viewer/theme.css';
+import App from './App.vue';
+import PetOwnerPage from './pages/PetOwnerPage.vue';
+import './petclinic.css';
+
+const toolkit = new URLSearchParams(location.search).get('toolkit') === 'native' ? 'native' : 'vaadin';
+const policy = document.documentElement.dataset;
+policy.causewayComponentToolkit = toolkit;
+policy.causewayPresentation = toolkit;
+policy.causewayActionButtons = toolkit;
+policy.causewayReferenceWidgets = toolkit;
+policy.causewayCollectionGrid = toolkit;
+policy.causewayApplicationMenubar = toolkit;
+policy.causewayFieldFamilies = toolkit === 'vaadin' ? 'basic,numeric,local-temporal' : '';
+policy.causewayReferenceMinimumSearchLength = '2';
+policy.causewayReferenceMaximumResults = '50';
+
+const componentsModuleUrl = '/causeway-webcomponents/index.mjs';
+await import(/* @vite-ignore */ componentsModuleUrl);
+
+const router = createRouter({
+  history: createWebHistory('/vue/'),
+  routes: createCausewayRouteRecords()
+});
+const viewer = createCausewayVueViewer({
+  router,
+  endpoint: '/graphql',
+  basePath: '/vue',
+  pages: {
+    'petclinic.PetOwner': PetOwnerPage
+  },
+  policies: {
+    error(error) {
+      console.error('Causeway Vue viewer policy error', error);
+    }
+  }
+});
+
+const app = createApp(App).use(router).use(viewer.plugin);
+await router.isReady();
+app.mount('#app');
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/PetOwnerPage.vue b/viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/PetOwnerPage.vue
new file mode 100644
index 00000000000..72b59f2f0a0
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/PetOwnerPage.vue
@@ -0,0 +1,119 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<script setup lang="ts">
+import type {CausewayRoutePageProps} from '@apache-causeway/vue-viewer';
+
+defineProps<CausewayRoutePageProps>();
+</script>
+
+<template>
+  <section
+    class="causeway-vue-route-page petclinic-owner-page"
+    data-causeway-route-page
+    data-testid="petclinic-vue-owner-page"
+    data-page-kind="pet-owner"
+    data-route-state="loading"
+    tabindex="-1"
+    aria-label="Pet owner page"
+  >
+    <cw-object-context
+      data-causeway-route-context
+      :logical-type="logicalTypeName"
+      :object-id="objectId"
+    >
+      <cw-action-results
+        data-causeway-page-result
+        class="petclinic-card"
+        aria-label="Pet owner action results"
+        hidden
+      />
+      <cw-breadcrumbs data-testid="petclinic-vue-breadcrumbs" />
+      <div class="petclinic-owner-heading">
+        <cw-object-header />
+        <div class="petclinic-toolbar" aria-label="Owner actions">
+          <cw-action id="allOwners" named="Show all owners" />
+          <cw-action id="relatedOwners" named="Show related owners">
+            <cw-standalone-collection named="Related owners">
+              <cw-collection-column id="name" label="Owner" />
+              <cw-collection-column id="knownAs" label="Known as" />
+            </cw-standalone-collection>
+          </cw-action>
+          <cw-action id="delete" named="Remove this owner" />
+        </div>
+      </div>
+
+      <div class="petclinic-owner-grid">
+        <section class="petclinic-card" aria-labelledby="owner-details-heading">
+          <h2 id="owner-details-heading">Owner details</h2>
+          <cw-property id="name" named="Full name">
+            <cw-action id="updateName" named="Change the owner's name" prompt-style="INLINE">
+              <cw-parameter id="name" named="Owner's full name" />
+            </cw-action>
+          </cw-property>
+          <cw-property id="knownAs" editable />
+          <cw-property id="telephoneNumber" editable />
+          <cw-property id="emailAddress" editable />
+          <cw-property id="notes" editable multi-line="5" />
+          <cw-property id="lastVisit" editable label-position="TOP" min="2000-01-01" max="today" />
+        </section>
+
+        <section class="petclinic-card" aria-labelledby="owner-pets-heading">
+          <h2 id="owner-pets-heading">Pets</h2>
+          <cw-collection id="pets" named="Companion animals" active paged="5" sortable filterable>
+            <cw-peek>
+              <section class="petclinic-preview" aria-label="Pet preview">
+                <cw-object-header />
+                <cw-property id="name" />
+                <cw-property id="species" />
+                <cw-property id="notes" editable multi-line="3" />
+                <cw-action id="clearNotes" named="Clear pet notes" />
+                <cw-collection id="visits" named="Pet visits" active paged="10">
+                  <cw-collection-column id="visitAt" label="When" />
+                  <cw-collection-column id="reason" label="Reason" />
+                </cw-collection>
+              </section>
+            </cw-peek>
+            <cw-collection-column id="name" label="Name" />
+            <cw-collection-column id="species" label="Species" />
+            <cw-collection-column id="notes" label="Notes" />
+            <cw-action id="addPet" named="Register a pet" prompt-style="DIALOG_SIDEBAR">
+              <cw-parameter id="name" named="Pet name" />
+            </cw-action>
+            <cw-action id="removePet" />
+          </cw-collection>
+        </section>
+
+        <section class="petclinic-card" aria-labelledby="owner-visits-heading">
+          <h2 id="owner-visits-heading">Visits</h2>
+          <cw-collection id="visits" named="Visit history" active paged="8">
+            <cw-peek />
+            <cw-collection-column id="visitAt" label="When" />
+            <cw-collection-column id="reason" label="Reason" />
+            <cw-action id="bookVisit" prompt-style="DIALOG_MODAL">
+              <cw-parameter id="visitDate" min="tomorrow" />
+              <cw-parameter id="visitTime" min="08:00" max="17:00" />
+              <cw-parameter id="reason" multi-line="3" />
+            </cw-action>
+          </cw-collection>
+        </section>
+      </div>
+      <cw-interaction-controller data-causeway-route-interactions />
+    </cw-object-context>
+  </section>
+</template>
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/src/petclinic.css b/viewers/webcomponents/sample-vue-petclinic/frontend/src/petclinic.css
new file mode 100644
index 00000000000..f44f742a475
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/src/petclinic.css
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+* { box-sizing: border-box; }
+body { margin: 0; background: var(--causeway-color-surface-subtle, #f3f6fa); font-family: system-ui, sans-serif; }
+.petclinic-vue-shell { min-height: 100vh; }
+.petclinic-skip-link { position: fixed; left: 1rem; top: -5rem; z-index: 1000; }
+.petclinic-skip-link:focus { top: 1rem; padding: 0.7rem; background: white; color: #075ea8; }
+.petclinic-navbar { display: flex; align-items: center; gap: 1.5rem; padding: 0.75rem clamp(1rem, 4vw, 3rem); background: #183b65; color: white; }
+.petclinic-brand { display: inline-flex; align-items: center; gap: 0.6rem; color: white; font-size: 1.2rem; font-weight: 700; text-decoration: none; }
+.petclinic-brand > span:first-child { display: grid; width: 2rem; height: 2rem; place-items: center; border-radius: 50%; background: #fff; color: #183b65; }
+.petclinic-navbar cw-menubars { flex: 1 1 auto; min-width: 0; }
+.petclinic-route-loading { padding: 0.5rem 2rem; background: #fff4ce; color: #543f00; }
+.petclinic-main { width: 100%; min-height: 65vh; padding: clamp(1rem, 3vw, 2rem); }
+.petclinic-owner-heading { display: flex; align-items: start; justify-content: space-between; gap: 1rem; }
+.petclinic-toolbar { display: flex; flex-wrap: wrap; justify-content: end; gap: 0.5rem; }
+.petclinic-owner-grid { display: grid; grid-template-columns: minmax(18rem, 0.8fr) minmax(25rem, 1.2fr); gap: 1rem; align-items: start; }
+.petclinic-card { display: block; min-width: 0; margin-block: 1rem; padding: 1rem; border: 1px solid #cbd5e1; border-radius: 0.5rem; background: var(--causeway-vue-surface, white); }
+.petclinic-preview { padding: 1rem; border-inline-start: 0.25rem solid #4d7cab; }
+.petclinic-shell-results { margin: 1rem clamp(1rem, 3vw, 2rem) 0; }
+.petclinic-footer { padding: 1rem 2rem; color: #526176; text-align: center; }
+.petclinic-visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
+
+@media (max-width: 48rem) {
+  .petclinic-navbar, .petclinic-owner-heading { align-items: stretch; flex-direction: column; }
+  .petclinic-toolbar { justify-content: start; }
+  .petclinic-owner-grid { grid-template-columns: minmax(0, 1fr); }
+  .petclinic-main { padding: 0.75rem; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+  *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
+}
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/tsconfig.json b/viewers/webcomponents/sample-vue-petclinic/frontend/tsconfig.json
new file mode 100644
index 00000000000..0b66a6e85fb
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/tsconfig.json
@@ -0,0 +1,14 @@
+{
+  "compilerOptions": {
+    "target": "ES2022",
+    "module": "ESNext",
+    "moduleResolution": "Bundler",
+    "strict": true,
+    "jsx": "preserve",
+    "lib": ["ES2022", "DOM", "DOM.Iterable"],
+    "types": ["vite/client"],
+    "skipLibCheck": true,
+    "useDefineForClassFields": true
+  },
+  "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
+}
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicDomainModule.java b/viewers/webcomponents/sample-vue-petclinic/frontend/vite.config.ts
similarity index 62%
copy from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicDomainModule.java
copy to viewers/webcomponents/sample-vue-petclinic/frontend/vite.config.ts
index 08fffd69475..444c43ca133 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicDomainModule.java
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/vite.config.ts
@@ -16,16 +16,26 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
 
-import org.springframework.boot.persistence.autoconfigure.EntityScan;
-import org.springframework.context.annotation.ComponentScan;
-import org.springframework.context.annotation.Configuration;
+import {resolve} from 'node:path';
+import vue from '@vitejs/plugin-vue';
+import {defineConfig} from 'vite';
 
-@Configuration
-@ComponentScan
-@EntityScan(basePackageClasses = PetClinicDomainModule.class)
-public class PetClinicDomainModule {
-
-    public static final String NAMESPACE = "petclinic";
-}
+export default defineConfig({
+  base: '/vue/',
+  resolve: {
+    dedupe: ['vue', 'vue-router']
+  },
+  plugins: [vue({
+    template: {
+      compilerOptions: {
+        isCustomElement: tag => tag.startsWith('cw-')
+      }
+    }
+  })],
+  build: {
+    outDir: resolve(import.meta.dirname, '../src/main/resources/static/vue'),
+    emptyOutDir: true,
+    sourcemap: true
+  }
+});
diff --git a/viewers/webcomponents/sample-htmx-petclinic/pom.xml b/viewers/webcomponents/sample-vue-petclinic/pom.xml
similarity index 56%
copy from viewers/webcomponents/sample-htmx-petclinic/pom.xml
copy to viewers/webcomponents/sample-vue-petclinic/pom.xml
index cad28135975..9123061cb84 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/pom.xml
+++ b/viewers/webcomponents/sample-vue-petclinic/pom.xml
@@ -29,15 +29,15 @@
         <version>4.0.0-SNAPSHOT</version>
     </parent>
 
-    <artifactId>causeway-viewer-webcomponents-sample-htmx-petclinic</artifactId>
-    <name>Apache Causeway Viewer - Web Components (HTMX Petclinic Sample)</name>
+    <artifactId>causeway-viewer-webcomponents-sample-vue-petclinic</artifactId>
+    <name>Apache Causeway Viewer - Web Components (Vue Petclinic Sample)</name>
 
     <properties>
         <maven.install.skip>true</maven.install.skip>
         <maven.deploy.skip>true</maven.deploy.skip>
-        <jar-plugin.automaticModuleName>org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic</jar-plugin.automaticModuleName>
-        <git-plugin.propertiesDir>org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic</git-plugin.propertiesDir>
-        <spring-boot.run.skip>${sample-htmx-petclinic.run.skip}</spring-boot.run.skip>
+        <jar-plugin.automaticModuleName>org.apache.causeway.viewer.webcomponents.sample.vue.petclinic</jar-plugin.automaticModuleName>
+        <git-plugin.propertiesDir>org/apache/causeway/viewer/webcomponents/sample/vue/petclinic</git-plugin.propertiesDir>
+        <spring-boot.run.skip>${sample-vue-petclinic.run.skip}</spring-boot.run.skip>
         <playwright.version>1.61.0</playwright.version>
     </properties>
 
@@ -49,28 +49,26 @@
         </dependency>
         <dependency>
             <groupId>org.apache.causeway.viewer</groupId>
-            <artifactId>causeway-viewer-graphql-viewer</artifactId>
+            <artifactId>causeway-viewer-webcomponents-sample-petclinic-domain</artifactId>
+            <version>${project.version}</version>
         </dependency>
         <dependency>
             <groupId>org.apache.causeway.viewer</groupId>
-            <artifactId>causeway-viewer-webcomponents-htmx</artifactId>
+            <artifactId>causeway-viewer-graphql-viewer</artifactId>
         </dependency>
         <dependency>
             <groupId>org.apache.causeway.viewer</groupId>
-            <artifactId>causeway-viewer-webcomponents-htmx-security-secman</artifactId>
-            <version>${project.version}</version>
+            <artifactId>causeway-viewer-webcomponents-foundation</artifactId>
         </dependency>
         <dependency>
-            <groupId>org.apache.causeway.extensions</groupId>
-            <artifactId>causeway-extensions-secman-persistence-jpa</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.causeway.extensions</groupId>
-            <artifactId>causeway-extensions-secman-encryption-spring</artifactId>
+            <groupId>org.apache.causeway.viewer</groupId>
+            <artifactId>causeway-viewer-webcomponents-vue</artifactId>
+            <version>${project.version}</version>
         </dependency>
         <dependency>
-            <groupId>org.apache.causeway.viewer</groupId>
-            <artifactId>causeway-viewer-wicket-viewer</artifactId>
+            <groupId>org.webjars</groupId>
+            <artifactId>font-awesome</artifactId>
+            <version>7.3.0</version>
         </dependency>
         <dependency>
             <groupId>org.apache.causeway.persistence</groupId>
@@ -85,7 +83,6 @@
             <artifactId>h2</artifactId>
             <scope>runtime</scope>
         </dependency>
-
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-test</artifactId>
@@ -103,9 +100,75 @@
         </dependency>
     </dependencies>
 
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes combine.children="append">
+                        <exclude>frontend/package-lock.json</exclude>
+                        <exclude>src/main/resources/static/vue/assets/**</exclude>
+                        <exclude>src/main/resources/static/vue/index.html</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-antrun-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>verify-vue-petclinic-assets</id>
+                        <phase>validate</phase>
+                        <goals><goal>run</goal></goals>
+                        <configuration>
+                            <target>
+                                <available file="${project.basedir}/src/main/resources/static/vue/index.html" property="vue.petclinic.assets.present"/>
+                                <fail unless="vue.petclinic.assets.present" message="Vue Petclinic assets are missing; run Maven with -Pregenerate-vue-petclinic."/>
+                            </target>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <executions><execution><goals><goal>repackage</goal></goals></execution></executions>
+            </plugin>
+        </plugins>
+    </build>
+
     <profiles>
         <profile>
-            <id>playwright</id>
+            <id>regenerate-vue-petclinic</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-antrun-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>regenerate-vue-petclinic</id>
+                                <phase>generate-resources</phase>
+                                <goals><goal>run</goal></goals>
+                                <configuration>
+                                    <target>
+                                        <exec executable="npm" dir="${project.basedir}/frontend" failonerror="true">
+                                            <arg value="ci"/><arg value="--ignore-scripts"/><arg value="--no-audit"/><arg value="--no-fund"/>
+                                        </exec>
+                                        <exec executable="npm" dir="${project.basedir}/frontend" failonerror="true">
+                                            <arg value="run"/><arg value="build"/>
+                                        </exec>
+                                    </target>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+        <profile>
+            <id>playwright-vue</id>
             <dependencies>
                 <dependency>
                     <groupId>com.microsoft.playwright</groupId>
@@ -124,14 +187,8 @@
                             <execution>
                                 <id>add-playwright-test-source</id>
                                 <phase>generate-test-sources</phase>
-                                <goals>
-                                    <goal>add-test-source</goal>
-                                </goals>
-                                <configuration>
-                                    <sources>
-                                        <source>src/playwright/java</source>
-                                    </sources>
-                                </configuration>
+                                <goals><goal>add-test-source</goal></goals>
+                                <configuration><sources><source>src/playwright/java</source></sources></configuration>
                             </execution>
                         </executions>
                     </plugin>
@@ -140,20 +197,4 @@
         </profile>
     </profiles>
 
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.springframework.boot</groupId>
-                <artifactId>spring-boot-maven-plugin</artifactId>
-                <executions>
-                    <execution>
-                        <goals>
-                            <goal>repackage</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
-        </plugins>
-    </build>
-
 </project>
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxApplication.java b/viewers/webcomponents/sample-vue-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVueApplication.java
similarity index 66%
copy from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxApplication.java
copy to viewers/webcomponents/sample-vue-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVueApplication.java
index 422e5a1421f..a083d6a24d8 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxApplication.java
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVueApplication.java
@@ -16,26 +16,20 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic;
+package org.apache.causeway.viewer.webcomponents.sample.vue.petclinic;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
-import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Import;
 import org.springframework.context.annotation.PropertySource;
 import org.springframework.context.annotation.PropertySources;
-import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-import org.springframework.security.web.SecurityFilterChain;
-
 import org.apache.causeway.core.config.presets.CausewayPresets;
 import org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
 import org.apache.causeway.persistence.jpa.eclipselink.CausewayModulePersistenceJpaEclipselink;
 import org.apache.causeway.security.bypass.CausewayModuleSecurityBypass;
 import org.apache.causeway.viewer.graphql.viewer.CausewayModuleViewerGraphqlViewer;
-import org.apache.causeway.viewer.webcomponents.htmx.CausewayModuleViewerWebcomponentsHtmx;
-import org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain.PetClinicDomainModule;
-import org.apache.causeway.viewer.wicket.viewer.CausewayModuleViewerWicketViewer;
+import org.apache.causeway.viewer.webcomponents.sample.petclinic.domain.PetClinicDomainModule;
 
 @SpringBootApplication
 @Import({
@@ -43,8 +37,6 @@
         CausewayModuleSecurityBypass.class,
         CausewayModulePersistenceJpaEclipselink.class,
         CausewayModuleViewerGraphqlViewer.class,
-        CausewayModuleViewerWebcomponentsHtmx.class,
-        CausewayModuleViewerWicketViewer.class,
         PetClinicDomainModule.class
 })
 @PropertySources({
@@ -52,17 +44,9 @@
         @PropertySource(CausewayPresets.SilenceMetaModel),
         @PropertySource(CausewayPresets.SilenceProgrammingModel)
 })
-public class PetClinicHtmxApplication extends SpringBootServletInitializer {
+public class PetClinicVueApplication extends SpringBootServletInitializer {
 
     public static void main(final String[] args) {
-        SpringApplication.run(PetClinicHtmxApplication.class, args);
-    }
-
-    @Bean
-    SecurityFilterChain petClinicPermissiveSecurityFilterChain(final HttpSecurity http) throws Exception {
-        return http
-                .authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll())
-                .csrf(csrf -> csrf.disable())
-                .build();
+        SpringApplication.run(PetClinicVueApplication.class, args);
     }
 }
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicDomainModule.java b/viewers/webcomponents/sample-vue-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVueRouteController.java
similarity index 64%
copy from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicDomainModule.java
copy to viewers/webcomponents/sample-vue-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVueRouteController.java
index 08fffd69475..d97bac62779 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicDomainModule.java
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVueRouteController.java
@@ -16,16 +16,21 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
+package org.apache.causeway.viewer.webcomponents.sample.vue.petclinic;
 
-import org.springframework.boot.persistence.autoconfigure.EntityScan;
-import org.springframework.context.annotation.ComponentScan;
-import org.springframework.context.annotation.Configuration;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
 
-@Configuration
-@ComponentScan
-@EntityScan(basePackageClasses = PetClinicDomainModule.class)
-public class PetClinicDomainModule {
+@Controller
+class PetClinicVueRouteController {
 
-    public static final String NAMESPACE = "petclinic";
+    @GetMapping({
+            "/vue",
+            "/vue/",
+            "/vue/object/{logicalType}/{objectId}",
+            "/vue/invalid-route"
+    })
+    String vueApplication() {
+        return "forward:/vue/index.html";
+    }
 }
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/application.properties b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/application.properties
new file mode 100644
index 00000000000..645b51d6a75
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/application.properties
@@ -0,0 +1,25 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+spring.application.name=Causeway Pet Clinic Vue
+spring.graphql.path=/graphql
+spring.graphql.graphiql.enabled=true
+causeway.viewer.graphql.api-scope=ALL
+causeway.viewer.graphql.schema-style=SIMPLE_AND_RICH
+causeway.viewer.graphql.resources.structural-metadata-response-type=DIRECT
+causeway.viewer.graphql.scalar-marshaller.local-time-format=HH:mm[:ss]
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-BntyzeMN.js b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-BntyzeMN.js
new file mode 100644
index 00000000000..dcbb1709f55
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-BntyzeMN.js
@@ -0,0 +1,2 @@
+(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))r(s);new MutationObserver(s=>{for(const i of s)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(o)}).observe(document,{childList:!0,subtree:!0});function n(s){const i={};return s.integrity&&(i.integrity=s.integrity),s.referrerPolicy&&(i.referrerPolicy=s.referr [...]
+//# sourceMappingURL=index-BntyzeMN.js.map
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-BntyzeMN.js.map b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-BntyzeMN.js.map
new file mode 100644
index 00000000000..68a543c41f2
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-BntyzeMN.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"index-BntyzeMN.js","sources":["../../../../../../frontend/node_modules/@vue/shared/dist/shared.esm-bundler.js","../../../../../../frontend/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js","../../../../../../frontend/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js","../../../../../../frontend/node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js","../../../../../../frontend/node_modules/vue-router/dist/devtools-EWN81iOl.mjs","../../ [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-BuWcbzr4.css b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-BuWcbzr4.css
new file mode 100644
index 00000000000..11356c7e0ed
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-BuWcbzr4.css
@@ -0,0 +1 @@
+:root{--causeway-vue-surface: #ffffff;--causeway-vue-text: #1f2937;--causeway-vue-muted: #596579;--causeway-vue-border: #d7dee8;--causeway-vue-danger: #9b1c1c;--causeway-vue-focus: #075ea8;color-scheme:light dark}.causeway-vue-route-page{box-sizing:border-box;min-width:0;width:100%;color:var(--causeway-vue-text)}.causeway-vue-route-page:focus-visible,[data-causeway-router-view]:focus-visible{outline:.2rem solid var(--causeway-vue-focus);outline-offset:.2rem}.causeway-vue-status{margin:1r [...]
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html
new file mode 100644
index 00000000000..9ad4ffdc303
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<html lang="en-GB">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <meta name="color-scheme" content="light dark">
+  <title>Pet Clinic · Vue</title>
+  <link rel="stylesheet" href="/causeway-webcomponents/component-styles.css">
+  <link rel="stylesheet" href="/causeway-webcomponents/theme.css">
+  <link rel="stylesheet" href="/webjars/font-awesome/7.3.0/css/all.min.css">
+  <script type="module" crossorigin src="/vue/assets/index-BntyzeMN.js"></script>
+  <link rel="stylesheet" crossorigin href="/vue/assets/index-BuWcbzr4.css">
+</head>
+<body>
+  <div id="app"></div>
+</body>
+</html>
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVuePlaywrightTest.java b/viewers/webcomponents/sample-vue-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVuePlaywrightTest.java
new file mode 100644
index 00000000000..e94e1d5422b
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVuePlaywrightTest.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.webcomponents.sample.vue.petclinic;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.microsoft.playwright.Browser;
+import com.microsoft.playwright.BrowserType;
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.Playwright;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest(
+        classes = PetClinicVueApplication.class,
+        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class PetClinicVuePlaywrightTest {
+
+    @LocalServerPort
+    private int port;
+
+    private final List<String> browserFailures = new ArrayList<>();
+    private Playwright playwright;
+    private Browser browser;
+    private Page page;
+
+    @BeforeAll
+    void startBrowser() {
+        playwright = Playwright.create();
+        final var options = new BrowserType.LaunchOptions()
+                .setHeadless(Boolean.parseBoolean(System.getProperty("playwright.headless", "true")));
+        final var executable = System.getProperty("playwright.chromium.executable", "").trim();
+        if (!executable.isEmpty()) {
+            options.setExecutablePath(Path.of(executable));
+        }
+        browser = playwright.chromium().launch(options);
+    }
+
+    @AfterAll
+    void stopBrowser() {
+        if (browser != null) browser.close();
+        if (playwright != null) playwright.close();
+    }
+
+    @BeforeEach
+    void openPage() {
+        browserFailures.clear();
+        page = browser.newPage(new Browser.NewPageOptions().setViewportSize(1440, 900));
+        page.onPageError(error -> browserFailures.add(error));
+        page.onConsoleMessage(message -> {
+            if ("error".equals(message.type()) && !message.text().contains("favicon.ico")) {
+                browserFailures.add(message.text());
+            }
+        });
+    }
+
+    @AfterEach
+    void closePage() {
+        assertThat(browserFailures).isEmpty();
+        page.close();
+    }
+
+    @Test
+    void customOwnerPageUsesOneStableShellAndDeclarativeRouteBoundary() {
+        open("/vue/object/petclinic.PetOwner/s_owner-mary");
+        page.locator("[data-causeway-route-page][data-route-state='ready']").waitFor();
+
+        assertThat(page.locator("[data-testid='petclinic-vue-application-shell']").count()).isEqualTo(1);
+        assertThat(page.locator("cw-graphql-client[data-causeway-shell-client]").count()).isEqualTo(1);
+        assertThat(page.locator("[data-causeway-route-page][data-route-state='ready']").count()).isEqualTo(1);
+        assertThat(page.locator("cw-object-context[data-causeway-route-context]").count()).isEqualTo(1);
+        assertThat(page.locator("cw-interaction-controller[data-causeway-route-interactions]").count()).isEqualTo(1);
+        assertThat(page.locator("body").innerText()).contains("Mary Smith (Mary)", "Pets currently registered");
+    }
+
+    @Test
+    void semanticNavigationUsesGenericFallbackAndBrowserHistory() {
+        open("/vue/object/petclinic.PetOwner/s_owner-mary");
+        page.locator("[data-route-state='ready']").waitFor();
+        page.evaluate("""
+                document.querySelector('[data-testid="petclinic-vue-application-shell"]').dispatchEvent(
+                  new CustomEvent('causeway-navigation-request', {
+                    bubbles: true,
+                    composed: true,
+                    cancelable: true,
+                    detail: {target: {logicalTypeName: 'petclinic.Pet', id: 's_pet-basil'}}
+                  }))
+                """);
+        page.waitForURL("**/vue/object/petclinic.Pet/s_pet-basil");
+        page.locator("[data-page-kind='generic'][data-route-state='ready']").waitFor();
+        page.waitForFunction("document.body.innerText.includes('Basil')");
+        assertThat(page.locator("body").innerText()).contains("Basil");
+
+        page.goBack();
+        page.locator("[data-page-kind='pet-owner'][data-route-state='ready']").waitFor();
+        assertThat(page.url()).endsWith("/vue/object/petclinic.PetOwner/s_owner-mary");
+
+        page.goForward();
+        page.locator("[data-page-kind='generic'][data-route-state='ready']").waitFor();
+        assertThat(page.url()).endsWith("/vue/object/petclinic.Pet/s_pet-basil");
+    }
+
+    @Test
+    void scalarActionResultsUseTheActivePageOutlet() {
+        open("/vue/object/petclinic.PetOwner/s_owner-mary");
+        page.locator("[data-route-state='ready']").waitFor();
+        page.evaluate("""
+                const source = document.querySelector('cw-action#allOwners');
+                source.dispatchEvent(new CustomEvent('causeway-action-request', {
+                  bubbles: true, composed: true, detail: {actionId: 'allOwners'}
+                }));
+                source.dispatchEvent(new CustomEvent('causeway-action-result', {
+                  bubbles: true, composed: true,
+                  detail: {actionId: 'allOwners', result: {kind: 'scalar', value: 'Completed in Vue'}}
+                }))
+                """);
+        page.waitForFunction("document.body.innerText.includes('Completed in Vue')");
+        assertThat(page.locator("cw-action-results[data-causeway-page-result]").innerText())
+                .contains("allOwners result", "Completed in Vue");
+    }
+
+    @Test
+    void directRefreshAndInvalidRoutesRemainWithinTheApplicationShell() {
+        open("/vue/object/petclinic.Pet/s_pet-basil");
+        page.locator("[data-page-kind='generic'][data-route-state='ready']").waitFor();
+        page.reload();
+        page.locator("[data-page-kind='generic'][data-route-state='ready']").waitFor();
+        assertThat(page.locator("cw-graphql-client[data-causeway-shell-client]").count()).isEqualTo(1);
+
+        open("/vue/invalid-route");
+        page.locator("[data-route-state='invalid-route']").waitFor();
+        assertThat(page.locator("[data-route-state='invalid-route']").count()).isEqualTo(1);
+        assertThat(page.locator("body").innerText()).contains("Invalid route");
+
+        open("/vue/object/petclinic.Pet/s_pet-does-not-exist");
+        page.locator("[data-route-state='unavailable']").waitFor();
+        assertThat(page.locator("body").innerText()).doesNotContain("authorization", "stack trace");
+    }
+
+    @Test
+    void bothToolkitPoliciesPreserveAccessibleShellLandmarksAndMenus() {
+        for (final String toolkit : List.of("native", "vaadin")) {
+            open("/vue/?toolkit=" + toolkit);
+            assertThat(page.locator("html").getAttribute("data-causeway-component-toolkit")).isEqualTo(toolkit);
+            assertThat(page.locator("header").count()).isEqualTo(1);
+            assertThat(page.locator("main#causeway-vue-route").count()).isEqualTo(1);
+            assertThat(page.locator("footer").count()).isEqualTo(1);
+            page.locator("cw-menubars[data-menu-state='ready']").waitFor();
+            assertThat(page.locator("cw-menubars[data-menu-state='ready']").count()).isEqualTo(1);
+            final var skipLink = page.locator("a[href='#causeway-vue-route']");
+            assertThat(skipLink.innerText()).isEqualTo("Skip to main content");
+            skipLink.focus();
+            page.keyboard().press("Enter");
+            assertThat(page.evaluate("document.activeElement?.id")).isEqualTo("causeway-vue-route");
+        }
+    }
+
+    private void open(final String path) {
+        page.navigate("http://localhost:" + port + path);
+        page.locator("[data-testid='petclinic-vue-application-shell']").waitFor();
+    }
+}
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/test/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVueApplicationIntegTest.java b/viewers/webcomponents/sample-vue-petclinic/src/test/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVueApplicationIntegTest.java
new file mode 100644
index 00000000000..69d9918f09f
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/test/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVueApplicationIntegTest.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.webcomponents.sample.vue.petclinic;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.regex.Pattern;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+
+import org.apache.causeway.viewer.webcomponents.sample.petclinic.domain.PetOwner;
+import org.apache.causeway.viewer.webcomponents.sample.petclinic.domain.PetOwnerRepository;
+import org.apache.causeway.viewer.webcomponents.sample.petclinic.domain.VisitRepository;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest(
+        classes = PetClinicVueApplication.class,
+        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+class PetClinicVueApplicationIntegTest {
+
+    private static final Pattern SCRIPT_SOURCE = Pattern.compile("src=\"(/vue/assets/[^\"]+\\.js)\"");
+
+    @LocalServerPort
+    private int port;
+
+    @Autowired
+    private PetOwnerRepository ownerRepository;
+
+    @Autowired
+    private VisitRepository visitRepository;
+
+    @Test
+    void reusesDeterministicPetclinicDomain() {
+        final var mary = ownerRepository.findById(PetOwner.MARY_ID);
+        assertThat(mary).isNotNull();
+        assertThat(mary.getName()).isEqualTo("Mary Smith");
+        assertThat(ownerRepository.findAll()).hasSize(10);
+        assertThat(visitRepository.findByPetOwner(mary)).hasSize(2);
+    }
+
+    @Test
+    void servesProductionVueApplicationAndCanonicalHistoryFallback() throws Exception {
+        final var index = get("/vue/");
+        assertThat(index.statusCode()).isEqualTo(200);
+        assertThat(index.body())
+                .contains("<div id=\"app\"></div>")
+                .contains("/causeway-webcomponents/component-styles.css")
+                .doesNotContain("/src/main.ts");
+
+        final var objectRoute = get("/vue/object/petclinic.PetOwner/s_owner-mary");
+        assertThat(objectRoute.statusCode()).isEqualTo(200);
+        assertThat(objectRoute.body()).isEqualTo(index.body());
+
+        final var scriptMatcher = SCRIPT_SOURCE.matcher(index.body());
+        assertThat(scriptMatcher.find()).isTrue();
+        assertThat(get(scriptMatcher.group(1)).statusCode()).isEqualTo(200);
+        assertThat(get("/causeway-webcomponents/index.mjs").statusCode()).isEqualTo(200);
+        assertThat(get("/causeway-webcomponents/vaadin-grid/vaadin-grid.js").statusCode()).isEqualTo(200);
+        assertThat(get("/webjars/font-awesome/7.3.0/css/all.min.css").statusCode()).isEqualTo(200);
+    }
+
+    @Test
+    void historyFallbackDoesNotCaptureBackendOrAssetRoutes() throws Exception {
+        assertThat(get("/vue/assets/does-not-exist.js").statusCode()).isEqualTo(404);
+        assertThat(get("/graphql").body()).doesNotContain("<div id=\"app\"></div>");
+        assertThat(get("/not-a-vue-route").body()).doesNotContain("<div id=\"app\"></div>");
+    }
+
+    private HttpResponse<String> get(final String path) throws Exception {
+        return HttpClient.newHttpClient().send(
+                HttpRequest.newBuilder(URI.create("http://localhost:" + port + path)).GET().build(),
+                HttpResponse.BodyHandlers.ofString());
+    }
+}
diff --git a/viewers/webcomponents/vue/LICENSE b/viewers/webcomponents/vue/LICENSE
new file mode 100644
index 00000000000..e6e77b08909
--- /dev/null
+++ b/viewers/webcomponents/vue/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/viewers/webcomponents/vue/NOTICE b/viewers/webcomponents/vue/NOTICE
new file mode 100644
index 00000000000..57bc3f720ab
--- /dev/null
+++ b/viewers/webcomponents/vue/NOTICE
@@ -0,0 +1,5 @@
+Apache Causeway Vue Web Component Viewer
+Copyright 2026 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (https://www.apache.org/).
diff --git a/viewers/webcomponents/vue/README.adoc b/viewers/webcomponents/vue/README.adoc
new file mode 100644
index 00000000000..739f9efdb6c
--- /dev/null
+++ b/viewers/webcomponents/vue/README.adoc
@@ -0,0 +1,142 @@
+= Apache Causeway Vue Web Component Viewer
+
+The package provides the thin Vue 3 and Vue Router 4 integration for Apache Causeway semantic web components.
+It owns route and host policy but does not duplicate GraphQL or domain state.
+
+== Installation
+
+Install `@apache-causeway/vue-viewer`, `vue`, and `vue-router` in the application frontend.
+Vue and Vue Router are peer dependencies of the viewer package.
+The Causeway component registration modules and any selected toolkit adapters remain same-origin application assets supplied by the Causeway backend.
+
+Configure the Vue plugin so `cw-` tags compile as native custom elements.
+
+[source,typescript]
+----
+vue({
+  template: {
+    compilerOptions: {
+      isCustomElement: tag => tag.startsWith('cw-')
+    }
+  }
+})
+----
+
+Create the router with the application's browser-history base, add the exported route records, and install the returned plugin.
+
+[source,typescript]
+----
+const router = createRouter({
+  history: createWebHistory('/vue'),
+  routes: [
+    ...createCausewayRouteRecords(),
+    ...applicationRoutes
+  ]
+});
+
+const viewer = createCausewayVueViewer({
+  router,
+  endpoint: '/graphql',
+  basePath: '/vue',
+  pages: {
+    'petclinic.PetOwner': PetOwnerPage,
+    'petclinic.HomePage': () => import('./pages/HomePage.vue')
+  }
+});
+
+createApp(App).use(router).use(viewer.plugin).mount('#app');
+----
+
+The application server must return its Vue `index.html` for `/vue`, `/vue/object/**`, and other intended client routes.
+That fallback must not capture `/graphql`, application assets, authentication endpoints, or unrelated backend routes.
+
+== Application-owned shell
+
+The root application component declares stable protocol landmarks rather than asking the viewer to manufacture them.
+The visual hierarchy remains application-owned.
+
+[source,vue]
+----
+<script setup lang="ts">
+import {ref} from 'vue';
+import {RouterView, useRoute} from 'vue-router';
+import {useCausewayShell, useCausewayViewer} from '@apache-causeway/vue-viewer';
+
+const viewer = useCausewayViewer();
+const route = useRoute();
+const shell = useCausewayShell(ref<HTMLElement | null>(null));
+</script>
+
+<template>
+  <div ref="shell">
+    <cw-graphql-client data-causeway-shell-client :endpoint="viewer.endpoint">
+      <header><cw-menubars /></header>
+      <div data-causeway-route-loading role="status" aria-live="polite" />
+      <div data-causeway-route-announcement aria-live="polite" aria-atomic="true" />
+      <cw-action-results data-causeway-shell-result hidden />
+      <main data-causeway-router-view>
+        <RouterView v-slot="{ Component }">
+          <component :is="Component" :key="route.path" />
+        </RouterView>
+      </main>
+    </cw-graphql-client>
+  </div>
+</template>
+----
+
+Use Vue's `.prop` modifier for structured custom-element values that are DOM properties rather than strings.
+For example, `:executor.prop="executor"` preserves the executor function instead of serializing it as an attribute.
+
+== Custom route pages
+
+A custom page receives immutable `logicalTypeName`, `objectId`, and `routeKey` props.
+It declares one marked route object context and one marked interaction controller inside that context.
+
+[source,vue]
+----
+<script setup lang="ts">
+import type {CausewayRoutePageProps} from '@apache-causeway/vue-viewer';
+defineProps<CausewayRoutePageProps>();
+</script>
+
+<template>
+  <section data-causeway-route-page tabindex="-1">
+    <cw-object-context
+      data-causeway-route-context
+      :logical-type="logicalTypeName"
+      :object-id="objectId"
+    >
+      <cw-object-header />
+      <cw-property id="name" editable />
+      <cw-interaction-controller data-causeway-route-interactions />
+    </cw-object-context>
+  </section>
+</template>
+----
+
+An unregistered exact logical type uses the package's generic page containing `<cw-object editable>`.
+Registrations are immutable after plugin creation and do not use type inheritance.
+
+== Public exports
+
+The package exports route codecs, route names and records, the viewer plugin factory, shell binding helpers, boundary validators, policy helpers, public TypeScript contracts, and the generic route components.
+Applications may replace navigation, home, result, and error policy through viewer options.
+Claimed semantic events are not also handled by the default policy.
+
+The canonical route grammar is `<base-path>/object/<logical-type>/<identifier>` using independently canonical UTF-8 percent-encoded segments.
+Malformed, unsafe, non-canonical, or oversized segments fail before an object context is mounted.
+
+== Lifecycle and presentation
+
+The stable shell keeps its GraphQL provider and menus connected while canonical route keys dispose obsolete object contexts.
+Object-context state remains authoritative for loading, ready, partial-error, and terminal-error presentation.
+The application owns shell CSS and may copy or override the package reference theme through documented custom properties.
+The semantic components continue to own object layout, menus, validation, interactions, results, collection previews, and toolkit selection.
+
+The first release supports client-rendered Vue applications only.
+Nuxt, server-side rendering, streaming hydration, runtime page-registry mutation, and inherited logical-type matching are not supported.
+
+== Verification
+
+Run `npm ci --ignore-scripts` followed by `npm run verify` to type-check, test, build, inspect, pack, install, and import the package.
+The Maven profile `regenerate-vue-viewer` performs the same locked regeneration for reactor maintainers.
diff --git a/viewers/webcomponents/vue/dist/boundary.d.ts b/viewers/webcomponents/vue/dist/boundary.d.ts
new file mode 100644
index 00000000000..7e43bfa47e6
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/boundary.d.ts
@@ -0,0 +1,19 @@
+import { ObjectRouteIdentity } from './route-codec';
+export declare const ROUTE_CONTEXT_SELECTOR = "cw-object-context[data-causeway-route-context]";
+export declare const ROUTE_INTERACTIONS_SELECTOR = "cw-interaction-controller[data-causeway-route-interactions]";
+export interface BoundaryValidation {
+    readonly valid: boolean;
+    readonly classification?: 'missing-context' | 'duplicate-context' | 'identity' | 'interactions';
+    readonly context?: HTMLElement;
+}
+export declare function validateRouteBoundary(root: ParentNode, identity: ObjectRouteIdentity): BoundaryValidation;
+export interface ShellLandmarks {
+    readonly shell: HTMLElement;
+    readonly client: HTMLElement;
+    readonly route: HTMLElement;
+    readonly loading: HTMLElement;
+    readonly announcement: HTMLElement;
+    readonly result: HTMLElement;
+}
+export declare function validateShellBoundary(shell: HTMLElement): ShellLandmarks;
+//# sourceMappingURL=boundary.d.ts.map
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/boundary.d.ts.map b/viewers/webcomponents/vue/dist/boundary.d.ts.map
new file mode 100644
index 00000000000..59b05a391ef
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/boundary.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"boundary.d.ts","sourceRoot":"","sources":["../src/boundary.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,eAAe,CAAC;AAEvD,eAAO,MAAM,sBAAsB,mDAAmD,CAAC;AACvF,eAAO,MAAM,2BAA2B,gEAAgE,CAAC;AAEzG,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,cAAc,CAAC,EAAE,iBAAiB,GAAG,mBAAmB,GAAG,UAAU,GAAG,cAAc,CAAC;IAChG,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC;CAChC;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,mBAAmB,GAAG,kBAAkB,CAczG;AAED,M [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/causeway-vue.css b/viewers/webcomponents/vue/dist/causeway-vue.css
new file mode 100644
index 00000000000..fd5ae43cf78
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/causeway-vue.css
@@ -0,0 +1 @@
+:root{--causeway-vue-surface: #ffffff;--causeway-vue-text: #1f2937;--causeway-vue-muted: #596579;--causeway-vue-border: #d7dee8;--causeway-vue-danger: #9b1c1c;--causeway-vue-focus: #075ea8;color-scheme:light dark}.causeway-vue-route-page{box-sizing:border-box;min-width:0;width:100%;color:var(--causeway-vue-text)}.causeway-vue-route-page:focus-visible,[data-causeway-router-view]:focus-visible{outline:.2rem solid var(--causeway-vue-focus);outline-offset:.2rem}.causeway-vue-status{margin:1r [...]
diff --git a/viewers/webcomponents/vue/dist/causeway-vue.js b/viewers/webcomponents/vue/dist/causeway-vue.js
new file mode 100644
index 00000000000..d856f4251e5
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/causeway-vue.js
@@ -0,0 +1,539 @@
+import { inject as re, onMounted as V, onBeforeUnmount as X, defineComponent as j, ref as A, openBlock as O, createElementBlock as q, createElementVNode as T, toDisplayString as ie, computed as R, h as b, defineAsyncComponent as ce, watch as se, createBlock as ue, resolveDynamicComponent as le, nextTick as de } from "vue";
+import { useRoute as pe } from "vue-router";
+const G = "cw-object-context[data-causeway-route-context]", fe = "cw-interaction-controller[data-causeway-route-interactions]";
+function ge(e, t) {
+  const a = [...e.querySelectorAll(G)].filter((r) => !r.parentElement?.closest(G));
+  if (a.length === 0) return Object.freeze({ valid: !1, classification: "missing-context" });
+  if (a.length !== 1) return Object.freeze({ valid: !1, classification: "duplicate-context" });
+  const n = a[0];
+  return n.getAttribute("logical-type") !== t.logicalTypeName || n.getAttribute("object-id") !== t.objectId ? Object.freeze({ valid: !1, classification: "identity" }) : [...n.querySelectorAll(fe)].filter((r) => r.closest(G) === n).length !== 1 ? Object.freeze({ valid: !1, classification: "interactions" }) : Object.freeze({ valid: !0, context: n });
+}
+function C(e, t) {
+  const a = e.querySelectorAll(t);
+  return a.length === 1 ? a[0] : null;
+}
+function Z(e) {
+  const t = C(e, "cw-graphql-client[data-causeway-shell-client]"), a = C(e, "[data-causeway-router-view]"), n = C(e, "[data-causeway-route-loading]"), o = C(e, "[data-causeway-route-announcement]"), r = C(e, "cw-action-results[data-causeway-shell-result]");
+  if (!t || !a || !n || !o || !r || !t.contains(a) || !t.contains(r))
+    throw new Error("The authored Vue application shell is invalid.");
+  return Object.freeze({ shell: e, client: t, route: a, loading: n, announcement: o, result: r });
+}
+const H = /* @__PURE__ */ Symbol("causeway-vue-viewer"), U = 4096, Q = 4096, he = "The requested application route is invalid.";
+function h() {
+  return new Error(he);
+}
+function ye(e) {
+  for (let t = 0; t < e.length; t += 1) {
+    const a = e.charCodeAt(t);
+    if (a >= 55296 && a <= 56319) {
+      const n = e.charCodeAt(t + 1);
+      if (!(n >= 56320 && n <= 57343)) return !0;
+      t += 1;
+    } else if (a >= 56320 && a <= 57343)
+      return !0;
+  }
+  return !1;
+}
+function I(e) {
+  const t = String(e ?? "");
+  if (!t || t.length > U || t === "." || t === ".." || /[\\/\u0000-\u001f\u007f-\u009f]/u.test(t) || ye(t) || new TextEncoder().encode(t).length > U) throw h();
+  let a;
+  try {
+    a = encodeURIComponent(t).replace(/[!'()*]/g, (n) => `%${n.charCodeAt(0).toString(16).toUpperCase()}`);
+  } catch {
+    throw h();
+  }
+  if (a.length > Q) throw h();
+  return a;
+}
+function B(e) {
+  if (!e || e.length > Q || !/^(?:[^%]|%[0-9A-F]{2})+$/u.test(e))
+    throw h();
+  let t;
+  try {
+    t = decodeURIComponent(e);
+  } catch {
+    throw h();
+  }
+  if (I(t) !== e) throw h();
+  return t;
+}
+function L(e) {
+  let t = String(e ?? "").trim();
+  if (!t.startsWith("/") || t.startsWith("//") || /[?#\\\u0000-\u001f]/u.test(t))
+    throw new Error("The Vue viewer base path is invalid.");
+  for (; t.length > 1 && t.endsWith("/"); ) t = t.slice(0, -1);
+  return t;
+}
+function ve(e, t) {
+  const a = L(e), n = `/object/${I(t?.logicalTypeName)}/${I(t?.id ?? t?.objectId)}`;
+  return a === "/" ? n : `${a}${n}`;
+}
+function z(e) {
+  return ve("/", e);
+}
+function J(e, t = "/") {
+  const a = String(e ?? ""), n = L(t), o = n === "/" ? "/object/" : `${n}/object/`;
+  if (!a.startsWith(o)) throw h();
+  const u = a.slice(o.length).split("/");
+  if (u.length !== 2) throw h();
+  return Object.freeze({
+    logicalTypeName: B(u[0]),
+    objectId: B(u[1])
+  });
+}
+function we(e) {
+  return z(e);
+}
+const D = "causeway-navigation-request", M = "causeway-action-request", W = "causeway-action-result", F = "causeway-object-context-state-change";
+function ee() {
+  let e = !1;
+  return {
+    get claimed() {
+      return e;
+    },
+    claim() {
+      return e ? !1 : (e = !0, !0);
+    }
+  };
+}
+function S(e) {
+  return Object.freeze({
+    router: e.router,
+    basePath: e.basePath,
+    shell: e.state.shell,
+    routeGeneration: e.state.routeGeneration
+  });
+}
+function me(e) {
+  if (e?.kind !== "object" || !e.value || typeof e.value != "object") return null;
+  const t = e.value._meta, a = t?.logicalTypeName, n = t?.id;
+  return typeof a != "string" || typeof n != "string" || !a || !n ? null : Object.freeze({ logicalTypeName: a, id: n, title: String(t?.title ?? n) });
+}
+function be(e) {
+  return [...e.querySelector("[data-causeway-router-view]")?.querySelectorAll("cw-action-results[data-causeway-page-result]") ?? []].filter((a) => a.isConnected);
+}
+function Ee(e) {
+  const t = e.querySelectorAll("cw-action-results[data-causeway-shell-result]");
+  return t.length === 1 ? t[0] : null;
+}
+function k(e) {
+  const t = be(e);
+  if (t.length > 1) throw new Error("The active Vue route has duplicate action-result outlets.");
+  if (t.length === 1) return t[0];
+  const a = Ee(e);
+  if (!a?.isConnected) throw new Error("The authored Vue shell result outlet is unavailable.");
+  return a;
+}
+function K(e, ...t) {
+  const a = e;
+  a.replacePresentation ? a.replacePresentation(...t) : e.replaceChildren(...t), e.hidden = t.length === 0;
+}
+function te(e, t) {
+  const a = t.result, n = document.createElement("h2");
+  if (n.textContent = t.actionId ? `${t.actionId} result` : "Action result", a?.kind === "collection") {
+    const r = document.createElement("cw-standalone-collection");
+    r.setAttribute("data-testid", "causeway-standalone-action-result"), r.setAttribute("named", String(t.resultPresentation?.named ?? n.textContent)), r.result = a, K(e, r);
+    return;
+  }
+  const o = document.createElement("output");
+  o.textContent = a?.kind === "scalar" ? String(a.value ?? "") : a?.kind === "void" ? "Completed" : "This result cannot be presented.", K(e, n, o);
+}
+async function Te(e, t) {
+  const a = e.state.shell, o = a?.querySelector("[data-causeway-router-view]")?.querySelector("cw-object-context[data-causeway-route-context]"), r = o?.context;
+  if (!o || !r?.refresh) return;
+  const u = e.state.routeGeneration, l = (i) => {
+    if (u !== e.state.routeGeneration) return s();
+    const d = i.detail?.state;
+    if (d?.status !== "terminal-error") {
+      (d?.status === "ready" || d?.status === "partial-error") && s();
+      return;
+    }
+    s(), (d.errors?.[0]?.extensions?.classification ?? d.errors?.[0]?.extensions?.code ?? d.error?.code) === "NOT_FOUND" && e.router.replace("/");
+  }, s = () => o.removeEventListener(F, l);
+  o.addEventListener(F, l), r.refresh(), te(k(a), t);
+}
+async function Y(e, t) {
+  const a = ee();
+  await e.policies.navigate?.(t, a, S(e)) === !0 && a.claim(), a.claimed || await e.router.push(z(t));
+}
+function ae(e, t) {
+  const a = /* @__PURE__ */ new WeakMap();
+  let n = null;
+  const o = (l) => {
+    const s = l.detail;
+    try {
+      const i = k(t);
+      s?.context && typeof s.context == "object" ? a.set(s.context, i) : n = i;
+    } catch (i) {
+      e.policies.error?.(i, S(e));
+    }
+  }, r = (l) => {
+    const s = l, i = s.detail?.target;
+    !i?.logicalTypeName || !(i.id ?? i.objectId) || (s.preventDefault(), Y(e, i).catch((d) => e.policies.error?.(d, S(e))));
+  }, u = (l) => {
+    const s = l, i = s.detail ?? {};
+    (async () => {
+      const d = ee();
+      if (await e.policies.result?.(i, d, S(e)) === !0 && d.claim(), d.claimed) return;
+      const f = me(i.result);
+      if (f) return Y(e, f);
+      const g = i.context && a.get(i.context) || n || k(t);
+      i.result?.kind === "void" ? await Te(e, i) : te(g, i), s.target?.dismissResult?.();
+    })().catch((d) => e.policies.error?.(d, S(e)));
+  };
+  return t.addEventListener(M, o, { capture: !0 }), t.addEventListener(D, r), t.addEventListener(W, u), () => {
+    t.removeEventListener(M, o, { capture: !0 }), t.removeEventListener(D, r), t.removeEventListener(W, u);
+  };
+}
+const je = /^[A-Za-z_][A-Za-z0-9_$-]*(?:\.[A-Za-z_][A-Za-z0-9_$-]*)*$/u;
+function _e(e) {
+  return e ? e instanceof Map ? [...e.entries()] : Array.isArray(e) ? e : Object.entries(e) : [];
+}
+function Ce(e) {
+  const t = /* @__PURE__ */ new Map();
+  for (const [a, n] of _e(e)) {
+    const o = String(a ?? "").trim();
+    if (!je.test(o)) throw new Error("A Vue page registration has an invalid logical type.");
+    if (!n || typeof n != "object" && typeof n != "function")
+      throw new Error(`The Vue page registration for ${o} is unsupported.`);
+    if (t.has(o)) throw new Error(`The Vue page registration for ${o} is duplicated.`);
+    t.set(o, n);
+  }
+  return t;
+}
+function xe(e) {
+  return typeof e == "function";
+}
+function ze(e) {
+  if (!e?.router) throw new Error("The Vue viewer requires the application router.");
+  const t = String(e.endpoint ?? "").trim();
+  if (!t) throw new Error("The Vue viewer GraphQL endpoint is required.");
+  const a = { shell: null, routeGeneration: 0 };
+  let n;
+  return n = Object.freeze({
+    plugin: {
+      install(r) {
+        r.provide(H, n);
+      }
+    },
+    router: e.router,
+    endpoint: t,
+    basePath: L(e.basePath ?? (e.router.options.history.base || "/")),
+    pages: Ce(e.pages),
+    policies: Object.freeze({ ...e.policies }),
+    developmentDiagnostics: e.developmentDiagnostics ?? !0,
+    state: a
+  }), n;
+}
+function $() {
+  const e = re(H);
+  if (!e) throw new Error("The Causeway Vue viewer plugin is not installed.");
+  return e;
+}
+function $e(e) {
+  const t = $();
+  let a = null;
+  return V(() => {
+    const n = e.value;
+    if (!n) throw new Error("The authored Vue application shell is missing.");
+    Z(n), t.state.shell = n, a = ae(t, n);
+  }), X(() => {
+    a?.(), a = null, t.state.shell === e.value && (t.state.shell = null);
+  }), e;
+}
+function Ue(e, t) {
+  const a = Z(t);
+  e.state.shell = t;
+  const n = ae(e, t);
+  return {
+    landmarks: a,
+    dispose() {
+      n(), e.state.shell === t && (e.state.shell = null);
+    }
+  };
+}
+const Se = ["data-route-state"], Ae = /* @__PURE__ */ j({
+  __name: "HomePage",
+  setup(e) {
+    const t = $(), a = A(null), n = A("loading"), o = A("Loading the application home page…");
+    function r(u) {
+      let l = null;
+      return u.dispatchEvent(new CustomEvent("causeway-graphql-client-request", {
+        bubbles: !0,
+        composed: !0,
+        detail: { provide(s) {
+          l ??= s;
+        } }
+      })), l;
+    }
+    return V(async () => {
+      const u = ++t.state.routeGeneration, l = a.value ? r(a.value) : null;
+      if (!l) {
+        n.value = "unsupported", o.value = "Choose an application action to begin.";
+        return;
+      }
+      try {
+        const s = await l.describeApplicationEntry();
+        if (u !== t.state.routeGeneration) return;
+        if (!s?.supported) {
+          n.value = "unsupported", o.value = "Choose an application action to begin.";
+          return;
+        }
+        const i = await l.readApplicationEntry({ description: s });
+        if (u !== t.state.routeGeneration) return;
+        const d = i?.data?.home, E = d?.object?._meta, f = E?.logicalTypeName ?? d?.logicalTypeName, g = E?.id, y = { claimed: !1, claim() {
+          return this.claimed ? !1 : (this.claimed = !0, !0);
+        } }, P = { router: t.router, basePath: t.basePath, shell: t.state.shell, routeGeneration: u };
+        if (await t.policies.home?.(i?.data, y, P) === !0 && y.claim(), u !== t.state.routeGeneration || y.claimed) return;
+        if (d?.kind === "OBJECT" && f && g) {
+          await t.router.replace(z({ logicalTypeName: f, id: g }));
+          return;
+        }
+        n.value = i?.errors?.length ? "partial-error" : "ready", o.value = "Choose an application action to begin.";
+      } catch (s) {
+        if (u !== t.state.routeGeneration) return;
+        t.policies.error?.(s, { router: t.router, basePath: t.basePath, shell: t.state.shell, routeGeneration: u }), n.value = "partial-error", o.value = "The home page is unavailable; application menus remain available.";
+      }
+    }), (u, l) => (O(), q("section", {
+      ref_key: "page",
+      ref: a,
+      class: "causeway-vue-route-page causeway-vue-status",
+      "data-route-state": n.value,
+      tabindex: "-1"
+    }, [
+      l[0] || (l[0] = T("h1", null, "Welcome", -1)),
+      T("p", null, ie(o.value), 1)
+    ], 8, Se));
+  }
+}), Oe = {
+  class: "causeway-vue-route-page causeway-vue-route-object",
+  "data-causeway-route-page": "",
+  "data-page-kind": "generic",
+  "data-route-state": "loading",
+  tabindex: "-1",
+  "aria-label": "Object page"
+}, Pe = ["logical-type", "object-id"], Ne = /* @__PURE__ */ j({
+  __name: "GenericObjectPage",
+  props: {
+    logicalTypeName: {},
+    objectId: {},
+    routeKey: {}
+  },
+  setup(e) {
+    return (t, a) => (O(), q("section", Oe, [
+      T("cw-object-context", {
+        "data-causeway-route-context": "",
+        "logical-type": e.logicalTypeName,
+        "object-id": e.objectId
+      }, [...a[0] || (a[0] = [
+        T("cw-object", { editable: "" }, null, -1),
+        T("cw-interaction-controller", { "data-causeway-route-interactions": "" }, null, -1)
+      ])], 8, Pe)
+    ]));
+  }
+}), Re = ["data-route-key"], qe = {
+  key: 0,
+  class: "causeway-vue-route-page causeway-vue-status causeway-vue-status-danger",
+  "data-route-state": "terminal-error",
+  tabindex: "-1",
+  role: "alert"
+}, Ge = /* @__PURE__ */ j({
+  __name: "ObjectRoutePage",
+  setup(e) {
+    const t = pe(), a = $(), n = A(null), o = A(null), r = R(() => J(t.path, "/")), u = j(() => () => b("section", {
+      class: "causeway-vue-route-page causeway-vue-status",
+      "data-route-state": "loading",
+      tabindex: -1,
+      role: "status"
+    }, [b("h1", "Loading page…")])), l = j(() => () => b("section", {
+      class: "causeway-vue-route-page causeway-vue-status causeway-vue-status-danger",
+      "data-route-state": "terminal-error",
+      tabindex: -1,
+      role: "alert"
+    }, [b("h1", "Page unavailable"), b("p", "The application page could not be loaded.")])), s = R(() => we(r.value)), i = R(() => a.pages.get(r.value.logicalTypeName)), d = R(() => {
+      const c = i.value;
+      return c ? xe(c) ? ce({
+        loader: async () => {
+          const p = await c();
+          return "default" in p ? p.default : p;
+        },
+        delay: 0,
+        timeout: 3e4,
+        loadingComponent: u,
+        errorComponent: l,
+        onError(p, v, w) {
+          a.policies.error?.(p, {
+            router: a.router,
+            basePath: a.basePath,
+            shell: a.state.shell,
+            routeGeneration: a.state.routeGeneration
+          }), w();
+        }
+      }) : c : Ne;
+    });
+    function E() {
+      const c = a.state.shell;
+      return {
+        route: c?.querySelector("[data-causeway-router-view]") ?? null,
+        loading: c?.querySelector("[data-causeway-route-loading]") ?? null,
+        announcement: c?.querySelector("[data-causeway-route-announcement]") ?? null
+      };
+    }
+    function f(c, p) {
+      const v = E().announcement;
+      v && (v.textContent = "", requestAnimationFrame(() => {
+        p === a.state.routeGeneration && v.isConnected && (v.textContent = c);
+      }));
+    }
+    function g(c) {
+      const p = E();
+      p.route?.setAttribute("aria-busy", String(c)), p.loading && (p.loading.hidden = !c);
+    }
+    async function y() {
+      const c = a.state.routeGeneration;
+      if (o.value = null, await de(), c !== a.state.routeGeneration || !n.value) return;
+      const p = ge(n.value, r.value);
+      p.valid || (o.value = p.classification ?? "invalid", g(!1), f("Page unavailable", c));
+    }
+    function P(c) {
+      const p = a.state.routeGeneration, v = c, w = v.detail?.state, N = v.target?.closest("[data-causeway-route-page]");
+      if (!w || !N || !n.value?.contains(N)) return;
+      let m = String(w.status ?? "terminal-error");
+      if (m === "terminal-error") {
+        const _ = w.errors?.[0]?.extensions?.classification ?? w.errors?.[0]?.extensions?.code ?? w.error?.code;
+        (_ === "NOT_FOUND" || _ === "ACCESS_DENIED") && (m = "unavailable");
+      }
+      if (N.dataset.routeState = m, m === "ready" || m === "partial-error") {
+        g(!1);
+        const _ = w.snapshot?.data?._meta?.title;
+        _ && (document.title = String(_));
+        for (const oe of N.querySelectorAll("cw-collection:not([active])"))
+          oe.activate?.();
+        f(m === "ready" ? "Page ready" : "Page ready with partial information", p);
+      } else (m === "terminal-error" || m === "unavailable") && (g(!1), f("Page unavailable", p));
+    }
+    return se(() => t.fullPath, () => {
+      a.state.routeGeneration += 1, g(!0), f("Loading page", a.state.routeGeneration), y();
+    }, { immediate: !0 }), V(async () => {
+      n.value?.addEventListener("causeway-object-context-state-change", P), await y();
+      const c = n.value?.querySelector(
+        "cw-object-context[data-causeway-route-context]"
+      );
+      c?.context?.state && c.dispatchEvent(new CustomEvent("causeway-object-context-state-change", {
+        bubbles: !0,
+        composed: !0,
+        detail: { state: c.context.state, context: c.context }
+      })), n.value?.querySelector("[data-causeway-route-page]")?.focus({ preventScroll: !0 });
+    }), X(() => n.value?.removeEventListener("causeway-object-context-state-change", P)), (c, p) => (O(), q("div", {
+      ref_key: "host",
+      ref: n,
+      class: "causeway-vue-object-route",
+      "data-route-key": s.value
+    }, [
+      o.value ? (O(), q("section", qe, [...p[0] || (p[0] = [
+        T("h1", null, "Page unavailable", -1),
+        T("p", null, "The application page has an invalid semantic context boundary.", -1)
+      ])])) : (O(), ue(le(d.value), {
+        key: s.value,
+        "logical-type-name": r.value.logicalTypeName,
+        "object-id": r.value.objectId,
+        "route-key": s.value,
+        onVnodeMounted: y,
+        onVnodeUpdated: y
+      }, null, 8, ["logical-type-name", "object-id", "route-key"]))
+    ], 8, Re));
+  }
+}), x = Object.freeze({
+  home: "causeway-home",
+  object: "causeway-object",
+  invalid: "causeway-invalid-route",
+  notFound: "causeway-not-found"
+});
+function ne(e, t, a, n) {
+  return j({
+    name: `Causeway${e.replace(/\W/gu, "")}Page`,
+    setup() {
+      return () => b("section", {
+        class: ["causeway-vue-route-page", "causeway-vue-status", a === "invalid-route" && "causeway-vue-status-danger"],
+        "data-route-state": a,
+        tabindex: -1,
+        role: n
+      }, [b("h1", e), b("p", t)]);
+    }
+  });
+}
+const Ie = ne(
+  "Invalid route",
+  "The requested application route is invalid.",
+  "invalid-route",
+  "alert"
+), ke = ne(
+  "Page unavailable",
+  "The requested page is unavailable.",
+  "not-found",
+  "alert"
+);
+function Be(e = {}) {
+  return [
+    { path: "/", name: x.home, component: e.homeComponent ?? Ae },
+    {
+      path: "/object/:logicalTypeName/:objectId",
+      name: x.object,
+      component: Ge,
+      beforeEnter(t) {
+        try {
+          return J(t.path, "/"), !0;
+        } catch {
+          return { name: x.invalid, replace: !0 };
+        }
+      }
+    },
+    {
+      path: "/invalid-route",
+      name: x.invalid,
+      component: Ie
+    },
+    {
+      path: "/:pathMatch(.*)*",
+      name: x.notFound,
+      component: e.notFoundComponent ?? ke
+    }
+  ];
+}
+export {
+  M as ACTION_REQUEST_EVENT,
+  W as ACTION_RESULT_EVENT,
+  x as CAUSEWAY_ROUTE_NAMES,
+  H as CAUSEWAY_VIEWER_KEY,
+  Ne as CausewayGenericObjectPage,
+  Ae as CausewayHomePage,
+  Ie as CausewayInvalidRoutePage,
+  ke as CausewayNotFoundPage,
+  Ge as CausewayObjectRoutePage,
+  he as INVALID_ROUTE_MESSAGE,
+  D as NAVIGATION_REQUEST_EVENT,
+  F as OBJECT_CONTEXT_STATE_EVENT,
+  G as ROUTE_CONTEXT_SELECTOR,
+  fe as ROUTE_INTERACTIONS_SELECTOR,
+  Ue as bindCausewayShell,
+  ve as canonicalObjectPath,
+  we as canonicalRouteKey,
+  z as canonicalRouterObjectPath,
+  Be as createCausewayRouteRecords,
+  ze as createCausewayVueViewer,
+  B as decodeRouteSegment,
+  I as encodeRouteSegment,
+  ae as installSemanticBridge,
+  xe as isPageLoader,
+  L as normalizeBasePath,
+  Ce as normalizePageRegistry,
+  J as parseCanonicalObjectPath,
+  te as presentSemanticResult,
+  k as resolveResultOutlet,
+  $e as useCausewayShell,
+  $ as useCausewayViewer,
+  ge as validateRouteBoundary,
+  Z as validateShellBoundary
+};
+//# sourceMappingURL=causeway-vue.js.map
diff --git a/viewers/webcomponents/vue/dist/causeway-vue.js.map b/viewers/webcomponents/vue/dist/causeway-vue.js.map
new file mode 100644
index 00000000000..63164cafb1c
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/causeway-vue.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"causeway-vue.js","sources":["../src/boundary.ts","../src/contracts.ts","../src/route-codec.ts","../src/policy.ts","../src/registry.ts","../src/plugin.ts","../src/components/HomePage.vue","../src/components/GenericObjectPage.vue","../src/components/ObjectRoutePage.vue","../src/routes.ts"],"sourcesContent":["/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work f [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/components/GenericObjectPage.vue.d.ts b/viewers/webcomponents/vue/dist/components/GenericObjectPage.vue.d.ts
new file mode 100644
index 00000000000..75600caeb4b
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/components/GenericObjectPage.vue.d.ts
@@ -0,0 +1,5 @@
+import { CausewayRoutePageProps } from '../contracts';
+declare const __VLS_export: import('vue').DefineComponent<CausewayRoutePageProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<CausewayRoutePageProps> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
+declare const _default: typeof __VLS_export;
+export default _default;
+//# sourceMappingURL=GenericObjectPage.vue.d.ts.map
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/components/GenericObjectPage.vue.d.ts.map b/viewers/webcomponents/vue/dist/components/GenericObjectPage.vue.d.ts.map
new file mode 100644
index 00000000000..a55521c1cec
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/components/GenericObjectPage.vue.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"GenericObjectPage.vue.d.ts","sourceRoot":"","sources":["../../src/components/GenericObjectPage.vue"],"names":[],"mappings":"AAkBA;AA8CA,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,cAAc,CAAC;AAsEzD,QAAA,MAAM,YAAY,wTAEhB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/components/HomePage.vue.d.ts b/viewers/webcomponents/vue/dist/components/HomePage.vue.d.ts
new file mode 100644
index 00000000000..f46e1154fef
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/components/HomePage.vue.d.ts
@@ -0,0 +1,4 @@
+declare const __VLS_export: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
+declare const _default: typeof __VLS_export;
+export default _default;
+//# sourceMappingURL=HomePage.vue.d.ts.map
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/components/HomePage.vue.d.ts.map b/viewers/webcomponents/vue/dist/components/HomePage.vue.d.ts.map
new file mode 100644
index 00000000000..402c61dd96b
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/components/HomePage.vue.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"HomePage.vue.d.ts","sourceRoot":"","sources":["../../src/components/HomePage.vue"],"names":[],"mappings":"AAkBA;AAuLA,QAAA,MAAM,YAAY,+QAChB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/components/ObjectRoutePage.vue.d.ts b/viewers/webcomponents/vue/dist/components/ObjectRoutePage.vue.d.ts
new file mode 100644
index 00000000000..f6d769f9153
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/components/ObjectRoutePage.vue.d.ts
@@ -0,0 +1,4 @@
+declare const __VLS_export: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
+declare const _default: typeof __VLS_export;
+export default _default;
+//# sourceMappingURL=ObjectRoutePage.vue.d.ts.map
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/components/ObjectRoutePage.vue.d.ts.map b/viewers/webcomponents/vue/dist/components/ObjectRoutePage.vue.d.ts.map
new file mode 100644
index 00000000000..662093e5a53
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/components/ObjectRoutePage.vue.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"ObjectRoutePage.vue.d.ts","sourceRoot":"","sources":["../../src/components/ObjectRoutePage.vue"],"names":[],"mappings":"AAkBA;AA+YA,QAAA,MAAM,YAAY,+QAChB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/contracts.d.ts b/viewers/webcomponents/vue/dist/contracts.d.ts
new file mode 100644
index 00000000000..dd9d303cc40
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/contracts.d.ts
@@ -0,0 +1,57 @@
+import { InjectionKey, Plugin } from 'vue';
+import { Router } from 'vue-router';
+import { CausewayPageRegistration, CausewayPageRegistryInput } from './registry';
+export interface CausewayObjectTarget {
+    readonly logicalTypeName?: string;
+    readonly id?: string;
+    readonly objectId?: string;
+    readonly title?: string;
+}
+export interface CausewaySemanticResult {
+    readonly kind: 'object' | 'scalar' | 'collection' | 'void' | string;
+    readonly value?: unknown;
+}
+export interface CausewayEventClaim {
+    readonly claimed: boolean;
+    claim(): boolean;
+}
+export interface CausewayPolicyContext {
+    readonly router: Router;
+    readonly basePath: string;
+    readonly shell: HTMLElement | null;
+    readonly routeGeneration: number;
+}
+export interface CausewayViewerPolicies {
+    readonly navigate?: (target: CausewayObjectTarget, claim: CausewayEventClaim, context: CausewayPolicyContext) => boolean | void | Promise<boolean | void>;
+    readonly home?: (entry: unknown, claim: CausewayEventClaim, context: CausewayPolicyContext) => boolean | void | Promise<boolean | void>;
+    readonly result?: (detail: unknown, claim: CausewayEventClaim, context: CausewayPolicyContext) => boolean | void | Promise<boolean | void>;
+    readonly error?: (error: unknown, context: CausewayPolicyContext) => void;
+}
+export interface CausewayViewerOptions {
+    readonly router: Router;
+    readonly endpoint: string;
+    readonly basePath?: string;
+    readonly pages?: CausewayPageRegistryInput;
+    readonly policies?: CausewayViewerPolicies;
+    readonly developmentDiagnostics?: boolean;
+}
+export interface CausewayViewerRuntime {
+    readonly plugin: Plugin;
+    readonly router: Router;
+    readonly endpoint: string;
+    readonly basePath: string;
+    readonly pages: ReadonlyMap<string, CausewayPageRegistration>;
+    readonly policies: CausewayViewerPolicies;
+    readonly developmentDiagnostics: boolean;
+    readonly state: {
+        shell: HTMLElement | null;
+        routeGeneration: number;
+    };
+}
+export declare const CAUSEWAY_VIEWER_KEY: InjectionKey<CausewayViewerRuntime>;
+export interface CausewayRoutePageProps {
+    readonly logicalTypeName: string;
+    readonly objectId: string;
+    readonly routeKey: string;
+}
+//# sourceMappingURL=contracts.d.ts.map
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/contracts.d.ts.map b/viewers/webcomponents/vue/dist/contracts.d.ts.map
new file mode 100644
index 00000000000..3e13a37b16b
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/contracts.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAC,YAAY,EAAE,MAAM,EAAC,MAAM,KAAK,CAAC;AAC9C,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAC,wBAAwB,EAAE,yBAAyB,EAAC,MAAM,YAAY,CAAC;AAEpF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/index.d.ts b/viewers/webcomponents/vue/dist/index.d.ts
new file mode 100644
index 00000000000..24629dc9f88
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/index.d.ts
@@ -0,0 +1,11 @@
+export * from './boundary';
+export * from './contracts';
+export * from './plugin';
+export * from './policy';
+export * from './registry';
+export * from './route-codec';
+export * from './routes';
+export { default as CausewayGenericObjectPage } from './components/GenericObjectPage.vue';
+export { default as CausewayHomePage } from './components/HomePage.vue';
+export { default as CausewayObjectRoutePage } from './components/ObjectRoutePage.vue';
+//# sourceMappingURL=index.d.ts.map
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/index.d.ts.map b/viewers/webcomponents/vue/dist/index.d.ts.map
new file mode 100644
index 00000000000..31e0ec2bf7a
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/index.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAmBA,OAAO,aAAa,CAAC;AAErB,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,OAAO,EAAC,OAAO,IAAI,yBAAyB,EAAC,MAAM,oCAAoC,CAAC;AACxF,OAAO,EAAC,OAAO,IAAI,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAC,OAAO,IAAI,uBAAuB,EAAC,MAAM,kCAAkC,CAAC"}
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/plugin.d.ts b/viewers/webcomponents/vue/dist/plugin.d.ts
new file mode 100644
index 00000000000..62b3a534da5
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/plugin.d.ts
@@ -0,0 +1,11 @@
+import { Ref } from 'vue';
+import { CausewayViewerOptions, CausewayViewerRuntime } from './contracts';
+import { ShellLandmarks } from './boundary';
+export declare function createCausewayVueViewer(options: CausewayViewerOptions): CausewayViewerRuntime;
+export declare function useCausewayViewer(): CausewayViewerRuntime;
+export declare function useCausewayShell(shellReference: Ref<HTMLElement | null>): Ref<HTMLElement | null>;
+export declare function bindCausewayShell(runtime: CausewayViewerRuntime, shell: HTMLElement): {
+    landmarks: ShellLandmarks;
+    dispose: () => void;
+};
+//# sourceMappingURL=plugin.d.ts.map
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/plugin.d.ts.map b/viewers/webcomponents/vue/dist/plugin.d.ts.map
new file mode 100644
index 00000000000..95ca872aa7c
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/plugin.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAkD,KAAK,GAAG,EAAC,MAAM,KAAK,CAAC;AAC9E,OAAO,EAAsB,KAAK,qBAAqB,EAAE,KAAK,qBAAqB,EAAC,MAAM,aAAa,CAAC;AACxG,OAAO,EAAwB,KAAK,cAAc,EAAC,MAAM,YAAY,CAAC;AAKtE,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,qBAAqB,GAAG,qBAAqB,CAsB7F;AAED,wBAAgB,iBAAiB,IAAI,qBAAqB,CAIzD;AAED,wBAAgB,gBAAgB,CAAC,cAAc,EAAE,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,CAgBjG;AAED,wBAAgB,iBAAiB,CAAC, [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/policy.d.ts b/viewers/webcomponents/vue/dist/policy.d.ts
new file mode 100644
index 00000000000..c44f04a1b92
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/policy.d.ts
@@ -0,0 +1,16 @@
+import { CausewaySemanticResult, CausewayViewerRuntime } from './contracts';
+export declare const NAVIGATION_REQUEST_EVENT = "causeway-navigation-request";
+export declare const ACTION_REQUEST_EVENT = "causeway-action-request";
+export declare const ACTION_RESULT_EVENT = "causeway-action-result";
+export declare const OBJECT_CONTEXT_STATE_EVENT = "causeway-object-context-state-change";
+interface ActionResultDetail {
+    readonly actionId?: string;
+    readonly context?: object;
+    readonly result?: CausewaySemanticResult;
+    readonly resultPresentation?: Record<string, unknown>;
+}
+export declare function resolveResultOutlet(shell: HTMLElement): HTMLElement;
+export declare function presentSemanticResult(outlet: HTMLElement, detail: ActionResultDetail): void;
+export declare function installSemanticBridge(runtime: CausewayViewerRuntime, shell: HTMLElement): () => void;
+export {};
+//# sourceMappingURL=policy.d.ts.map
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/policy.d.ts.map b/viewers/webcomponents/vue/dist/policy.d.ts.map
new file mode 100644
index 00000000000..c549f5fba3a
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/policy.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAIV,sBAAsB,EACtB,qBAAqB,EACtB,MAAM,aAAa,CAAC;AAGrB,eAAO,MAAM,wBAAwB,gCAAgC,CAAC;AACtE,eAAO,MAAM,oBAAoB,4BAA4B,CAAC;AAC9D,eAAO,MAAM,mBAAmB,2BAA2B,CAAC;AAC5D,eAAO,MAAM,0BAA0B,yCAAyC,CAAC;AAEjF,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,sBAAsB,CAAC;IACzC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC, [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/registry.d.ts b/viewers/webcomponents/vue/dist/registry.d.ts
new file mode 100644
index 00000000000..474a85bfda9
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/registry.d.ts
@@ -0,0 +1,9 @@
+import { Component } from 'vue';
+export type CausewayPageLoader = () => Promise<Component | {
+    default: Component;
+}>;
+export type CausewayPageRegistration = Component | CausewayPageLoader;
+export type CausewayPageRegistryInput = ReadonlyMap<string, CausewayPageRegistration> | ReadonlyArray<readonly [string, CausewayPageRegistration]> | Readonly<Record<string, CausewayPageRegistration>>;
+export declare function normalizePageRegistry(input?: CausewayPageRegistryInput): ReadonlyMap<string, CausewayPageRegistration>;
+export declare function isPageLoader(registration: CausewayPageRegistration): registration is CausewayPageLoader;
+//# sourceMappingURL=registry.d.ts.map
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/registry.d.ts.map b/viewers/webcomponents/vue/dist/registry.d.ts.map
new file mode 100644
index 00000000000..1042d0484d7
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/registry.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,KAAK,CAAC;AAEnC,MAAM,MAAM,kBAAkB,GAAG,MAAM,OAAO,CAAC,SAAS,GAAG;IAAC,OAAO,EAAE,SAAS,CAAA;CAAC,CAAC,CAAC;AACjF,MAAM,MAAM,wBAAwB,GAAG,SAAS,GAAG,kBAAkB,CAAC;AACtE,MAAM,MAAM,yBAAyB,GAAG,WAAW,CAAC,MAAM,EAAE,wBAAwB,CAAC,GACjF,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC,GAC1D,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC,CAAC;AAWvD,wBAAgB,qBAAqB,CAAC [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/route-codec.d.ts b/viewers/webcomponents/vue/dist/route-codec.d.ts
new file mode 100644
index 00000000000..f7512bf830a
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/route-codec.d.ts
@@ -0,0 +1,21 @@
+export declare const INVALID_ROUTE_MESSAGE = "The requested application route is invalid.";
+export interface ObjectRouteIdentity {
+    readonly logicalTypeName: string;
+    readonly objectId: string;
+}
+export declare function encodeRouteSegment(value: unknown): string;
+export declare function decodeRouteSegment(encoded: string): string;
+export declare function normalizeBasePath(basePath: unknown): string;
+export declare function canonicalObjectPath(basePath: unknown, target: {
+    readonly logicalTypeName?: unknown;
+    readonly id?: unknown;
+    readonly objectId?: unknown;
+}): string;
+export declare function canonicalRouterObjectPath(target: {
+    readonly logicalTypeName?: unknown;
+    readonly id?: unknown;
+    readonly objectId?: unknown;
+}): string;
+export declare function parseCanonicalObjectPath(pathname: unknown, basePath?: unknown): Readonly<ObjectRouteIdentity>;
+export declare function canonicalRouteKey(identity: ObjectRouteIdentity): string;
+//# sourceMappingURL=route-codec.d.ts.map
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/route-codec.d.ts.map b/viewers/webcomponents/vue/dist/route-codec.d.ts.map
new file mode 100644
index 00000000000..e87b9413d3e
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/route-codec.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"route-codec.d.ts","sourceRoot":"","sources":["../src/route-codec.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,qBAAqB,gDAAgD,CAAC;AAEnF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAoBD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAgBzD;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAY1D;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAO3D;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE;IAC7D,QAAQ,CAAC,eAAe, [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/routes.d.ts b/viewers/webcomponents/vue/dist/routes.d.ts
new file mode 100644
index 00000000000..53d13e718e1
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/routes.d.ts
@@ -0,0 +1,16 @@
+import { Component } from 'vue';
+import { RouteRecordRaw } from 'vue-router';
+export declare const CAUSEWAY_ROUTE_NAMES: Readonly<{
+    home: "causeway-home";
+    object: "causeway-object";
+    invalid: "causeway-invalid-route";
+    notFound: "causeway-not-found";
+}>;
+export declare const CausewayInvalidRoutePage: Component;
+export declare const CausewayNotFoundPage: Component;
+export interface CausewayRouteRecordOptions {
+    readonly homeComponent?: Component;
+    readonly notFoundComponent?: Component;
+}
+export declare function createCausewayRouteRecords(options?: CausewayRouteRecordOptions): RouteRecordRaw[];
+//# sourceMappingURL=routes.d.ts.map
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/routes.d.ts.map b/viewers/webcomponents/vue/dist/routes.d.ts.map
new file mode 100644
index 00000000000..47be09f3ae9
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/routes.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAqB,KAAK,SAAS,EAAC,MAAM,KAAK,CAAC;AACvD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,YAAY,CAAC;AAK/C,eAAO,MAAM,oBAAoB;;;;;EAK/B,CAAC;AAgBH,eAAO,MAAM,wBAAwB,WAKpC,CAAC;AAEF,eAAO,MAAM,oBAAoB,WAKhC,CAAC;AAEF,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,CAAC;IACnC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,CAAC;CACxC;AAED,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,0BAA+B,GAAG,cAAc,EAAE,CA2BrG"}
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/package-lock.json b/viewers/webcomponents/vue/package-lock.json
new file mode 100644
index 00000000000..608826500ee
--- /dev/null
+++ b/viewers/webcomponents/vue/package-lock.json
@@ -0,0 +1,3515 @@
+{
+  "name": "@apache-causeway/vue-viewer",
+  "version": "4.0.0-SNAPSHOT",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "@apache-causeway/vue-viewer",
+      "version": "4.0.0-SNAPSHOT",
+      "license": "Apache-2.0",
+      "devDependencies": {
+        "@types/node": "24.12.0",
+        "@vitejs/plugin-vue": "6.0.8",
+        "@vue/test-utils": "2.4.6",
+        "jsdom": "27.4.0",
+        "typescript": "5.9.3",
+        "vite": "7.3.6",
+        "vite-plugin-dts": "5.1.0",
+        "vitest": "4.1.11",
+        "vue": "3.5.42",
+        "vue-router": "4.6.4",
+        "vue-tsc": "3.2.6"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0",
+        "vue-router": "^4.6.0"
+      }
+    },
+    "node_modules/@acemir/cssom": {
+      "version": "0.9.31",
+      "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz",
+      "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@asamuzakjp/css-color": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz",
+      "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@csstools/css-calc": "^3.0.0",
+        "@csstools/css-color-parser": "^4.0.1",
+        "@csstools/css-parser-algorithms": "^4.0.0",
+        "@csstools/css-tokenizer": "^4.0.0",
+        "lru-cache": "^11.2.5"
+      }
+    },
+    "node_modules/@asamuzakjp/dom-selector": {
+      "version": "6.8.1",
+      "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz",
+      "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@asamuzakjp/nwsapi": "^2.3.9",
+        "bidi-js": "^1.0.3",
+        "css-tree": "^3.1.0",
+        "is-potential-custom-element-name": "^1.0.1",
+        "lru-cache": "^11.2.6"
+      }
+    },
+    "node_modules/@asamuzakjp/nwsapi": {
+      "version": "2.3.9",
+      "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+      "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+      "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+      "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
+      "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.8"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+      "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@csstools/color-helpers": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz",
+      "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT-0",
+      "engines": {
+        "node": ">=20.19.0"
+      }
+    },
+    "node_modules/@csstools/css-calc": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz",
+      "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "peerDependencies": {
+        "@csstools/css-parser-algorithms": "^4.0.0",
+        "@csstools/css-tokenizer": "^4.0.0"
+      }
+    },
+    "node_modules/@csstools/css-color-parser": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.2.tgz",
+      "integrity": "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@csstools/color-helpers": "^6.1.1",
+        "@csstools/css-calc": "^3.3.0"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "peerDependencies": {
+        "@csstools/css-parser-algorithms": "^4.0.0",
+        "@csstools/css-tokenizer": "^4.0.0"
+      }
+    },
+    "node_modules/@csstools/css-parser-algorithms": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
+      "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "peerDependencies": {
+        "@csstools/css-tokenizer": "^4.0.0"
+      }
+    },
+    "node_modules/@csstools/css-syntax-patches-for-csstree": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.12.tgz",
+      "integrity": "sha512-3vLQK+dXxhBMR2Wx99PTCifE+vHtW2ndZWyla8yK813ev6oGhyn8Lja8jCyGAWTJ+LEYZK7EVtJxrDj8ztevJw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT-0",
+      "peerDependencies": {
+        "css-tree": "^3.2.1"
+      },
+      "peerDependenciesMeta": {
+        "css-tree": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@csstools/css-tokenizer": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
+      "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.19.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
+      "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
+      "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
+      "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
+      "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
+      "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
+      "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
+      "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
+      "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
+      "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
+      "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
+      "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
+      "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
+      "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
+      "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
+      "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
+      "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
+      "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
+      "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
+      "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
+      "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
+      "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
+      "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
+      "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
+      "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
+      "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
+      "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@exodus/bytes": {
+      "version": "1.15.1",
+      "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
+      "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "@noble/hashes": "^1.8.0 || ^2.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@noble/hashes": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+      "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@napi-rs/lzma-linux-x64-gnu": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
+      "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^22.20 || ^24.12 || >=25"
+      }
+    },
+    "node_modules/@one-ini/wasm": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
+      "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+      "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@rollup/pluginutils": {
+      "version": "5.4.0",
+      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz",
+      "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "^1.0.0",
+        "estree-walker": "^2.0.2",
+        "picomatch": "^4.0.2"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+      },
+      "peerDependenciesMeta": {
+        "rollup": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz",
+      "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz",
+      "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz",
+      "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz",
+      "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz",
+      "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz",
+      "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz",
+      "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz",
+      "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz",
+      "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz",
+      "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz",
+      "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-musl": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz",
+      "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz",
+      "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-musl": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz",
+      "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz",
+      "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz",
+      "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz",
+      "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz",
+      "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz",
+      "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openbsd-x64": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz",
+      "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz",
+      "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz",
+      "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz",
+      "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz",
+      "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz",
+      "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@standard-schema/spec": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+      "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/chai": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+      "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/deep-eql": "*",
+        "assertion-error": "^2.0.1"
+      }
+    },
+    "node_modules/@types/deep-eql": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+      "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+      "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "24.12.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
+      "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.16.0"
+      }
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "6.0.8",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz",
+      "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@rolldown/pluginutils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@vitest/expect": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz",
+      "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@standard-schema/spec": "^1.1.0",
+        "@types/chai": "^5.2.2",
+        "@vitest/spy": "4.1.11",
+        "@vitest/utils": "4.1.11",
+        "chai": "^6.2.2",
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/mocker": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz",
+      "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "4.1.11",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^0.30.21"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "msw": "^2.4.9",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "msw": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitest/mocker/node_modules/estree-walker": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+      "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "^1.0.0"
+      }
+    },
+    "node_modules/@vitest/pretty-format": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz",
+      "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/runner": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz",
+      "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/utils": "4.1.11",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/snapshot": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz",
+      "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "4.1.11",
+        "@vitest/utils": "4.1.11",
+        "magic-string": "^0.30.21",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/spy": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz",
+      "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/utils": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz",
+      "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "4.1.11",
+        "convert-source-map": "^2.0.0",
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@volar/language-core": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
+      "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/source-map": "2.4.28"
+      }
+    },
+    "node_modules/@volar/source-map": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz",
+      "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@volar/typescript": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz",
+      "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "2.4.28",
+        "path-browserify": "^1.0.1",
+        "vscode-uri": "^3.0.8"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.42.tgz",
+      "integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.8",
+        "@vue/shared": "3.5.42",
+        "entities": "^7.0.1",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-core/node_modules/entities": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz",
+      "integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.42",
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz",
+      "integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.8",
+        "@vue/compiler-core": "3.5.42",
+        "@vue/compiler-dom": "3.5.42",
+        "@vue/compiler-ssr": "3.5.42",
+        "@vue/shared": "3.5.42",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.19",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz",
+      "integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.42",
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@vue/language-core": {
+      "version": "3.2.6",
+      "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.6.tgz",
+      "integrity": "sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "2.4.28",
+        "@vue/compiler-dom": "^3.5.0",
+        "@vue/shared": "^3.5.0",
+        "alien-signals": "^3.0.0",
+        "muggle-string": "^0.4.1",
+        "path-browserify": "^1.0.1",
+        "picomatch": "^4.0.2"
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.42.tgz",
+      "integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.42.tgz",
+      "integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.42",
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz",
+      "integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.42",
+        "@vue/runtime-core": "3.5.42",
+        "@vue/shared": "3.5.42",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.42.tgz",
+      "integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.42",
+        "@vue/runtime-dom": "3.5.42",
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.42.tgz",
+      "integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@vue/test-utils": {
+      "version": "2.4.6",
+      "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz",
+      "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "js-beautify": "^1.14.9",
+        "vue-component-type-helpers": "^2.0.0"
+      }
+    },
+    "node_modules/abbrev": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
+      "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.18.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
+      "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/alien-signals": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz",
+      "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/ansi-regex": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz",
+      "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+      "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/assertion-error": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+      "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/bidi-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+      "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "require-from-string": "^2.0.2"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
+      "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/chai": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+      "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/commander": {
+      "version": "10.0.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+      "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/compare-versions": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
+      "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/confbox": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
+      "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/config-chain": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
+      "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ini": "^1.3.4",
+        "proto-list": "~1.2.1"
+      }
+    },
+    "node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/css-tree": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+      "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mdn-data": "2.27.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+      }
+    },
+    "node_modules/cssstyle": {
+      "version": "5.3.7",
+      "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz",
+      "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@asamuzakjp/css-color": "^4.1.1",
+        "@csstools/css-syntax-patches-for-csstree": "^1.0.21",
+        "css-tree": "^3.1.0",
+        "lru-cache": "^11.2.4"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/data-urls": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz",
+      "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "whatwg-mimetype": "^5.0.0",
+        "whatwg-url": "^15.1.0"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/data-urls/node_modules/whatwg-mimetype": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+      "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/decimal.js": {
+      "version": "10.6.0",
+      "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+      "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/editorconfig": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz",
+      "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@one-ini/wasm": "0.1.1",
+        "commander": "^10.0.0",
+        "minimatch": "^9.0.1",
+        "semver": "^7.5.3"
+      },
+      "bin": {
+        "editorconfig": "bin/editorconfig"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/entities": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
+      "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/es-module-lexer": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
+      "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/esbuild": {
+      "version": "0.28.2",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
+      "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.28.2",
+        "@esbuild/android-arm": "0.28.2",
+        "@esbuild/android-arm64": "0.28.2",
+        "@esbuild/android-x64": "0.28.2",
+        "@esbuild/darwin-arm64": "0.28.2",
+        "@esbuild/darwin-x64": "0.28.2",
+        "@esbuild/freebsd-arm64": "0.28.2",
+        "@esbuild/freebsd-x64": "0.28.2",
+        "@esbuild/linux-arm": "0.28.2",
+        "@esbuild/linux-arm64": "0.28.2",
+        "@esbuild/linux-ia32": "0.28.2",
+        "@esbuild/linux-loong64": "0.28.2",
+        "@esbuild/linux-mips64el": "0.28.2",
+        "@esbuild/linux-ppc64": "0.28.2",
+        "@esbuild/linux-riscv64": "0.28.2",
+        "@esbuild/linux-s390x": "0.28.2",
+        "@esbuild/linux-x64": "0.28.2",
+        "@esbuild/netbsd-arm64": "0.28.2",
+        "@esbuild/netbsd-x64": "0.28.2",
+        "@esbuild/openbsd-arm64": "0.28.2",
+        "@esbuild/openbsd-x64": "0.28.2",
+        "@esbuild/openharmony-arm64": "0.28.2",
+        "@esbuild/sunos-x64": "0.28.2",
+        "@esbuild/win32-arm64": "0.28.2",
+        "@esbuild/win32-ia32": "0.28.2",
+        "@esbuild/win32-x64": "0.28.2"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/expect-type": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+      "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/exsolve": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz",
+      "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/foreground-child": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+      "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "cross-spawn": "^7.0.6",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/glob": {
+      "version": "10.5.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+      "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+      "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob-to-regexp": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+      "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+      "dev": true,
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/html-encoding-sniffer": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+      "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@exodus/bytes": "^1.6.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      }
+    },
+    "node_modules/http-proxy-agent": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+      "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/ini": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-potential-custom-element-name": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+      "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/jackspeak": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+      "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
+    "node_modules/js-beautify": {
+      "version": "1.15.4",
+      "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz",
+      "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "config-chain": "^1.1.13",
+        "editorconfig": "^1.0.4",
+        "glob": "^10.4.2",
+        "js-cookie": "^3.0.5",
+        "nopt": "^7.2.1"
+      },
+      "bin": {
+        "css-beautify": "js/bin/css-beautify.js",
+        "html-beautify": "js/bin/html-beautify.js",
+        "js-beautify": "js/bin/js-beautify.js"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/js-cookie": {
+      "version": "3.0.8",
+      "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz",
+      "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/jsdom": {
+      "version": "27.4.0",
+      "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz",
+      "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@acemir/cssom": "^0.9.28",
+        "@asamuzakjp/dom-selector": "^6.7.6",
+        "@exodus/bytes": "^1.6.0",
+        "cssstyle": "^5.3.4",
+        "data-urls": "^6.0.0",
+        "decimal.js": "^10.6.0",
+        "html-encoding-sniffer": "^6.0.0",
+        "http-proxy-agent": "^7.0.2",
+        "https-proxy-agent": "^7.0.6",
+        "is-potential-custom-element-name": "^1.0.1",
+        "parse5": "^8.0.0",
+        "saxes": "^6.0.0",
+        "symbol-tree": "^3.2.4",
+        "tough-cookie": "^6.0.0",
+        "w3c-xmlserializer": "^5.0.0",
+        "webidl-conversions": "^8.0.0",
+        "whatwg-mimetype": "^4.0.0",
+        "whatwg-url": "^15.1.0",
+        "ws": "^8.18.3",
+        "xml-name-validator": "^5.0.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "canvas": "^3.0.0"
+      },
+      "peerDependenciesMeta": {
+        "canvas": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/kolorist": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
+      "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/local-pkg": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz",
+      "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mlly": "^1.7.4",
+        "pkg-types": "^2.3.0",
+        "quansync": "^0.2.11"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "11.5.2",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
+      "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/mdn-data": {
+      "version": "2.27.1",
+      "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+      "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+      "dev": true,
+      "license": "CC0-1.0"
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.9",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+      "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+      "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/mlly": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
+      "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "acorn": "^8.16.0",
+        "pathe": "^2.0.3",
+        "pkg-types": "^1.3.1",
+        "ufo": "^1.6.3"
+      }
+    },
+    "node_modules/mlly/node_modules/confbox": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+      "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/mlly/node_modules/pkg-types": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+      "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "confbox": "^0.1.8",
+        "mlly": "^1.7.4",
+        "pathe": "^2.0.1"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/muggle-string": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+      "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.18",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+      "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/nopt": {
+      "version": "7.2.1",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
+      "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "abbrev": "^2.0.0"
+      },
+      "bin": {
+        "nopt": "bin/nopt.js"
+      },
+      "engines": {
+        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+      }
+    },
+    "node_modules/obug": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
+      "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
+      "dev": true,
+      "funding": [
+        "https://github.com/sponsors/sxzz",
+        "https://opencollective.com/debug"
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.20.0"
+      }
+    },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+      "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0"
+    },
+    "node_modules/parse5": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
+      "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "entities": "^8.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/inikulin/parse5?sponsor=1"
+      }
+    },
+    "node_modules/path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-scurry": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/path-scurry/node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+      "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pkg-types": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz",
+      "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "confbox": "^0.2.4",
+        "exsolve": "^1.0.8",
+        "pathe": "^2.0.3"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.26",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
+      "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.17",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/proto-list": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
+      "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/quansync": {
+      "version": "0.2.11",
+      "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
+      "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/antfu"
+        },
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/sxzz"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "4.63.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz",
+      "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.9"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
+        "@rollup/rollup-android-arm-eabi": "4.63.1",
+        "@rollup/rollup-android-arm64": "4.63.1",
+        "@rollup/rollup-darwin-arm64": "4.63.1",
+        "@rollup/rollup-darwin-x64": "4.63.1",
+        "@rollup/rollup-freebsd-arm64": "4.63.1",
+        "@rollup/rollup-freebsd-x64": "4.63.1",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.63.1",
+        "@rollup/rollup-linux-arm-musleabihf": "4.63.1",
+        "@rollup/rollup-linux-arm64-gnu": "4.63.1",
+        "@rollup/rollup-linux-arm64-musl": "4.63.1",
+        "@rollup/rollup-linux-loong64-gnu": "4.63.1",
+        "@rollup/rollup-linux-loong64-musl": "4.63.1",
+        "@rollup/rollup-linux-ppc64-gnu": "4.63.1",
+        "@rollup/rollup-linux-ppc64-musl": "4.63.1",
+        "@rollup/rollup-linux-riscv64-gnu": "4.63.1",
+        "@rollup/rollup-linux-riscv64-musl": "4.63.1",
+        "@rollup/rollup-linux-s390x-gnu": "4.63.1",
+        "@rollup/rollup-linux-x64-gnu": "4.63.1",
+        "@rollup/rollup-linux-x64-musl": "4.63.1",
+        "@rollup/rollup-openbsd-x64": "4.63.1",
+        "@rollup/rollup-openharmony-arm64": "4.63.1",
+        "@rollup/rollup-win32-arm64-msvc": "4.63.1",
+        "@rollup/rollup-win32-ia32-msvc": "4.63.1",
+        "@rollup/rollup-win32-x64-gnu": "4.63.1",
+        "@rollup/rollup-win32-x64-msvc": "4.63.1",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/saxes": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+      "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "xmlchars": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=v12.22.7"
+      }
+    },
+    "node_modules/semver": {
+      "version": "7.8.5",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+      "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/siginfo": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+      "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/stackback": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+      "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/std-env": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
+      "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/string-width-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+      "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.2.2"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/symbol-tree": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+      "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tinybench": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+      "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tinyexec": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz",
+      "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.17",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+      "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tinyrainbow": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz",
+      "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/tldts": {
+      "version": "7.4.11",
+      "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.11.tgz",
+      "integrity": "sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tldts-core": "^7.4.11"
+      },
+      "bin": {
+        "tldts": "bin/cli.js"
+      }
+    },
+    "node_modules/tldts-core": {
+      "version": "7.4.11",
+      "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.11.tgz",
+      "integrity": "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tough-cookie": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
+      "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "tldts": "^7.0.5"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/tr46": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+      "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "punycode": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/ufo": {
+      "version": "1.6.4",
+      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
+      "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/undici-types": {
+      "version": "7.16.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+      "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/unplugin": {
+      "version": "2.3.11",
+      "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
+      "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/remapping": "^2.3.5",
+        "acorn": "^8.15.0",
+        "picomatch": "^4.0.3",
+        "webpack-virtual-modules": "^0.6.2"
+      },
+      "engines": {
+        "node": ">=18.12.0"
+      }
+    },
+    "node_modules/unplugin-dts": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/unplugin-dts/-/unplugin-dts-1.1.0.tgz",
+      "integrity": "sha512-KZJ+qk+lmd9dJY/PJvDRFL9BUtVubKDiX7Ai/X6mlkCArcQNzwmupKjVqHbRA42uqO5ZfFRFc+o8/OCbQ1GonQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@rollup/pluginutils": "^5.1.4",
+        "@volar/typescript": "^2.4.26",
+        "compare-versions": "^6.1.1",
+        "debug": "^4.4.0",
+        "glob-to-regexp": "0.4.1",
+        "kolorist": "^1.8.0",
+        "local-pkg": "^1.1.1",
+        "magic-string": "^0.30.17",
+        "unplugin": "^2.3.2"
+      },
+      "peerDependencies": {
+        "@microsoft/api-extractor": ">=7",
+        "@rspack/core": "^1",
+        "@vue/language-core": "^3.1.5",
+        "esbuild": "*",
+        "rolldown": "*",
+        "rollup": ">=3",
+        "typescript": ">=4",
+        "vite": ">=3",
+        "webpack": "^4 || ^5"
+      },
+      "peerDependenciesMeta": {
+        "@microsoft/api-extractor": {
+          "optional": true
+        },
+        "@rspack/core": {
+          "optional": true
+        },
+        "@vue/language-core": {
+          "optional": true
+        },
+        "esbuild": {
+          "optional": true
+        },
+        "rolldown": {
+          "optional": true
+        },
+        "rollup": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        },
+        "webpack": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vite": {
+      "version": "7.3.6",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
+      "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.27.0 || ^0.28.0",
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3",
+        "postcss": "^8.5.6",
+        "rollup": "^4.43.0",
+        "tinyglobby": "^0.2.15"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^20.19.0 || >=22.12.0",
+        "jiti": ">=1.21.0",
+        "less": "^4.0.0",
+        "lightningcss": "^1.21.0",
+        "sass": "^1.70.0",
+        "sass-embedded": "^1.70.0",
+        "stylus": ">=0.54.8",
+        "sugarss": "^5.0.0",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vite-plugin-dts": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-5.1.0.tgz",
+      "integrity": "sha512-MfLc2G+mXPUDGGxpHQeYbdunavFP9UpT9yxiAsTgUXRxfEd3HqysSAM4E72jhz5eIz3OOexCHkqiVlQq8ok9/A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "unplugin-dts": "1.1.0"
+      },
+      "peerDependencies": {
+        "@microsoft/api-extractor": ">=7",
+        "rollup": ">=3",
+        "vite": ">=3"
+      },
+      "peerDependenciesMeta": {
+        "@microsoft/api-extractor": {
+          "optional": true
+        },
+        "rollup": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vitest": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz",
+      "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/expect": "4.1.11",
+        "@vitest/mocker": "4.1.11",
+        "@vitest/pretty-format": "4.1.11",
+        "@vitest/runner": "4.1.11",
+        "@vitest/snapshot": "4.1.11",
+        "@vitest/spy": "4.1.11",
+        "@vitest/utils": "4.1.11",
+        "es-module-lexer": "^2.0.0",
+        "expect-type": "^1.3.0",
+        "magic-string": "^0.30.21",
+        "obug": "^2.1.1",
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.3",
+        "std-env": "^4.0.0-rc.1",
+        "tinybench": "^2.9.0",
+        "tinyexec": "^1.0.2",
+        "tinyglobby": "^0.2.15",
+        "tinyrainbow": "^3.1.0",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+        "why-is-node-running": "^2.3.0"
+      },
+      "bin": {
+        "vitest": "vitest.mjs"
+      },
+      "engines": {
+        "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "@edge-runtime/vm": "*",
+        "@opentelemetry/api": "^1.9.0",
+        "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+        "@vitest/browser-playwright": "4.1.11",
+        "@vitest/browser-preview": "4.1.11",
+        "@vitest/browser-webdriverio": "4.1.11",
+        "@vitest/coverage-istanbul": "4.1.11",
+        "@vitest/coverage-v8": "4.1.11",
+        "@vitest/ui": "4.1.11",
+        "happy-dom": "*",
+        "jsdom": "*",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@edge-runtime/vm": {
+          "optional": true
+        },
+        "@opentelemetry/api": {
+          "optional": true
+        },
+        "@types/node": {
+          "optional": true
+        },
+        "@vitest/browser-playwright": {
+          "optional": true
+        },
+        "@vitest/browser-preview": {
+          "optional": true
+        },
+        "@vitest/browser-webdriverio": {
+          "optional": true
+        },
+        "@vitest/coverage-istanbul": {
+          "optional": true
+        },
+        "@vitest/coverage-v8": {
+          "optional": true
+        },
+        "@vitest/ui": {
+          "optional": true
+        },
+        "happy-dom": {
+          "optional": true
+        },
+        "jsdom": {
+          "optional": true
+        },
+        "vite": {
+          "optional": false
+        }
+      }
+    },
+    "node_modules/vscode-uri": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz",
+      "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vue": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.42.tgz",
+      "integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.42",
+        "@vue/compiler-sfc": "3.5.42",
+        "@vue/runtime-dom": "3.5.42",
+        "@vue/server-renderer": "3.5.42",
+        "@vue/shared": "3.5.42"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-component-type-helpers": {
+      "version": "2.2.12",
+      "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.12.tgz",
+      "integrity": "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vue-router": {
+      "version": "4.6.4",
+      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
+      "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/vue-tsc": {
+      "version": "3.2.6",
+      "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.6.tgz",
+      "integrity": "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/typescript": "2.4.28",
+        "@vue/language-core": "3.2.6"
+      },
+      "bin": {
+        "vue-tsc": "bin/vue-tsc.js"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.0"
+      }
+    },
+    "node_modules/w3c-xmlserializer": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+      "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "xml-name-validator": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/webidl-conversions": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+      "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/webpack-virtual-modules": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
+      "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/whatwg-mimetype": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+      "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/whatwg-url": {
+      "version": "15.1.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz",
+      "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tr46": "^6.0.0",
+        "webidl-conversions": "^8.0.0"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/why-is-node-running": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+      "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "siginfo": "^2.0.0",
+        "stackback": "0.0.2"
+      },
+      "bin": {
+        "why-is-node-running": "cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ws": {
+      "version": "8.21.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
+      "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/xml-name-validator": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+      "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/xmlchars": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+      "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+      "dev": true,
+      "license": "MIT"
+    }
+  }
+}
diff --git a/viewers/webcomponents/vue/package.json b/viewers/webcomponents/vue/package.json
new file mode 100644
index 00000000000..82e3815c81b
--- /dev/null
+++ b/viewers/webcomponents/vue/package.json
@@ -0,0 +1,51 @@
+{
+  "name": "@apache-causeway/vue-viewer",
+  "version": "4.0.0-SNAPSHOT",
+  "description": "Router-led Vue integration for Apache Causeway semantic web components",
+  "type": "module",
+  "license": "Apache-2.0",
+  "sideEffects": false,
+  "files": [
+    "dist",
+    "README.adoc",
+    "LICENSE",
+    "NOTICE"
+  ],
+  "main": "./dist/causeway-vue.js",
+  "module": "./dist/causeway-vue.js",
+  "types": "./dist/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./dist/index.d.ts",
+      "import": "./dist/causeway-vue.js"
+    },
+    "./theme.css": "./dist/causeway-vue.css"
+  },
+  "scripts": {
+    "build": "vite build",
+    "check": "vue-tsc --noEmit && vitest run",
+    "test": "vitest run",
+    "typecheck": "vue-tsc --noEmit",
+    "verify": "npm run check && npm run build && node verify-package.mjs"
+  },
+  "peerDependencies": {
+    "vue": "^3.5.0",
+    "vue-router": "^4.6.0"
+  },
+  "devDependencies": {
+    "@types/node": "24.12.0",
+    "@vitejs/plugin-vue": "6.0.8",
+    "@vue/test-utils": "2.4.6",
+    "jsdom": "27.4.0",
+    "typescript": "5.9.3",
+    "vite": "7.3.6",
+    "vite-plugin-dts": "5.1.0",
+    "vitest": "4.1.11",
+    "vue": "3.5.42",
+    "vue-router": "4.6.4",
+    "vue-tsc": "3.2.6"
+  },
+  "engines": {
+    "node": ">=20"
+  }
+}
diff --git a/viewers/webcomponents/vue/pom.xml b/viewers/webcomponents/vue/pom.xml
new file mode 100644
index 00000000000..f6fe5ce3a8c
--- /dev/null
+++ b/viewers/webcomponents/vue/pom.xml
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.causeway.viewer</groupId>
+        <artifactId>causeway-viewer-webcomponents</artifactId>
+        <version>4.0.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>causeway-viewer-webcomponents-vue</artifactId>
+    <name>Apache Causeway Viewer - Web Components (Vue Integration)</name>
+    <packaging>jar</packaging>
+
+    <properties>
+        <jar-plugin.automaticModuleName>org.apache.causeway.viewer.webcomponents.vue</jar-plugin.automaticModuleName>
+        <git-plugin.propertiesDir>org/apache/causeway/viewer/webcomponents/vue</git-plugin.propertiesDir>
+    </properties>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>dist</directory>
+                <targetPath>META-INF/resources/causeway-vue</targetPath>
+                <filtering>false</filtering>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes combine.children="append">
+                        <exclude>package-lock.json</exclude>
+                        <exclude>dist/**</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-antrun-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>verify-vue-distribution</id>
+                        <phase>validate</phase>
+                        <goals>
+                            <goal>run</goal>
+                        </goals>
+                        <configuration>
+                            <target>
+                                <available file="${project.basedir}/dist/causeway-vue.js" property="vue.distribution.present"/>
+                                <fail unless="vue.distribution.present" message="Vue distribution is missing; run Maven with -Pregenerate-vue-viewer."/>
+                            </target>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <profiles>
+        <profile>
+            <id>regenerate-vue-viewer</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-antrun-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>regenerate-vue-viewer</id>
+                                <phase>generate-resources</phase>
+                                <goals>
+                                    <goal>run</goal>
+                                </goals>
+                                <configuration>
+                                    <target>
+                                        <exec executable="npm" dir="${project.basedir}" failonerror="true">
+                                            <arg value="ci"/>
+                                            <arg value="--ignore-scripts"/>
+                                            <arg value="--no-audit"/>
+                                            <arg value="--no-fund"/>
+                                        </exec>
+                                        <exec executable="npm" dir="${project.basedir}" failonerror="true">
+                                            <arg value="run"/>
+                                            <arg value="verify"/>
+                                        </exec>
+                                    </target>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
+</project>
diff --git a/viewers/webcomponents/vue/src/boundary.ts b/viewers/webcomponents/vue/src/boundary.ts
new file mode 100644
index 00000000000..83e03907bae
--- /dev/null
+++ b/viewers/webcomponents/vue/src/boundary.ts
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type {ObjectRouteIdentity} from './route-codec';
+
+export const ROUTE_CONTEXT_SELECTOR = 'cw-object-context[data-causeway-route-context]';
+export const ROUTE_INTERACTIONS_SELECTOR = 'cw-interaction-controller[data-causeway-route-interactions]';
+
+export interface BoundaryValidation {
+  readonly valid: boolean;
+  readonly classification?: 'missing-context' | 'duplicate-context' | 'identity' | 'interactions';
+  readonly context?: HTMLElement;
+}
+
+export function validateRouteBoundary(root: ParentNode, identity: ObjectRouteIdentity): BoundaryValidation {
+  const contexts = [...root.querySelectorAll<HTMLElement>(ROUTE_CONTEXT_SELECTOR)]
+    .filter(context => !context.parentElement?.closest(ROUTE_CONTEXT_SELECTOR));
+  if (contexts.length === 0) return Object.freeze({valid: false, classification: 'missing-context'});
+  if (contexts.length !== 1) return Object.freeze({valid: false, classification: 'duplicate-context'});
+  const context = contexts[0];
+  if (context.getAttribute('logical-type') !== identity.logicalTypeName
+      || context.getAttribute('object-id') !== identity.objectId) {
+    return Object.freeze({valid: false, classification: 'identity'});
+  }
+  const controllers = [...context.querySelectorAll<HTMLElement>(ROUTE_INTERACTIONS_SELECTOR)]
+    .filter(controller => controller.closest(ROUTE_CONTEXT_SELECTOR) === context);
+  if (controllers.length !== 1) return Object.freeze({valid: false, classification: 'interactions'});
+  return Object.freeze({valid: true, context});
+}
+
+export interface ShellLandmarks {
+  readonly shell: HTMLElement;
+  readonly client: HTMLElement;
+  readonly route: HTMLElement;
+  readonly loading: HTMLElement;
+  readonly announcement: HTMLElement;
+  readonly result: HTMLElement;
+}
+
+function exactlyOne(root: ParentNode, selector: string): HTMLElement | null {
+  const values = root.querySelectorAll<HTMLElement>(selector);
+  return values.length === 1 ? values[0] : null;
+}
+
+export function validateShellBoundary(shell: HTMLElement): ShellLandmarks {
+  const client = exactlyOne(shell, 'cw-graphql-client[data-causeway-shell-client]');
+  const route = exactlyOne(shell, '[data-causeway-router-view]');
+  const loading = exactlyOne(shell, '[data-causeway-route-loading]');
+  const announcement = exactlyOne(shell, '[data-causeway-route-announcement]');
+  const result = exactlyOne(shell, 'cw-action-results[data-causeway-shell-result]');
+  if (!client || !route || !loading || !announcement || !result
+      || !client.contains(route) || !client.contains(result)) {
+    throw new Error('The authored Vue application shell is invalid.');
+  }
+  return Object.freeze({shell, client, route, loading, announcement, result});
+}
diff --git a/viewers/webcomponents/vue/src/components/GenericObjectPage.vue b/viewers/webcomponents/vue/src/components/GenericObjectPage.vue
new file mode 100644
index 00000000000..f752a61dce8
--- /dev/null
+++ b/viewers/webcomponents/vue/src/components/GenericObjectPage.vue
@@ -0,0 +1,43 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<script setup lang="ts">
+import type {CausewayRoutePageProps} from '../contracts';
+
+defineProps<CausewayRoutePageProps>();
+</script>
+
+<template>
+  <section
+    class="causeway-vue-route-page causeway-vue-route-object"
+    data-causeway-route-page
+    data-page-kind="generic"
+    data-route-state="loading"
+    tabindex="-1"
+    aria-label="Object page"
+  >
+    <cw-object-context
+      data-causeway-route-context
+      :logical-type="logicalTypeName"
+      :object-id="objectId"
+    >
+      <cw-object editable />
+      <cw-interaction-controller data-causeway-route-interactions />
+    </cw-object-context>
+  </section>
+</template>
diff --git a/viewers/webcomponents/vue/src/components/HomePage.vue b/viewers/webcomponents/vue/src/components/HomePage.vue
new file mode 100644
index 00000000000..11f1e707876
--- /dev/null
+++ b/viewers/webcomponents/vue/src/components/HomePage.vue
@@ -0,0 +1,86 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<script setup lang="ts">
+import {onMounted, ref} from 'vue';
+import {canonicalRouterObjectPath} from '../route-codec';
+import {useCausewayViewer} from '../plugin';
+
+const runtime = useCausewayViewer();
+const page = ref<HTMLElement | null>(null);
+const state = ref<'loading' | 'ready' | 'partial-error' | 'unsupported'>('loading');
+const message = ref('Loading the application home page…');
+
+function requestClient(element: HTMLElement): any {
+  let client: any = null;
+  element.dispatchEvent(new CustomEvent('causeway-graphql-client-request', {
+    bubbles: true,
+    composed: true,
+    detail: {provide(candidate: any) { client ??= candidate; }}
+  }));
+  return client;
+}
+
+onMounted(async () => {
+  const generation = ++runtime.state.routeGeneration;
+  const client = page.value ? requestClient(page.value) : null;
+  if (!client) {
+    state.value = 'unsupported';
+    message.value = 'Choose an application action to begin.';
+    return;
+  }
+  try {
+    const description = await client.describeApplicationEntry();
+    if (generation !== runtime.state.routeGeneration) return;
+    if (!description?.supported) {
+      state.value = 'unsupported';
+      message.value = 'Choose an application action to begin.';
+      return;
+    }
+    const response = await client.readApplicationEntry({description});
+    if (generation !== runtime.state.routeGeneration) return;
+    const home = response?.data?.home;
+    const metadata = home?.object?._meta;
+    const logicalTypeName = metadata?.logicalTypeName ?? home?.logicalTypeName;
+    const id = metadata?.id;
+    const claim = {claimed: false, claim() { if (this.claimed) return false; this.claimed = true; return true; }};
+    const policyContext = {router: runtime.router, basePath: runtime.basePath, shell: runtime.state.shell, routeGeneration: generation};
+    const handled = await runtime.policies.home?.(response?.data, claim, policyContext);
+    if (handled === true) claim.claim();
+    if (generation !== runtime.state.routeGeneration || claim.claimed) return;
+    if (home?.kind === 'OBJECT' && logicalTypeName && id) {
+      await runtime.router.replace(canonicalRouterObjectPath({logicalTypeName, id}));
+      return;
+    }
+    state.value = response?.errors?.length ? 'partial-error' : 'ready';
+    message.value = 'Choose an application action to begin.';
+  } catch (error) {
+    if (generation !== runtime.state.routeGeneration) return;
+    runtime.policies.error?.(error, {router: runtime.router, basePath: runtime.basePath, shell: runtime.state.shell, routeGeneration: generation});
+    state.value = 'partial-error';
+    message.value = 'The home page is unavailable; application menus remain available.';
+  }
+});
+</script>
+
+<template>
+  <section ref="page" class="causeway-vue-route-page causeway-vue-status" :data-route-state="state" tabindex="-1">
+    <h1>Welcome</h1>
+    <p>{{ message }}</p>
+  </section>
+</template>
diff --git a/viewers/webcomponents/vue/src/components/ObjectRoutePage.vue b/viewers/webcomponents/vue/src/components/ObjectRoutePage.vue
new file mode 100644
index 00000000000..dee2b998e39
--- /dev/null
+++ b/viewers/webcomponents/vue/src/components/ObjectRoutePage.vue
@@ -0,0 +1,183 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<script setup lang="ts">
+import {computed, defineAsyncComponent, defineComponent, h, nextTick, onBeforeUnmount, onMounted, ref, watch} from 'vue';
+import {useRoute} from 'vue-router';
+import {validateRouteBoundary} from '../boundary';
+import {useCausewayViewer} from '../plugin';
+import {isPageLoader} from '../registry';
+import {canonicalRouteKey, parseCanonicalObjectPath} from '../route-codec';
+import GenericObjectPage from './GenericObjectPage.vue';
+
+const route = useRoute();
+const runtime = useCausewayViewer();
+const host = ref<HTMLElement | null>(null);
+const boundaryError = ref<string | null>(null);
+const identity = computed(() => parseCanonicalObjectPath(route.path, '/'));
+const LoadingPage = defineComponent(() => () => h('section', {
+  class: 'causeway-vue-route-page causeway-vue-status',
+  'data-route-state': 'loading',
+  tabindex: -1,
+  role: 'status'
+}, [h('h1', 'Loading page…')]));
+const ErrorPage = defineComponent(() => () => h('section', {
+  class: 'causeway-vue-route-page causeway-vue-status causeway-vue-status-danger',
+  'data-route-state': 'terminal-error',
+  tabindex: -1,
+  role: 'alert'
+}, [h('h1', 'Page unavailable'), h('p', 'The application page could not be loaded.') ]));
+const routeKey = computed(() => canonicalRouteKey(identity.value));
+const registration = computed(() => runtime.pages.get(identity.value.logicalTypeName));
+const selectedPage = computed(() => {
+  const selected = registration.value;
+  if (!selected) return GenericObjectPage;
+  if (!isPageLoader(selected)) return selected;
+  return defineAsyncComponent({
+    loader: async () => {
+      const loaded = await selected();
+      return 'default' in loaded ? loaded.default : loaded;
+    },
+    delay: 0,
+    timeout: 30000,
+    loadingComponent: LoadingPage,
+    errorComponent: ErrorPage,
+    onError(error, _retry, fail) {
+      runtime.policies.error?.(error, {
+        router: runtime.router,
+        basePath: runtime.basePath,
+        shell: runtime.state.shell,
+        routeGeneration: runtime.state.routeGeneration
+      });
+      fail();
+    }
+  });
+});
+
+function landmarks() {
+  const shell = runtime.state.shell;
+  return {
+    route: shell?.querySelector<HTMLElement>('[data-causeway-router-view]') ?? null,
+    loading: shell?.querySelector<HTMLElement>('[data-causeway-route-loading]') ?? null,
+    announcement: shell?.querySelector<HTMLElement>('[data-causeway-route-announcement]') ?? null
+  };
+}
+
+function announce(message: string, generation: number): void {
+  const target = landmarks().announcement;
+  if (!target) return;
+  target.textContent = '';
+  requestAnimationFrame(() => {
+    if (generation === runtime.state.routeGeneration && target.isConnected) target.textContent = message;
+  });
+}
+
+function setBusy(busy: boolean): void {
+  const shellLandmarks = landmarks();
+  shellLandmarks.route?.setAttribute('aria-busy', String(busy));
+  if (shellLandmarks.loading) shellLandmarks.loading.hidden = !busy;
+}
+
+async function validateBoundary(): Promise<void> {
+  const generation = runtime.state.routeGeneration;
+  boundaryError.value = null;
+  await nextTick();
+  if (generation !== runtime.state.routeGeneration || !host.value) return;
+  const validation = validateRouteBoundary(host.value, identity.value);
+  if (!validation.valid) {
+    boundaryError.value = validation.classification ?? 'invalid';
+    setBusy(false);
+    announce('Page unavailable', generation);
+  }
+}
+
+function onObjectState(rawEvent: Event): void {
+  const generation = runtime.state.routeGeneration;
+  const event = rawEvent as CustomEvent;
+  const state = event.detail?.state;
+  const page = (event.target as Element | null)?.closest<HTMLElement>('[data-causeway-route-page]');
+  if (!state || !page || !host.value?.contains(page)) return;
+  let status = String(state.status ?? 'terminal-error');
+  if (status === 'terminal-error') {
+    const code = state.errors?.[0]?.extensions?.classification
+      ?? state.errors?.[0]?.extensions?.code
+      ?? state.error?.code;
+    if (code === 'NOT_FOUND' || code === 'ACCESS_DENIED') status = 'unavailable';
+  }
+  page.dataset.routeState = status;
+  if (status === 'ready' || status === 'partial-error') {
+    setBusy(false);
+    const title = state.snapshot?.data?._meta?.title;
+    if (title) document.title = String(title);
+    for (const collection of page.querySelectorAll<HTMLElement>('cw-collection:not([active])')) {
+      (collection as HTMLElement & {activate?: () => void}).activate?.();
+    }
+    announce(status === 'ready' ? 'Page ready' : 'Page ready with partial information', generation);
+  } else if (status === 'terminal-error' || status === 'unavailable') {
+    setBusy(false);
+    announce('Page unavailable', generation);
+  }
+}
+
+watch(() => route.fullPath, () => {
+  runtime.state.routeGeneration += 1;
+  setBusy(true);
+  announce('Loading page', runtime.state.routeGeneration);
+  void validateBoundary();
+}, {immediate: true});
+onMounted(async () => {
+  host.value?.addEventListener('causeway-object-context-state-change', onObjectState);
+  await validateBoundary();
+  const contextElement = host.value?.querySelector<HTMLElement & {context?: {state?: unknown}}>(
+    'cw-object-context[data-causeway-route-context]');
+  if (contextElement?.context?.state) {
+    contextElement.dispatchEvent(new CustomEvent('causeway-object-context-state-change', {
+      bubbles: true,
+      composed: true,
+      detail: {state: contextElement.context.state, context: contextElement.context}
+    }));
+  }
+  host.value?.querySelector<HTMLElement>('[data-causeway-route-page]')?.focus({preventScroll: true});
+});
+onBeforeUnmount(() => host.value?.removeEventListener('causeway-object-context-state-change', onObjectState));
+</script>
+
+<template>
+  <div ref="host" class="causeway-vue-object-route" :data-route-key="routeKey">
+    <section
+      v-if="boundaryError"
+      class="causeway-vue-route-page causeway-vue-status causeway-vue-status-danger"
+      data-route-state="terminal-error"
+      tabindex="-1"
+      role="alert"
+    >
+      <h1>Page unavailable</h1>
+      <p>The application page has an invalid semantic context boundary.</p>
+    </section>
+    <component
+      :is="selectedPage"
+      v-else
+      :key="routeKey"
+      :logical-type-name="identity.logicalTypeName"
+      :object-id="identity.objectId"
+      :route-key="routeKey"
+      @vue:mounted="validateBoundary"
+      @vue:updated="validateBoundary"
+    />
+  </div>
+</template>
diff --git a/viewers/webcomponents/vue/src/contracts.ts b/viewers/webcomponents/vue/src/contracts.ts
new file mode 100644
index 00000000000..2cc3c18b577
--- /dev/null
+++ b/viewers/webcomponents/vue/src/contracts.ts
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type {InjectionKey, Plugin} from 'vue';
+import type {Router} from 'vue-router';
+import type {CausewayPageRegistration, CausewayPageRegistryInput} from './registry';
+
+export interface CausewayObjectTarget {
+  readonly logicalTypeName?: string;
+  readonly id?: string;
+  readonly objectId?: string;
+  readonly title?: string;
+}
+
+export interface CausewaySemanticResult {
+  readonly kind: 'object' | 'scalar' | 'collection' | 'void' | string;
+  readonly value?: unknown;
+}
+
+export interface CausewayEventClaim {
+  readonly claimed: boolean;
+  claim(): boolean;
+}
+
+export interface CausewayPolicyContext {
+  readonly router: Router;
+  readonly basePath: string;
+  readonly shell: HTMLElement | null;
+  readonly routeGeneration: number;
+}
+
+export interface CausewayViewerPolicies {
+  readonly navigate?: (target: CausewayObjectTarget, claim: CausewayEventClaim, context: CausewayPolicyContext) => boolean | void | Promise<boolean | void>;
+  readonly home?: (entry: unknown, claim: CausewayEventClaim, context: CausewayPolicyContext) => boolean | void | Promise<boolean | void>;
+  readonly result?: (detail: unknown, claim: CausewayEventClaim, context: CausewayPolicyContext) => boolean | void | Promise<boolean | void>;
+  readonly error?: (error: unknown, context: CausewayPolicyContext) => void;
+}
+
+export interface CausewayViewerOptions {
+  readonly router: Router;
+  readonly endpoint: string;
+  readonly basePath?: string;
+  readonly pages?: CausewayPageRegistryInput;
+  readonly policies?: CausewayViewerPolicies;
+  readonly developmentDiagnostics?: boolean;
+}
+
+export interface CausewayViewerRuntime {
+  readonly plugin: Plugin;
+  readonly router: Router;
+  readonly endpoint: string;
+  readonly basePath: string;
+  readonly pages: ReadonlyMap<string, CausewayPageRegistration>;
+  readonly policies: CausewayViewerPolicies;
+  readonly developmentDiagnostics: boolean;
+  readonly state: {
+    shell: HTMLElement | null;
+    routeGeneration: number;
+  };
+}
+
+export const CAUSEWAY_VIEWER_KEY: InjectionKey<CausewayViewerRuntime> = Symbol('causeway-vue-viewer');
+
+export interface CausewayRoutePageProps {
+  readonly logicalTypeName: string;
+  readonly objectId: string;
+  readonly routeKey: string;
+}
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetSpecies.java b/viewers/webcomponents/vue/src/env.d.ts
similarity index 77%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetSpecies.java
rename to viewers/webcomponents/vue/src/env.d.ts
index 587f9f5ebee..14a6ee33751 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetSpecies.java
+++ b/viewers/webcomponents/vue/src/env.d.ts
@@ -16,13 +16,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
 
-public enum PetSpecies {
-    BIRD,
-    CAT,
-    DOG,
-    HAMSTER,
-    LIZARD,
-    SNAKE
+/// <reference types="vite/client" />
+
+declare module '*.vue' {
+  import type {DefineComponent} from 'vue';
+  const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>;
+  export default component;
 }
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicDomainModule.java b/viewers/webcomponents/vue/src/index.ts
similarity index 63%
copy from viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicDomainModule.java
copy to viewers/webcomponents/vue/src/index.ts
index 08fffd69475..ff16b6a8778 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicDomainModule.java
+++ b/viewers/webcomponents/vue/src/index.ts
@@ -16,16 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.sample.htmx.petclinic.domain;
 
-import org.springframework.boot.persistence.autoconfigure.EntityScan;
-import org.springframework.context.annotation.ComponentScan;
-import org.springframework.context.annotation.Configuration;
+import './theme.css';
 
-@Configuration
-@ComponentScan
-@EntityScan(basePackageClasses = PetClinicDomainModule.class)
-public class PetClinicDomainModule {
-
-    public static final String NAMESPACE = "petclinic";
-}
+export * from './boundary';
+export * from './contracts';
+export * from './plugin';
+export * from './policy';
+export * from './registry';
+export * from './route-codec';
+export * from './routes';
+export {default as CausewayGenericObjectPage} from './components/GenericObjectPage.vue';
+export {default as CausewayHomePage} from './components/HomePage.vue';
+export {default as CausewayObjectRoutePage} from './components/ObjectRoutePage.vue';
diff --git a/viewers/webcomponents/vue/src/plugin.ts b/viewers/webcomponents/vue/src/plugin.ts
new file mode 100644
index 00000000000..dccdfc4f8d1
--- /dev/null
+++ b/viewers/webcomponents/vue/src/plugin.ts
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {inject, onBeforeUnmount, onMounted, type Plugin, type Ref} from 'vue';
+import {CAUSEWAY_VIEWER_KEY, type CausewayViewerOptions, type CausewayViewerRuntime} from './contracts';
+import {validateShellBoundary, type ShellLandmarks} from './boundary';
+import {installSemanticBridge} from './policy';
+import {normalizeBasePath} from './route-codec';
+import {normalizePageRegistry} from './registry';
+
+export function createCausewayVueViewer(options: CausewayViewerOptions): CausewayViewerRuntime {
+  if (!options?.router) throw new Error('The Vue viewer requires the application router.');
+  const endpoint = String(options.endpoint ?? '').trim();
+  if (!endpoint) throw new Error('The Vue viewer GraphQL endpoint is required.');
+  const state = {shell: null as HTMLElement | null, routeGeneration: 0};
+  let runtime!: CausewayViewerRuntime;
+  const plugin: Plugin = {
+    install(app) {
+      app.provide(CAUSEWAY_VIEWER_KEY, runtime);
+    }
+  };
+  runtime = Object.freeze({
+    plugin,
+    router: options.router,
+    endpoint,
+    basePath: normalizeBasePath(options.basePath ?? (options.router.options.history.base || '/')),
+    pages: normalizePageRegistry(options.pages),
+    policies: Object.freeze({...options.policies}),
+    developmentDiagnostics: options.developmentDiagnostics ?? true,
+    state
+  });
+  return runtime;
+}
+
+export function useCausewayViewer(): CausewayViewerRuntime {
+  const runtime = inject(CAUSEWAY_VIEWER_KEY);
+  if (!runtime) throw new Error('The Causeway Vue viewer plugin is not installed.');
+  return runtime;
+}
+
+export function useCausewayShell(shellReference: Ref<HTMLElement | null>): Ref<HTMLElement | null> {
+  const runtime = useCausewayViewer();
+  let cleanup: (() => void) | null = null;
+  onMounted(() => {
+    const shell = shellReference.value;
+    if (!shell) throw new Error('The authored Vue application shell is missing.');
+    validateShellBoundary(shell);
+    runtime.state.shell = shell;
+    cleanup = installSemanticBridge(runtime, shell);
+  });
+  onBeforeUnmount(() => {
+    cleanup?.();
+    cleanup = null;
+    if (runtime.state.shell === shellReference.value) runtime.state.shell = null;
+  });
+  return shellReference;
+}
+
+export function bindCausewayShell(runtime: CausewayViewerRuntime, shell: HTMLElement): {landmarks: ShellLandmarks; dispose: () => void} {
+  const landmarks = validateShellBoundary(shell);
+  runtime.state.shell = shell;
+  const cleanup = installSemanticBridge(runtime, shell);
+  return {
+    landmarks,
+    dispose() {
+      cleanup();
+      if (runtime.state.shell === shell) runtime.state.shell = null;
+    }
+  };
+}
diff --git a/viewers/webcomponents/vue/src/policy.ts b/viewers/webcomponents/vue/src/policy.ts
new file mode 100644
index 00000000000..56dea4749da
--- /dev/null
+++ b/viewers/webcomponents/vue/src/policy.ts
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type {
+  CausewayEventClaim,
+  CausewayObjectTarget,
+  CausewayPolicyContext,
+  CausewaySemanticResult,
+  CausewayViewerRuntime
+} from './contracts';
+import {canonicalRouterObjectPath} from './route-codec';
+
+export const NAVIGATION_REQUEST_EVENT = 'causeway-navigation-request';
+export const ACTION_REQUEST_EVENT = 'causeway-action-request';
+export const ACTION_RESULT_EVENT = 'causeway-action-result';
+export const OBJECT_CONTEXT_STATE_EVENT = 'causeway-object-context-state-change';
+
+interface ActionResultDetail {
+  readonly actionId?: string;
+  readonly context?: object;
+  readonly result?: CausewaySemanticResult;
+  readonly resultPresentation?: Record<string, unknown>;
+}
+
+function createClaim(): CausewayEventClaim {
+  let claimed = false;
+  return {
+    get claimed() { return claimed; },
+    claim() {
+      if (claimed) return false;
+      claimed = true;
+      return true;
+    }
+  };
+}
+
+function context(runtime: CausewayViewerRuntime): CausewayPolicyContext {
+  return Object.freeze({
+    router: runtime.router,
+    basePath: runtime.basePath,
+    shell: runtime.state.shell,
+    routeGeneration: runtime.state.routeGeneration
+  });
+}
+
+function objectIdentity(result: CausewaySemanticResult | undefined): CausewayObjectTarget | null {
+  if (result?.kind !== 'object' || !result.value || typeof result.value !== 'object') return null;
+  const metadata = (result.value as {_meta?: Record<string, unknown>})._meta;
+  const logicalTypeName = metadata?.logicalTypeName;
+  const id = metadata?.id;
+  if (typeof logicalTypeName !== 'string' || typeof id !== 'string' || !logicalTypeName || !id) return null;
+  return Object.freeze({logicalTypeName, id, title: String(metadata?.title ?? id)});
+}
+
+function eligiblePageOutlets(shell: HTMLElement): HTMLElement[] {
+  const route = shell.querySelector<HTMLElement>('[data-causeway-router-view]');
+  return [...(route?.querySelectorAll<HTMLElement>('cw-action-results[data-causeway-page-result]') ?? [])]
+    .filter(outlet => outlet.isConnected);
+}
+
+function shellOutlet(shell: HTMLElement): HTMLElement | null {
+  const outlets = shell.querySelectorAll<HTMLElement>('cw-action-results[data-causeway-shell-result]');
+  return outlets.length === 1 ? outlets[0] : null;
+}
+
+export function resolveResultOutlet(shell: HTMLElement): HTMLElement {
+  const page = eligiblePageOutlets(shell);
+  if (page.length > 1) throw new Error('The active Vue route has duplicate action-result outlets.');
+  if (page.length === 1) return page[0];
+  const fallback = shellOutlet(shell);
+  if (!fallback?.isConnected) throw new Error('The authored Vue shell result outlet is unavailable.');
+  return fallback;
+}
+
+function replacePresentation(outlet: HTMLElement, ...nodes: Node[]): void {
+  const enhanced = outlet as HTMLElement & {replacePresentation?: (...nodes: Node[]) => void};
+  if (enhanced.replacePresentation) enhanced.replacePresentation(...nodes);
+  else outlet.replaceChildren(...nodes);
+  outlet.hidden = nodes.length === 0;
+}
+
+export function presentSemanticResult(outlet: HTMLElement, detail: ActionResultDetail): void {
+  const result = detail.result;
+  const heading = document.createElement('h2');
+  heading.textContent = detail.actionId ? `${detail.actionId} result` : 'Action result';
+  if (result?.kind === 'collection') {
+    const collection = document.createElement('cw-standalone-collection') as HTMLElement & {result?: CausewaySemanticResult};
+    collection.setAttribute('data-testid', 'causeway-standalone-action-result');
+    collection.setAttribute('named', String(detail.resultPresentation?.named ?? heading.textContent));
+    collection.result = result;
+    replacePresentation(outlet, collection);
+    return;
+  }
+  const output = document.createElement('output');
+  output.textContent = result?.kind === 'scalar' ? String(result.value ?? '') :
+    result?.kind === 'void' ? 'Completed' : 'This result cannot be presented.';
+  replacePresentation(outlet, heading, output);
+}
+
+async function refreshAfterVoid(runtime: CausewayViewerRuntime, detail: ActionResultDetail): Promise<void> {
+  const shell = runtime.state.shell;
+  const routeRegion = shell?.querySelector<HTMLElement>('[data-causeway-router-view]');
+  const contextElement = routeRegion?.querySelector<HTMLElement>('cw-object-context[data-causeway-route-context]') as
+    (HTMLElement & {context?: {refresh?: () => void}}) | null;
+  const objectContext = contextElement?.context;
+  if (!contextElement || !objectContext?.refresh) return;
+  const generation = runtime.state.routeGeneration;
+  const onState = (rawEvent: Event) => {
+    if (generation !== runtime.state.routeGeneration) return cleanup();
+    const state = (rawEvent as CustomEvent).detail?.state;
+    if (state?.status !== 'terminal-error') {
+      if (state?.status === 'ready' || state?.status === 'partial-error') cleanup();
+      return;
+    }
+    cleanup();
+    const code = state.errors?.[0]?.extensions?.classification
+      ?? state.errors?.[0]?.extensions?.code
+      ?? state.error?.code;
+    if (code === 'NOT_FOUND') void runtime.router.replace('/');
+  };
+  const cleanup = () => contextElement.removeEventListener(OBJECT_CONTEXT_STATE_EVENT, onState);
+  contextElement.addEventListener(OBJECT_CONTEXT_STATE_EVENT, onState);
+  objectContext.refresh();
+  presentSemanticResult(resolveResultOutlet(shell!), detail);
+}
+
+async function applyNavigation(runtime: CausewayViewerRuntime, target: CausewayObjectTarget): Promise<void> {
+  const claim = createClaim();
+  const handled = await runtime.policies.navigate?.(target, claim, context(runtime));
+  if (handled === true) claim.claim();
+  if (!claim.claimed) await runtime.router.push(canonicalRouterObjectPath(target));
+}
+
+export function installSemanticBridge(runtime: CausewayViewerRuntime, shell: HTMLElement): () => void {
+  const destinations = new WeakMap<object, HTMLElement>();
+  let unscopedDestination: HTMLElement | null = null;
+  const onActionRequest = (rawEvent: Event) => {
+    const detail = (rawEvent as CustomEvent).detail as ActionResultDetail | undefined;
+    try {
+      const outlet = resolveResultOutlet(shell);
+      if (detail?.context && typeof detail.context === 'object') destinations.set(detail.context, outlet);
+      else unscopedDestination = outlet;
+    } catch (error) {
+      runtime.policies.error?.(error, context(runtime));
+    }
+  };
+  const onNavigation = (rawEvent: Event) => {
+    const event = rawEvent as CustomEvent;
+    const target = event.detail?.target as CausewayObjectTarget | undefined;
+    if (!target?.logicalTypeName || !(target.id ?? target.objectId)) return;
+    event.preventDefault();
+    void applyNavigation(runtime, target).catch(error => runtime.policies.error?.(error, context(runtime)));
+  };
+  const onResult = (rawEvent: Event) => {
+    const event = rawEvent as CustomEvent<ActionResultDetail>;
+    const detail = event.detail ?? {};
+    void (async () => {
+      const claim = createClaim();
+      const handled = await runtime.policies.result?.(detail, claim, context(runtime));
+      if (handled === true) claim.claim();
+      if (claim.claimed) return;
+      const identity = objectIdentity(detail.result);
+      if (identity) return applyNavigation(runtime, identity);
+      const destination = detail.context && destinations.get(detail.context) || unscopedDestination || resolveResultOutlet(shell);
+      if (detail.result?.kind === 'void') await refreshAfterVoid(runtime, detail);
+      else presentSemanticResult(destination, detail);
+      (event.target as {dismissResult?: () => void} | null)?.dismissResult?.();
+    })().catch(error => runtime.policies.error?.(error, context(runtime)));
+  };
+  shell.addEventListener(ACTION_REQUEST_EVENT, onActionRequest, {capture: true});
+  shell.addEventListener(NAVIGATION_REQUEST_EVENT, onNavigation);
+  shell.addEventListener(ACTION_RESULT_EVENT, onResult);
+  return () => {
+    shell.removeEventListener(ACTION_REQUEST_EVENT, onActionRequest, {capture: true});
+    shell.removeEventListener(NAVIGATION_REQUEST_EVENT, onNavigation);
+    shell.removeEventListener(ACTION_RESULT_EVENT, onResult);
+  };
+}
diff --git a/viewers/webcomponents/vue/src/registry.ts b/viewers/webcomponents/vue/src/registry.ts
new file mode 100644
index 00000000000..af88a1649c7
--- /dev/null
+++ b/viewers/webcomponents/vue/src/registry.ts
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type {Component} from 'vue';
+
+export type CausewayPageLoader = () => Promise<Component | {default: Component}>;
+export type CausewayPageRegistration = Component | CausewayPageLoader;
+export type CausewayPageRegistryInput = ReadonlyMap<string, CausewayPageRegistration>
+  | ReadonlyArray<readonly [string, CausewayPageRegistration]>
+  | Readonly<Record<string, CausewayPageRegistration>>;
+
+const LOGICAL_TYPE = /^[A-Za-z_][A-Za-z0-9_$-]*(?:\.[A-Za-z_][A-Za-z0-9_$-]*)*$/u;
+
+function entries(input: CausewayPageRegistryInput | undefined): ReadonlyArray<readonly [string, CausewayPageRegistration]> {
+  if (!input) return [];
+  if (input instanceof Map) return [...input.entries()];
+  if (Array.isArray(input)) return input;
+  return Object.entries(input);
+}
+
+export function normalizePageRegistry(input?: CausewayPageRegistryInput): ReadonlyMap<string, CausewayPageRegistration> {
+  const normalized = new Map<string, CausewayPageRegistration>();
+  for (const [rawKey, registration] of entries(input)) {
+    const key = String(rawKey ?? '').trim();
+    if (!LOGICAL_TYPE.test(key)) throw new Error('A Vue page registration has an invalid logical type.');
+    if ((!registration || (typeof registration !== 'object' && typeof registration !== 'function'))) {
+      throw new Error(`The Vue page registration for ${key} is unsupported.`);
+    }
+    if (normalized.has(key)) throw new Error(`The Vue page registration for ${key} is duplicated.`);
+    normalized.set(key, registration);
+  }
+  return normalized;
+}
+
+export function isPageLoader(registration: CausewayPageRegistration): registration is CausewayPageLoader {
+  return typeof registration === 'function';
+}
diff --git a/viewers/webcomponents/vue/src/route-codec.ts b/viewers/webcomponents/vue/src/route-codec.ts
new file mode 100644
index 00000000000..e402ab67f9d
--- /dev/null
+++ b/viewers/webcomponents/vue/src/route-codec.ts
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+const MAX_UTF8_BYTES = 4096;
+const MAX_ENCODED_LENGTH = 4096;
+export const INVALID_ROUTE_MESSAGE = 'The requested application route is invalid.';
+
+export interface ObjectRouteIdentity {
+  readonly logicalTypeName: string;
+  readonly objectId: string;
+}
+
+function invalidRoute(): Error {
+  return new Error(INVALID_ROUTE_MESSAGE);
+}
+
+function hasUnpairedSurrogate(text: string): boolean {
+  for (let index = 0; index < text.length; index += 1) {
+    const current = text.charCodeAt(index);
+    if (current >= 0xd800 && current <= 0xdbff) {
+      const next = text.charCodeAt(index + 1);
+      if (!(next >= 0xdc00 && next <= 0xdfff)) return true;
+      index += 1;
+    } else if (current >= 0xdc00 && current <= 0xdfff) {
+      return true;
+    }
+  }
+  return false;
+}
+
+export function encodeRouteSegment(value: unknown): string {
+  const text = String(value ?? '');
+  if (!text || text.length > MAX_UTF8_BYTES || text === '.' || text === '..'
+      || /[\\/\u0000-\u001f\u007f-\u009f]/u.test(text) || hasUnpairedSurrogate(text)) {
+    throw invalidRoute();
+  }
+  if (new TextEncoder().encode(text).length > MAX_UTF8_BYTES) throw invalidRoute();
+  let encoded: string;
+  try {
+    encoded = encodeURIComponent(text).replace(/[!'()*]/g, character =>
+      `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
+  } catch {
+    throw invalidRoute();
+  }
+  if (encoded.length > MAX_ENCODED_LENGTH) throw invalidRoute();
+  return encoded;
+}
+
+export function decodeRouteSegment(encoded: string): string {
+  if (!encoded || encoded.length > MAX_ENCODED_LENGTH || !/^(?:[^%]|%[0-9A-F]{2})+$/u.test(encoded)) {
+    throw invalidRoute();
+  }
+  let decoded: string;
+  try {
+    decoded = decodeURIComponent(encoded);
+  } catch {
+    throw invalidRoute();
+  }
+  if (encodeRouteSegment(decoded) !== encoded) throw invalidRoute();
+  return decoded;
+}
+
+export function normalizeBasePath(basePath: unknown): string {
+  let value = String(basePath ?? '').trim();
+  if (!value.startsWith('/') || value.startsWith('//') || /[?#\\\u0000-\u001f]/u.test(value)) {
+    throw new Error('The Vue viewer base path is invalid.');
+  }
+  while (value.length > 1 && value.endsWith('/')) value = value.slice(0, -1);
+  return value;
+}
+
+export function canonicalObjectPath(basePath: unknown, target: {
+  readonly logicalTypeName?: unknown;
+  readonly id?: unknown;
+  readonly objectId?: unknown;
+}): string {
+  const base = normalizeBasePath(basePath);
+  const suffix = `/object/${encodeRouteSegment(target?.logicalTypeName)}/${encodeRouteSegment(target?.id ?? target?.objectId)}`;
+  return base === '/' ? suffix : `${base}${suffix}`;
+}
+
+export function canonicalRouterObjectPath(target: {
+  readonly logicalTypeName?: unknown;
+  readonly id?: unknown;
+  readonly objectId?: unknown;
+}): string {
+  return canonicalObjectPath('/', target);
+}
+
+export function parseCanonicalObjectPath(pathname: unknown, basePath: unknown = '/'): Readonly<ObjectRouteIdentity> {
+  const path = String(pathname ?? '');
+  const base = normalizeBasePath(basePath);
+  const prefix = base === '/' ? '/object/' : `${base}/object/`;
+  if (!path.startsWith(prefix)) throw invalidRoute();
+  const tail = path.slice(prefix.length);
+  const segments = tail.split('/');
+  if (segments.length !== 2) throw invalidRoute();
+  return Object.freeze({
+    logicalTypeName: decodeRouteSegment(segments[0]),
+    objectId: decodeRouteSegment(segments[1])
+  });
+}
+
+export function canonicalRouteKey(identity: ObjectRouteIdentity): string {
+  return canonicalRouterObjectPath(identity);
+}
diff --git a/viewers/webcomponents/vue/src/routes.ts b/viewers/webcomponents/vue/src/routes.ts
new file mode 100644
index 00000000000..85e9d7a738d
--- /dev/null
+++ b/viewers/webcomponents/vue/src/routes.ts
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {defineComponent, h, type Component} from 'vue';
+import type {RouteRecordRaw} from 'vue-router';
+import HomePage from './components/HomePage.vue';
+import ObjectRoutePage from './components/ObjectRoutePage.vue';
+import {parseCanonicalObjectPath} from './route-codec';
+
+export const CAUSEWAY_ROUTE_NAMES = Object.freeze({
+  home: 'causeway-home',
+  object: 'causeway-object',
+  invalid: 'causeway-invalid-route',
+  notFound: 'causeway-not-found'
+});
+
+function statusPage(title: string, message: string, state: string, role?: string): Component {
+  return defineComponent({
+    name: `Causeway${title.replace(/\W/gu, '')}Page`,
+    setup() {
+      return () => h('section', {
+        class: ['causeway-vue-route-page', 'causeway-vue-status', state === 'invalid-route' && 'causeway-vue-status-danger'],
+        'data-route-state': state,
+        tabindex: -1,
+        role
+      }, [h('h1', title), h('p', message)]);
+    }
+  });
+}
+
+export const CausewayInvalidRoutePage = statusPage(
+  'Invalid route',
+  'The requested application route is invalid.',
+  'invalid-route',
+  'alert'
+);
+
+export const CausewayNotFoundPage = statusPage(
+  'Page unavailable',
+  'The requested page is unavailable.',
+  'not-found',
+  'alert'
+);
+
+export interface CausewayRouteRecordOptions {
+  readonly homeComponent?: Component;
+  readonly notFoundComponent?: Component;
+}
+
+export function createCausewayRouteRecords(options: CausewayRouteRecordOptions = {}): RouteRecordRaw[] {
+  return [
+    {path: '/', name: CAUSEWAY_ROUTE_NAMES.home, component: options.homeComponent ?? HomePage},
+    {
+      path: '/object/:logicalTypeName/:objectId',
+      name: CAUSEWAY_ROUTE_NAMES.object,
+      component: ObjectRoutePage,
+      beforeEnter(to) {
+        try {
+          parseCanonicalObjectPath(to.path, '/');
+          return true;
+        } catch {
+          return {name: CAUSEWAY_ROUTE_NAMES.invalid, replace: true};
+        }
+      }
+    },
+    {
+      path: '/invalid-route',
+      name: CAUSEWAY_ROUTE_NAMES.invalid,
+      component: CausewayInvalidRoutePage
+    },
+    {
+      path: '/:pathMatch(.*)*',
+      name: CAUSEWAY_ROUTE_NAMES.notFound,
+      component: options.notFoundComponent ?? CausewayNotFoundPage
+    }
+  ];
+}
diff --git a/viewers/webcomponents/vue/src/theme.css b/viewers/webcomponents/vue/src/theme.css
new file mode 100644
index 00000000000..c7dea838c10
--- /dev/null
+++ b/viewers/webcomponents/vue/src/theme.css
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
... 629 lines suppressed ...


(causeway) 12/16: docs(openspec): plan secured Vue Petclinic variant

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 325ca7434e2009589e3ab6531958a4de4329b646
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 3 15:16:28 2026 +0100

    docs(openspec): plan secured Vue Petclinic variant
---
 .../.openspec.yaml                                 |   2 +
 .../add-secured-vue-petclinic-variant/design.md    |  97 +++++++++++
 .../add-secured-vue-petclinic-variant/proposal.md  |  30 ++++
 .../specs/generic-vue-web-component-viewer/spec.md |  40 +++++
 .../spec.md                                        |  34 ++++
 .../vue-viewer-local-secman-authentication/spec.md | 179 +++++++++++++++++++++
 .../spec.md                                        |  68 ++++++++
 .../add-secured-vue-petclinic-variant/tasks.md     |  49 ++++++
 8 files changed, 499 insertions(+)

diff --git a/openspec/changes/add-secured-vue-petclinic-variant/.openspec.yaml b/openspec/changes/add-secured-vue-petclinic-variant/.openspec.yaml
new file mode 100644
index 00000000000..9696e00f11e
--- /dev/null
+++ b/openspec/changes/add-secured-vue-petclinic-variant/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-09-03
diff --git a/openspec/changes/add-secured-vue-petclinic-variant/design.md b/openspec/changes/add-secured-vue-petclinic-variant/design.md
new file mode 100644
index 00000000000..1eee716be10
--- /dev/null
+++ b/openspec/changes/add-secured-vue-petclinic-variant/design.md
@@ -0,0 +1,97 @@
+## Context
+
+The HTMX Petclinic sample already has an opt-in secured application, a dedicated security integration module, seeded local SecMan users, server-rendered login, CSRF-protected GraphQL, saved-route restoration, host-owned POST logout, and headless acceptance coverage.
+The Vue Petclinic sample currently starts only with bypass security, and its application-owned static document has no source of authenticated-user or CSRF state.
+The existing SecMan `UserDetailsService`, principal, and Causeway authentication converter are intentionally located inside the HTMX integration, so copying them into Vue would duplicate security-sensitive behavior and couple future fixes to two implementations.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Provide an opt-in secured Vue runtime equivalent to the established secured HTMX journey.
+- Share SecMan credential lookup and Causeway principal conversion without sharing viewer routing, login presentation, or shell policy.
+- Keep generic Vue, generic HTMX, foundation components, and ordinary sample launch behavior authentication-neutral.
+- Protect GraphQL POSTs and logout with current Spring CSRF evidence.
+- Preserve safe Vue deep links through login and fail closed on session loss.
+- Provide simple dedicated secured launch scripts for both reference viewers.
+
+**Non-Goals:**
+
+- OAuth, OIDC, external identity-provider logout, registration, password reset, remember-me, or multi-factor authentication.
+- A universal authentication abstraction for non-Spring or non-SecMan applications.
+- Moving route, shell, login-page, or redirect policy into the shared credential bridge.
+- Server-side rendering or hydration of the Vue application.
+- Changing the ordinary bypass-secured Petclinic launchers.
+
+## Decisions
+
+### Extract a presentation-neutral bridge inside the web-components reactor
+
+A new optional `security-secman` module will own the SecMan-backed Spring `UserDetailsService`, serializable principal, Causeway authentication converter, and DAO authentication provider.
+It will import the existing Spring security and SecMan integration modules but no HTMX or Vue viewer module.
+The existing HTMX SecMan integration and the new Vue SecMan integration will both consume it.
+This avoids a Vue-to-HTMX dependency and avoids duplicating credential, lock-state, role, tenancy, locale, and credential-erasure behavior.
+A future broader promotion into shared Causeway security can move this bounded module without changing viewer contracts.
+
+Alternatives rejected include making HTMX bridge classes public for Vue consumption, because that would invert viewer layering, and copying those classes into Vue, because security fixes could drift.
+
+### Keep server security policy viewer-specific
+
+The Vue integration will own Vue route matchers, login and logout paths, request caching, authentication entry behavior, CSRF context delivery, and login-page presentation.
+The HTMX integration will retain its current HTMX-specific shell, fragment-expiry, and unsafe-request behavior.
+Sharing only credential and principal conversion keeps host routing and lifecycle policy explicit.
+
+### Keep login server-rendered and the authenticated shell client-rendered
+
+Anonymous users will receive a server-rendered login page that performs no GraphQL request and posts credentials with Spring CSRF evidence.
+After authentication, the existing production Vue application remains client-rendered and application-owned.
+This avoids making login dependent on Vue assets, GraphQL, or custom-element initialization.
+
+### Publish bounded authentication context to the secured Vue host
+
+The Vue integration will expose an authenticated no-store JSON context endpoint containing only the current username, CSRF header name, CSRF parameter name, CSRF token, login path, and logout path.
+The application-owned Vue document will contain an inert authentication-context metadata slot whose default value is empty.
+The secured route controller will serve the same built document with that slot bound to the configured context endpoint, while the ordinary static runtime leaves it empty and performs no authentication bootstrap request.
+
+The Vue bootstrap will fetch this context before mounting, install a CSRF-decorating GraphQL executor, and configure the exact framework Logout action policy to submit the application-owned logout form.
+The stable Vue shell will render the current user and a native POST logout form from this context.
+No token will enter GraphQL documents, variables, URLs, router state, local storage, or action results.
+
+### Restore only safe Vue GET routes
+
+A request cache will retain only canonical same-origin GET routes beneath the configured Vue base path.
+Login, logout, authentication-context, GraphQL, static assets, unsafe methods, malformed paths, and external destinations will fall back to the Vue root.
+Session fixation protection will migrate the session identifier after successful login.
+
+### Treat authentication loss as full-document navigation
+
+A protected full-page Vue route will redirect to the server login page.
+A GraphQL `401` observed by the secured executor will trigger full-document login navigation using the current safe Vue route as a continuation hint.
+An authenticated `403` will remain authorization or CSRF failure and will not be relabelled as session expiry.
+
+### Add dedicated secured launch scripts
+
+`sample-htmx-petclinic/run-secured.sh` and `sample-vue-petclinic/run-secured.sh` will select explicit Maven secured-run profiles.
+Existing `run.sh` scripts and default profiles remain unchanged.
+Both scripts will retain JDK discovery, `JAVA_HOME`, `MVN`, and pass-through argument behavior.
+
+## Risks / Trade-offs
+
+- **[Risk] The shared bridge changes existing HTMX authentication wiring.** → Keep the same principal data and provider policy, retain existing HTMX integration and browser suites, and compare all failure modes.
+- **[Risk] A static Vue document cannot contain per-session CSRF values.** → Bind only the context-endpoint URL into the secured document and fetch current no-store context before GraphQL initialization.
+- **[Risk] Authentication context may become stale after session rotation or expiry.** → Obtain it after login, use the current session token, and perform full-document login navigation on `401` rather than retrying with stale state.
+- **[Risk] Public frontend assets expose implementation code.** → Permit only immutable presentation assets; protect routes, authentication context, and GraphQL data independently.
+- **[Risk] Multiple security chains may claim unrelated paths.** → Use explicit path matchers and configurable ordering, and test that unrelated paths remain outside the integration.
+
+## Migration Plan
+
+1. Add and verify the shared bridge while retaining HTMX behavior.
+2. Add the optional Vue integration and its bounded configuration.
+3. Add the secured Vue sample application, authentication-aware frontend host code, and deterministic users.
+4. Add secured launch profiles and scripts.
+5. Run ordinary and secured HTMX and Vue integration and headless suites.
+6. Roll back by removing the optional Vue integration import and secured profiles; ordinary viewers remain unchanged.
+
+## Open Questions
+
+None.
diff --git a/openspec/changes/add-secured-vue-petclinic-variant/proposal.md b/openspec/changes/add-secured-vue-petclinic-variant/proposal.md
new file mode 100644
index 00000000000..4d5845aa982
--- /dev/null
+++ b/openspec/changes/add-secured-vue-petclinic-variant/proposal.md
@@ -0,0 +1,30 @@
+## Why
+
+The HTMX Petclinic reference application has a SecMan-backed secured variant, but the corresponding Vue application currently demonstrates only bypass authentication.
+A secured Vue variant is needed to prove that the host-owned action policy, CSRF-protected GraphQL traffic, session lifecycle, deep-link restoration, and native logout contract work for an application-owned Vue shell.
+
+## What Changes
+
+- Add an optional viewer-neutral SecMan-to-Spring authentication bridge that can be reused by secured web-component hosts without importing HTMX or Vue presentation behavior.
+- Refactor the existing HTMX SecMan integration to consume the shared bridge without changing its routes or security behavior.
+- Add an optional Vue SecMan integration with scoped form login, safe Vue-route restoration, CSRF metadata, session-expiry handling, and POST logout.
+- Add a deterministic secured Vue Petclinic application variant and headless authentication coverage corresponding to the existing secured HTMX journey.
+- Add dedicated `run-secured.sh` scripts for the HTMX and Vue Petclinic samples while preserving their existing unsecured `run.sh` scripts.
+- Update HTMX, Vue, shared-security, sample, and top-level web-component documentation with secured run commands, credentials, ownership boundaries, and verification guidance.
+
+## Capabilities
+
+### New Capabilities
+
+- `web-component-secman-spring-authentication-bridge`: Provides an optional presentation-neutral SecMan-backed Spring username/password authentication bridge for web-component hosts.
+- `vue-viewer-local-secman-authentication`: Provides optional local SecMan form authentication, CSRF-safe Vue GraphQL traffic, host-owned logout, and secured Vue Petclinic acceptance.
+
+### Modified Capabilities
+
+- `htmx-viewer-local-secman-authentication`: Reuses the shared authentication bridge and exposes the secured sample through a dedicated run script without changing authenticated behavior.
+- `generic-vue-web-component-viewer`: Documents and verifies the host integration points used by a secured Vue shell while keeping generic Vue authentication-neutral.
+
+## Impact
+
+The change affects the web-components reactor, a new shared security integration module, the HTMX SecMan integration, a new Vue SecMan integration, both Petclinic sample modules, Vue frontend assets, Maven run and test profiles, headless Playwright coverage, shell scripts, and documentation.
+It adds no authentication behavior to generic foundation, HTMX, or Vue modules unless an application explicitly imports the corresponding integration.
diff --git a/openspec/changes/add-secured-vue-petclinic-variant/specs/generic-vue-web-component-viewer/spec.md b/openspec/changes/add-secured-vue-petclinic-variant/specs/generic-vue-web-component-viewer/spec.md
new file mode 100644
index 00000000000..8a4f96dacf4
--- /dev/null
+++ b/openspec/changes/add-secured-vue-petclinic-variant/specs/generic-vue-web-component-viewer/spec.md
@@ -0,0 +1,40 @@
+## ADDED Requirements
+
+### Requirement: Authentication-neutral secured-host integration points
+
+The generic Vue viewer SHALL remain authentication-neutral while allowing an application-owned secured shell to supply an authenticated GraphQL executor, current-user chrome, native logout form, and exact pre-invocation Logout policy.
+The viewer MUST NOT fetch credentials or CSRF state, create login or logout endpoints, persist tokens, or infer authentication from route or result paths.
+
+#### Scenario: Secured application binds an executor
+
+- **WHEN** an authenticated Vue application binds a CSRF-decorating executor to its stable `<cw-graphql-client>`
+- **THEN** semantic components use that executor without knowing the authentication mechanism
+- **AND** generic routing, contexts, interaction results, and lifecycle remain unchanged
+
+#### Scenario: Secured application owns logout chrome
+
+- **WHEN** an application renders current-user and POST logout controls in its stable shell and claims exact framework Logout requests
+- **THEN** the host policy can submit its native logout contract before GraphQL invocation
+- **AND** the viewer neither manufactures nor duplicates authentication chrome
+
+#### Scenario: Generic application has no authentication context
+
+- **WHEN** the same Vue application runs without an authentication integration
+- **THEN** it may omit executor decoration and secured-session chrome
+- **AND** the generic viewer introduces no authentication request or endpoint
+
+### Requirement: Dedicated secured Vue launcher
+
+The Vue Petclinic sample SHALL provide a repository-root-relative `run-secured.sh` launcher that selects its secured Maven profile while preserving the environment and argument behavior of `run.sh`.
+
+#### Scenario: Secured Vue script runs
+
+- **WHEN** a maintainer executes `viewers/webcomponents/sample-vue-petclinic/run-secured.sh`
+- **THEN** Maven launches the secured Vue Petclinic application and local login flow
+- **AND** `JAVA_HOME`, `MVN`, and additional Maven arguments are honored
+
+#### Scenario: Ordinary Vue script runs
+
+- **WHEN** a maintainer executes the existing Vue `run.sh`
+- **THEN** the bypass-secured ordinary Vue application remains selected
+- **AND** the new secured integration remains opt-in
diff --git a/openspec/changes/add-secured-vue-petclinic-variant/specs/htmx-viewer-local-secman-authentication/spec.md b/openspec/changes/add-secured-vue-petclinic-variant/specs/htmx-viewer-local-secman-authentication/spec.md
new file mode 100644
index 00000000000..810e98d2e36
--- /dev/null
+++ b/openspec/changes/add-secured-vue-petclinic-variant/specs/htmx-viewer-local-secman-authentication/spec.md
@@ -0,0 +1,34 @@
+## ADDED Requirements
+
+### Requirement: HTMX authentication consumes the shared bridge
+
+The HTMX local SecMan integration SHALL consume the presentation-neutral web-component SecMan Spring bridge for credential lookup, principal state, Causeway user conversion, and DAO authentication-provider construction.
+It MUST retain ownership of HTMX paths, login presentation, request caching, CSRF transport, session-expiry handling, shell chrome, logout, and security-chain scope.
+
+#### Scenario: Secured HTMX starts after bridge extraction
+
+- **WHEN** the HTMX SecMan integration is imported
+- **THEN** the shared bridge supplies equivalent credential and principal behavior
+- **AND** existing HTMX routes, login, CSRF, user context, session, and logout outcomes remain unchanged
+
+#### Scenario: Vue security is absent
+
+- **WHEN** an application installs only the HTMX SecMan integration
+- **THEN** no Vue route, asset, authentication context, policy, or security chain is introduced
+- **AND** the shared bridge remains presentation-neutral
+
+### Requirement: Dedicated secured HTMX launcher
+
+The HTMX Petclinic sample SHALL provide a repository-root-relative `run-secured.sh` launcher that selects the established secured Maven profile while retaining the environment and argument behavior of `run.sh`.
+
+#### Scenario: Secured HTMX script runs
+
+- **WHEN** a maintainer executes `viewers/webcomponents/sample-htmx-petclinic/run-secured.sh`
+- **THEN** Maven launches `PetClinicHtmxSecuredApplication` through the secured profile
+- **AND** `JAVA_HOME`, `MVN`, and additional Maven arguments are honored
+
+#### Scenario: Ordinary HTMX script runs
+
+- **WHEN** a maintainer executes the existing `run.sh`
+- **THEN** the ordinary HTMX and Wicket comparison runtime remains selected
+- **AND** no local SecMan form integration is enabled by the new launcher
diff --git a/openspec/changes/add-secured-vue-petclinic-variant/specs/vue-viewer-local-secman-authentication/spec.md b/openspec/changes/add-secured-vue-petclinic-variant/specs/vue-viewer-local-secman-authentication/spec.md
new file mode 100644
index 00000000000..5a19468167a
--- /dev/null
+++ b/openspec/changes/add-secured-vue-petclinic-variant/specs/vue-viewer-local-secman-authentication/spec.md
@@ -0,0 +1,179 @@
+## ADDED Requirements
+
+### Requirement: Optional local SecMan authentication for Vue
+
+The project SHALL provide local SecMan-backed form authentication as an explicitly installed Vue web-component host integration.
+Generic Vue, Vue Router, foundation components, and the ordinary Vue Petclinic runtime MUST retain their existing dependency and security behavior when the integration is absent.
+
+#### Scenario: Vue integration is installed
+
+- **WHEN** an application imports the Vue SecMan integration and its documented prerequisites
+- **THEN** configured Vue routes, authentication context, and same-origin GraphQL require an authenticated Spring browser session
+- **AND** the host provides local login and logout behavior
+
+#### Scenario: Vue integration is absent
+
+- **WHEN** an application uses the generic Vue viewer without the integration
+- **THEN** no SecMan user lookup, security chain, login route, logout route, authentication bootstrap, or session policy is added
+- **AND** application-owned security remains unchanged
+
+### Requirement: CSRF protection is mandatory
+
+The integration SHALL require `causeway.security.spring.allow-csrf-filters=true` and MUST fail startup with a bounded diagnostic when it is false or absent.
+Every login POST, authenticated GraphQL POST, and logout POST SHALL carry current Spring CSRF evidence.
+
+#### Scenario: Secured Vue starts with CSRF enabled
+
+- **WHEN** the integration is installed with CSRF filters allowed
+- **THEN** startup validates its paths and security-chain configuration
+- **AND** browser-session POST requests remain CSRF protected
+
+#### Scenario: CSRF evidence is missing
+
+- **WHEN** login, GraphQL, or logout is submitted without valid current CSRF evidence
+- **THEN** Spring rejects the request without changing authenticated state
+- **AND** no protected domain result is returned
+
+### Requirement: Accessible server-rendered login
+
+The integration SHALL provide a branded server-rendered login page outside the Vue and GraphQL runtime.
+The page MUST require no Vue, custom-element, or GraphQL initialization and MUST submit credentials through a CSRF-protected form.
+
+#### Scenario: Anonymous user opens login
+
+- **WHEN** an unauthenticated browser requests the configured Vue login page
+- **THEN** it receives labelled username and password fields, a submit control, application branding, and current CSRF evidence
+- **AND** initial focus and instructions work without frontend JavaScript
+
+#### Scenario: Login fails
+
+- **WHEN** credentials are rejected
+- **THEN** the login page presents one generic announced failure
+- **AND** no username, account state, role, tenancy, or internal failure is disclosed
+
+#### Scenario: Logout completed
+
+- **WHEN** the login page is reached after logout
+- **THEN** it announces successful sign-out
+- **AND** no former route, user detail, CSRF token, or session identifier is exposed
+
+### Requirement: Safe Vue route restoration
+
+The integration SHALL restore only a valid same-origin GET route beneath the configured Vue base path after login and SHALL migrate the session identifier on successful authentication.
+Absent, malformed, unsafe, authentication, GraphQL, asset, or unrelated destinations MUST fall back to the configured Vue root.
+
+#### Scenario: Protected deep link is requested
+
+- **WHEN** an anonymous browser requests a canonical Vue object route
+- **THEN** successful login returns it to that exact safe route
+- **AND** the authenticated session identifier differs from the anonymous login session
+
+#### Scenario: Saved destination is unsafe
+
+- **WHEN** a candidate destination is cross-origin, malformed, non-GET, outside Vue routes, or an authentication, GraphQL, or asset path
+- **THEN** login returns to the Vue root
+- **AND** no untrusted redirect is followed
+
+### Requirement: Bounded authenticated Vue context
+
+The integration SHALL expose an authenticated no-store context containing only the current username, CSRF header name, CSRF parameter name, CSRF token, login path, and logout path.
+The ordinary Vue document SHALL identify no context endpoint, while the secured document SHALL bind the configured same-origin endpoint without embedding user or token values into committed assets.
+
+#### Scenario: Secured Vue bootstraps
+
+- **WHEN** an authenticated secured Vue document loads
+- **THEN** the host fetches current authentication context before its first GraphQL request
+- **AND** the stable shell presents the current username and host logout control
+
+#### Scenario: Ordinary Vue bootstraps
+
+- **WHEN** the ordinary bypass Vue application loads
+- **THEN** it makes no authentication-context request and presents no secured-session chrome
+- **AND** existing routes and GraphQL behavior remain unchanged
+
+#### Scenario: Context is returned
+
+- **WHEN** an authenticated browser requests the context endpoint
+- **THEN** the response is private and no-store and contains only the bounded documented fields
+- **AND** the token is absent from URLs, Vue Router state, local storage, GraphQL documents, variables, and semantic results
+
+### Requirement: CSRF-safe GraphQL and authentication-loss handling
+
+The secured Vue host SHALL decorate every same-origin GraphQL POST with the current context's CSRF header and token.
+A GraphQL `401` SHALL cause full-document login navigation with the current safe Vue route, while an authenticated `403` SHALL remain an authorization or CSRF failure.
+
+#### Scenario: GraphQL operation executes
+
+- **WHEN** the secured semantic component client posts a query or mutation
+- **THEN** the current CSRF header and token accompany the request
+- **AND** authorized results retain their ordinary semantic handling
+
+#### Scenario: Session expires during GraphQL
+
+- **WHEN** a GraphQL request receives `401` after authentication is lost
+- **THEN** the host performs full-document navigation to login with a safe continuation
+- **AND** obsolete route or component errors do not replace the authentication outcome
+
+#### Scenario: Authenticated request is forbidden
+
+- **WHEN** an authenticated GraphQL request receives `403`
+- **THEN** the viewer retains bounded authorization or CSRF error behavior
+- **AND** does not claim the session expired
+
+### Requirement: Host-owned Vue logout
+
+The authenticated Vue shell SHALL present a native form that posts to the configured logout endpoint with current CSRF evidence.
+The Vue action policy SHALL claim exact `causeway.security.LogoutMenu#logout` requests and submit that host form rather than invoking GraphQL.
+Successful logout MUST clear authentication, invalidate the session, apply configured cookie cleanup, and redirect to the signed-out login page.
+
+#### Scenario: User activates host logout
+
+- **WHEN** an authenticated user submits the shell logout control
+- **THEN** Spring validates CSRF, invalidates the session, and redirects to signed-out login
+- **AND** subsequent Vue and GraphQL requests require authentication
+
+#### Scenario: Framework Logout request is published
+
+- **WHEN** stale or custom markup publishes the exact framework Logout action request
+- **THEN** the Vue host claims it before GraphQL and submits the native logout contract
+- **AND** ordinary action invocation emits no successful result
+
+#### Scenario: Browser returns after logout
+
+- **WHEN** the user navigates back after successful logout
+- **THEN** protected Vue content is not restored as authenticated
+- **AND** a new login is required
+
+### Requirement: Scoped Vue security chain
+
+The integration SHALL protect only its configured Vue routes, authentication context, GraphQL endpoint, and required assets while permitting login and immutable public presentation assets.
+It MUST NOT claim unrelated Wicket, HTMX, actuator, console, application, or bearer-token paths.
+
+#### Scenario: Protected Vue data is requested anonymously
+
+- **WHEN** an anonymous request targets a Vue route, authentication context, or GraphQL
+- **THEN** the integration applies its authentication entry behavior
+- **AND** no protected domain or session context is returned
+
+#### Scenario: Unrelated path is requested
+
+- **WHEN** a request is outside configured matchers
+- **THEN** the integration neither authorizes nor rejects it
+- **AND** another application security chain may remain authoritative
+
+### Requirement: Secured Vue Petclinic acceptance runtime
+
+The Vue Petclinic sample SHALL provide a deterministic secured application variant using the same shared domain, production frontend assets, local user credentials, and profile context as secured HTMX.
+It SHALL provide integration and headless browser coverage without requiring HTMX, Wicket, bypass security, OAuth, or a live identity provider.
+
+#### Scenario: Secured Vue runtime starts
+
+- **WHEN** the secured Vue run profile starts
+- **THEN** anonymous Vue and GraphQL access is protected and login remains available without GraphQL
+- **AND** the ordinary Vue runtime remains available through its unchanged profile
+
+#### Scenario: Secured browser journey runs
+
+- **WHEN** headless automation exercises invalid and valid login, deep-link restoration, authorized reads and mutations, CSRF rejection, host logout, history, and session expiry
+- **THEN** each transition satisfies the authentication, routing, CSRF, and confidentiality contracts
+- **AND** unexpected browser, GraphQL, HTTP, persistence, or security failures fail the suite
diff --git a/openspec/changes/add-secured-vue-petclinic-variant/specs/web-component-secman-spring-authentication-bridge/spec.md b/openspec/changes/add-secured-vue-petclinic-variant/specs/web-component-secman-spring-authentication-bridge/spec.md
new file mode 100644
index 00000000000..09746293731
--- /dev/null
+++ b/openspec/changes/add-secured-vue-petclinic-variant/specs/web-component-secman-spring-authentication-bridge/spec.md
@@ -0,0 +1,68 @@
+## ADDED Requirements
+
+### Requirement: Optional presentation-neutral SecMan Spring bridge
+
+The project SHALL provide an explicitly imported SecMan-backed Spring username/password authentication bridge for web-component hosts.
+The bridge MUST NOT install viewer routes, login pages, logout pages, request caches, security filter chains, or presentation behavior.
+
+#### Scenario: Bridge is imported
+
+- **WHEN** a host imports the shared bridge with Spring security, SecMan integration, and a SecMan password encoder available
+- **THEN** it can authenticate local SecMan credentials through a Spring DAO authentication provider
+- **AND** no HTMX or Vue dependency or route is introduced
+
+#### Scenario: Bridge is absent
+
+- **WHEN** an application does not import the shared bridge
+- **THEN** no user-details service, authentication provider, or bridge-specific principal converter is installed
+- **AND** existing bypass, bearer, pre-authentication, Wicket, or application security remains authoritative
+
+### Requirement: SecMan credentials and status determine authentication
+
+The bridge SHALL load a matching SecMan `ApplicationUser` inside a controlled anonymous interaction and SHALL authenticate its encrypted password through the configured SecMan `PasswordEncoder`.
+Absent users, wrong passwords, locked users, passwordless users, and internal lookup failures MUST fail closed without disclosing the account condition.
+
+#### Scenario: Valid unlocked user signs in
+
+- **WHEN** an unlocked local user supplies the matching password
+- **THEN** Spring authenticates the username without persisting plaintext credentials
+- **AND** the resulting principal contains the bounded SecMan profile needed by Causeway
+
+#### Scenario: Credentials are not usable
+
+- **WHEN** the user is absent, locked, passwordless, or supplies an incorrect password
+- **THEN** authentication fails with the same public invalid-credentials outcome
+- **AND** the response does not distinguish the account condition
+
+#### Scenario: User lookup fails
+
+- **WHEN** SecMan lookup or controlled interaction fails unexpectedly
+- **THEN** authentication fails closed
+- **AND** no persistence exception, user record, password, role, tenancy, locale, or stack trace reaches the browser
+
+### Requirement: Causeway principal preserves SecMan context
+
+A successful authentication SHALL produce a serializable credentials-erasing principal and SHALL convert it to a Causeway `UserMemento` containing the effective username, roles, tenancy path, language locale, number locale, and time locale captured at authentication time.
+The conversion MUST NOT require a second unscoped repository lookup.
+
+#### Scenario: Authenticated user has profile context
+
+- **WHEN** a SecMan user has roles, tenancy, and locale preferences
+- **THEN** authenticated Causeway requests receive the corresponding refined `UserMemento`
+- **AND** SecMan authorization observes the same username and effective roles
+
+#### Scenario: Authentication credentials are erased
+
+- **WHEN** Spring completes credential validation
+- **THEN** plaintext and encrypted password material are absent from the retained session principal
+- **AND** serialization and diagnostics expose no credential value
+
+### Requirement: Existing HTMX behavior remains compatible
+
+The shared bridge SHALL preserve the existing HTMX local SecMan authentication outcomes when that integration switches from its provisional local classes.
+
+#### Scenario: HTMX consumes the bridge
+
+- **WHEN** the secured HTMX integration authenticates valid, invalid, absent, locked, or passwordless users through the shared bridge
+- **THEN** login outcomes, user context, CSRF behavior, session migration, and logout behavior remain unchanged
+- **AND** HTMX-specific route and presentation policy remains outside the bridge
diff --git a/openspec/changes/add-secured-vue-petclinic-variant/tasks.md b/openspec/changes/add-secured-vue-petclinic-variant/tasks.md
new file mode 100644
index 00000000000..36c2cafd108
--- /dev/null
+++ b/openspec/changes/add-secured-vue-petclinic-variant/tasks.md
@@ -0,0 +1,49 @@
+## 1. Shared SecMan Spring Bridge
+
+- [ ] 1.1 Add the optional presentation-neutral `security-secman` module to the web-components reactor with bounded dependencies and module metadata.
+- [ ] 1.2 Move the SecMan principal, controlled user-details lookup, Causeway authentication converter, and DAO provider wiring into the shared module.
+- [ ] 1.3 Add focused tests for valid, absent, locked, passwordless, failed-lookup, role, tenancy, locale, serialization, and credential-erasure behavior.
+- [ ] 1.4 Refactor `htmx-security-secman` to consume the shared bridge without changing HTMX-specific route or shell policy.
+- [ ] 1.5 Run existing secured HTMX integration and headless browser regressions against the extracted bridge.
+
+## 2. Vue SecMan Server Integration
+
+- [ ] 2.1 Add the optional `vue-security-secman` module, bounded configuration properties, module import, and startup validation.
+- [ ] 2.2 Implement context-aware Vue, login, logout, authentication-context, GraphQL, asset, and unrelated-path matchers.
+- [ ] 2.3 Implement safe Vue GET request caching, authentication entry behavior, generic failure, successful deep-link restoration, session migration, and signed-out redirect.
+- [ ] 2.4 Implement the accessible server-rendered login page and same-origin public login stylesheet without Vue or GraphQL startup.
+- [ ] 2.5 Implement the authenticated no-store context endpoint with bounded username, CSRF names and token, and configured host paths.
+- [ ] 2.6 Configure current-CSRF login and logout POSTs, session invalidation, authentication clearing, and cookie cleanup.
+- [ ] 2.7 Add server integration tests for startup prerequisites, path scope, anonymous protection, failure equivalence, login, safe restoration, context confidentiality, CSRF rejection, logout, and session fixation.
+
+## 3. Secured Vue Host Runtime
+
+- [ ] 3.1 Add `PetClinicVueSecuredApplication`, deterministic SecMan user data, secured properties, and dependencies without altering the ordinary application.
+- [ ] 3.2 Add an inert authentication-context metadata slot to the application-owned Vue document and bind it only in the secured route response.
+- [ ] 3.3 Add typed frontend authentication bootstrap and a CSRF-decorating GraphQL executor with full-document `401` login handling.
+- [ ] 3.4 Add stable current-user and native CSRF POST logout chrome to the Vue shell.
+- [ ] 3.5 Configure the Vue action policy to claim only exact framework Logout and submit the host logout form.
+- [ ] 3.6 Preserve ordinary Vue startup without an authentication-context request or secured shell chrome.
+- [ ] 3.7 Regenerate and verify committed Vue production assets.
+
+## 4. Secured Acceptance and Launchers
+
+- [ ] 4.1 Add secured Vue Spring integration coverage using the shared deterministic credentials and route identities.
+- [ ] 4.2 Add a headless secured Vue journey covering generic invalid login, valid deep-link login, authorized reads and mutation, CSRF headers and rejection, host logout, history, and session expiry.
+- [ ] 4.3 Add `sample-htmx-petclinic/run-secured.sh` selecting the established secured HTMX profile.
+- [ ] 4.4 Add the secured Vue Maven run profile and `sample-vue-petclinic/run-secured.sh`.
+- [ ] 4.5 Verify both ordinary run scripts remain mapped to their existing applications and both secured scripts pass arguments and environment settings unchanged.
+
+## 5. Documentation
+
+- [ ] 5.1 Document the shared bridge ownership boundary and opt-in dependency behavior.
+- [ ] 5.2 Update HTMX security and sample documentation to use the dedicated secured launcher and describe unchanged behavior.
+- [ ] 5.3 Update Vue viewer, Vue security, Vue sample, and top-level web-component documentation with setup, routes, credentials, CSRF, logout, session expiry, and run commands.
+- [ ] 5.4 Document that local-resource navigation, framework Logout identity, and host authentication remain separate contracts.
+
+## 6. Verification
+
+- [ ] 6.1 Run shared bridge, HTMX security, Vue security, and sample Java integration suites.
+- [ ] 6.2 Run foundation and Vue frontend checks, declaration/build/pack verification, and stale generated-output checks.
+- [ ] 6.3 Run ordinary and secured HTMX and Vue headless Playwright suites.
+- [ ] 6.4 Run script syntax checks, relevant Maven package verification, Apache RAT, IDE inspections/build, strict OpenSpec validation, and whitespace checks.


(causeway) 06/16: feat(webcomponents): reconcile Vue Petclinic presentation

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 4f46b220d4f22438c92e405aea255247fd700ae8
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 3 11:30:53 2026 +0100

    feat(webcomponents): reconcile Vue Petclinic presentation
---
 .../reconcile-vue-petclinic-with-htmx/tasks.md     |  56 ++--
 viewers/webcomponents/README.adoc                  |   1 +
 .../domain/PetClinicDataConfiguration.java         |   8 +-
 .../sample/petclinic/domain/ViewerFallback.java    |  76 +++++
 .../src/main/resources/menubars.layout.xml         |   0
 .../sample-vue-petclinic/PRESENTATION-PARITY.adoc  |  65 ++++
 .../webcomponents/sample-vue-petclinic/README.adoc |  14 +-
 .../sample-vue-petclinic/frontend/src/App.vue      |  72 +++-
 .../sample-vue-petclinic/frontend/src/main.ts      |   8 +-
 .../frontend/src/pages/HomePage.vue                |  85 +++++
 .../frontend/src/pages/PetOwnerPage.vue            | 208 +++++++-----
 .../frontend/src/pages/PetPage.vue                 |  72 ++++
 .../frontend/src/pages/VisitPage.vue               |  72 ++++
 .../frontend/src/petclinic.css                     | 373 +++++++++++++++++++--
 .../resources/static/vue/assets/index-BntyzeMN.js  |   2 -
 .../static/vue/assets/index-BntyzeMN.js.map        |   1 -
 .../resources/static/vue/assets/index-BuWcbzr4.css |   1 -
 .../resources/static/vue/assets/index-CklV2Pe0.js  |   2 +
 .../static/vue/assets/index-CklV2Pe0.js.map        |   1 +
 .../resources/static/vue/assets/index-CqcjgOE7.css |   1 +
 .../src/main/resources/static/vue/index.html       |   4 +-
 .../vue/petclinic/PetClinicVuePlaywrightTest.java  | 162 +++++++--
 .../PetClinicVueApplicationIntegTest.java          |  14 +
 23 files changed, 1122 insertions(+), 176 deletions(-)

diff --git a/openspec/changes/reconcile-vue-petclinic-with-htmx/tasks.md b/openspec/changes/reconcile-vue-petclinic-with-htmx/tasks.md
index 6c6dbc1c242..e67c61babd1 100644
--- a/openspec/changes/reconcile-vue-petclinic-with-htmx/tasks.md
+++ b/openspec/changes/reconcile-vue-petclinic-with-htmx/tasks.md
@@ -1,45 +1,45 @@
 ## 1. Freeze the HTMX presentation baseline
 
-- [ ] 1.1 Record a route-by-route parity matrix for the HTMX shell and HomePage, PetOwner, Pet, and Visit pages, including ordered landmarks, headings, selected members, actions, result outlets, collection declarations, paging values, and preview affordances.
-- [ ] 1.2 Add or extend headless assertions that protect the authoritative HTMX semantic composition without changing its rendered presentation.
-- [ ] 1.3 Record tolerant wide- and narrow-viewport invariants for header geometry, content inset, column relationships, breakpoint collapse, footer placement, palette, typography, focus treatment, and horizontal overflow.
-- [ ] 1.4 Diagnose why the Vue application currently reverses primary menus and exposes Logout, User, and Configuration separately from the HTMX System grouping, identifying the supported projection or application-configuration correction before changing code.
-- [ ] 1.5 Select an existing suitable unregistered logical type for generic-fallback acceptance, or specify a bounded menu-excluded acceptance fixture if none exists.
+- [x] 1.1 Record a route-by-route parity matrix for the HTMX shell and HomePage, PetOwner, Pet, and Visit pages, including ordered landmarks, headings, selected members, actions, result outlets, collection declarations, paging values, and preview affordances.
+- [x] 1.2 Add or extend headless assertions that protect the authoritative HTMX semantic composition without changing its rendered presentation.
+- [x] 1.3 Record tolerant wide- and narrow-viewport invariants for header geometry, content inset, column relationships, breakpoint collapse, footer placement, palette, typography, focus treatment, and horizontal overflow.
+- [x] 1.4 Diagnose why the Vue application currently reverses primary menus and exposes Logout, User, and Configuration separately from the HTMX System grouping, identifying the supported projection or application-configuration correction before changing code.
+- [x] 1.5 Select an existing suitable unregistered logical type for generic-fallback acceptance, or specify a bounded menu-excluded acceptance fixture if none exists.
 
 ## 2. Reconcile the Vue application shell
 
-- [ ] 2.1 Refactor `App.vue` to mirror the HTMX shell landmarks, branding, loading and announcement regions, shell result outlet, main-region semantics, and footer content while retaining Vue Router bindings and one stable GraphQL client.
-- [ ] 2.2 Reconcile menu order and utility grouping through supported semantic menu projection and application configuration, without hardcoded replacement actions or payload rewriting.
-- [ ] 2.3 Rework `petclinic.css` to use the HTMX Petclinic class vocabulary, foundation design tokens, wide shell geometry, responsive breakpoints, typography, palette, content spacing, and footer alignment.
-- [ ] 2.4 Reconcile document titles with the HTMX `· Pet Clinic` convention and replace the persistent route-container border with an accessible keyboard focus treatment.
-- [ ] 2.5 Add headless shell tests for wide and narrow layouts, menu keyboard operation, focus, reduced motion, overflow, title, footer, and stable provider and route boundaries.
+- [x] 2.1 Refactor `App.vue` to mirror the HTMX shell landmarks, branding, loading and announcement regions, shell result outlet, main-region semantics, and footer content while retaining Vue Router bindings and one stable GraphQL client.
+- [x] 2.2 Reconcile menu order and utility grouping through supported semantic menu projection and application configuration, without hardcoded replacement actions or payload rewriting.
+- [x] 2.3 Rework `petclinic.css` to use the HTMX Petclinic class vocabulary, foundation design tokens, wide shell geometry, responsive breakpoints, typography, palette, content spacing, and footer alignment.
+- [x] 2.4 Reconcile document titles with the HTMX `· Pet Clinic` convention and replace the persistent route-container border with an accessible keyboard focus treatment.
+- [x] 2.5 Add headless shell tests for wide and narrow layouts, menu keyboard operation, focus, reduced motion, overflow, title, footer, and stable provider and route boundaries.
 
 ## 3. Reconcile the PetOwner page
 
-- [ ] 3.1 Restructure `PetOwnerPage.vue` into the HTMX object heading, details column, and collections column with separate Identity, Contact, Details, Pets, and Visits sections in reference order.
-- [ ] 3.2 Restore the `noOwners` action, complete related-owner standalone result declaration, `daysSinceLastVisit`, member descriptions, label positions, and action parameter presentation from the HTMX page.
-- [ ] 3.3 Reconcile Pets and Visits collection columns, page sizes, filtering, sorting, actions, prompt styles, parameters, row previews, and nested preview collections with the HTMX declarations.
-- [ ] 3.4 Extend Vue acceptance tests to verify PetOwner semantic parity, column containment at wide width, single-column order at narrow width, action results, property edits, collection operations, previews, and both toolkit policies.
+- [x] 3.1 Restructure `PetOwnerPage.vue` into the HTMX object heading, details column, and collections column with separate Identity, Contact, Details, Pets, and Visits sections in reference order.
+- [x] 3.2 Restore the `noOwners` action, complete related-owner standalone result declaration, `daysSinceLastVisit`, member descriptions, label positions, and action parameter presentation from the HTMX page.
+- [x] 3.3 Reconcile Pets and Visits collection columns, page sizes, filtering, sorting, actions, prompt styles, parameters, row previews, and nested preview collections with the HTMX declarations.
+- [x] 3.4 Extend Vue acceptance tests to verify PetOwner semantic parity, column containment at wide width, single-column order at narrow width, action results, property edits, collection operations, previews, and both toolkit policies.
 
 ## 4. Add the remaining exact-type Vue pages
 
-- [ ] 4.1 Implement and register `HomePage.vue` with the HTMX object heading, two collection sections, declared columns, filters, sorting, page sizes, and row previews.
-- [ ] 4.2 Implement and register `PetPage.vue` with the HTMX breadcrumb, heading, Identity and Details grouping, selected properties, editability, and absence of technical or intentionally omitted members.
-- [ ] 4.3 Implement and register `VisitPage.vue` with the HTMX breadcrumb, heading, Appointment and Details grouping, temporal presentation, selected properties, editability, and absence of technical members.
-- [ ] 4.4 Add direct-link, refresh, history, semantic-structure, responsive-layout, and interaction coverage for each new exact-type page.
+- [x] 4.1 Implement and register `HomePage.vue` with the HTMX object heading, two collection sections, declared columns, filters, sorting, page sizes, and row previews.
+- [x] 4.2 Implement and register `PetPage.vue` with the HTMX breadcrumb, heading, Identity and Details grouping, selected properties, editability, and absence of technical or intentionally omitted members.
+- [x] 4.3 Implement and register `VisitPage.vue` with the HTMX breadcrumb, heading, Appointment and Details grouping, temporal presentation, selected properties, editability, and absence of technical members.
+- [x] 4.4 Add direct-link, refresh, history, semantic-structure, responsive-layout, and interaction coverage for each new exact-type page.
 
 ## 5. Preserve deliberate generic fallback
 
-- [ ] 5.1 Add the bounded shared-domain acceptance fixture selected in task 1.5 only if no existing logical type can safely demonstrate fallback, preserving Petclinic menu contents and established seed cardinalities.
-- [ ] 5.2 Move generic-page direct-link, refresh, and browser-history coverage from Pet or Visit to the deliberately unregistered acceptance type.
-- [ ] 5.3 Verify exact-type precedence for all four reconciled Petclinic routes and generic `<cw-object>` rendering for the fallback fixture.
+- [x] 5.1 Add the bounded shared-domain acceptance fixture selected in task 1.5 only if no existing logical type can safely demonstrate fallback, preserving Petclinic menu contents and established seed cardinalities.
+- [x] 5.2 Move generic-page direct-link, refresh, and browser-history coverage from Pet or Visit to the deliberately unregistered acceptance type.
+- [x] 5.3 Verify exact-type precedence for all four reconciled Petclinic routes and generic `<cw-object>` rendering for the fallback fixture.
 
 ## 6. Regression, generated assets, and documentation
 
-- [ ] 6.1 Add tolerant computed-style and bounding-box assertions for the agreed shell and route invariants rather than pixel-perfect screenshot comparisons.
-- [ ] 6.2 Verify invalid routes, absent objects, partial errors, stale-generation rejection, announcements, semantic navigation and results, and provider lifecycle still behave as before.
-- [ ] 6.3 Run the Vue package unit, declaration, build, pack, and clean-consumer checks and resolve any integration regression without changing public APIs unnecessarily.
-- [ ] 6.4 Regenerate and verify committed Vue production assets and confirm the stale-generated-output check passes.
-- [ ] 6.5 Run Vue and HTMX integration and headless Playwright suites, including native and Vaadin policies and representative wide and narrow viewports.
-- [ ] 6.6 Update the Vue Petclinic and web-components documentation to identify HTMX as the presentation reference and explain where generic fallback is demonstrated.
-- [ ] 6.7 Run relevant Maven verification, frontend tests, script syntax checks, Apache RAT, IDE inspections/build, and whitespace checks.
+- [x] 6.1 Add tolerant computed-style and bounding-box assertions for the agreed shell and route invariants rather than pixel-perfect screenshot comparisons.
+- [x] 6.2 Verify invalid routes, absent objects, partial errors, stale-generation rejection, announcements, semantic navigation and results, and provider lifecycle still behave as before.
+- [x] 6.3 Run the Vue package unit, declaration, build, pack, and clean-consumer checks and resolve any integration regression without changing public APIs unnecessarily.
+- [x] 6.4 Regenerate and verify committed Vue production assets and confirm the stale-generated-output check passes.
+- [x] 6.5 Run Vue and HTMX integration and headless Playwright suites, including native and Vaadin policies and representative wide and narrow viewports.
+- [x] 6.6 Update the Vue Petclinic and web-components documentation to identify HTMX as the presentation reference and explain where generic fallback is demonstrated.
+- [x] 6.7 Run relevant Maven verification, frontend tests, script syntax checks, Apache RAT, IDE inspections/build, and whitespace checks.
diff --git a/viewers/webcomponents/README.adoc b/viewers/webcomponents/README.adoc
index 0449054433d..96837849c2a 100644
--- a/viewers/webcomponents/README.adoc
+++ b/viewers/webcomponents/README.adoc
@@ -107,6 +107,7 @@ The preferred manual test serves the page and GraphQL endpoint from the same Cau
 This avoids cross-origin and authentication-cookie differences that can obscure component behavior.
 
 The repository provides HTMX and Vue Petclinic applications over one shared deterministic JPA/H2 domain and rich GraphQL contract, plus `viewers/webcomponents/sample-html` as the stable low-level vanilla-component fixture.
+The HTMX Petclinic application is the presentation reference for the Vue sample's application-owned shell and exact-type HomePage, PetOwner, Pet, and Visit pages.
 Start either Petclinic viewer from the repository root with:
 
 [source,shell]
diff --git a/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicDataConfiguration.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicDataConfiguration.java
index 4b63f02dead..d59c0a19247 100644
--- a/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicDataConfiguration.java
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/PetClinicDataConfiguration.java
@@ -21,6 +21,8 @@
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 
+import jakarta.persistence.EntityManager;
+
 import org.springframework.boot.ApplicationRunner;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -37,8 +39,12 @@ ApplicationRunner loadPetClinicData(
             final TransactionService transactionService,
             final ClockService clockService,
             final PetOwnerRepository ownerRepository,
-            final VisitRepository visitRepository) {
+            final VisitRepository visitRepository,
+            final EntityManager entityManager) {
         return args -> transactionService.runTransactional(Propagation.REQUIRED, () -> {
+            if (entityManager.find(ViewerFallback.class, ViewerFallback.ID) == null) {
+                entityManager.persist(new ViewerFallback(ViewerFallback.ID, "Rendered by the generic Vue page."));
+            }
             if (ownerRepository.findById(PetOwner.MARY_ID) != null) {
                 return;
             }
diff --git a/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/ViewerFallback.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/ViewerFallback.java
new file mode 100644
index 00000000000..70a65d94843
--- /dev/null
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/ViewerFallback.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.webcomponents.sample.petclinic.domain;
+
+import jakarta.inject.Named;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.Version;
+
+import org.apache.causeway.applib.annotation.DomainObject;
+import org.apache.causeway.applib.annotation.DomainObjectLayout;
+import org.apache.causeway.applib.annotation.ObjectSupport;
+
+@Entity
+@Table(name = "petclinic_viewer_fallback")
+@Named(PetClinicDomainModule.NAMESPACE + ".ViewerFallback")
+@DomainObject
+@DomainObjectLayout
+public class ViewerFallback {
+
+    public static final String ID = "viewer-fallback";
+
+    @Id
+    @Column(nullable = false, length = 40)
+    private String id;
+
+    @Version
+    @Column(nullable = false)
+    private long version;
+
+    @Column(nullable = false, length = 80)
+    private String message;
+
+    protected ViewerFallback() {
+    }
+
+    public ViewerFallback(final String id, final String message) {
+        this.id = id;
+        this.message = message;
+    }
+
+    @ObjectSupport
+    public String title() {
+        return "Generic viewer fallback";
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public long getVersion() {
+        return version;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+}
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/menubars.layout.xml b/viewers/webcomponents/sample-petclinic-domain/src/main/resources/menubars.layout.xml
similarity index 100%
rename from viewers/webcomponents/sample-htmx-petclinic/src/main/resources/menubars.layout.xml
rename to viewers/webcomponents/sample-petclinic-domain/src/main/resources/menubars.layout.xml
diff --git a/viewers/webcomponents/sample-vue-petclinic/PRESENTATION-PARITY.adoc b/viewers/webcomponents/sample-vue-petclinic/PRESENTATION-PARITY.adoc
new file mode 100644
index 00000000000..a56be09d2f9
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/PRESENTATION-PARITY.adoc
@@ -0,0 +1,65 @@
+= Petclinic presentation parity
+
+// Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.
+// See the NOTICE file distributed with this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 .
+// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and limitations under the License.
+
+The HTMX Petclinic application is the presentation authority for the Vue Petclinic application.
+Parity means equivalent information architecture, declared semantic members, ordering, grouping, responsive relationships, and application theme; framework wrappers and exact rendered pixels need not match.
+
+== Shell baseline
+
+At a wide viewport the shell presents one 3.25rem navigation row with the `C` mark and `Pet Clinic` brand, followed by `Pet Owners`, `Visits`, and the utility grouping defined by the shared `menubars.layout.xml` resource.
+The route and footer use one-space-unit side insets, the main region uses the reference vertical spacing, and the footer places `Powered by Apache Causeway` opposite the viewer label.
+Document titles use the authoritative object title followed by ` · Pet Clinic`.
+Programmatic route focus does not draw a persistent frame around the whole page, while ordinary interactive controls and the skip-link target retain visible keyboard focus.
+At widths of 48rem or less shell navigation wraps vertically, and at widths of 60rem or less route grids collapse to one column without horizontal page overflow.
+
+== Route baseline
+
+[cols="1,2,2,2,2",options="header"]
+|===
+|Route
+|Ordered sections
+|Declared properties
+|Actions and results
+|Collections
+
+|`petclinic.HomePage`
+|Object header; Pet owners; Upcoming visits
+|None
+|None
+|`petOwners`: page 5, sortable, filterable, preview, Owner/Telephone/Email; `futureVisits`: page 10, filterable, preview, When/Reason/Notes
+
+|`petclinic.PetOwner`
+|Breadcrumbs; object heading; Identity; Contact; Details; Pets; Visits
+|Identity: name and knownAs; Contact: telephoneNumber and emailAddress; Details: notes, lastVisit, and daysSinceLastVisit
+|Page result; allOwners; noOwners; relatedOwners with Owner/Known as/Notes standalone result; delete; updateName inline; addPet sidebar; removePet; bookVisit modal
+|`pets`: page 5, sortable, filterable, preview, Name/Species/Notes; nested preview visits: page 10, When/Reason; owner `visits`: page 8, preview, When/Reason/Notes
+
+|`petclinic.Pet`
+|Page result; breadcrumbs; object header; Identity; Details
+|Identity: petOwner and name; Details: species and editable multiline notes
+|Sidebar result
+|None
+
+|`petclinic.Visit`
+|Page result; breadcrumbs; object header; Appointment; Details
+|Appointment: pet and visitAt; Details: editable reason and editable multiline notes
+|Dialog result
+|None
+|===
+
+Technical `id` and `version` properties and members not listed above are intentionally absent from the four exact-type pages.
+The unregistered `petclinic.ViewerFallback` fixture, with stable identifier `s_viewer-fallback`, demonstrates generic `<cw-object>` fallback independently of those routes.
+
+== Regression strategy
+
+Headless tests assert semantic structure, route boundaries, menu labels, title and footer content, and member declarations for every route.
+Computed-style checks use tolerant ranges and relationships for header height, shell inset, grid columns, responsive collapse, focus treatment, and overflow rather than screenshot equality.
+Native and Vaadin policies are exercised where toolkit rendering can alter presentation.
diff --git a/viewers/webcomponents/sample-vue-petclinic/README.adoc b/viewers/webcomponents/sample-vue-petclinic/README.adoc
index 7d8f3cb55e8..faea2a1879d 100644
--- a/viewers/webcomponents/sample-vue-petclinic/README.adoc
+++ b/viewers/webcomponents/sample-vue-petclinic/README.adoc
@@ -7,6 +7,7 @@
 This module is the executable reference application for the generic Vue web-component viewer.
 It uses the shared deterministic Petclinic domain, the Causeway GraphQL endpoint, Vue 3, Vue Router 4, and the framework-neutral `cw-` elements.
 The Vue application owns its HTML document, stable shell, router, history fallback, theme, and exact logical-type page registration.
+Its application-owned presentation follows the HTMX Petclinic shell and page compositions documented in link:PRESENTATION-PARITY.adoc[`PRESENTATION-PARITY.adoc`].
 The semantic web components remain authoritative for metadata, values, validation, interactions, collections, previews, and results.
 
 == Run
@@ -22,8 +23,10 @@ Open these URLs after Spring Boot starts:
 
 * `http://localhost:8080/vue/` uses the default Vaadin component policy.
 * `http://localhost:8080/vue/?toolkit=native` uses the native component policy.
-* `http://localhost:8080/vue/object/petclinic.PetOwner/s_owner-mary` opens Mary Smith through the custom Vue owner page.
-* `http://localhost:8080/vue/object/petclinic.Pet/s_pet-basil` opens Basil through the generic `<cw-object>` fallback page.
+* `http://localhost:8080/vue/object/petclinic.PetOwner/s_owner-mary` opens Mary Smith through the exact-type Vue owner page.
+* `http://localhost:8080/vue/object/petclinic.Pet/s_pet-basil` opens Basil through the exact-type Vue pet page.
+* `http://localhost:8080/vue/object/petclinic.Visit/s_visit-basil-checkup` opens Basil's visit through the exact-type Vue visit page.
+* `http://localhost:8080/vue/object/petclinic.ViewerFallback/s_viewer-fallback` demonstrates the generic `<cw-object>` fallback page.
 * `http://localhost:8080/graphiql` opens GraphiQL.
 
 Pass Maven properties to the script when needed.
@@ -56,7 +59,7 @@ Install Playwright Chromium on the first run when necessary:
 ./viewers/webcomponents/sample-vue-petclinic/test.sh --install-browser
 ----
 
-The browser suite covers the stable application shell, direct links, refresh, back and forward navigation, custom-page precedence, generic fallback, invalid and absent objects, semantic navigation and results, menus, keyboard skip navigation, and both toolkit policies.
+The browser suite covers the HTMX presentation baseline, wide and narrow layout invariants, the four exact-type pages, stable application shell, direct links, refresh, back and forward navigation, generic fallback, invalid and absent objects, semantic navigation and results, menus, keyboard skip navigation, and both toolkit policies.
 Set `-Dplaywright.headless=false` only when deliberately debugging in a visible browser.
 Set `-Dplaywright.chromium.executable=/absolute/path/to/chromium` or `-Dplaywright.chromium.channel=chrome` to use an installed browser.
 Any arguments after `--install-browser` are passed to the Maven test invocation.
@@ -81,8 +84,9 @@ The frontend config treats `cw-` names as native custom elements and deduplicate
 
 `frontend/index.html` is the application-owned HTML document.
 `frontend/src/App.vue` declares the stable GraphQL provider, menus, loading and announcement regions, default result outlet, and keyed router view.
-`frontend/src/pages/PetOwnerPage.vue` is an exact-type custom page for `petclinic.PetOwner`.
-Other domain types remain unregistered and therefore demonstrate the generic route-page fallback.
+The files under `frontend/src/pages` provide exact-type application compositions for `petclinic.HomePage`, `petclinic.PetOwner`, `petclinic.Pet`, and `petclinic.Visit`.
+The menu layout is shared with HTMX through the framework-neutral Petclinic domain resources.
+The deliberately unregistered `petclinic.ViewerFallback` fixture demonstrates the generic route-page fallback without making a user-visible Petclinic route inconsistent.
 `PetClinicVueRouteController` forwards only the intended client routes to the production `index.html` without capturing GraphQL or static assets.
 
 The generic Vue viewer package and its application integration contract are documented in `../vue/README.adoc`.
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/src/App.vue b/viewers/webcomponents/sample-vue-petclinic/frontend/src/App.vue
index 9b0df234a7a..c7a90ed9d95 100644
--- a/viewers/webcomponents/sample-vue-petclinic/frontend/src/App.vue
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/src/App.vue
@@ -7,7 +7,7 @@
   "License"); you may not use this file except in compliance
   with the License.  You may obtain a copy of the License at
 
-      https://www.apache.org/licenses/LICENSE-2.0
+       https://www.apache.org/licenses/LICENSE-2.0
 
   Unless required by applicable law or agreed to in writing,
   software distributed under the License is distributed on an
@@ -17,43 +17,85 @@
   under the License.
 -->
 <script setup lang="ts">
-import {ref} from 'vue';
+import {onBeforeUnmount, onMounted, ref} from 'vue';
 import {RouterView, useRoute} from 'vue-router';
 import {useCausewayShell, useCausewayViewer} from '@apache-causeway/vue-viewer';
 
 const viewer = useCausewayViewer();
 const route = useRoute();
 const shell = useCausewayShell(ref<HTMLElement | null>(null));
+let titleObserver: MutationObserver | undefined;
+
+function applyApplicationTitle(): void {
+  const title = document.title.trim();
+  if (title && !title.endsWith(' · Pet Clinic')) {
+    document.title = `${title} · Pet Clinic`;
+  }
+}
+
+onMounted(() => {
+  applyApplicationTitle();
+  const titleElement = document.querySelector('title');
+  if (titleElement) {
+    titleObserver = new MutationObserver(applyApplicationTitle);
+    titleObserver.observe(titleElement, {childList: true});
+  }
+});
+
+onBeforeUnmount(() => titleObserver?.disconnect());
 </script>
 
 <template>
-  <div ref="shell" class="petclinic-vue-shell" data-testid="petclinic-vue-application-shell">
-    <a class="petclinic-skip-link" href="#causeway-vue-route">Skip to main content</a>
+  <div ref="shell" class="causeway-app-shell" data-testid="petclinic-vue-application-shell">
+    <a class="causeway-skip-link" href="#causeway-vue-route">Skip to main content</a>
     <cw-graphql-client data-causeway-shell-client :endpoint="viewer.endpoint">
-      <header class="petclinic-navbar">
-        <a class="petclinic-brand" href="/vue/" @click.prevent="viewer.router.push('/')">
-          <span aria-hidden="true">C</span>
-          <span>Pet Clinic</span>
-        </a>
-        <cw-menubars />
+      <header class="causeway-shell-header">
+        <div class="causeway-shell-navbar">
+          <a class="causeway-shell-brand" href="/vue/" aria-label="Pet Clinic home" @click.prevent="viewer.router.push('/')">
+            <span class="causeway-shell-mark" aria-hidden="true">C</span>
+            <span>Pet Clinic</span>
+          </a>
+          <cw-menubars />
+        </div>
       </header>
-      <div data-causeway-route-loading class="petclinic-route-loading" role="status" aria-live="polite" hidden>
+      <div
+        data-causeway-route-loading
+        class="causeway-route-loading"
+        role="status"
+        aria-live="polite"
+        hidden
+      >
         Loading page…
       </div>
-      <div data-causeway-route-announcement class="petclinic-visually-hidden" aria-live="polite" aria-atomic="true" />
+      <div
+        data-causeway-route-announcement
+        class="causeway-visually-hidden"
+        aria-live="polite"
+        aria-atomic="true"
+      />
       <cw-action-results
+        id="causeway-result"
         data-causeway-shell-result
-        class="petclinic-shell-results"
+        class="causeway-shell-result"
         aria-label="Application action results"
         aria-live="polite"
         hidden
       />
-      <main id="causeway-vue-route" data-causeway-router-view class="petclinic-main" aria-busy="false" tabindex="-1">
+      <main
+        id="causeway-vue-route"
+        data-causeway-router-view
+        class="causeway-shell-main"
+        aria-busy="false"
+        tabindex="-1"
+      >
         <RouterView v-slot="{ Component }">
           <component :is="Component" :key="route.path" />
         </RouterView>
       </main>
-      <footer class="petclinic-footer">Powered by Apache Causeway · Vue viewer</footer>
+      <footer class="causeway-shell-footer">
+        <span>Powered by Apache Causeway</span>
+        <span>Vue viewer</span>
+      </footer>
     </cw-graphql-client>
   </div>
 </template>
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/src/main.ts b/viewers/webcomponents/sample-vue-petclinic/frontend/src/main.ts
index aa1e9bb0cb3..39814f53db0 100644
--- a/viewers/webcomponents/sample-vue-petclinic/frontend/src/main.ts
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/src/main.ts
@@ -25,7 +25,10 @@ import {
 } from '@apache-causeway/vue-viewer';
 import '@apache-causeway/vue-viewer/theme.css';
 import App from './App.vue';
+import HomePage from './pages/HomePage.vue';
 import PetOwnerPage from './pages/PetOwnerPage.vue';
+import PetPage from './pages/PetPage.vue';
+import VisitPage from './pages/VisitPage.vue';
 import './petclinic.css';
 
 const toolkit = new URLSearchParams(location.search).get('toolkit') === 'native' ? 'native' : 'vaadin';
@@ -52,7 +55,10 @@ const viewer = createCausewayVueViewer({
   endpoint: '/graphql',
   basePath: '/vue',
   pages: {
-    'petclinic.PetOwner': PetOwnerPage
+    'petclinic.HomePage': HomePage,
+    'petclinic.PetOwner': PetOwnerPage,
+    'petclinic.Pet': PetPage,
+    'petclinic.Visit': VisitPage
   },
   policies: {
     error(error) {
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/HomePage.vue b/viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/HomePage.vue
new file mode 100644
index 00000000000..39719ec0f21
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/HomePage.vue
@@ -0,0 +1,85 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+       https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<script setup lang="ts">
+import type {CausewayRoutePageProps} from '@apache-causeway/vue-viewer';
+
+defineProps<CausewayRoutePageProps>();
+</script>
+
+<template>
+  <section
+    class="causeway-vue-route-page causeway-route-page causeway-route-object"
+    data-causeway-route-page
+    data-testid="petclinic-vue-home-page"
+    data-page-kind="home"
+    data-route-state="loading"
+    tabindex="-1"
+    aria-label="Object page"
+  >
+    <cw-object-context
+      data-causeway-route-context
+      :logical-type="logicalTypeName"
+      :object-id="objectId"
+    >
+      <article class="petclinic-page petclinic-dashboard" data-testid="petclinic-custom-home">
+        <cw-object-header />
+        <div class="petclinic-dashboard-grid">
+          <section aria-labelledby="petclinic-owner-heading">
+            <h2 id="petclinic-owner-heading">Pet owners</h2>
+            <cw-collection id="petOwners" label="Pet owners" active paged="5" sortable filterable>
+              <cw-peek>
+                <section class="petclinic-preview" aria-label="Owner preview">
+                  <cw-object-header />
+                  <cw-property id="knownAs" editable />
+                  <cw-property id="telephoneNumber" />
+                  <cw-property id="emailAddress" />
+                  <cw-action id="updateName" named="Change owner name" />
+                  <cw-collection id="pets" named="Companion animals" active paged="2">
+                    <cw-collection-column id="name" label="Name" />
+                    <cw-collection-column id="species" label="Species" />
+                  </cw-collection>
+                </section>
+              </cw-peek>
+              <cw-collection-column id="name" label="Owner" />
+              <cw-collection-column id="telephoneNumber" label="Telephone" />
+              <cw-collection-column id="emailAddress" label="Email" />
+            </cw-collection>
+          </section>
+          <section aria-labelledby="petclinic-visit-heading">
+            <h2 id="petclinic-visit-heading">Upcoming visits</h2>
+            <cw-collection id="futureVisits" named="Next appointments" active paged="10" filterable>
+              <cw-peek>
+                <section class="petclinic-preview" aria-label="Visit preview">
+                  <cw-object-header />
+                  <cw-property id="visitAt" named="Appointment" />
+                  <cw-property id="reason" editable />
+                  <cw-property id="notes" editable multi-line="3" />
+                </section>
+              </cw-peek>
+              <cw-collection-column id="visitAt" label="When" />
+              <cw-collection-column id="reason" label="Reason" />
+              <cw-collection-column id="notes" label="Notes" />
+            </cw-collection>
+          </section>
+        </div>
+      </article>
+      <cw-interaction-controller data-causeway-route-interactions />
+    </cw-object-context>
+  </section>
+</template>
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/PetOwnerPage.vue b/viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/PetOwnerPage.vue
index 72b59f2f0a0..f1477a726c4 100644
--- a/viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/PetOwnerPage.vue
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/PetOwnerPage.vue
@@ -7,7 +7,7 @@
   "License"); you may not use this file except in compliance
   with the License.  You may obtain a copy of the License at
 
-      https://www.apache.org/licenses/LICENSE-2.0
+       https://www.apache.org/licenses/LICENSE-2.0
 
   Unless required by applicable law or agreed to in writing,
   software distributed under the License is distributed on an
@@ -24,95 +24,153 @@ defineProps<CausewayRoutePageProps>();
 
 <template>
   <section
-    class="causeway-vue-route-page petclinic-owner-page"
+    class="causeway-vue-route-page causeway-route-page causeway-route-object"
     data-causeway-route-page
     data-testid="petclinic-vue-owner-page"
     data-page-kind="pet-owner"
     data-route-state="loading"
     tabindex="-1"
-    aria-label="Pet owner page"
+    aria-label="Object page"
   >
     <cw-object-context
       data-causeway-route-context
       :logical-type="logicalTypeName"
       :object-id="objectId"
     >
-      <cw-action-results
-        data-causeway-page-result
-        class="petclinic-card"
-        aria-label="Pet owner action results"
-        hidden
-      />
-      <cw-breadcrumbs data-testid="petclinic-vue-breadcrumbs" />
-      <div class="petclinic-owner-heading">
-        <cw-object-header />
-        <div class="petclinic-toolbar" aria-label="Owner actions">
-          <cw-action id="allOwners" named="Show all owners" />
-          <cw-action id="relatedOwners" named="Show related owners">
-            <cw-standalone-collection named="Related owners">
-              <cw-collection-column id="name" label="Owner" />
-              <cw-collection-column id="knownAs" label="Known as" />
-            </cw-standalone-collection>
-          </cw-action>
-          <cw-action id="delete" named="Remove this owner" />
+      <article class="petclinic-page petclinic-owner-page" data-testid="petclinic-owner-page">
+        <cw-action-results
+          data-causeway-page-result
+          class="petclinic-card"
+          aria-label="Pet owner action results"
+          data-testid="petclinic-action-results"
+          hidden
+        />
+        <cw-breadcrumbs data-testid="petclinic-breadcrumbs" />
+        <div class="petclinic-object-heading">
+          <cw-object-header />
+          <div class="petclinic-page-toolbar" aria-label="Owner actions">
+            <cw-action id="allOwners" named="Show all owners" />
+            <cw-action id="noOwners" named="Show empty owner result" />
+            <cw-action id="relatedOwners" named="Show related owners">
+              <cw-standalone-collection named="Related owners">
+                <cw-collection-column id="name" label="Owner" />
+                <cw-collection-column id="knownAs" label="Known as" />
+                <cw-collection-column id="notes" label="Notes" />
+              </cw-standalone-collection>
+            </cw-action>
+            <cw-action id="delete" named="Remove this owner" />
+          </div>
         </div>
-      </div>
 
-      <div class="petclinic-owner-grid">
-        <section class="petclinic-card" aria-labelledby="owner-details-heading">
-          <h2 id="owner-details-heading">Owner details</h2>
-          <cw-property id="name" named="Full name">
-            <cw-action id="updateName" named="Change the owner's name" prompt-style="INLINE">
-              <cw-parameter id="name" named="Owner's full name" />
-            </cw-action>
-          </cw-property>
-          <cw-property id="knownAs" editable />
-          <cw-property id="telephoneNumber" editable />
-          <cw-property id="emailAddress" editable />
-          <cw-property id="notes" editable multi-line="5" />
-          <cw-property id="lastVisit" editable label-position="TOP" min="2000-01-01" max="today" />
-        </section>
+        <div class="petclinic-object-grid">
+          <div class="petclinic-object-details">
+            <section class="petclinic-card" aria-labelledby="petclinic-owner-identity-heading">
+              <h2 id="petclinic-owner-identity-heading">Identity</h2>
+              <div class="petclinic-field-list">
+                <cw-property id="name" named="Full name">
+                  <cw-action id="updateName" named="Change the owner's name" prompt-style="INLINE">
+                    <cw-parameter
+                      id="name"
+                      named="Owner's full name"
+                      described-as="The complete name used to identify this pet owner."
+                      description-as="tooltip"
+                    />
+                  </cw-action>
+                </cw-property>
+                <cw-property
+                  id="knownAs"
+                  editable
+                  described-as="The familiar or preferred name used by this owner."
+                />
+              </div>
+            </section>
 
-        <section class="petclinic-card" aria-labelledby="owner-pets-heading">
-          <h2 id="owner-pets-heading">Pets</h2>
-          <cw-collection id="pets" named="Companion animals" active paged="5" sortable filterable>
-            <cw-peek>
-              <section class="petclinic-preview" aria-label="Pet preview">
-                <cw-object-header />
-                <cw-property id="name" />
-                <cw-property id="species" />
-                <cw-property id="notes" editable multi-line="3" />
-                <cw-action id="clearNotes" named="Clear pet notes" />
-                <cw-collection id="visits" named="Pet visits" active paged="10">
-                  <cw-collection-column id="visitAt" label="When" />
-                  <cw-collection-column id="reason" label="Reason" />
-                </cw-collection>
-              </section>
-            </cw-peek>
-            <cw-collection-column id="name" label="Name" />
-            <cw-collection-column id="species" label="Species" />
-            <cw-collection-column id="notes" label="Notes" />
-            <cw-action id="addPet" named="Register a pet" prompt-style="DIALOG_SIDEBAR">
-              <cw-parameter id="name" named="Pet name" />
-            </cw-action>
-            <cw-action id="removePet" />
-          </cw-collection>
-        </section>
+            <section class="petclinic-card" aria-labelledby="petclinic-owner-contact-heading">
+              <h2 id="petclinic-owner-contact-heading">Contact</h2>
+              <div class="petclinic-field-list">
+                <cw-property id="telephoneNumber" editable />
+                <cw-property id="emailAddress" editable />
+              </div>
+            </section>
 
-        <section class="petclinic-card" aria-labelledby="owner-visits-heading">
-          <h2 id="owner-visits-heading">Visits</h2>
-          <cw-collection id="visits" named="Visit history" active paged="8">
-            <cw-peek />
-            <cw-collection-column id="visitAt" label="When" />
-            <cw-collection-column id="reason" label="Reason" />
-            <cw-action id="bookVisit" prompt-style="DIALOG_MODAL">
-              <cw-parameter id="visitDate" min="tomorrow" />
-              <cw-parameter id="visitTime" min="08:00" max="17:00" />
-              <cw-parameter id="reason" multi-line="3" />
-            </cw-action>
-          </cw-collection>
-        </section>
-      </div>
+            <section class="petclinic-card" aria-labelledby="petclinic-owner-details-heading">
+              <h2 id="petclinic-owner-details-heading">Details</h2>
+              <div class="petclinic-field-list">
+                <cw-property id="notes" editable multi-line="5" />
+                <cw-property id="lastVisit" editable label-position="TOP" min="2000-01-01" max="today" />
+                <cw-property id="daysSinceLastVisit" />
+              </div>
+            </section>
+          </div>
+
+          <div class="petclinic-object-collections">
+            <section class="petclinic-card" aria-labelledby="petclinic-owner-pets-heading">
+              <h2 id="petclinic-owner-pets-heading">Pets</h2>
+              <cw-collection id="pets" named="Companion animals" active paged="5" sortable filterable>
+                <cw-peek>
+                  <section class="petclinic-preview" aria-label="Pet preview">
+                    <cw-object-header />
+                    <cw-property id="name" />
+                    <cw-property id="species" />
+                    <cw-property id="notes" editable multi-line="3" />
+                    <cw-action id="clearNotes" named="Clear pet notes" />
+                    <cw-collection id="visits" named="Pet visits" active paged="10">
+                      <cw-collection-column id="visitAt" label="When" />
+                      <cw-collection-column id="reason" label="Reason" />
+                    </cw-collection>
+                  </section>
+                </cw-peek>
+                <cw-collection-column id="name" label="Name" />
+                <cw-collection-column id="species" label="Species" />
+                <cw-collection-column id="notes" label="Notes" />
+                <cw-action id="addPet" named="Register a pet" prompt-style="DIALOG_SIDEBAR">
+                  <cw-parameter
+                    id="name"
+                    named="Pet name"
+                    described-as="The name used for this companion animal."
+                    description-as="label"
+                  />
+                </cw-action>
+                <cw-action id="removePet" />
+              </cw-collection>
+            </section>
+
+            <section class="petclinic-card" aria-labelledby="petclinic-owner-visits-heading">
+              <h2 id="petclinic-owner-visits-heading">Visits</h2>
+              <cw-collection
+                id="visits"
+                named="Visit history"
+                described-as="All visits recorded for this owner's pets."
+                active
+                paged="8"
+              >
+                <cw-peek>
+                  <section class="petclinic-preview" aria-label="Visit preview">
+                    <cw-object-header />
+                    <cw-property id="visitAt" named="Appointment" />
+                    <cw-property id="reason" editable />
+                    <cw-property id="notes" editable multi-line="3" />
+                  </section>
+                </cw-peek>
+                <cw-collection-column id="visitAt" label="When" />
+                <cw-collection-column id="reason" label="Reason" />
+                <cw-collection-column id="notes" label="Notes" />
+                <cw-action id="bookVisit" prompt-style="DIALOG_MODAL">
+                  <cw-parameter id="visitDate" min="tomorrow" />
+                  <cw-parameter id="visitTime" min="08:00" max="17:00" />
+                  <cw-parameter
+                    id="reason"
+                    named="Reason for visit"
+                    described-as="Describe the purpose of the appointment."
+                    description-as="label"
+                    multi-line="3"
+                  />
+                </cw-action>
+              </cw-collection>
+            </section>
+          </div>
+        </div>
+      </article>
       <cw-interaction-controller data-causeway-route-interactions />
     </cw-object-context>
   </section>
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/PetPage.vue b/viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/PetPage.vue
new file mode 100644
index 00000000000..6b225ff12f3
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/PetPage.vue
@@ -0,0 +1,72 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+       https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<script setup lang="ts">
+import type {CausewayRoutePageProps} from '@apache-causeway/vue-viewer';
+
+defineProps<CausewayRoutePageProps>();
+</script>
+
+<template>
+  <section
+    class="causeway-vue-route-page causeway-route-page causeway-route-object"
+    data-causeway-route-page
+    data-testid="petclinic-vue-pet-page"
+    data-page-kind="pet"
+    data-route-state="loading"
+    tabindex="-1"
+    aria-label="Object page"
+  >
+    <cw-object-context
+      data-causeway-route-context
+      :logical-type="logicalTypeName"
+      :object-id="objectId"
+    >
+      <article class="petclinic-page petclinic-pet-page" data-testid="petclinic-pet-page">
+        <cw-action-results
+          data-causeway-page-result
+          aria-label="Pet action results"
+          presentation-style="SIDEBAR"
+          data-testid="petclinic-sidebar-results"
+          hidden
+        />
+        <cw-breadcrumbs data-testid="petclinic-breadcrumbs" />
+        <cw-object-header />
+
+        <div class="petclinic-object-grid">
+          <section class="petclinic-card" aria-labelledby="petclinic-pet-identity-heading">
+            <h2 id="petclinic-pet-identity-heading">Identity</h2>
+            <div class="petclinic-field-list">
+              <cw-property id="petOwner" />
+              <cw-property id="name" />
+            </div>
+          </section>
+
+          <section class="petclinic-card" aria-labelledby="petclinic-pet-details-heading">
+            <h2 id="petclinic-pet-details-heading">Details</h2>
+            <div class="petclinic-field-list">
+              <cw-property id="species" />
+              <cw-property id="notes" editable multi-line="5" />
+            </div>
+          </section>
+        </div>
+      </article>
+      <cw-interaction-controller data-causeway-route-interactions />
+    </cw-object-context>
+  </section>
+</template>
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/VisitPage.vue b/viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/VisitPage.vue
new file mode 100644
index 00000000000..9c6a13e1897
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/src/pages/VisitPage.vue
@@ -0,0 +1,72 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+       https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<script setup lang="ts">
+import type {CausewayRoutePageProps} from '@apache-causeway/vue-viewer';
+
+defineProps<CausewayRoutePageProps>();
+</script>
+
+<template>
+  <section
+    class="causeway-vue-route-page causeway-route-page causeway-route-object"
+    data-causeway-route-page
+    data-testid="petclinic-vue-visit-page"
+    data-page-kind="visit"
+    data-route-state="loading"
+    tabindex="-1"
+    aria-label="Object page"
+  >
+    <cw-object-context
+      data-causeway-route-context
+      :logical-type="logicalTypeName"
+      :object-id="objectId"
+    >
+      <article class="petclinic-page petclinic-visit-page" data-testid="petclinic-visit-page">
+        <cw-action-results
+          data-causeway-page-result
+          aria-label="Visit action results"
+          presentation-style="DIALOG"
+          data-testid="petclinic-dialog-results"
+          hidden
+        />
+        <cw-breadcrumbs data-testid="petclinic-breadcrumbs" />
+        <cw-object-header />
+
+        <div class="petclinic-object-grid">
+          <section class="petclinic-card" aria-labelledby="petclinic-visit-appointment-heading">
+            <h2 id="petclinic-visit-appointment-heading">Appointment</h2>
+            <div class="petclinic-field-list">
+              <cw-property id="pet" />
+              <cw-property id="visitAt" />
+            </div>
+          </section>
+
+          <section class="petclinic-card" aria-labelledby="petclinic-visit-details-heading">
+            <h2 id="petclinic-visit-details-heading">Details</h2>
+            <div class="petclinic-field-list">
+              <cw-property id="reason" editable />
+              <cw-property id="notes" editable multi-line="5" />
+            </div>
+          </section>
+        </div>
+      </article>
+      <cw-interaction-controller data-causeway-route-interactions />
+    </cw-object-context>
+  </section>
+</template>
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/src/petclinic.css b/viewers/webcomponents/sample-vue-petclinic/frontend/src/petclinic.css
index f44f742a475..582563df292 100644
--- a/viewers/webcomponents/sample-vue-petclinic/frontend/src/petclinic.css
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/src/petclinic.css
@@ -17,33 +17,358 @@
  * under the License.
  */
 
-* { box-sizing: border-box; }
-body { margin: 0; background: var(--causeway-color-surface-subtle, #f3f6fa); font-family: system-ui, sans-serif; }
-.petclinic-vue-shell { min-height: 100vh; }
-.petclinic-skip-link { position: fixed; left: 1rem; top: -5rem; z-index: 1000; }
-.petclinic-skip-link:focus { top: 1rem; padding: 0.7rem; background: white; color: #075ea8; }
-.petclinic-navbar { display: flex; align-items: center; gap: 1.5rem; padding: 0.75rem clamp(1rem, 4vw, 3rem); background: #183b65; color: white; }
-.petclinic-brand { display: inline-flex; align-items: center; gap: 0.6rem; color: white; font-size: 1.2rem; font-weight: 700; text-decoration: none; }
-.petclinic-brand > span:first-child { display: grid; width: 2rem; height: 2rem; place-items: center; border-radius: 50%; background: #fff; color: #183b65; }
-.petclinic-navbar cw-menubars { flex: 1 1 auto; min-width: 0; }
-.petclinic-route-loading { padding: 0.5rem 2rem; background: #fff4ce; color: #543f00; }
-.petclinic-main { width: 100%; min-height: 65vh; padding: clamp(1rem, 3vw, 2rem); }
-.petclinic-owner-heading { display: flex; align-items: start; justify-content: space-between; gap: 1rem; }
-.petclinic-toolbar { display: flex; flex-wrap: wrap; justify-content: end; gap: 0.5rem; }
-.petclinic-owner-grid { display: grid; grid-template-columns: minmax(18rem, 0.8fr) minmax(25rem, 1.2fr); gap: 1rem; align-items: start; }
-.petclinic-card { display: block; min-width: 0; margin-block: 1rem; padding: 1rem; border: 1px solid #cbd5e1; border-radius: 0.5rem; background: var(--causeway-vue-surface, white); }
-.petclinic-preview { padding: 1rem; border-inline-start: 0.25rem solid #4d7cab; }
-.petclinic-shell-results { margin: 1rem clamp(1rem, 3vw, 2rem) 0; }
-.petclinic-footer { padding: 1rem 2rem; color: #526176; text-align: center; }
-.petclinic-visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
+:root {
+  --causeway-content-width: 100%;
+  --causeway-shell-width: 100%;
+}
+
+* {
+  box-sizing: border-box;
+}
+
+body {
+  margin: 0;
+  background: var(--causeway-page-background);
+  color: var(--causeway-text);
+  font-family: var(--causeway-font-family);
+  font-size: var(--causeway-font-size-base);
+  line-height: var(--causeway-line-height);
+}
+
+.causeway-app-shell {
+  min-height: 100vh;
+  margin: 0;
+  background: var(--causeway-page-background);
+  color: var(--causeway-text);
+}
+
+.causeway-app-shell > cw-graphql-client {
+  min-height: 100vh;
+  display: grid;
+  grid-template-rows: auto auto auto 1fr auto;
+}
+
+.causeway-skip-link {
+  position: fixed;
+  z-index: 10000;
+  inset-block-start: var(--causeway-space-2);
+  inset-inline-start: var(--causeway-space-2);
+  padding: var(--causeway-space-2) var(--causeway-space-3);
+  border-radius: var(--causeway-radius-sm);
+  background: var(--causeway-surface);
+  color: var(--causeway-link);
+  transform: translateY(-180%);
+}
+
+.causeway-skip-link:focus {
+  transform: translateY(0);
+}
+
+.causeway-shell-header {
+  position: sticky;
+  z-index: 100;
+  inset-block-start: 0;
+  color: var(--causeway-nav-text);
+  background: var(--causeway-nav-background);
+  box-shadow: var(--causeway-shadow-sm);
+}
+
+.causeway-shell-navbar {
+  width: min(100%, var(--causeway-shell-width));
+  min-height: var(--causeway-nav-height);
+  margin-inline: auto;
+  padding-inline: var(--causeway-space-4);
+  display: flex;
+  align-items: center;
+  gap: var(--causeway-space-3);
+}
+
+.causeway-shell-navbar > cw-menubars {
+  min-width: 0;
+  flex: 1 1 auto;
+  flex-direction: row;
+  align-items: stretch;
+}
+
+.causeway-shell-navbar > cw-menubars > cw-menubar-primary {
+  min-width: 0;
+  flex: 1 1 auto;
+}
+
+.causeway-shell-navbar > cw-menubars > :is(cw-menubar-secondary, cw-menubar-tertiary) {
+  flex: 0 1 auto;
+}
+
+.causeway-shell-brand {
+  min-height: 2.75rem;
+  display: inline-flex;
+  flex: 0 0 auto;
+  align-items: center;
+  gap: var(--causeway-space-2);
+  color: var(--causeway-nav-text);
+  font-size: var(--causeway-font-size-lg);
+  font-weight: 650;
+  text-decoration: none;
+}
+
+.causeway-shell-brand:hover {
+  color: var(--causeway-nav-text);
+  text-decoration: none;
+}
+
+.causeway-shell-mark {
+  width: 2rem;
+  height: 2rem;
+  display: inline-grid;
+  place-items: center;
+  border: 2px solid currentColor;
+  border-radius: 50%;
+  font-weight: 800;
+}
+
+.causeway-route-loading {
+  position: fixed;
+  z-index: 120;
+  inset-block-start: calc(var(--causeway-nav-height) + var(--causeway-space-2));
+  inset-inline-end: var(--causeway-space-4);
+  padding: var(--causeway-space-2) var(--causeway-space-3);
+  border: 1px solid var(--causeway-border);
+  border-radius: var(--causeway-radius-pill);
+  background: var(--causeway-surface);
+  color: var(--causeway-text);
+  box-shadow: var(--causeway-shadow-md);
+}
+
+.causeway-shell-result,
+.causeway-shell-main,
+.causeway-shell-footer {
+  width: min(calc(100% - (2 * var(--causeway-space-4))), var(--causeway-content-width));
+  margin-inline: auto;
+}
+
+.causeway-shell-result {
+  margin-block-start: var(--causeway-space-4);
+  padding: var(--causeway-space-4);
+  border: 1px solid var(--causeway-border);
+  border-inline-start: 0.3rem solid var(--causeway-info);
+  border-radius: var(--causeway-radius-md);
+  background: var(--causeway-surface);
+  box-shadow: var(--causeway-shadow-sm);
+}
+
+.causeway-shell-result[hidden] {
+  display: none;
+}
+
+.causeway-shell-main {
+  min-width: 0;
+  padding-block: var(--causeway-space-5) var(--causeway-space-8);
+}
+
+.causeway-shell-main:focus-visible {
+  outline: 0.2rem solid var(--causeway-focus);
+  outline-offset: -0.2rem;
+}
+
+.causeway-shell-main .causeway-object-identity {
+  display: none;
+}
+
+.causeway-shell-footer {
+  padding-block: var(--causeway-space-4);
+  display: flex;
+  justify-content: space-between;
+  gap: var(--causeway-space-3);
+  border-block-start: 1px solid var(--causeway-border);
+  color: var(--causeway-muted);
+  font-size: var(--causeway-font-size-sm);
+}
+
+.causeway-route-page,
+.causeway-route-page:focus-visible,
+.causeway-vue-route-page:focus-visible {
+  min-width: 0;
+  outline: none;
+}
+
+.petclinic-dashboard-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: var(--causeway-space-4);
+}
+
+.petclinic-dashboard-grid > section {
+  min-width: 0;
+}
+
+.petclinic-dashboard-grid > section > h2 {
+  margin: 0;
+  padding: var(--causeway-space-3) var(--causeway-space-4);
+  border: 1px solid var(--causeway-border);
+  border-block-end: 0;
+  border-radius: var(--causeway-radius-md) var(--causeway-radius-md) 0 0;
+  background: var(--causeway-surface-subtle);
+  font-size: 1rem;
+}
+
+.petclinic-dashboard-grid > section > cw-collection :is(.causeway-collection-label, .causeway-collection-description) {
+  position: absolute !important;
+  width: 1px !important;
+  height: 1px !important;
+  padding: 0 !important;
+  margin: -1px !important;
+  overflow: hidden !important;
+  clip: rect(0, 0, 0, 0) !important;
+  white-space: nowrap !important;
+  border: 0 !important;
+}
+
+.petclinic-dashboard-grid > section > cw-collection .causeway-collection {
+  border-start-start-radius: 0;
+  border-start-end-radius: 0;
+}
+
+.petclinic-page {
+  display: grid;
+  gap: var(--causeway-space-4);
+  min-width: 0;
+  max-width: 100%;
+  overflow-x: clip;
+}
+
+.petclinic-page,
+.petclinic-page * {
+  box-sizing: border-box;
+}
+
+.petclinic-page cw-property,
+.petclinic-page cw-collection {
+  min-width: 0;
+  max-width: 100%;
+}
+
+.petclinic-object-grid {
+  display: grid;
+  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
+  gap: var(--causeway-space-4);
+  align-items: start;
+}
+
+.petclinic-object-details,
+.petclinic-object-collections,
+.petclinic-field-list {
+  display: grid;
+  gap: var(--causeway-space-3);
+  min-width: 0;
+}
+
+.petclinic-field-list {
+  --causeway-property-label-column: minmax(7.5rem, 2fr);
+}
+
+.petclinic-card {
+  display: grid;
+  gap: var(--causeway-space-3);
+  min-width: 0;
+  padding: var(--causeway-space-4);
+  border: 1px solid var(--causeway-border);
+  border-radius: var(--causeway-radius-md);
+  background: var(--causeway-surface);
+}
+
+.petclinic-card > h2 {
+  margin: 0;
+  font-size: 1rem;
+}
+
+.petclinic-object-heading {
+  align-items: center;
+  border-block-end: 1px solid var(--causeway-border);
+  display: flex;
+  flex-wrap: wrap;
+  gap: var(--causeway-space-5);
+  min-width: 0;
+}
+
+.petclinic-object-heading > cw-object-header {
+  flex: 0 1 auto;
+}
+
+.petclinic-object-heading .causeway-object-header {
+  border-block-end: 0;
+  margin-block-end: 0;
+}
+
+.petclinic-page-toolbar {
+  align-items: center;
+  display: flex;
+  flex: 0 1 auto;
+  flex-wrap: wrap;
+  gap: var(--causeway-space-2);
+}
+
+.petclinic-preview {
+  padding: var(--causeway-space-4);
+  border-inline-start: 0.25rem solid var(--causeway-info);
+}
+
+.causeway-visually-hidden {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  margin: -1px;
+  padding: 0;
+  overflow: hidden;
+  clip: rect(0 0 0 0);
+  white-space: nowrap;
+  border: 0;
+}
+
+@media (max-width: 60rem) {
+  .petclinic-dashboard-grid,
+  .petclinic-object-grid {
+    grid-template-columns: minmax(0, 1fr);
+  }
+}
 
 @media (max-width: 48rem) {
-  .petclinic-navbar, .petclinic-owner-heading { align-items: stretch; flex-direction: column; }
-  .petclinic-toolbar { justify-content: start; }
-  .petclinic-owner-grid { grid-template-columns: minmax(0, 1fr); }
-  .petclinic-main { padding: 0.75rem; }
+  .causeway-shell-navbar {
+    min-height: auto;
+    padding-block: var(--causeway-space-2);
+    align-items: flex-start;
+    flex-wrap: wrap;
+  }
+
+  .causeway-shell-navbar > cw-menubars {
+    flex-basis: 100%;
+    flex-direction: column;
+  }
+
+  .causeway-shell-result,
+  .causeway-shell-main,
+  .causeway-shell-footer {
+    width: min(calc(100% - (2 * var(--causeway-space-3))), var(--causeway-content-width));
+  }
+
+  .causeway-shell-main {
+    padding-block-start: var(--causeway-space-4);
+  }
+
+  .causeway-shell-footer {
+    flex-direction: column;
+  }
 }
 
 @media (prefers-reduced-motion: reduce) {
-  *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
+  *,
+  *::before,
+  *::after {
+    scroll-behavior: auto !important;
+    animation-duration: 0.01ms !important;
+    transition-duration: 0.01ms !important;
+  }
+
+  .causeway-skip-link {
+    transition: none;
+  }
 }
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-BntyzeMN.js b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-BntyzeMN.js
deleted file mode 100644
index dcbb1709f55..00000000000
--- a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-BntyzeMN.js
+++ /dev/null
@@ -1,2 +0,0 @@
-(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))r(s);new MutationObserver(s=>{for(const i of s)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(o)}).observe(document,{childList:!0,subtree:!0});function n(s){const i={};return s.integrity&&(i.integrity=s.integrity),s.referrerPolicy&&(i.referrerPolicy=s.referr [...]
-//# sourceMappingURL=index-BntyzeMN.js.map
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-BntyzeMN.js.map b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-BntyzeMN.js.map
deleted file mode 100644
index 68a543c41f2..00000000000
--- a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-BntyzeMN.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"index-BntyzeMN.js","sources":["../../../../../../frontend/node_modules/@vue/shared/dist/shared.esm-bundler.js","../../../../../../frontend/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js","../../../../../../frontend/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js","../../../../../../frontend/node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js","../../../../../../frontend/node_modules/vue-router/dist/devtools-EWN81iOl.mjs","../../ [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-BuWcbzr4.css b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-BuWcbzr4.css
deleted file mode 100644
index 11356c7e0ed..00000000000
--- a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-BuWcbzr4.css
+++ /dev/null
@@ -1 +0,0 @@
-:root{--causeway-vue-surface: #ffffff;--causeway-vue-text: #1f2937;--causeway-vue-muted: #596579;--causeway-vue-border: #d7dee8;--causeway-vue-danger: #9b1c1c;--causeway-vue-focus: #075ea8;color-scheme:light dark}.causeway-vue-route-page{box-sizing:border-box;min-width:0;width:100%;color:var(--causeway-vue-text)}.causeway-vue-route-page:focus-visible,[data-causeway-router-view]:focus-visible{outline:.2rem solid var(--causeway-vue-focus);outline-offset:.2rem}.causeway-vue-status{margin:1r [...]
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CklV2Pe0.js b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CklV2Pe0.js
new file mode 100644
index 00000000000..9955a28b772
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CklV2Pe0.js
@@ -0,0 +1,2 @@
+(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const r of i)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&s(o)}).observe(document,{childList:!0,subtree:!0});function n(i){const r={};return i.integrity&&(r.integrity=i.integrity),i.referrerPolicy&&(r.referrerPolicy=i.referr [...]
+//# sourceMappingURL=index-CklV2Pe0.js.map
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CklV2Pe0.js.map b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CklV2Pe0.js.map
new file mode 100644
index 00000000000..87a5b0c0cbc
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CklV2Pe0.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"index-CklV2Pe0.js","sources":["../../../../../../frontend/node_modules/@vue/shared/dist/shared.esm-bundler.js","../../../../../../frontend/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js","../../../../../../frontend/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js","../../../../../../frontend/node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js","../../../../../../frontend/node_modules/vue-router/dist/devtools-EWN81iOl.mjs","../../ [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CqcjgOE7.css b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CqcjgOE7.css
new file mode 100644
index 00000000000..e940b94c261
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CqcjgOE7.css
@@ -0,0 +1 @@
+:root{--causeway-vue-surface: #ffffff;--causeway-vue-text: #1f2937;--causeway-vue-muted: #596579;--causeway-vue-border: #d7dee8;--causeway-vue-danger: #9b1c1c;--causeway-vue-focus: #075ea8;color-scheme:light dark}.causeway-vue-route-page{box-sizing:border-box;min-width:0;width:100%;color:var(--causeway-vue-text)}.causeway-vue-route-page:focus-visible,[data-causeway-router-view]:focus-visible{outline:.2rem solid var(--causeway-vue-focus);outline-offset:.2rem}.causeway-vue-status{margin:1r [...]
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html
index 9ad4ffdc303..fa4b8b83fb4 100644
--- a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html
@@ -26,8 +26,8 @@
   <link rel="stylesheet" href="/causeway-webcomponents/component-styles.css">
   <link rel="stylesheet" href="/causeway-webcomponents/theme.css">
   <link rel="stylesheet" href="/webjars/font-awesome/7.3.0/css/all.min.css">
-  <script type="module" crossorigin src="/vue/assets/index-BntyzeMN.js"></script>
-  <link rel="stylesheet" crossorigin href="/vue/assets/index-BuWcbzr4.css">
+  <script type="module" crossorigin src="/vue/assets/index-CklV2Pe0.js"></script>
+  <link rel="stylesheet" crossorigin href="/vue/assets/index-CqcjgOE7.css">
 </head>
 <body>
   <div id="app"></div>
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVuePlaywrightTest.java b/viewers/webcomponents/sample-vue-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVuePlaywrightTest.java
index e94e1d5422b..de80c48a92e 100644
--- a/viewers/webcomponents/sample-vue-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVuePlaywrightTest.java
+++ b/viewers/webcomponents/sample-vue-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVuePlaywrightTest.java
@@ -78,7 +78,13 @@ void openPage() {
         page.onPageError(error -> browserFailures.add(error));
         page.onConsoleMessage(message -> {
             if ("error".equals(message.type()) && !message.text().contains("favicon.ico")) {
-                browserFailures.add(message.text());
+                browserFailures.add("console: " + message.text());
+            }
+        });
+        page.onRequestFailed(request -> {
+            final var failure = request.failure();
+            if (!(request.url().contains("/graphql") && failure != null && failure.contains("ERR_ABORTED"))) {
+                browserFailures.add("request: " + request.method() + " " + request.url() + " " + failure);
             }
         });
     }
@@ -103,30 +109,132 @@ void customOwnerPageUsesOneStableShellAndDeclarativeRouteBoundary() {
     }
 
     @Test
-    void semanticNavigationUsesGenericFallbackAndBrowserHistory() {
-        open("/vue/object/petclinic.PetOwner/s_owner-mary");
-        page.locator("[data-route-state='ready']").waitFor();
-        page.evaluate("""
-                document.querySelector('[data-testid="petclinic-vue-application-shell"]').dispatchEvent(
-                  new CustomEvent('causeway-navigation-request', {
-                    bubbles: true,
-                    composed: true,
-                    cancelable: true,
-                    detail: {target: {logicalTypeName: 'petclinic.Pet', id: 's_pet-basil'}}
-                  }))
-                """);
-        page.waitForURL("**/vue/object/petclinic.Pet/s_pet-basil");
-        page.locator("[data-page-kind='generic'][data-route-state='ready']").waitFor();
-        page.waitForFunction("document.body.innerText.includes('Basil')");
+    void semanticNavigationUsesExactPagesGenericFallbackAndBrowserHistory() {
+        open("/vue/object/petclinic.Pet/s_pet-basil");
+        page.locator("[data-page-kind='pet'][data-route-state='ready']").waitFor();
         assertThat(page.locator("body").innerText()).contains("Basil");
 
+        navigateTo("petclinic.ViewerFallback", "s_viewer-fallback");
+        page.waitForURL("**/vue/object/petclinic.ViewerFallback/s_viewer-fallback");
+        page.locator("[data-page-kind='generic'][data-route-state='ready']").waitFor();
+        page.waitForFunction("() => document.body.innerText.includes('Generic viewer fallback')");
+        assertThat(page.locator("body").innerText()).contains("Generic viewer fallback", "Message");
+
         page.goBack();
-        page.locator("[data-page-kind='pet-owner'][data-route-state='ready']").waitFor();
-        assertThat(page.url()).endsWith("/vue/object/petclinic.PetOwner/s_owner-mary");
+        page.locator("[data-page-kind='pet'][data-route-state='ready']").waitFor();
+        assertThat(page.url()).endsWith("/vue/object/petclinic.Pet/s_pet-basil");
 
         page.goForward();
         page.locator("[data-page-kind='generic'][data-route-state='ready']").waitFor();
-        assertThat(page.url()).endsWith("/vue/object/petclinic.Pet/s_pet-basil");
+        assertThat(page.url()).endsWith("/vue/object/petclinic.ViewerFallback/s_viewer-fallback");
+    }
+
+    @Test
+    void exactPetclinicPagesMatchTheHtmxSemanticComposition() {
+        open("/vue/");
+        page.locator("[data-page-kind='home'][data-route-state='ready']").waitFor();
+        page.waitForFunction("() => document.querySelector('cw-collection#petOwners')?.collectionState?.rows?.length === 5");
+        page.waitForFunction("() => document.querySelector('cw-collection#futureVisits')?.collectionState?.rows?.length === 10");
+        assertThat(page.locator(".petclinic-dashboard-grid > section > h2")
+                .evaluateAll("elements => elements.map(element => element.textContent.trim()).join(',')"))
+                .isEqualTo("Pet owners,Upcoming visits");
+        assertThat(page.locator("cw-collection#petOwners").getAttribute("paged")).isEqualTo("5");
+        assertThat(page.locator("cw-collection#futureVisits").getAttribute("paged")).isEqualTo("10");
+        assertThat(page.locator("cw-collection#petOwners > cw-peek").count()).isEqualTo(1);
+        assertThat(page.locator("cw-collection#futureVisits > cw-peek").count()).isEqualTo(1);
+        assertThat(page.locator("cw-collection#petOwners [data-causeway-peek-toggle]").count()).isGreaterThan(0);
+        assertThat(page.title()).endsWith(" · Pet Clinic");
+
+        open("/vue/object/petclinic.PetOwner/s_owner-mary");
+        page.locator("[data-page-kind='pet-owner'][data-route-state='ready']").waitFor();
+        page.waitForFunction("() => document.querySelector('.petclinic-object-collections > section:last-child cw-collection#visits')?.collectionState?.rows?.length === 2");
+        assertThat(page.locator(".petclinic-owner-page h2")
+                .evaluateAll("elements => elements.map(element => element.textContent.trim()).join(',')"))
+                .isEqualTo("Identity,Contact,Details,Pets,Companion animals,Visits,Visit history");
+        assertThat(page.locator(".petclinic-page-toolbar > cw-action")
+                .evaluateAll("elements => elements.map(element => element.id).join(',')"))
+                .isEqualTo("allOwners,noOwners,relatedOwners,delete");
+        assertThat(page.locator("cw-action#relatedOwners cw-standalone-collection > cw-collection-column")
+                .evaluateAll("elements => elements.map(element => element.id).join(',')"))
+                .isEqualTo("name,knownAs,notes");
+        assertThat(page.locator(".petclinic-object-details cw-property")
+                .evaluateAll("elements => elements.map(element => element.id).join(',')"))
+                .isEqualTo("name,knownAs,telephoneNumber,emailAddress,notes,lastVisit,daysSinceLastVisit");
+        assertThat(page.locator(".petclinic-object-collections > section").count()).isEqualTo(2);
+        assertThat(page.locator("cw-collection#visits > cw-collection-column")
+                .evaluateAll("elements => elements.map(element => element.id).join(',')"))
+                .isEqualTo("visitAt,reason,notes");
+        assertThat(page.locator("cw-collection#visits").getAttribute("paged")).isEqualTo("8");
+        assertThat(page.locator("cw-collection#visits > cw-peek").count()).isEqualTo(1);
+        assertThat(page.locator("cw-collection#visits [data-causeway-peek-toggle]").count()).isGreaterThan(0);
+
+        open("/vue/object/petclinic.Pet/s_pet-basil");
+        page.locator("[data-page-kind='pet'][data-route-state='ready']").waitFor();
+        assertThat(page.locator(".petclinic-pet-page h2")
+                .evaluateAll("elements => elements.map(element => element.textContent.trim()).join(',')"))
+                .isEqualTo("Identity,Details");
+        assertThat(page.locator(".petclinic-pet-page cw-property")
+                .evaluateAll("elements => elements.map(element => element.id).join(',')"))
+                .isEqualTo("petOwner,name,species,notes");
+        assertThat(page.locator(".petclinic-pet-page cw-property#id, .petclinic-pet-page cw-property#version").count()).isZero();
+
+        open("/vue/object/petclinic.Visit/s_visit-basil-checkup");
+        page.locator("[data-page-kind='visit'][data-route-state='ready']").waitFor();
+        assertThat(page.locator(".petclinic-visit-page h2")
+                .evaluateAll("elements => elements.map(element => element.textContent.trim()).join(',')"))
+                .isEqualTo("Appointment,Details");
+        assertThat(page.locator(".petclinic-visit-page cw-property")
+                .evaluateAll("elements => elements.map(element => element.id).join(',')"))
+                .isEqualTo("pet,visitAt,reason,notes");
+        assertThat(page.locator(".petclinic-visit-page cw-property#id, .petclinic-visit-page cw-property#version").count()).isZero();
+    }
+
+    @Test
+    void shellAndOwnerLayoutMatchTheWideAndNarrowReferenceInvariants() {
+        open("/vue/object/petclinic.PetOwner/s_owner-mary");
+        page.locator("[data-route-state='ready']").waitFor();
+        page.locator("cw-menubars[data-menu-state='ready']").waitFor();
+
+        final var wide = (List<Number>) page.locator("[data-testid='petclinic-vue-application-shell']").evaluate("""
+                shell => {
+                  const header = shell.querySelector('header').getBoundingClientRect();
+                  const main = shell.querySelector('main').getBoundingClientRect();
+                  const details = shell.querySelector('.petclinic-object-details').getBoundingClientRect();
+                  const collections = shell.querySelector('.petclinic-object-collections').getBoundingClientRect();
+                  return [header.height, main.left, details.left, details.right, collections.left, collections.top - details.top,
+                    document.documentElement.scrollWidth - document.documentElement.clientWidth];
+                }
+                """);
+        assertThat(wide.get(0).doubleValue()).isBetween(50.0, 54.0);
+        assertThat(wide.get(1).doubleValue()).isBetween(15.0, 17.0);
+        assertThat(wide.get(4).doubleValue()).isGreaterThanOrEqualTo(wide.get(3).doubleValue());
+        assertThat(Math.abs(wide.get(5).doubleValue())).isLessThan(1.0);
+        assertThat(wide.get(6).intValue()).isZero();
+        assertThat(page.locator("header.causeway-shell-header").evaluate("element => getComputedStyle(element).backgroundColor"))
+                .isEqualTo("rgb(23, 105, 170)");
+        assertThat(page.locator("body").evaluate("element => getComputedStyle(element).fontFamily"))
+                .asString().contains("Inter");
+        assertThat(page.locator("cw-menubars").innerText()).containsSubsequence("Pet Owners", "Visits", "System");
+        assertThat(page.locator("footer").innerText()).contains("Powered by Apache Causeway", "Vue viewer");
+        assertThat(page.locator(".causeway-object-identity").isVisible()).isFalse();
+        assertThat(page.locator("[data-causeway-route-page]").evaluate("element => getComputedStyle(element).outlineStyle"))
+                .isEqualTo("none");
+
+        page.setViewportSize(500, 900);
+        page.waitForFunction("() => { const grid = document.querySelector('.petclinic-object-grid'); return getComputedStyle(grid).gridTemplateColumns.split(' ').length === 1; }");
+        final var narrow = (List<Number>) page.locator("[data-testid='petclinic-vue-application-shell']").evaluate("""
+                shell => {
+                  const details = shell.querySelector('.petclinic-object-details').getBoundingClientRect();
+                  const collections = shell.querySelector('.petclinic-object-collections').getBoundingClientRect();
+                  const footer = shell.querySelector('footer');
+                  return [collections.top - details.bottom,
+                    document.documentElement.scrollWidth - document.documentElement.clientWidth,
+                    getComputedStyle(footer).flexDirection === 'column' ? 1 : 0];
+                }
+                """);
+        assertThat(narrow.get(0).doubleValue()).isGreaterThanOrEqualTo(0.0);
+        assertThat(narrow.get(1).intValue()).isZero();
+        assertThat(narrow.get(2).intValue()).isEqualTo(1);
     }
 
     @Test
@@ -151,9 +259,9 @@ void scalarActionResultsUseTheActivePageOutlet() {
     @Test
     void directRefreshAndInvalidRoutesRemainWithinTheApplicationShell() {
         open("/vue/object/petclinic.Pet/s_pet-basil");
-        page.locator("[data-page-kind='generic'][data-route-state='ready']").waitFor();
+        page.locator("[data-page-kind='pet'][data-route-state='ready']").waitFor();
         page.reload();
-        page.locator("[data-page-kind='generic'][data-route-state='ready']").waitFor();
+        page.locator("[data-page-kind='pet'][data-route-state='ready']").waitFor();
         assertThat(page.locator("cw-graphql-client[data-causeway-shell-client]").count()).isEqualTo(1);
 
         open("/vue/invalid-route");
@@ -184,6 +292,18 @@ void bothToolkitPoliciesPreserveAccessibleShellLandmarksAndMenus() {
         }
     }
 
+    private void navigateTo(final String logicalTypeName, final String id) {
+        page.evaluate("""
+                ([logicalTypeName, id]) => document.querySelector('[data-testid="petclinic-vue-application-shell"]').dispatchEvent(
+                  new CustomEvent('causeway-navigation-request', {
+                    bubbles: true,
+                    composed: true,
+                    cancelable: true,
+                    detail: {target: {logicalTypeName, id}}
+                  }))
+                """, List.of(logicalTypeName, id));
+    }
+
     private void open(final String path) {
         page.navigate("http://localhost:" + port + path);
         page.locator("[data-testid='petclinic-vue-application-shell']").waitFor();
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/test/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVueApplicationIntegTest.java b/viewers/webcomponents/sample-vue-petclinic/src/test/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVueApplicationIntegTest.java
index 69d9918f09f..fbf8a58e610 100644
--- a/viewers/webcomponents/sample-vue-petclinic/src/test/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVueApplicationIntegTest.java
+++ b/viewers/webcomponents/sample-vue-petclinic/src/test/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVueApplicationIntegTest.java
@@ -24,13 +24,18 @@
 import java.net.http.HttpResponse;
 import java.util.regex.Pattern;
 
+import jakarta.persistence.EntityManager;
+
 import org.junit.jupiter.api.Test;
+
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.core.io.ResourceLoader;
 
 import org.apache.causeway.viewer.webcomponents.sample.petclinic.domain.PetOwner;
 import org.apache.causeway.viewer.webcomponents.sample.petclinic.domain.PetOwnerRepository;
+import org.apache.causeway.viewer.webcomponents.sample.petclinic.domain.ViewerFallback;
 import org.apache.causeway.viewer.webcomponents.sample.petclinic.domain.VisitRepository;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -51,6 +56,12 @@ class PetClinicVueApplicationIntegTest {
     @Autowired
     private VisitRepository visitRepository;
 
+    @Autowired
+    private EntityManager entityManager;
+
+    @Autowired
+    private ResourceLoader resourceLoader;
+
     @Test
     void reusesDeterministicPetclinicDomain() {
         final var mary = ownerRepository.findById(PetOwner.MARY_ID);
@@ -58,6 +69,9 @@ void reusesDeterministicPetclinicDomain() {
         assertThat(mary.getName()).isEqualTo("Mary Smith");
         assertThat(ownerRepository.findAll()).hasSize(10);
         assertThat(visitRepository.findByPetOwner(mary)).hasSize(2);
+        assertThat(entityManager.find(ViewerFallback.class, ViewerFallback.ID).getMessage())
+                .isEqualTo("Rendered by the generic Vue page.");
+        assertThat(resourceLoader.getResource("classpath:menubars.layout.xml").exists()).isTrue();
     }
 
     @Test


(causeway) 05/16: docs(openspec): plan Vue Petclinic reconciliation

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit fb4af5e2bb3e4848de96c791c70d2d55cc37848a
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 3 09:50:50 2026 +0100

    docs(openspec): plan Vue Petclinic reconciliation
---
 .../.openspec.yaml                                 |   2 +
 .../reconcile-vue-petclinic-with-htmx/design.md    | 111 +++++++++++++++++++++
 .../reconcile-vue-petclinic-with-htmx/proposal.md  |  31 ++++++
 .../specs/generic-vue-web-component-viewer/spec.md |  61 +++++++++++
 .../reconcile-vue-petclinic-with-htmx/tasks.md     |  45 +++++++++
 5 files changed, 250 insertions(+)

diff --git a/openspec/changes/reconcile-vue-petclinic-with-htmx/.openspec.yaml b/openspec/changes/reconcile-vue-petclinic-with-htmx/.openspec.yaml
new file mode 100644
index 00000000000..9696e00f11e
--- /dev/null
+++ b/openspec/changes/reconcile-vue-petclinic-with-htmx/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-09-03
diff --git a/openspec/changes/reconcile-vue-petclinic-with-htmx/design.md b/openspec/changes/reconcile-vue-petclinic-with-htmx/design.md
new file mode 100644
index 00000000000..2772ccbae96
--- /dev/null
+++ b/openspec/changes/reconcile-vue-petclinic-with-htmx/design.md
@@ -0,0 +1,111 @@
+## Context
+
+The HTMX and Vue Petclinic applications share deterministic domain classes, GraphQL behavior, web components, route syntax, and fixture identities, but they do not currently share a presentation contract.
+A headless comparison at a 1440-pixel viewport found that the Vue shell uses a two-row 116-pixel dark header instead of the HTMX single-row 52-pixel header, reverses the primary menu order, exposes three utility menus instead of the HTMX `System` group, applies different typography and spacing, omits the Wicket comparison footer link, and leaves a route-container focus border visible around the page.
+
+The route differences are more substantial.
+HTMX provides application-authored pages for `petclinic.HomePage`, `petclinic.PetOwner`, `petclinic.Pet`, and `petclinic.Visit`, whereas Vue registers only `petclinic.PetOwner` and delegates the other three routes to generic `<cw-object>` composition.
+The Vue owner page also combines the HTMX Identity, Contact, and Details sections, omits `noOwners` and `daysSinceLastVisit`, places Visits in the narrow grid column, and omits portions of the authoritative standalone-result and visit-collection declarations.
+Generic Vue pages consequently expose technical `id` and `version` members and different actions or collections on Pet and Visit routes.
+
+The comparison concerns application-owned composition and theme rather than a difference in canonical domain behavior.
+The existing ownership boundary remains: metadata and semantic components determine meaning, while each host application selects pages, binds values, routes semantic events, and owns shell geometry.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Make the Vue Petclinic shell and its four user-visible route types presentation-equivalent to HTMX at wide and narrow viewports.
+- Preserve the HTMX member selection, grouping, labels, descriptions, action placement, collection behavior, and paging values in source-visible Vue pages.
+- Keep Vue routing and lifecycle semantics intact while eliminating accidental visible differences such as technical members, title suffixes, menu grouping, and persistent container outlines.
+- Retain executable generic-fallback coverage without using one of the four reconciled Petclinic routes as the fallback example.
+- Add stable regression checks for semantic structure and high-value computed presentation properties without requiring pixel-identical rendering.
+
+**Non-Goals:**
+
+- Changing HTMX page composition, the shared Petclinic domain, GraphQL metadata, or custom-element behavior to accommodate Vue.
+- Making HTMX and Vue DOM trees byte-for-byte identical.
+- Replacing host-owned shells with a framework-neutral shell runtime or shared server-side template language.
+- Introducing visual screenshot goldens whose antialiasing or browser-version sensitivity would make the build unreliable.
+- Changing Vue Router ownership, canonical route encoding, SSR policy, component-toolkit policy, or package peer dependencies.
+- Reconciling Wicket itself with either web-component viewer.
+
+## Decisions
+
+### Use an explicit presentation-equivalence contract rather than pixel identity
+
+Parity means that the same shell regions, menu groups, route headings, sections, declared members, actions, result outlets, collection columns, preview affordances, pagination limits, and responsive column relationships are presented in the same order.
+Tests will also compare bounded computed properties such as header row count and height range, content insets, column ratios, breakpoint collapse, footer alignment, typography family, and palette tokens.
+Minor browser rendering differences and framework-required wrapper elements are permitted.
+
+Pixel snapshots were considered but rejected because font rasterization, Vaadin internals, and browser upgrades would create noise unrelated to presentation intent.
+A purely subjective manual review was also rejected because it would not prevent recurrence.
+
+### Keep HTMX source authoritative and translate it into Vue single-file components
+
+The four HTMX page templates and the Petclinic HTMX application stylesheet define the initial parity baseline.
+Vue will register exact-type single-file components for HomePage, PetOwner, Pet, and Visit, using Vue property bindings only where structured or dynamic values require them.
+The components will preserve the required one-context and one-controller boundary contract and will not reproduce metadata or interaction logic in Vue state.
+
+Reusing generic `<cw-object>` for Pet, Visit, or HomePage was rejected because generic layout intentionally exposes the complete effective grid and therefore cannot reproduce application-authored member selection or section composition.
+Generating Vue templates from HTMX resources was rejected because the formats have different binding and lifecycle semantics and such generation would introduce a new template compiler for a small sample.
+
+### Reuse the authoritative class vocabulary and port only application-owned CSS
+
+Vue pages will adopt the HTMX Petclinic class names and document structure where the semantics match, and `petclinic.css` will express the corresponding application-owned rules using the established foundation design tokens.
+The Vue shell will mirror the HTMX shell geometry and branding while retaining Vue event and router bindings.
+Viewer-specific HTMX selectors and transport behavior will not be copied.
+
+A new shared presentation module was considered but rejected for this change because it would couple a framework-neutral domain sample to browser assets or add a module whose only consumers are two acceptance applications.
+The parity tests provide the drift control while allowing each host to remain independently authored.
+
+### Reconcile menus through supported projection and application configuration
+
+The implementation will first identify whether the current menu difference comes from module composition, authentication chrome, application-menu metadata, or shell styling.
+It will then use existing menu projection and shell contracts to present the HTMX reference order and grouping; it will not rewrite GraphQL menu payloads, duplicate menu actions, or hide domain actions with arbitrary selectors.
+At a wide viewport the visible top-level presentation will be Pet Owners, Visits, and the reference utility grouping, with narrow behavior preserving keyboard access and all actions.
+
+Hardcoded replacement menus were rejected because they would duplicate semantic service-action dispatch and undermine the generic menu components.
+
+### Preserve generic fallback with a deliberate acceptance fixture
+
+All four visible Petclinic route types will become exact registrations.
+Generic fallback will be exercised through a deliberately unregistered acceptance-only logical type or an existing suitable fixture that is not part of the reconciled route set.
+If a new fixture is required, it will live in the shared sample domain without Vue dependencies and will have stable identity and bounded metadata.
+
+Leaving one user-visible Petclinic route generic was rejected because it makes parity impossible.
+Removing generic-fallback acceptance was rejected because fallback remains a required public viewer capability.
+
+### Test semantic parity and responsive invariants headlessly
+
+The Vue Playwright suite will cover each reconciled route in both native and Vaadin policies where component presentation can differ, and representative wide and narrow viewports will cover shell and grid behavior.
+Assertions will identify the expected sections and members, their order and containment, menu labels and grouping, result outlets, collection configuration and row previews, document-title suffix, footer content, and absence of technical fields.
+Computed-style and bounding-box assertions will use tolerances and structural relationships rather than exact full-page coordinates.
+
+The existing HTMX acceptance behavior will remain the oracle.
+Where a shared fixture or helper is practical, both suites will consume the same parity expectations; otherwise the Vue tests will name the corresponding HTMX source contract explicitly.
+All browser execution remains headless by default.
+
+## Risks / Trade-offs
+
+- [Risk] Independently authored HTMX and Vue templates can drift again. → Mitigation: encode the parity matrix in focused acceptance assertions and document HTMX as the change authority.
+- [Risk] Menu differences may reveal a missing generic Vue shell integration capability rather than sample CSS. → Mitigation: use existing semantic menu and authentication contracts first; if a public viewer API change is truly required, stop implementation and update this proposal/spec before expanding scope.
+- [Risk] A visible focus ring may be removed in pursuit of screenshot parity. → Mitigation: retain a visible keyboard focus indication on the heading or route landmark and suppress only accidental persistent container framing.
+- [Risk] Exact custom pages reduce the sample's obvious generic-fallback demonstration. → Mitigation: retain a dedicated fallback fixture and document both paths.
+- [Risk] CSS parity can become overfitted to one desktop width. → Mitigation: specify relationships, token usage, and breakpoints and test at least one wide and one narrow viewport.
+- [Risk] Adding an acceptance fixture to the shared domain can affect menus or fixture cardinalities. → Mitigation: prefer an existing suitable type; if adding one, keep it excluded from normal Petclinic menus and seeded collections.
+
+## Migration Plan
+
+1. Capture the authoritative shell and four-route parity matrix in tests or a small test fixture before changing Vue markup.
+2. Reconcile shell structure, menu projection, title and footer behavior, and responsive theme rules.
+3. Add and register HomePage, Pet, and Visit Vue components and reconcile PetOwner against its HTMX template.
+4. Move generic-fallback acceptance to the deliberate fixture and regenerate committed Vite assets.
+5. Run Vue package tests, Vue and HTMX integration and Playwright suites, both component-toolkit policies, frontend stale-output checks, Maven verification, and licensing checks.
+
+Rollback consists of reverting the Vue sample source, generated assets, and tests because no persisted data, route format, or public API migration is involved.
+
+## Open Questions
+
+None that block implementation.
+The menu investigation is intentionally an implementation task bounded by the existing projection contracts; discovery of a required public Vue viewer API would require planning review before code proceeds.
diff --git a/openspec/changes/reconcile-vue-petclinic-with-htmx/proposal.md b/openspec/changes/reconcile-vue-petclinic-with-htmx/proposal.md
new file mode 100644
index 00000000000..fc1a3473b2a
--- /dev/null
+++ b/openspec/changes/reconcile-vue-petclinic-with-htmx/proposal.md
@@ -0,0 +1,31 @@
+## Why
+
+The Vue Petclinic acceptance application uses the same domain and web components as the HTMX application, but its shell, route compositions, and responsive layout currently present materially different navigation, content, and styling.
+The HTMX application already demonstrates the intended Petclinic experience, so it should become the explicit reference against which the Vue host is reconciled without moving presentation authority into shared domain components.
+
+## What Changes
+
+- Treat the HTMX Petclinic shell and its HomePage, PetOwner, Pet, and Visit route compositions as the authoritative presentation reference for the Vue Petclinic application.
+- Reconcile Vue branding, menu order and grouping, shell dimensions, colors, typography, content spacing, footer content, document titles, route focus treatment, and responsive behavior with the HTMX application.
+- Provide exact-type Vue page registrations for the four Petclinic route types and reproduce the HTMX pages' section grouping, member selection, action placement, collection columns, paging, filtering, sorting, row previews, descriptions, and parameter presentation.
+- Remove Vue-only exposure of technical or unintended members from those custom Petclinic pages and restore HTMX-only omissions currently present in Vue, including missing owner actions, derived properties, visit notes, and row-preview affordances.
+- Preserve the generic Vue fallback as a viewer capability and acceptance scenario using a deliberately unregistered acceptance type or fixture rather than relying on a visible Petclinic route to remain inconsistent.
+- Add headless cross-viewer acceptance coverage at representative desktop and narrow viewports, using semantic and computed-layout assertions rather than brittle pixel-perfect screenshot matching.
+- Keep routing, GraphQL identity, metadata, validation, invocation, navigation, lifecycle, and component-toolkit policy unchanged and authoritative in their existing layers.
+
+## Capabilities
+
+### New Capabilities
+
+None.
+
+### Modified Capabilities
+
+- `generic-vue-web-component-viewer`: Strengthen the executable Vue Petclinic acceptance requirement so its application-owned shell and exact-type pages remain presentation-equivalent to the authoritative HTMX Petclinic experience while generic fallback remains independently demonstrated.
+
+## Impact
+
+- Affects the Vue sample shell, application stylesheet, exact-type page registry, Vue single-file page components, generated frontend assets, documentation, and Playwright/integration tests under `viewers/webcomponents/sample-vue-petclinic`.
+- May add a small acceptance-only logical type or equivalent fixture in `viewers/webcomponents/sample-petclinic-domain` if needed to retain an executable generic-fallback scenario without changing the four visible Petclinic routes.
+- Uses `viewers/webcomponents/sample-htmx-petclinic` as a read-only reference and regression oracle; no HTMX presentation change is intended.
+- Does not change the public Vue viewer API, canonical route syntax, GraphQL schema, foundation custom-element contracts, or Vue/Vue Router dependency policy.
diff --git a/openspec/changes/reconcile-vue-petclinic-with-htmx/specs/generic-vue-web-component-viewer/spec.md b/openspec/changes/reconcile-vue-petclinic-with-htmx/specs/generic-vue-web-component-viewer/spec.md
new file mode 100644
index 00000000000..99b201ae0ce
--- /dev/null
+++ b/openspec/changes/reconcile-vue-petclinic-with-htmx/specs/generic-vue-web-component-viewer/spec.md
@@ -0,0 +1,61 @@
+## MODIFIED Requirements
+
+### Requirement: Executable Vue Petclinic acceptance application
+
+The project SHALL provide a production-built Vue Petclinic application that reuses the same deterministic Petclinic domain, public logical types, and fixture identities as the HTMX sample without copying domain implementation.
+The application SHALL demonstrate source-visible exact-type Vue pages, generic fallback, a stable application-owned shell, menus, interactions, results, canonical routing, and server refresh fallback.
+For the shell and the `petclinic.HomePage`, `petclinic.PetOwner`, `petclinic.Pet`, and `petclinic.Visit` routes, the HTMX Petclinic application SHALL be the authoritative presentation reference.
+Presentation equivalence SHALL preserve user-visible information architecture and responsive relationships without requiring framework wrapper markup or browser pixels to be identical.
+
+#### Scenario: Reconciled Petclinic page loads
+
+- **WHEN** HomePage, PetOwner, Pet, or Visit is opened through a direct canonical Vue bookmark
+- **THEN** an exact-type Vue single-file component binds the declared object context and interaction controller
+- **AND** it presents the same ordered headings, sections, selected semantic members, action placement, result outlets, descriptions, collection columns, paging, filtering, sorting, and row-preview affordances as the corresponding HTMX Petclinic page
+- **AND** it does not expose technical or additional members omitted by that HTMX page
+
+#### Scenario: Vue PetOwner page matches the reference composition
+
+- **WHEN** the deterministic PetOwner fixture is rendered by Vue
+- **THEN** Identity, Contact, and Details occupy the reference details column while Pets and Visits occupy the reference collections column
+- **AND** the owner actions include the empty-result action and the declared standalone related-owner result
+- **AND** the derived last-visit value, visit notes column, paging values, parameter presentation, and nested preview declarations match the HTMX page
+
+#### Scenario: Vue shell is presented at a wide viewport
+
+- **WHEN** a reconciled route is ready at the documented wide acceptance viewport
+- **THEN** branding, primary menu order, utility-menu grouping, header geometry, content inset, typography, palette, footer content, and document-title suffix are equivalent to the HTMX shell
+- **AND** the route uses the available width and reference column relationships without an accidental persistent container outline
+- **AND** keyboard users retain a visible focus indication on an appropriate heading or route landmark
+
+#### Scenario: Vue shell is presented at a narrow viewport
+
+- **WHEN** a reconciled route is ready at the documented narrow acceptance viewport
+- **THEN** shell navigation and page columns collapse in the same order and at an equivalent breakpoint to the HTMX presentation
+- **AND** menus, actions, results, fields, collections, previews, and pagination remain visible and keyboard operable without horizontal page overflow
+
+#### Scenario: Generic fallback fixture loads
+
+- **WHEN** a deliberately unregistered acceptance logical type is opened through a direct canonical Vue bookmark
+- **THEN** the generic Vue page renders `<cw-object>` for the same GraphQL identity
+- **AND** direct refresh and browser history retain that route
+- **AND** none of the four reconciled Petclinic route types is used to demonstrate fallback
+
+#### Scenario: Existing HTMX Petclinic is rebuilt
+
+- **WHEN** Vue presentation is reconciled with the HTMX reference
+- **THEN** existing HTMX routes, page composition, shell, seed cardinalities, stable identities, and acceptance behavior remain unchanged
+- **AND** the shared domain introduces no Vue dependency or presentation authority
+
+#### Scenario: Presentation parity is regression tested
+
+- **WHEN** the Vue browser profile executes
+- **THEN** it verifies semantic parity for all four reconciled route types and high-value computed shell and layout relationships at representative wide and narrow viewports
+- **AND** comparisons use bounded structural or computed-style assertions rather than pixel-perfect screenshot equality
+- **AND** it covers both supported component-toolkit policies where their presentation differs
+
+#### Scenario: Acceptance suite runs
+
+- **WHEN** the Vue browser profile executes
+- **THEN** it covers direct links, refresh, back and forward, exact custom-page precedence, deliberate generic fallback, menus, property and action interactions, object and collection results, partial errors, invalid routes, absent objects, responsive layout, keyboard focus, announcements, and both supported component toolkit policies
+- **AND** browser console errors, page errors, failed resource requests, and accessibility violations fail the suite
diff --git a/openspec/changes/reconcile-vue-petclinic-with-htmx/tasks.md b/openspec/changes/reconcile-vue-petclinic-with-htmx/tasks.md
new file mode 100644
index 00000000000..6c6dbc1c242
--- /dev/null
+++ b/openspec/changes/reconcile-vue-petclinic-with-htmx/tasks.md
@@ -0,0 +1,45 @@
+## 1. Freeze the HTMX presentation baseline
+
+- [ ] 1.1 Record a route-by-route parity matrix for the HTMX shell and HomePage, PetOwner, Pet, and Visit pages, including ordered landmarks, headings, selected members, actions, result outlets, collection declarations, paging values, and preview affordances.
+- [ ] 1.2 Add or extend headless assertions that protect the authoritative HTMX semantic composition without changing its rendered presentation.
+- [ ] 1.3 Record tolerant wide- and narrow-viewport invariants for header geometry, content inset, column relationships, breakpoint collapse, footer placement, palette, typography, focus treatment, and horizontal overflow.
+- [ ] 1.4 Diagnose why the Vue application currently reverses primary menus and exposes Logout, User, and Configuration separately from the HTMX System grouping, identifying the supported projection or application-configuration correction before changing code.
+- [ ] 1.5 Select an existing suitable unregistered logical type for generic-fallback acceptance, or specify a bounded menu-excluded acceptance fixture if none exists.
+
+## 2. Reconcile the Vue application shell
+
+- [ ] 2.1 Refactor `App.vue` to mirror the HTMX shell landmarks, branding, loading and announcement regions, shell result outlet, main-region semantics, and footer content while retaining Vue Router bindings and one stable GraphQL client.
+- [ ] 2.2 Reconcile menu order and utility grouping through supported semantic menu projection and application configuration, without hardcoded replacement actions or payload rewriting.
+- [ ] 2.3 Rework `petclinic.css` to use the HTMX Petclinic class vocabulary, foundation design tokens, wide shell geometry, responsive breakpoints, typography, palette, content spacing, and footer alignment.
+- [ ] 2.4 Reconcile document titles with the HTMX `· Pet Clinic` convention and replace the persistent route-container border with an accessible keyboard focus treatment.
+- [ ] 2.5 Add headless shell tests for wide and narrow layouts, menu keyboard operation, focus, reduced motion, overflow, title, footer, and stable provider and route boundaries.
+
+## 3. Reconcile the PetOwner page
+
+- [ ] 3.1 Restructure `PetOwnerPage.vue` into the HTMX object heading, details column, and collections column with separate Identity, Contact, Details, Pets, and Visits sections in reference order.
+- [ ] 3.2 Restore the `noOwners` action, complete related-owner standalone result declaration, `daysSinceLastVisit`, member descriptions, label positions, and action parameter presentation from the HTMX page.
+- [ ] 3.3 Reconcile Pets and Visits collection columns, page sizes, filtering, sorting, actions, prompt styles, parameters, row previews, and nested preview collections with the HTMX declarations.
+- [ ] 3.4 Extend Vue acceptance tests to verify PetOwner semantic parity, column containment at wide width, single-column order at narrow width, action results, property edits, collection operations, previews, and both toolkit policies.
+
+## 4. Add the remaining exact-type Vue pages
+
+- [ ] 4.1 Implement and register `HomePage.vue` with the HTMX object heading, two collection sections, declared columns, filters, sorting, page sizes, and row previews.
+- [ ] 4.2 Implement and register `PetPage.vue` with the HTMX breadcrumb, heading, Identity and Details grouping, selected properties, editability, and absence of technical or intentionally omitted members.
+- [ ] 4.3 Implement and register `VisitPage.vue` with the HTMX breadcrumb, heading, Appointment and Details grouping, temporal presentation, selected properties, editability, and absence of technical members.
+- [ ] 4.4 Add direct-link, refresh, history, semantic-structure, responsive-layout, and interaction coverage for each new exact-type page.
+
+## 5. Preserve deliberate generic fallback
+
+- [ ] 5.1 Add the bounded shared-domain acceptance fixture selected in task 1.5 only if no existing logical type can safely demonstrate fallback, preserving Petclinic menu contents and established seed cardinalities.
+- [ ] 5.2 Move generic-page direct-link, refresh, and browser-history coverage from Pet or Visit to the deliberately unregistered acceptance type.
+- [ ] 5.3 Verify exact-type precedence for all four reconciled Petclinic routes and generic `<cw-object>` rendering for the fallback fixture.
+
+## 6. Regression, generated assets, and documentation
+
+- [ ] 6.1 Add tolerant computed-style and bounding-box assertions for the agreed shell and route invariants rather than pixel-perfect screenshot comparisons.
+- [ ] 6.2 Verify invalid routes, absent objects, partial errors, stale-generation rejection, announcements, semantic navigation and results, and provider lifecycle still behave as before.
+- [ ] 6.3 Run the Vue package unit, declaration, build, pack, and clean-consumer checks and resolve any integration regression without changing public APIs unnecessarily.
+- [ ] 6.4 Regenerate and verify committed Vue production assets and confirm the stale-generated-output check passes.
+- [ ] 6.5 Run Vue and HTMX integration and headless Playwright suites, including native and Vaadin policies and representative wide and narrow viewports.
+- [ ] 6.6 Update the Vue Petclinic and web-components documentation to identify HTMX as the presentation reference and explain where generic fallback is demonstrated.
+- [ ] 6.7 Run relevant Maven verification, frontend tests, script syntax checks, Apache RAT, IDE inspections/build, and whitespace checks.


(causeway) 08/16: docs(openspec): plan web component logout and local redirects

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 39199d115c7dc8fcc56992b00643086997b21cb5
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 3 12:58:08 2026 +0100

    docs(openspec): plan web component logout and local redirects
---
 .../.openspec.yaml                                 |   2 +
 .../design.md                                      | 104 +++++++++++++++++++++
 .../proposal.md                                    |  33 +++++++
 .../specs/domain-web-components/spec.md            |  55 +++++++++++
 .../generic-htmx-web-component-viewer/spec.md      |  60 ++++++++++++
 .../specs/generic-vue-web-component-viewer/spec.md |  84 +++++++++++++++++
 .../tasks.md                                       |  44 +++++++++
 7 files changed, 382 insertions(+)

diff --git a/openspec/changes/support-web-component-logout-and-local-redirects/.openspec.yaml b/openspec/changes/support-web-component-logout-and-local-redirects/.openspec.yaml
new file mode 100644
index 00000000000..9696e00f11e
--- /dev/null
+++ b/openspec/changes/support-web-component-logout-and-local-redirects/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-09-03
diff --git a/openspec/changes/support-web-component-logout-and-local-redirects/design.md b/openspec/changes/support-web-component-logout-and-local-redirects/design.md
new file mode 100644
index 00000000000..19a672d7574
--- /dev/null
+++ b/openspec/changes/support-web-component-logout-and-local-redirects/design.md
@@ -0,0 +1,104 @@
+## Context
+
+The shared action components publish a cancelable `causeway-action-request` before dispatch and a `causeway-action-result` after successful invocation.
+HTMX and Vue both invoke service actions through the same foundation contexts, so the framework Logout menu currently reaches GraphQL in both viewers.
+The invocation fails before either host receives a redirect, because `LogoutMenu.logout()` has the polymorphic Java return type `Object`, GraphQL advertises its result as `String`, and execution supplies no managed result for the `results` fetcher.
+
+The existing authenticated HTMX SecMan integration already demonstrates the correct security boundary.
+It removes the exact framework Logout action, blocks stale action requests, and submits a host-owned POST logout form with current CSRF evidence.
+The generic HTMX runtime applies that exclusion only when authentication metadata is present, and Vue has no equivalent authentication integration.
+
+`LocalResourcePath` is independently documented as a browser/client redirect value.
+GraphQL preserves its `path` and `openUrlStrategy` in `LocalResourcePathValue`, but foundation normalization currently classifies every structured non-collection result as an object and the host bridges do not recognize this value type.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Ensure generic HTMX and Vue never invoke the exact framework Logout action through GraphQL unless a host has explicitly claimed a safe authentication operation.
+- Hide or disable the unusable framework Logout menu affordance when no host logout capability is installed.
+- Preserve the existing HTMX SecMan CSRF-protected logout flow.
+- Provide an explicit pre-invocation host policy boundary suitable for a future Vue authentication integration.
+- Preserve typed `LocalResourcePathValue` as a distinct semantic action result.
+- Perform validated same-origin local-resource navigation according to `OpenUrlStrategy`.
+- Keep action identity, result data, and host navigation policy explicit and testable.
+
+**Non-Goals:**
+
+- Do not change Wicket logout behavior, core `LogoutMenu`, Spring Security, SecMan, or OAuth implementations.
+- Do not make GET `/logout` a universal authentication endpoint.
+- Do not infer logout semantics from a path string or from an action label.
+- Do not hide arbitrary application actions named Logout or all actions returning `LocalResourcePath`.
+- Do not use Vue Router or HTMX fragment navigation for application-local resource paths.
+- Do not add a Vue security implementation in this change.
+
+## Decisions
+
+### Use exact action identity for the reserved Logout operation
+
+The reserved operation is identified only by service logical type `causeway.security.LogoutMenu` and action id `logout`.
+A shared identity helper will prevent HTMX and Vue policies from drifting, while menu filtering and request interception remain host responsibilities.
+
+Filtering by return type is rejected because Logout is declared as `Object`, its action metadata has no result logical type, and its GraphQL result is advertised as `String`.
+Filtering every `LocalResourcePath` action is also rejected because those actions can represent legitimate application-local navigation.
+
+### Fail closed before GraphQL invocation
+
+An unclaimed framework Logout action will be absent from ordinary menu controls and any stale or custom `causeway-action-request` for that identity will be canceled before dispatch.
+The action will not be invoked and no synthetic success result will be shown.
+A bounded announcement or diagnostic may explain that logout requires a host authentication integration without disclosing session details.
+
+Post-result error suppression is rejected because Logout has already crossed the wrong security boundary and can mutate authentication state before a result is available.
+
+### Extend the existing host policy boundary rather than adding security to components
+
+Foundation action controls will continue to publish canonical service/action identity through the cancelable request event.
+The Vue viewer will expose a pre-invocation action policy alongside its navigation and result policies.
+The HTMX host policy will expose equivalent claim handling while retaining its existing authentication-metadata interception.
+A host claim means that the host owns the operation and MUST prevent ordinary GraphQL dispatch before performing its own behavior.
+
+Foundation components will not know about CSRF, sessions, login pages, or security providers.
+Authenticated hosts remain responsible for endpoint selection, HTTP method, current anti-forgery evidence, session cleanup, and post-logout navigation.
+
+### Keep secure Logout distinct from local-resource navigation
+
+A path equal to `/logout` has no intrinsic authentication meaning.
+Hosts MUST NOT reinterpret a `LocalResourcePath` as logout merely because its text matches that path.
+Secure logout commonly requires POST and CSRF evidence, and its effective endpoint can include an application context and viewer base path.
+
+This deliberately rejects changing `LogoutMenu` to return a universal GET redirect.
+
+### Introduce a distinct local-resource semantic result
+
+Action-result normalization will recognize the exact GraphQL output type `LocalResourcePathValue` before applying generic structured-object classification.
+The immutable semantic result will retain a distinct kind and a bounded value containing `path` and `openUrlStrategy`.
+Object and service action contexts will share the normalization logic so their behavior cannot diverge.
+Malformed values will fail closed as unsupported results rather than falling through to object navigation or string presentation.
+
+### Resolve and validate navigation at the host boundary
+
+Each host will resolve the supplied path against an application-local base under its current origin.
+HTMX can receive the servlet context path from its server-rendered document contract.
+Vue will accept an application-local resource base or resolver through viewer options, with a same-origin default suitable for root deployments.
+
+The resolved URL MUST retain the current origin and MUST reject scheme-relative, cross-origin, credential-bearing, malformed, or otherwise unsupported targets.
+`SAME_WINDOW` uses full-document navigation rather than the object router.
+`NEW_WINDOW` uses a new browsing context with opener isolation, subject to browser popup policy.
+Unknown strategies fail closed.
+
+A host policy may claim a local-resource result to apply stricter deployment policy, but cannot alter the canonical action result.
+
+### Test Logout and local-resource behavior independently
+
+Logout acceptance will assert that the framework menu action is absent when unclaimed and that a synthetic stale request produces no GraphQL invocation.
+The existing secured HTMX suite will continue proving CSRF-protected host logout.
+Local-resource fixtures will use actions explicitly declared as `LocalResourcePath`, not the framework Logout action, and will verify same-window, new-window, context-path, malformed-target, and cross-origin behavior.
+
+## Risks / Trade-offs
+
+- **Risk: Suppressing Logout removes the only apparent sign-out affordance in an application without authentication integration.** → Such an affordance is currently broken and potentially unsafe; documentation will require authenticated hosts to supply an owned logout control.
+- **Risk: Exact framework identity is a deliberate coupling.** → Centralize it as a reviewed compatibility constant and test that similarly named application actions remain unaffected.
+- **Risk: Vue applications can be deployed beneath context paths unknown to a static bundle.** → Make the local-resource base explicit in viewer configuration and test root and nested deployments.
+- **Risk: Browsers can block asynchronous `NEW_WINDOW` navigation.** → Document browser policy, preserve a host claim hook, and verify that the default uses opener isolation without promising popup bypass.
+- **Risk: General result normalization changes both object and service actions.** → Centralize classification and retain regression coverage for scalar, object, collection, void, Blob, Clob, and unsupported structured results.
+- **Risk: A permissive path validator could become an open redirect.** → Resolve with the URL API, require the current origin, reject credentials and scheme-relative input, and never derive logout semantics from the target.
diff --git a/openspec/changes/support-web-component-logout-and-local-redirects/proposal.md b/openspec/changes/support-web-component-logout-and-local-redirects/proposal.md
new file mode 100644
index 00000000000..96111254ea9
--- /dev/null
+++ b/openspec/changes/support-web-component-logout-and-local-redirects/proposal.md
@@ -0,0 +1,33 @@
+## Why
+
+The framework Logout service is a viewer-specific abstraction that fails when HTMX or Vue invokes it through GraphQL, leaving both generic viewers with a visible action that produces a server-side error instead of ending a browser session.
+`LocalResourcePath` is already a public redirect value and has a structured GraphQL representation, but the web-component result contract and host viewers do not yet interpret it as safe browser navigation.
+
+## What Changes
+
+- Treat the exact framework `causeway.security.LogoutMenu#logout` action as a host-owned authentication operation rather than an ordinary GraphQL domain action.
+- Suppress that framework action by default in generic HTMX and Vue menu presentation unless an installed host authentication integration explicitly claims it.
+- Prevent stale or custom requests for the framework Logout action from reaching GraphQL when no safe host logout handler is available.
+- Preserve the existing authenticated HTMX SecMan behavior, which uses a CSRF-protected POST logout control outside domain invocation.
+- Add a bounded host interception contract so future Vue or other authentication integrations can claim Logout without coupling foundation components to Spring Security, SecMan, Wicket, or OAuth.
+- Classify typed `LocalResourcePathValue` action results distinctly from navigable domain objects and other structured values.
+- Interpret valid same-origin local-resource results using `SAME_WINDOW` or `NEW_WINDOW` behavior while rejecting malformed, cross-origin, or unsupported targets without navigation.
+- Keep logout independent from `LocalResourcePath` result handling because secure logout can require POST, current CSRF evidence, a configured viewer/context path, and host-specific session cleanup.
+
+## Capabilities
+
+### New Capabilities
+
+_None._
+
+### Modified Capabilities
+
+- `domain-web-components`: Add semantic classification for local-resource action results and a host-claimable pre-invocation boundary for reserved host operations.
+- `generic-htmx-web-component-viewer`: Suppress unsafe framework Logout invocation by default and perform bounded local-resource navigation.
+- `generic-vue-web-component-viewer`: Suppress unsafe framework Logout invocation by default, expose host policy interception, and perform bounded local-resource navigation.
+
+## Impact
+
+The change affects action-request dispatch, menu projection, semantic action-result normalization, HTMX result handling, Vue host policies, and browser acceptance coverage under `viewers/webcomponents`.
+It reuses the existing GraphQL `LocalResourcePathValue` shape and existing authenticated HTMX logout form rather than changing core security, the Logout service, GraphQL schema authority, or Wicket behavior.
+Applications with similarly named logout actions remain unaffected because matching uses the exact framework service and action identity.
diff --git a/openspec/changes/support-web-component-logout-and-local-redirects/specs/domain-web-components/spec.md b/openspec/changes/support-web-component-logout-and-local-redirects/specs/domain-web-components/spec.md
new file mode 100644
index 00000000000..90aa767e56d
--- /dev/null
+++ b/openspec/changes/support-web-component-logout-and-local-redirects/specs/domain-web-components/spec.md
@@ -0,0 +1,55 @@
+## ADDED Requirements
+
+### Requirement: Host-owned action interception boundary
+
+The foundation SHALL publish canonical target identity before any service or object action invocation and SHALL allow an enclosing host to claim and cancel the request synchronously.
+A claimed or canceled request MUST NOT execute GraphQL validation or invocation and MUST NOT emit a synthetic successful action result.
+The exact framework Logout operation SHALL be identified by service logical type `causeway.security.LogoutMenu` and action id `logout`, not by its label, result path, or a partial identity match.
+
+#### Scenario: Host claims an action request
+
+- **WHEN** a host claims or cancels a published action request using its canonical target and action identity
+- **THEN** the component does not validate or invoke that action through GraphQL
+- **AND** the host remains responsible for any replacement behavior, announcement, and navigation
+
+#### Scenario: Framework Logout is requested
+
+- **WHEN** a component publishes an action request for `causeway.security.LogoutMenu#logout`
+- **THEN** the request contains sufficient exact identity for the host to recognize the reserved authentication operation before invocation
+- **AND** no path string or post-invocation result is required to recognize it
+
+#### Scenario: Application action has a logout-like name
+
+- **WHEN** an application action is labeled Logout or returns `/logout` but does not have the exact framework service and action identity
+- **THEN** the foundation does not classify it as the reserved framework Logout operation
+- **AND** ordinary host and domain-action policy continues to apply
+
+### Requirement: Local resource action-result semantics
+
+The foundation SHALL classify an action result of exact GraphQL output type `LocalResourcePathValue` as a distinct local-resource semantic result before applying generic structured-object classification.
+The result SHALL preserve only the authoritative `path` and `openUrlStrategy` fields and MUST NOT be treated as a navigable domain object, ordinary scalar, or authentication instruction.
+Object and service action contexts SHALL apply the same classification contract.
+
+#### Scenario: Typed local resource result is returned
+
+- **WHEN** a successfully invoked action has exact output type `LocalResourcePathValue` and returns valid path and strategy fields
+- **THEN** the emitted action result has the local-resource kind with those fields preserved
+- **AND** the result does not acquire object identity or route semantics
+
+#### Scenario: Structured non-resource result is returned
+
+- **WHEN** a structured result has a different GraphQL output type
+- **THEN** it retains the applicable existing object or unsupported-result behavior
+- **AND** field names resembling `path` or `openUrlStrategy` do not opt it into local-resource semantics
+
+#### Scenario: Local resource result is malformed
+
+- **WHEN** a `LocalResourcePathValue` result lacks a non-empty path or supported opening strategy
+- **THEN** the result fails closed with a bounded unsupported-result diagnostic
+- **AND** no navigation request is emitted
+
+#### Scenario: Result kinds are regression tested
+
+- **WHEN** foundation result-normalization tests execute
+- **THEN** local-resource, scalar, object, collection, void, and other structured result classifications remain deterministic
+- **AND** service and object action execution produce equivalent classifications
diff --git a/openspec/changes/support-web-component-logout-and-local-redirects/specs/generic-htmx-web-component-viewer/spec.md b/openspec/changes/support-web-component-logout-and-local-redirects/specs/generic-htmx-web-component-viewer/spec.md
new file mode 100644
index 00000000000..2c593fdf2e7
--- /dev/null
+++ b/openspec/changes/support-web-component-logout-and-local-redirects/specs/generic-htmx-web-component-viewer/spec.md
@@ -0,0 +1,60 @@
+## ADDED Requirements
+
+### Requirement: Generic HTMX framework Logout safety
+
+The generic HTMX viewer SHALL treat `causeway.security.LogoutMenu#logout` as a host-owned authentication operation and MUST NOT invoke it through GraphQL by default.
+When no authentication integration claims that operation, the viewer SHALL omit its ordinary menu affordance and SHALL cancel stale or custom action requests for the same exact identity.
+An installed authentication integration SHALL remain responsible for presenting an accessible logout control, submitting the configured method and current CSRF evidence, clearing the session, and selecting the post-logout destination.
+
+#### Scenario: Generic HTMX viewer has no authentication integration
+
+- **WHEN** application menus contain the framework Logout action but the HTMX host has no registered logout capability
+- **THEN** the action is absent from ordinary semantic menu controls
+- **AND** the viewer does not imply that a browser session can be ended
+
+#### Scenario: Stale Logout request is published
+
+- **WHEN** stale or custom markup publishes an action request for the exact framework Logout identity without a host logout claim
+- **THEN** the HTMX viewer cancels the request before GraphQL validation or invocation
+- **AND** it emits a bounded unavailable-operation announcement or diagnostic without a successful result
+
+#### Scenario: Authenticated HTMX integration is active
+
+- **WHEN** an installed HTMX authentication integration claims Logout and supplies its host-owned control
+- **THEN** the existing protected logout flow remains available outside domain action invocation
+- **AND** its endpoint, HTTP method, CSRF, session, cookie, and redirect policies remain authoritative
+
+#### Scenario: Similar application action is present
+
+- **WHEN** another service exposes a similarly named action or a local-resource result containing `/logout`
+- **THEN** the HTMX viewer does not suppress or reinterpret it as framework Logout
+- **AND** normal action and local-resource policies apply
+
+### Requirement: HTMX local-resource result navigation
+
+The HTMX viewer SHALL resolve a local-resource semantic result against the current application context and SHALL perform full-document navigation according to its supported opening strategy.
+It MUST validate the resolved target as same-origin and application-local before navigation and MUST NOT interpret any target path as an authentication operation.
+
+#### Scenario: Same-window local resource is returned
+
+- **WHEN** an action returns a valid local-resource result with `SAME_WINDOW`
+- **THEN** the HTMX host navigates the current browsing context to the context-aware local target
+- **AND** it does not issue an HTMX fragment request or canonical object-route transition
+
+#### Scenario: New-window local resource is returned
+
+- **WHEN** an action returns a valid local-resource result with `NEW_WINDOW`
+- **THEN** the HTMX host requests a new opener-isolated browsing context for the local target
+- **AND** the current object route remains unchanged
+
+#### Scenario: Local resource target is unsafe
+
+- **WHEN** a result path is malformed, scheme-relative, cross-origin, credential-bearing, outside the configured application-local boundary, or paired with an unknown strategy
+- **THEN** the HTMX host refuses navigation and reports a bounded result-policy error
+- **AND** no target value is repaired, rewritten to another origin, or treated as Logout
+
+#### Scenario: Application uses a servlet context path
+
+- **WHEN** the HTMX application is deployed beneath a non-root servlet context and returns an application-local resource path
+- **THEN** resolution preserves the authoritative servlet context exactly once
+- **AND** the viewer does not confuse the HTMX route base with the application context
diff --git a/openspec/changes/support-web-component-logout-and-local-redirects/specs/generic-vue-web-component-viewer/spec.md b/openspec/changes/support-web-component-logout-and-local-redirects/specs/generic-vue-web-component-viewer/spec.md
new file mode 100644
index 00000000000..1ed85de29ed
--- /dev/null
+++ b/openspec/changes/support-web-component-logout-and-local-redirects/specs/generic-vue-web-component-viewer/spec.md
@@ -0,0 +1,84 @@
+## ADDED Requirements
+
+### Requirement: Vue host-owned action policy
+
+The Vue viewer SHALL expose a pre-invocation action policy that receives canonical action identity, a single-claim token, and the current immutable viewer-policy context.
+A claimed action MUST NOT proceed to foundation GraphQL validation or invocation, and asynchronous policy failure SHALL reach the configured viewer error policy without creating a successful action result.
+The policy SHALL compose with application-owned routing and lifecycle disposal without introducing authentication state into Vue route components.
+
+#### Scenario: Vue action policy claims a request
+
+- **WHEN** the configured Vue action policy claims a service or object action request
+- **THEN** ordinary GraphQL dispatch is canceled before invocation
+- **AND** the application policy owns any replacement effect
+
+#### Scenario: Vue action policy does not claim a request
+
+- **WHEN** the configured action policy returns without claiming an ordinary action
+- **THEN** existing parameter, validation, confirmation, invocation, result, refresh, and navigation behavior continues
+- **AND** the action is not duplicated
+
+#### Scenario: Vue action policy fails
+
+- **WHEN** a synchronous or asynchronous action policy throws or rejects
+- **THEN** the viewer reports the error through its configured error policy
+- **AND** fail-closed actions such as framework Logout do not fall through to GraphQL invocation
+
+### Requirement: Generic Vue framework Logout safety
+
+The generic Vue viewer SHALL classify `causeway.security.LogoutMenu#logout` as a host-owned authentication operation and MUST NOT invoke it through GraphQL by default.
+When no application authentication policy claims that operation, the viewer SHALL omit its ordinary menu affordance and SHALL cancel stale or custom requests for the exact identity.
+A future authentication integration SHALL own endpoint selection, HTTP method, current anti-forgery evidence, session cleanup, and post-logout navigation.
+
+#### Scenario: Vue application has no authentication integration
+
+- **WHEN** application menus contain the framework Logout action and no Vue host logout capability is configured
+- **THEN** the action is absent from ordinary semantic menu controls
+- **AND** direct or stale requests cannot reach GraphQL invocation
+
+#### Scenario: Vue application supplies a logout policy
+
+- **WHEN** an application explicitly registers a policy that claims the framework Logout identity
+- **THEN** the viewer delegates the operation before GraphQL dispatch
+- **AND** the application remains responsible for a secure accessible logout affordance and complete browser-session behavior
+
+#### Scenario: Similar application action is present
+
+- **WHEN** another service exposes a similarly named action or a local-resource result containing `/logout`
+- **THEN** the Vue viewer does not suppress or reinterpret it as framework Logout
+- **AND** ordinary action or local-resource policy applies
+
+### Requirement: Vue local-resource result navigation
+
+The Vue viewer SHALL interpret a local-resource semantic result through a bounded host policy and a documented application-local resource base.
+Its default behavior SHALL perform validated full-document same-origin navigation according to `SAME_WINDOW` or `NEW_WINDOW` and MUST NOT send the target through Vue Router.
+
+#### Scenario: Same-window local resource is returned
+
+- **WHEN** an action returns a valid local-resource result with `SAME_WINDOW`
+- **THEN** the Vue host performs full-document navigation to the resolved application-local target
+- **AND** the canonical object router does not claim the target
+
+#### Scenario: New-window local resource is returned
+
+- **WHEN** an action returns a valid local-resource result with `NEW_WINDOW`
+- **THEN** the Vue host requests a new opener-isolated browsing context for the resolved target
+- **AND** the current route generation remains active unless ordinary lifecycle policy later changes it
+
+#### Scenario: Application claims local-resource navigation
+
+- **WHEN** the configured result policy claims a valid local-resource result
+- **THEN** default browser navigation does not run
+- **AND** the application can apply stricter deployment policy without mutating the canonical result
+
+#### Scenario: Local resource target is unsafe
+
+- **WHEN** a path is malformed, scheme-relative, cross-origin, credential-bearing, outside the configured application-local boundary, or paired with an unknown strategy
+- **THEN** the Vue viewer refuses navigation and reports the failure through its error policy
+- **AND** it does not repair the value, route it as a domain object, or infer Logout semantics
+
+#### Scenario: Vue application uses a nested deployment context
+
+- **WHEN** the application configures a non-root local-resource base and receives an application-local path
+- **THEN** resolution preserves that deployment context exactly once
+- **AND** it remains independent of the Vue object-route base path
diff --git a/openspec/changes/support-web-component-logout-and-local-redirects/tasks.md b/openspec/changes/support-web-component-logout-and-local-redirects/tasks.md
new file mode 100644
index 00000000000..cd3626c4188
--- /dev/null
+++ b/openspec/changes/support-web-component-logout-and-local-redirects/tasks.md
@@ -0,0 +1,44 @@
+## 1. Reproduce and Bound the Contracts
+
+- [ ] 1.1 Add focused browser or integration coverage that reproduces the current HTMX and Vue framework Logout GraphQL failure before changing dispatch policy.
+- [ ] 1.2 Record the exact `causeway.security.LogoutMenu#logout` identity, cancelable action-request contract, `LocalResourcePathValue` GraphQL shape, and existing authenticated HTMX exclusion as compatibility baselines.
+- [ ] 1.3 Add negative fixtures for a similarly named application action and a non-logout local-resource path so exact matching remains testable.
+
+## 2. Foundation Action and Result Semantics
+
+- [ ] 2.1 Centralize the exact reserved framework Logout action identity and helper without importing core security into browser components.
+- [ ] 2.2 Verify and, where necessary, strengthen action controls so a synchronously claimed or canceled `causeway-action-request` performs no validation, invocation, or synthetic result emission.
+- [ ] 2.3 Centralize object and service action-result normalization and classify exact `LocalResourcePathValue` output as an immutable local-resource semantic result.
+- [ ] 2.4 Fail closed for malformed local-resource payloads while preserving existing scalar, object, collection, void, Blob, Clob, and unsupported structured-result behavior.
+- [ ] 2.5 Add foundation unit and integration tests for exact Logout matching, pre-invocation cancellation, result classification, and nonmatching actions.
+
+## 3. Safe Local-Resource Navigation
+
+- [ ] 3.1 Implement a reusable local-resource target resolver that accepts an explicit application-local base and rejects malformed, scheme-relative, cross-origin, credential-bearing, escaping, or unsupported targets.
+- [ ] 3.2 Implement `SAME_WINDOW` full-document navigation and opener-isolated `NEW_WINDOW` behavior without passing resource targets to an object router.
+- [ ] 3.3 Preserve host policy claims and bounded error reporting so applications can replace or further restrict default navigation without mutating canonical results.
+- [ ] 3.4 Add unit coverage for root and nested deployment bases, context preservation exactly once, both opening strategies, popup-policy behavior, and unsafe-target rejection.
+
+## 4. HTMX Host Policy
+
+- [ ] 4.1 Remove the unclaimed framework Logout action from generic HTMX semantic menu controls while leaving similarly named application actions untouched.
+- [ ] 4.2 Add a fail-closed HTMX action-request guard that blocks stale or custom framework Logout requests before GraphQL and composes with the existing authentication metadata handler.
+- [ ] 4.3 Extend the HTMX document/runtime contract with an authoritative servlet-context local-resource base and handle local-resource semantic results through the shared resolver.
+- [ ] 4.4 Verify that authenticated HTMX SecMan continues to use its accessible CSRF-protected POST logout control and that generic HTMX adds no security endpoint or session behavior.
+- [ ] 4.5 Add HTMX unit, controller, integration, and headless browser coverage for default Logout suppression, stale-request blocking, claimed authenticated logout, local redirects, nested contexts, and unsafe targets.
+
+## 5. Vue Host Policy
+
+- [ ] 5.1 Add a typed pre-invocation Vue action policy with canonical detail, single-claim semantics, immutable policy context, disposal safety, and error-policy integration.
+- [ ] 5.2 Remove the unclaimed framework Logout action from Vue semantic menu controls and block direct or stale requests before GraphQL while preserving nonmatching actions.
+- [ ] 5.3 Add a documented Vue application-local resource base or resolver option independent of the Vue object-route base.
+- [ ] 5.4 Handle local-resource semantic results through host result claims and the shared safe-navigation contract.
+- [ ] 5.5 Add Vue unit, declaration, package-consumer, and headless browser coverage for action claims, default Logout suppression, no GraphQL invocation, local redirects, nested deployment, unsafe targets, and route lifecycle stability.
+
+## 6. Samples, Documentation, and Verification
+
+- [ ] 6.1 Add or reuse bounded typed `LocalResourcePath` acceptance actions without placing a misleading framework Logout action in an unauthenticated sample menu.
+- [ ] 6.2 Update foundation, HTMX, Vue, and security documentation to distinguish host-owned secure logout from ordinary local-resource navigation and explain deployment-base configuration.
+- [ ] 6.3 Regenerate and verify committed frontend assets and confirm stale-generated-output checks pass.
+- [ ] 6.4 Run foundation and viewer frontend tests, Vue declaration/build/pack verification, relevant Maven integration suites, and HTMX and Vue headless Playwright suites.
+- [ ] 6.5 Run secured HTMX logout regression coverage, script syntax checks, Apache RAT, IDE inspections/build, strict OpenSpec validation, and whitespace checks.


(causeway) 13/16: feat(webcomponents): add secured Vue Petclinic variant

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit b2ad141a494978b4cd67196453f833a794487f00
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 3 17:23:42 2026 +0100

    feat(webcomponents): add secured Vue Petclinic variant
---
 .../add-secured-vue-petclinic-variant/tasks.md     |  64 +++---
 viewers/webcomponents/README.adoc                  |  18 +-
 .../webcomponents/htmx-security-secman/README.adoc |  11 +-
 viewers/webcomponents/htmx-security-secman/pom.xml |   9 +-
 ...oduleViewerWebcomponentsHtmxSecuritySecman.java |   6 +-
 .../secman/HtmxSecmanSecurityConfiguration.java    |  28 +--
 .../secman/HtmxSecmanHttpSecurityTest.java         |   6 +-
 viewers/webcomponents/pom.xml                      |  13 ++
 .../sample-htmx-petclinic/README.adoc              |   5 +-
 .../run-secured.sh}                                |  22 +--
 .../webcomponents/sample-vue-petclinic/README.adoc |  26 ++-
 .../sample-vue-petclinic/frontend/index.html       |   1 +
 .../sample-vue-petclinic/frontend/src/App.vue      |  18 +-
 .../frontend/src/authentication.ts                 | 106 ++++++++++
 .../sample-vue-petclinic/frontend/src/main.ts      |  15 +-
 .../frontend/src/petclinic.css                     |  27 +++
 viewers/webcomponents/sample-vue-petclinic/pom.xml |  62 ++++++
 .../{test.sh => run-secured.sh}                    |  22 +--
 .../resources/static/vue/assets/index-3mHRS6NB.js  |   2 -
 .../static/vue/assets/index-3mHRS6NB.js.map        |   1 -
 .../resources/static/vue/assets/index-Bs2eP4Py.js  |   2 +
 .../static/vue/assets/index-Bs2eP4Py.js.map        |   1 +
 .../resources/static/vue/assets/index-CqcjgOE7.css |   1 -
 .../resources/static/vue/assets/index-DAyo-XFi.css |   1 +
 .../src/main/resources/static/vue/index.html       |   5 +-
 .../PetClinicVueSecuredPlaywrightAcceptance.java   | 217 +++++++++++++++++++++
 .../PetClinicVueSecuredApplication_IntegTest.java  | 198 +++++++++++++++++++
 .../PetClinicSecmanDataConfiguration.java          |  93 +++++++++
 .../PetClinicVueSecuredApplication.java            |  62 ++++++
 .../PetClinicVueSecuredRouteController.java        |  69 +++++++
 .../resources/secured-application.properties       |  23 +++
 viewers/webcomponents/sample-vue-petclinic/test.sh |   9 +-
 viewers/webcomponents/security-secman/README.adoc  |  44 +++++
 .../pom.xml                                        |  32 +--
 ...AuthenticationConverterOfSecmanUserDetails.java |   2 +-
 ...ayModuleViewerWebcomponentsSecuritySecman.java} |  10 +-
 .../SecmanSpringAuthenticationConfiguration.java   |  56 ++++++
 .../security/secman/SecmanUserDetails.java         |   2 +-
 .../security/secman/SecmanUserDetailsService.java  |   2 +-
 .../secman/SecmanAuthenticationBridgeTest.java     |  34 +++-
 .../webcomponents/vue-security-secman/README.adoc  | 127 ++++++++++++
 .../pom.xml                                        |  35 ++--
 ...oduleViewerWebcomponentsVueSecuritySecman.java} |  16 +-
 .../VueSecmanAuthenticationContextFilter.java      |  95 +++++++++
 .../secman/VueSecmanAuthenticationEntryPoint.java  |  55 ++++++
 .../VueSecmanAuthenticationFailureHandler.java     |  52 +++++
 .../VueSecmanAuthenticationSuccessHandler.java     |  74 +++++++
 .../security/secman/VueSecmanLoginPageFilter.java  | 153 +++++++++++++++
 .../vue/security/secman/VueSecmanPaths.java        |  89 +++++++++
 .../secman/VueSecmanSecurityConfiguration.java}    | 116 ++++-------
 .../secman/VueSecmanSecurityProperties.java        | 120 ++++++++++++
 .../secman/VueSecmanSecurityValidator.java         |  67 +++++++
 .../secman/VueSecmanHttpSecurityTest.java}         |  88 +++++----
 .../secman/VueSecmanSecurityFoundationTest.java    | 101 ++++++++++
 viewers/webcomponents/vue/README.adoc              |  11 ++
 55 files changed, 2202 insertions(+), 322 deletions(-)

diff --git a/openspec/changes/add-secured-vue-petclinic-variant/tasks.md b/openspec/changes/add-secured-vue-petclinic-variant/tasks.md
index 36c2cafd108..81da0d265af 100644
--- a/openspec/changes/add-secured-vue-petclinic-variant/tasks.md
+++ b/openspec/changes/add-secured-vue-petclinic-variant/tasks.md
@@ -1,49 +1,49 @@
 ## 1. Shared SecMan Spring Bridge
 
-- [ ] 1.1 Add the optional presentation-neutral `security-secman` module to the web-components reactor with bounded dependencies and module metadata.
-- [ ] 1.2 Move the SecMan principal, controlled user-details lookup, Causeway authentication converter, and DAO provider wiring into the shared module.
-- [ ] 1.3 Add focused tests for valid, absent, locked, passwordless, failed-lookup, role, tenancy, locale, serialization, and credential-erasure behavior.
-- [ ] 1.4 Refactor `htmx-security-secman` to consume the shared bridge without changing HTMX-specific route or shell policy.
-- [ ] 1.5 Run existing secured HTMX integration and headless browser regressions against the extracted bridge.
+- [x] 1.1 Add the optional presentation-neutral `security-secman` module to the web-components reactor with bounded dependencies and module metadata.
+- [x] 1.2 Move the SecMan principal, controlled user-details lookup, Causeway authentication converter, and DAO provider wiring into the shared module.
+- [x] 1.3 Add focused tests for valid, absent, locked, passwordless, failed-lookup, role, tenancy, locale, serialization, and credential-erasure behavior.
+- [x] 1.4 Refactor `htmx-security-secman` to consume the shared bridge without changing HTMX-specific route or shell policy.
+- [x] 1.5 Run existing secured HTMX integration and headless browser regressions against the extracted bridge.
 
 ## 2. Vue SecMan Server Integration
 
-- [ ] 2.1 Add the optional `vue-security-secman` module, bounded configuration properties, module import, and startup validation.
-- [ ] 2.2 Implement context-aware Vue, login, logout, authentication-context, GraphQL, asset, and unrelated-path matchers.
-- [ ] 2.3 Implement safe Vue GET request caching, authentication entry behavior, generic failure, successful deep-link restoration, session migration, and signed-out redirect.
-- [ ] 2.4 Implement the accessible server-rendered login page and same-origin public login stylesheet without Vue or GraphQL startup.
-- [ ] 2.5 Implement the authenticated no-store context endpoint with bounded username, CSRF names and token, and configured host paths.
-- [ ] 2.6 Configure current-CSRF login and logout POSTs, session invalidation, authentication clearing, and cookie cleanup.
-- [ ] 2.7 Add server integration tests for startup prerequisites, path scope, anonymous protection, failure equivalence, login, safe restoration, context confidentiality, CSRF rejection, logout, and session fixation.
+- [x] 2.1 Add the optional `vue-security-secman` module, bounded configuration properties, module import, and startup validation.
+- [x] 2.2 Implement context-aware Vue, login, logout, authentication-context, GraphQL, asset, and unrelated-path matchers.
+- [x] 2.3 Implement safe Vue GET request caching, authentication entry behavior, generic failure, successful deep-link restoration, session migration, and signed-out redirect.
+- [x] 2.4 Implement the accessible server-rendered login page and same-origin public login stylesheet without Vue or GraphQL startup.
+- [x] 2.5 Implement the authenticated no-store context endpoint with bounded username, CSRF names and token, and configured host paths.
+- [x] 2.6 Configure current-CSRF login and logout POSTs, session invalidation, authentication clearing, and cookie cleanup.
+- [x] 2.7 Add server integration tests for startup prerequisites, path scope, anonymous protection, failure equivalence, login, safe restoration, context confidentiality, CSRF rejection, logout, and session fixation.
 
 ## 3. Secured Vue Host Runtime
 
-- [ ] 3.1 Add `PetClinicVueSecuredApplication`, deterministic SecMan user data, secured properties, and dependencies without altering the ordinary application.
-- [ ] 3.2 Add an inert authentication-context metadata slot to the application-owned Vue document and bind it only in the secured route response.
-- [ ] 3.3 Add typed frontend authentication bootstrap and a CSRF-decorating GraphQL executor with full-document `401` login handling.
-- [ ] 3.4 Add stable current-user and native CSRF POST logout chrome to the Vue shell.
-- [ ] 3.5 Configure the Vue action policy to claim only exact framework Logout and submit the host logout form.
-- [ ] 3.6 Preserve ordinary Vue startup without an authentication-context request or secured shell chrome.
-- [ ] 3.7 Regenerate and verify committed Vue production assets.
+- [x] 3.1 Add `PetClinicVueSecuredApplication`, deterministic SecMan user data, secured properties, and dependencies without altering the ordinary application.
+- [x] 3.2 Add an inert authentication-context metadata slot to the application-owned Vue document and bind it only in the secured route response.
+- [x] 3.3 Add typed frontend authentication bootstrap and a CSRF-decorating GraphQL executor with full-document `401` login handling.
+- [x] 3.4 Add stable current-user and native CSRF POST logout chrome to the Vue shell.
+- [x] 3.5 Configure the Vue action policy to claim only exact framework Logout and submit the host logout form.
+- [x] 3.6 Preserve ordinary Vue startup without an authentication-context request or secured shell chrome.
+- [x] 3.7 Regenerate and verify committed Vue production assets.
 
 ## 4. Secured Acceptance and Launchers
 
-- [ ] 4.1 Add secured Vue Spring integration coverage using the shared deterministic credentials and route identities.
-- [ ] 4.2 Add a headless secured Vue journey covering generic invalid login, valid deep-link login, authorized reads and mutation, CSRF headers and rejection, host logout, history, and session expiry.
-- [ ] 4.3 Add `sample-htmx-petclinic/run-secured.sh` selecting the established secured HTMX profile.
-- [ ] 4.4 Add the secured Vue Maven run profile and `sample-vue-petclinic/run-secured.sh`.
-- [ ] 4.5 Verify both ordinary run scripts remain mapped to their existing applications and both secured scripts pass arguments and environment settings unchanged.
+- [x] 4.1 Add secured Vue Spring integration coverage using the shared deterministic credentials and route identities.
+- [x] 4.2 Add a headless secured Vue journey covering generic invalid login, valid deep-link login, authorized reads and mutation, CSRF headers and rejection, host logout, history, and session expiry.
+- [x] 4.3 Add `sample-htmx-petclinic/run-secured.sh` selecting the established secured HTMX profile.
+- [x] 4.4 Add the secured Vue Maven run profile and `sample-vue-petclinic/run-secured.sh`.
+- [x] 4.5 Verify both ordinary run scripts remain mapped to their existing applications and both secured scripts pass arguments and environment settings unchanged.
 
 ## 5. Documentation
 
-- [ ] 5.1 Document the shared bridge ownership boundary and opt-in dependency behavior.
-- [ ] 5.2 Update HTMX security and sample documentation to use the dedicated secured launcher and describe unchanged behavior.
-- [ ] 5.3 Update Vue viewer, Vue security, Vue sample, and top-level web-component documentation with setup, routes, credentials, CSRF, logout, session expiry, and run commands.
-- [ ] 5.4 Document that local-resource navigation, framework Logout identity, and host authentication remain separate contracts.
+- [x] 5.1 Document the shared bridge ownership boundary and opt-in dependency behavior.
+- [x] 5.2 Update HTMX security and sample documentation to use the dedicated secured launcher and describe unchanged behavior.
+- [x] 5.3 Update Vue viewer, Vue security, Vue sample, and top-level web-component documentation with setup, routes, credentials, CSRF, logout, session expiry, and run commands.
+- [x] 5.4 Document that local-resource navigation, framework Logout identity, and host authentication remain separate contracts.
 
 ## 6. Verification
 
-- [ ] 6.1 Run shared bridge, HTMX security, Vue security, and sample Java integration suites.
-- [ ] 6.2 Run foundation and Vue frontend checks, declaration/build/pack verification, and stale generated-output checks.
-- [ ] 6.3 Run ordinary and secured HTMX and Vue headless Playwright suites.
-- [ ] 6.4 Run script syntax checks, relevant Maven package verification, Apache RAT, IDE inspections/build, strict OpenSpec validation, and whitespace checks.
+- [x] 6.1 Run shared bridge, HTMX security, Vue security, and sample Java integration suites.
+- [x] 6.2 Run foundation and Vue frontend checks, declaration/build/pack verification, and stale generated-output checks.
+- [x] 6.3 Run ordinary and secured HTMX and Vue headless Playwright suites.
+- [x] 6.4 Run script syntax checks, relevant Maven package verification, Apache RAT, IDE inspections/build, strict OpenSpec validation, and whitespace checks.
diff --git a/viewers/webcomponents/README.adoc b/viewers/webcomponents/README.adoc
index 96837849c2a..6e39cc0810d 100644
--- a/viewers/webcomponents/README.adoc
+++ b/viewers/webcomponents/README.adoc
@@ -5,7 +5,8 @@
 The Causeway web-component modules provide framework-neutral browser components backed by the Causeway rich GraphQL schema.
 The foundation module includes a GraphQL client provider, coordinated application menu bars, service-action prompts and invocation, coordinated object contexts, safe effective-grid and fallback composite objects, toolkit-qualified read-only scalar and ordinary-action presentation with native rollback, explicitly enabled property editing, normalized interaction results, lazy collections, and an optional cohesive theme.
 It deliberately has no dependency on HTMX, React, Vue, Svelte, Wicket, Bootstrap, or a browser component-authoring framework.
-The separate optional HTMX module adds canonical routing, a stable shell, history, custom-page selection, and fragment lifecycle above those components.
+The separate optional HTMX and Vue modules add thin host routing, stable application shells, history, exact page selection, and lifecycle above those components.
+Optional `security-secman`, `htmx-security-secman`, and `vue-security-secman` modules provide a shared local credential bridge plus viewer-owned browser-session policy without adding authentication to the generic viewers.
 
 This page explains how to run the automated verification and how to exercise the components manually against a Causeway application.
 
@@ -108,7 +109,7 @@ This avoids cross-origin and authentication-cookie differences that can obscure
 
 The repository provides HTMX and Vue Petclinic applications over one shared deterministic JPA/H2 domain and rich GraphQL contract, plus `viewers/webcomponents/sample-html` as the stable low-level vanilla-component fixture.
 The HTMX Petclinic application is the presentation reference for the Vue sample's application-owned shell and exact-type HomePage, PetOwner, Pet, and Visit pages.
-Start either Petclinic viewer from the repository root with:
+Start either ordinary Petclinic viewer from the repository root with:
 
 [source,shell]
 ----
@@ -116,10 +117,19 @@ Start either Petclinic viewer from the repository root with:
 ./viewers/webcomponents/sample-vue-petclinic/run.sh
 ----
 
-Run only one application at a time because both use port 8080 by default.
+Start the corresponding local SecMan and Spring Security variants with:
+
+[source,shell]
+----
+./viewers/webcomponents/sample-htmx-petclinic/run-secured.sh
+./viewers/webcomponents/sample-vue-petclinic/run-secured.sh
+----
+
+Both secured applications use username `sven` and password `pass`, require current CSRF evidence for GraphQL and logout POSTs, and restore safe viewer deep links after login.
+Run only one application at a time because all four use port 8080 by default.
 Open `http://localhost:8080/htmx` for HTMX or `http://localhost:8080/vue/` for Vue.
 The HTMX application also exposes `/wicket/`; sign in as `sven` with password `pass` for comparison.
-See link:sample-htmx-petclinic/README.adoc[`sample-htmx-petclinic/README.adoc`], link:sample-vue-petclinic/README.adoc[`sample-vue-petclinic/README.adoc`], link:htmx/README.adoc[`htmx/README.adoc`], and link:vue/README.adoc[`vue/README.adoc`] for routes, customization, and acceptance contracts.
+See link:sample-htmx-petclinic/README.adoc[`sample-htmx-petclinic/README.adoc`], link:sample-vue-petclinic/README.adoc[`sample-vue-petclinic/README.adoc`], link:htmx/README.adoc[`htmx/README.adoc`], link:vue/README.adoc[`vue/README.adoc`], link:security-secman/README.adoc[`security-secman/README.adoc`], link:htmx-security-secman/README.adoc[`htmx-security-secman/README.adoc`], and link:vue-security-secman/README.adoc[`vue-security-secman/README.adoc`] for routes, customization, security  [...]
 
 The vanilla sample supplies its own deterministic JPA/H2 data, the same rich GraphQL endpoint, packaged foundation modules, and stable low-level automation hooks.
 Start it from the repository root with:
diff --git a/viewers/webcomponents/htmx-security-secman/README.adoc b/viewers/webcomponents/htmx-security-secman/README.adoc
index 66fc7b3ff02..68140cfc2f2 100644
--- a/viewers/webcomponents/htmx-security-secman/README.adoc
+++ b/viewers/webcomponents/htmx-security-secman/README.adoc
@@ -105,17 +105,18 @@ That control remains independent of typed `LocalResourcePathValue` action result
 A local-resource path equal to `/logout` does not acquire authentication semantics.
 Invalid passwords, absent users, locked users, passwordless users, and internal lookup failures all produce the same public credential failure.
 
-== Provisional bridge boundary
+== Shared bridge boundary
 
-`SecmanUserDetailsService`, `SecmanUserDetails`, and `AuthenticationConverterOfSecmanUserDetails` form an intentionally HTMX-local provisional SecMan-to-Spring bridge.
-They perform controlled anonymous lookup, preserve the effective SecMan roles and user profile needed for the Causeway `UserMemento`, erase credentials, and avoid an unscoped second repository lookup.
-They must not be treated as a stable shared-security SPI while they remain in this viewer integration.
-A separate planned change, `promote-htmx-secman-spring-security-bridge`, covers reconciliation and possible promotion into shared Causeway security.
+The integration consumes `causeway-viewer-webcomponents-security-secman` for SecMan credential lookup, its credentials-erasing session principal, Causeway `UserMemento` conversion, and DAO authentication-provider construction.
+That bridge performs controlled anonymous lookup, preserves effective roles, tenancy, and locales captured at authentication time, and avoids a second unscoped repository lookup.
+It deliberately owns no HTMX or Vue routes, login UI, request cache, CSRF transport, session-expiry navigation, or logout presentation.
+This HTMX module continues to own all of those viewer-specific policies.
 
 This module explicitly excludes OAuth 2.0, OIDC, external-provider buttons, bearer-token APIs, delegated-user auto-creation, provider logout, registration, password reset, remember-me, multi-factor authentication, and brute-force infrastructure.
 The planned `add-oauth-oidc-authentication-to-htmx-viewer` change covers provider authentication separately.
 Wicket and HTMX coexistence with active Spring CSRF filters is also deferred to `enable-csrf-safe-wicket-htmx-coexistence`.
 The secured Petclinic acceptance configuration therefore does not import Wicket or bypass security.
+Run it from the repository root with `./viewers/webcomponents/sample-htmx-petclinic/run-secured.sh`.
 
 == Rollback
 
diff --git a/viewers/webcomponents/htmx-security-secman/pom.xml b/viewers/webcomponents/htmx-security-secman/pom.xml
index a037ed7db25..e3605186ebd 100644
--- a/viewers/webcomponents/htmx-security-secman/pom.xml
+++ b/viewers/webcomponents/htmx-security-secman/pom.xml
@@ -44,12 +44,9 @@
             <version>${project.version}</version>
         </dependency>
         <dependency>
-            <groupId>org.apache.causeway.security</groupId>
-            <artifactId>causeway-security-spring</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.causeway.extensions</groupId>
-            <artifactId>causeway-extensions-secman-integration</artifactId>
+            <groupId>org.apache.causeway.viewer</groupId>
+            <artifactId>causeway-viewer-webcomponents-security-secman</artifactId>
+            <version>${project.version}</version>
         </dependency>
         <dependency>
             <groupId>org.apache.causeway.extensions</groupId>
diff --git a/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/CausewayModuleViewerWebcomponentsHtmxSecuritySecman.java b/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/CausewayModuleViewerWebcomponentsHtmxSecuritySecman.java
index 8da7f6f1cab..944b85124a5 100644
--- a/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/CausewayModuleViewerWebcomponentsHtmxSecuritySecman.java
+++ b/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/CausewayModuleViewerWebcomponentsHtmxSecuritySecman.java
@@ -22,15 +22,13 @@
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
 
-import org.apache.causeway.extensions.secman.integration.CausewayModuleExtSecmanIntegration;
-import org.apache.causeway.security.spring.CausewayModuleSecuritySpring;
 import org.apache.causeway.viewer.webcomponents.htmx.CausewayModuleViewerWebcomponentsHtmx;
+import org.apache.causeway.viewer.webcomponents.security.secman.CausewayModuleViewerWebcomponentsSecuritySecman;
 
 @Configuration
 @Import({
         CausewayModuleViewerWebcomponentsHtmx.class,
-        CausewayModuleSecuritySpring.class,
-        CausewayModuleExtSecmanIntegration.class,
+        CausewayModuleViewerWebcomponentsSecuritySecman.class,
         HtmxSecmanSecurityConfiguration.class
 })
 @EnableConfigurationProperties(HtmxSecmanSecurityProperties.class)
diff --git a/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/HtmxSecmanSecurityConfiguration.java b/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/HtmxSecmanSecurityConfiguration.java
index 443891b3780..06ab5bb8af2 100644
--- a/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/HtmxSecmanSecurityConfiguration.java
+++ b/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/HtmxSecmanSecurityConfiguration.java
@@ -36,7 +36,6 @@
 import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.csrf.CsrfFilter;
 import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
@@ -45,9 +44,7 @@
 import org.springframework.security.web.util.matcher.OrRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 
-import org.apache.causeway.applib.services.iactn.InteractionService;
 import org.apache.causeway.core.config.CausewayConfiguration;
-import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUserRepository;
 import org.apache.causeway.viewer.webcomponents.htmx.HtmxAuthenticationShell;
 import org.apache.causeway.viewer.webcomponents.htmx.HtmxViewerProperties;
 
@@ -69,18 +66,6 @@ HtmxSecmanSecurityValidator htmxSecmanSecurityValidator(
         return new HtmxSecmanSecurityValidator(causewayConfiguration, securityProperties, paths);
     }
 
-    @Bean
-    SecmanUserDetailsService secmanUserDetailsService(
-            final ApplicationUserRepository applicationUserRepository,
-            final InteractionService interactionService) {
-        return new SecmanUserDetailsService(applicationUserRepository, interactionService);
-    }
-
-    @Bean
-    AuthenticationConverterOfSecmanUserDetails authenticationConverterOfSecmanUserDetails() {
-        return new AuthenticationConverterOfSecmanUserDetails();
-    }
-
     @Bean
     HtmxAuthenticationShell secmanHtmxAuthenticationShell(
             final HtmxViewerProperties viewerProperties,
@@ -104,23 +89,14 @@ RequestCache htmxSecmanRequestCache(final HtmxSecmanPaths paths) {
         return requestCache;
     }
 
-    @Bean
-    DaoAuthenticationProvider htmxSecmanAuthenticationProvider(
-            final SecmanUserDetailsService userDetailsService,
-            final @Qualifier("Secman") PasswordEncoder passwordEncoder) {
-        final var provider = new DaoAuthenticationProvider(userDetailsService);
-        provider.setPasswordEncoder(passwordEncoder);
-        provider.setHideUserNotFoundExceptions(true);
-        return provider;
-    }
-
     @Bean
     SecurityFilterChain htmxSecmanSecurityFilterChain(
             final HttpSecurity http,
             final HtmxSecmanPaths paths,
             final HtmxSecmanSecurityProperties securityProperties,
             final HtmxViewerProperties viewerProperties,
-            final DaoAuthenticationProvider authenticationProvider,
+            final @Qualifier("webcomponentsSecmanAuthenticationProvider")
+            DaoAuthenticationProvider authenticationProvider,
             final RequestCache requestCache) throws Exception {
         final var requestMatcher = PathPatternRequestMatcher.withDefaults();
         final var viewerRoute = new OrRequestMatcher(
diff --git a/viewers/webcomponents/htmx-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/HtmxSecmanHttpSecurityTest.java b/viewers/webcomponents/htmx-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/HtmxSecmanHttpSecurityTest.java
index dcfc7e0e364..ea87b06e22c 100644
--- a/viewers/webcomponents/htmx-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/HtmxSecmanHttpSecurityTest.java
+++ b/viewers/webcomponents/htmx-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/HtmxSecmanHttpSecurityTest.java
@@ -50,6 +50,7 @@
 import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUserRepository;
 import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUserStatus;
 import org.apache.causeway.viewer.webcomponents.htmx.HtmxViewerProperties;
+import org.apache.causeway.viewer.webcomponents.security.secman.SecmanSpringAuthenticationConfiguration;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.hamcrest.Matchers.containsString;
@@ -209,7 +210,10 @@ private ApplicationUser applicationUser(final String username, final String pass
     @Configuration
     @EnableWebMvc
     @EnableWebSecurity
-    @org.springframework.context.annotation.Import(HtmxSecmanSecurityConfiguration.class)
+    @org.springframework.context.annotation.Import({
+            SecmanSpringAuthenticationConfiguration.class,
+            HtmxSecmanSecurityConfiguration.class
+    })
     static class TestConfiguration {
 
         @Bean
diff --git a/viewers/webcomponents/pom.xml b/viewers/webcomponents/pom.xml
index 01e2c969b7f..0a6cadb2416 100644
--- a/viewers/webcomponents/pom.xml
+++ b/viewers/webcomponents/pom.xml
@@ -47,7 +47,9 @@
         <module>foundation</module>
         <module>vue</module>
         <module>htmx</module>
+        <module>security-secman</module>
         <module>htmx-security-secman</module>
+        <module>vue-security-secman</module>
         <module>sample-petclinic-domain</module>
         <module>sample-html</module>
         <module>sample-htmx-petclinic</module>
@@ -82,6 +84,17 @@
                 <defaultGoal>spring-boot:run</defaultGoal>
             </build>
         </profile>
+        <profile>
+            <id>run-sample-vue-petclinic-secured</id>
+            <properties>
+                <sample-vue-petclinic.run.skip>false</sample-vue-petclinic.run.skip>
+                <sample-vue-petclinic.secured>true</sample-vue-petclinic.secured>
+                <spring-boot.run.main-class>org.apache.causeway.viewer.webcomponents.sample.vue.petclinicsecured.PetClinicVueSecuredApplication</spring-boot.run.main-class>
+            </properties>
+            <build>
+                <defaultGoal>spring-boot:run</defaultGoal>
+            </build>
+        </profile>
         <profile>
             <id>run-sample-htmx-petclinic-secured</id>
             <properties>
diff --git a/viewers/webcomponents/sample-htmx-petclinic/README.adoc b/viewers/webcomponents/sample-htmx-petclinic/README.adoc
index ee529fea235..9cd9b913709 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/README.adoc
+++ b/viewers/webcomponents/sample-htmx-petclinic/README.adoc
@@ -36,11 +36,12 @@ Run the separate local SecMan-authenticated configuration with:
 
 [source,bash]
 ----
-mvn -f viewers/webcomponents/pom.xml -Prun-sample-htmx-petclinic-secured
+./viewers/webcomponents/sample-htmx-petclinic/run-secured.sh
 ----
 
 Open `http://localhost:8080/htmx` and sign in with username `sven` and password `pass`.
-This secured configuration enables Spring CSRF protection and intentionally omits Wicket and bypass security.
+This secured configuration enables Spring CSRF protection, consumes the shared presentation-neutral SecMan Spring bridge, and intentionally omits Wicket and bypass security.
+The secured launcher preserves the same `JAVA_HOME`, `MVN`, and pass-through Maven argument behavior as `run.sh`.
 
 == Headless Playwright acceptance
 
diff --git a/viewers/webcomponents/sample-vue-petclinic/test.sh b/viewers/webcomponents/sample-htmx-petclinic/run-secured.sh
similarity index 60%
copy from viewers/webcomponents/sample-vue-petclinic/test.sh
copy to viewers/webcomponents/sample-htmx-petclinic/run-secured.sh
index d943e14f72d..047858b7072 100755
--- a/viewers/webcomponents/sample-vue-petclinic/test.sh
+++ b/viewers/webcomponents/sample-htmx-petclinic/run-secured.sh
@@ -20,28 +20,12 @@
 set -euo pipefail
 
 ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
-MVN="${MVN:-mvn}"
-POM="$ROOT/viewers/webcomponents/pom.xml"
-MODULE="sample-vue-petclinic"
-PROFILE="playwright-vue"
 
 if [[ -z "${JAVA_HOME:-}" && -x /usr/libexec/java_home ]]; then
   export JAVA_HOME="$(/usr/libexec/java_home -v 21)"
 fi
 
-if [[ "${1:-}" == "--install-browser" ]]; then
-  shift
-  "$MVN" -f "$POM" -pl "$MODULE" -am install -DskipTests
-  "$MVN" -f "$ROOT/viewers/webcomponents/sample-vue-petclinic/pom.xml" -P"$PROFILE" \
-    -DskipTests test-compile \
-    -Dexec.mainClass=com.microsoft.playwright.CLI \
-    -Dexec.classpathScope=test \
-    -Dexec.args="install chromium" \
-    exec:java
-fi
-
-exec "$MVN" -f "$POM" -pl "$MODULE" -am -P"$PROFILE" \
-  -Dtest=PetClinicVueApplicationIntegTest,PetClinicVuePlaywrightTest \
-  -Dsurefire.failIfNoSpecifiedTests=false \
-  test \
+exec "${MVN:-mvn}" \
+  -f "$ROOT/viewers/webcomponents/pom.xml" \
+  -Prun-sample-htmx-petclinic-secured \
   "$@"
diff --git a/viewers/webcomponents/sample-vue-petclinic/README.adoc b/viewers/webcomponents/sample-vue-petclinic/README.adoc
index faea2a1879d..41b6ca29bef 100644
--- a/viewers/webcomponents/sample-vue-petclinic/README.adoc
+++ b/viewers/webcomponents/sample-vue-petclinic/README.adoc
@@ -40,9 +40,24 @@ For example, this uses port 9090:
 
 The script respects an existing `JAVA_HOME`, discovers JDK 21 through macOS `/usr/libexec/java_home` when `JAVA_HOME` is unset, and uses `MVN` instead of `mvn` when that environment variable is supplied.
 
+=== Secured variant
+
+Run the local SecMan and Spring Security variant with:
+
+[source,bash]
+----
+./viewers/webcomponents/sample-vue-petclinic/run-secured.sh
+----
+
+Open any `/vue` route and sign in with username `sven` and password `pass`.
+The secured launcher preserves the environment and additional Maven argument behavior of `run.sh` while selecting `PetClinicVueSecuredApplication`.
+The application restores only safe Vue GET deep links, fetches bounded no-store username and CSRF context before mounting, decorates every GraphQL POST with the current CSRF header, and renders an application-owned POST logout form.
+Invalid, absent, locked, and passwordless accounts have the same public failure, and successful authentication migrates the session identifier.
+The exact framework action `causeway.security.LogoutMenu#logout` is claimed by host policy and submits the native form before GraphQL; similarly named application actions and typed local-resource results remain independent.
+
 == Test
 
-The test script runs the Spring integration tests and the Playwright acceptance suite.
+The test script runs the ordinary and secured Spring integration tests and both Playwright acceptance suites.
 Playwright uses headless Chromium by default and does not take foreground focus.
 
 Run the suite with:
@@ -59,7 +74,8 @@ Install Playwright Chromium on the first run when necessary:
 ./viewers/webcomponents/sample-vue-petclinic/test.sh --install-browser
 ----
 
-The browser suite covers the HTMX presentation baseline, wide and narrow layout invariants, the four exact-type pages, stable application shell, direct links, refresh, back and forward navigation, generic fallback, invalid and absent objects, semantic navigation and results, menus, keyboard skip navigation, and both toolkit policies.
+The ordinary browser suite covers the HTMX presentation baseline, wide and narrow layout invariants, the four exact-type pages, stable application shell, direct links, refresh, back and forward navigation, generic fallback, invalid and absent objects, semantic navigation and results, menus, keyboard skip navigation, and both toolkit policies.
+The secured browser journey additionally covers generic invalid login, valid deep-link restoration, CSRF-bearing reads and mutation, missing-CSRF rejection, exact host Logout policy, native logout, browser history, and session expiry.
 Set `-Dplaywright.headless=false` only when deliberately debugging in a visible browser.
 Set `-Dplaywright.chromium.executable=/absolute/path/to/chromium` or `-Dplaywright.chromium.channel=chrome` to use an installed browser.
 Any arguments after `--install-browser` are passed to the Maven test invocation.
@@ -83,10 +99,12 @@ The frontend config treats `cw-` names as native custom elements and deduplicate
 == Application structure
 
 `frontend/index.html` is the application-owned HTML document.
-`frontend/src/App.vue` declares the stable GraphQL provider, menus, loading and announcement regions, default result outlet, and keyed router view.
+`frontend/src/App.vue` declares the stable GraphQL provider, menus, loading and announcement regions, default result outlet, keyed router view, and conditional application-owned authentication chrome.
+`frontend/src/authentication.ts` performs no work for the ordinary document and, for the secured document only, fetches current bounded session context and supplies the CSRF-decorating executor.
 The files under `frontend/src/pages` provide exact-type application compositions for `petclinic.HomePage`, `petclinic.PetOwner`, `petclinic.Pet`, and `petclinic.Visit`.
 The menu layout is shared with HTMX through the framework-neutral Petclinic domain resources.
 The deliberately unregistered `petclinic.ViewerFallback` fixture demonstrates the generic route-page fallback without making a user-visible Petclinic route inconsistent.
-`PetClinicVueRouteController` forwards only the intended client routes to the production `index.html` without capturing GraphQL or static assets.
+`PetClinicVueRouteController` forwards only the intended ordinary client routes to the production `index.html` without capturing GraphQL or static assets.
+The secured source set adds `PetClinicVueSecuredRouteController`, which serves the same committed document with only its authentication-context endpoint slot bound; it never embeds a username or token into static assets.
 
 The generic Vue viewer package and its application integration contract are documented in `../vue/README.adoc`.
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/index.html b/viewers/webcomponents/sample-vue-petclinic/frontend/index.html
index 7ae32a7376d..1225a4a817c 100644
--- a/viewers/webcomponents/sample-vue-petclinic/frontend/index.html
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/index.html
@@ -22,6 +22,7 @@
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <meta name="color-scheme" content="light dark">
+  <meta name="causeway-authentication-context" content="">
   <title>Pet Clinic · Vue</title>
   <link rel="stylesheet" href="/causeway-webcomponents/component-styles.css">
   <link rel="stylesheet" href="/causeway-webcomponents/theme.css">
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/src/App.vue b/viewers/webcomponents/sample-vue-petclinic/frontend/src/App.vue
index c7a90ed9d95..c3ce9559c0a 100644
--- a/viewers/webcomponents/sample-vue-petclinic/frontend/src/App.vue
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/src/App.vue
@@ -20,6 +20,7 @@
 import {onBeforeUnmount, onMounted, ref} from 'vue';
 import {RouterView, useRoute} from 'vue-router';
 import {useCausewayShell, useCausewayViewer} from '@apache-causeway/vue-viewer';
+import {authenticationContext, graphQlExecutor} from './authentication';
 
 const viewer = useCausewayViewer();
 const route = useRoute();
@@ -48,7 +49,7 @@ onBeforeUnmount(() => titleObserver?.disconnect());
 <template>
   <div ref="shell" class="causeway-app-shell" data-testid="petclinic-vue-application-shell">
     <a class="causeway-skip-link" href="#causeway-vue-route">Skip to main content</a>
-    <cw-graphql-client data-causeway-shell-client :endpoint="viewer.endpoint">
+    <cw-graphql-client data-causeway-shell-client :endpoint="viewer.endpoint" :executor.prop="graphQlExecutor">
       <header class="causeway-shell-header">
         <div class="causeway-shell-navbar">
           <a class="causeway-shell-brand" href="/vue/" aria-label="Pet Clinic home" @click.prevent="viewer.router.push('/')">
@@ -56,6 +57,21 @@ onBeforeUnmount(() => titleObserver?.disconnect());
             <span>Pet Clinic</span>
           </a>
           <cw-menubars />
+          <div v-if="authenticationContext" class="causeway-authentication" data-testid="vue-authentication-shell">
+            <span class="causeway-authentication-user">Signed in as {{ authenticationContext.username }}</span>
+            <form
+              method="post"
+              :action="authenticationContext.logoutPath"
+              data-causeway-authentication-logout
+            >
+              <input
+                type="hidden"
+                :name="authenticationContext.csrfParameterName"
+                :value="authenticationContext.csrfToken"
+              >
+              <button type="submit">Sign out</button>
+            </form>
+          </div>
         </div>
       </header>
       <div
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/src/authentication.ts b/viewers/webcomponents/sample-vue-petclinic/frontend/src/authentication.ts
new file mode 100644
index 00000000000..8d9c0f0f25d
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/src/authentication.ts
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {shallowRef} from 'vue';
+
+export interface AuthenticationContext {
+  readonly username: string;
+  readonly csrfHeaderName: string;
+  readonly csrfParameterName: string;
+  readonly csrfToken: string;
+  readonly loginPath: string;
+  readonly logoutPath: string;
+}
+
+interface GraphQLRequest {
+  readonly document: string;
+  readonly variables?: Record<string, unknown>;
+  readonly operationName?: string;
+  readonly signal?: AbortSignal;
+}
+
+export type GraphQLExecutor = (request: GraphQLRequest) => Promise<unknown>;
+
+export const authenticationContext = shallowRef<AuthenticationContext | null>(null);
+export const graphQlExecutor = shallowRef<GraphQLExecutor | undefined>(undefined);
+
+export async function bootstrapAuthentication(endpoint = '/graphql'): Promise<void> {
+  const contextEndpoint = document.querySelector<HTMLMetaElement>(
+    'meta[name="causeway-authentication-context"]'
+  )?.content.trim();
+  if (!contextEndpoint) return;
+
+  const response = await fetch(contextEndpoint, {
+    credentials: 'same-origin',
+    headers: {accept: 'application/json'},
+    cache: 'no-store'
+  });
+  if (response.status === 401) {
+    location.assign('/vue/login');
+    return;
+  }
+  if (!response.ok) throw new Error(`Authentication context failed with HTTP ${response.status}.`);
+  const context = await response.json() as AuthenticationContext;
+  authenticationContext.value = Object.freeze(context);
+  graphQlExecutor.value = createSecuredExecutor(endpoint, context);
+}
+
+function createSecuredExecutor(endpoint: string, context: AuthenticationContext): GraphQLExecutor {
+  return async ({document, variables = {}, operationName, signal}: GraphQLRequest) => {
+    const response = await fetch(endpoint, {
+      method: 'POST',
+      credentials: 'same-origin',
+      headers: {
+        accept: 'application/graphql-response+json, application/json',
+        'content-type': 'application/json',
+        [context.csrfHeaderName]: context.csrfToken
+      },
+      body: JSON.stringify({query: document, variables, operationName}),
+      signal
+    });
+    if (response.status === 401) {
+      const continuation = safeVueContinuation();
+      const separator = context.loginPath.includes('?') ? '&' : '?';
+      location.assign(context.loginPath + (continuation
+        ? `${separator}continue=${encodeURIComponent(continuation)}`
+        : ''));
+      throw new Error('The authenticated session has expired.');
+    }
+    const text = await response.text();
+    let body: unknown;
+    try {
+      body = text ? JSON.parse(text) : {};
+    } catch (cause) {
+      throw new Error(`GraphQL response was not valid JSON (HTTP ${response.status}).`, {cause});
+    }
+    if (!response.ok) {
+      const error = new Error(`GraphQL request failed with HTTP ${response.status}.`) as Error & {status: number; responseBody: unknown};
+      error.status = response.status;
+      error.responseBody = body;
+      throw error;
+    }
+    return body;
+  };
+}
+
+function safeVueContinuation(): string | null {
+  const candidate = `${location.pathname}${location.search}`;
+  if (candidate === '/vue' || candidate === '/vue/' || candidate.startsWith('/vue/object/')) return candidate;
+  return null;
+}
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/src/main.ts b/viewers/webcomponents/sample-vue-petclinic/frontend/src/main.ts
index 6f07856e6dd..aefd01d5714 100644
--- a/viewers/webcomponents/sample-vue-petclinic/frontend/src/main.ts
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/src/main.ts
@@ -21,7 +21,8 @@ import {createApp} from 'vue';
 import {createRouter, createWebHistory} from 'vue-router';
 import {
   createCausewayRouteRecords,
-  createCausewayVueViewer
+  createCausewayVueViewer,
+  isFrameworkLogoutAction
 } from '@apache-causeway/vue-viewer';
 import '@apache-causeway/vue-viewer/theme.css';
 import App from './App.vue';
@@ -30,6 +31,7 @@ import PetOwnerPage from './pages/PetOwnerPage.vue';
 import PetPage from './pages/PetPage.vue';
 import VisitPage from './pages/VisitPage.vue';
 import './petclinic.css';
+import {authenticationContext, bootstrapAuthentication} from './authentication';
 
 const toolkit = new URLSearchParams(location.search).get('toolkit') === 'native' ? 'native' : 'vaadin';
 const policy = document.documentElement.dataset;
@@ -43,6 +45,8 @@ policy.causewayFieldFamilies = toolkit === 'vaadin' ? 'basic,numeric,local-tempo
 policy.causewayReferenceMinimumSearchLength = '2';
 policy.causewayReferenceMaximumResults = '50';
 
+await bootstrapAuthentication('/graphql');
+
 const componentsModuleUrl = '/causeway-webcomponents/index.mjs';
 await import(/* @vite-ignore */ componentsModuleUrl);
 
@@ -62,7 +66,16 @@ const viewer = createCausewayVueViewer({
     'petclinic.Visit': VisitPage
   },
   policies: {
+    action(detail, claim) {
+      if (!authenticationContext.value || !isFrameworkLogoutAction(detail)) return;
+      if (!claim.claim()) return true;
+      const form = document.querySelector<HTMLFormElement>('[data-causeway-authentication-logout]');
+      if (!form) throw new Error('The secured Vue logout form is unavailable.');
+      form.requestSubmit();
+      return true;
+    },
     error(error) {
+      if (error instanceof Error && error.message === 'The authenticated session has expired.') return;
       console.error('Causeway Vue viewer policy error', error);
     }
   }
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/src/petclinic.css b/viewers/webcomponents/sample-vue-petclinic/frontend/src/petclinic.css
index 582563df292..c6f39150446 100644
--- a/viewers/webcomponents/sample-vue-petclinic/frontend/src/petclinic.css
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/src/petclinic.css
@@ -99,6 +99,33 @@ body {
   flex: 0 1 auto;
 }
 
+.causeway-authentication {
+  display: flex;
+  align-items: center;
+  gap: var(--causeway-space-2);
+  white-space: nowrap;
+}
+
+.causeway-authentication form {
+  margin: 0;
+}
+
+.causeway-authentication button {
+  min-height: 2.25rem;
+  padding: var(--causeway-space-1) var(--causeway-space-3);
+  border: 1px solid currentColor;
+  border-radius: var(--causeway-radius-sm);
+  color: inherit;
+  background: transparent;
+  font: inherit;
+  cursor: pointer;
+}
+
+.causeway-authentication button:focus-visible {
+  outline: 3px solid var(--causeway-focus);
+  outline-offset: 2px;
+}
+
 .causeway-shell-brand {
   min-height: 2.75rem;
   display: inline-flex;
diff --git a/viewers/webcomponents/sample-vue-petclinic/pom.xml b/viewers/webcomponents/sample-vue-petclinic/pom.xml
index 9123061cb84..ae5147831a1 100644
--- a/viewers/webcomponents/sample-vue-petclinic/pom.xml
+++ b/viewers/webcomponents/sample-vue-petclinic/pom.xml
@@ -139,6 +139,68 @@
     </build>
 
     <profiles>
+        <profile>
+            <id>secured-vue-petclinic</id>
+            <activation>
+                <property>
+                    <name>sample-vue-petclinic.secured</name>
+                    <value>true</value>
+                </property>
+            </activation>
+            <dependencies>
+                <dependency>
+                    <groupId>org.apache.causeway.viewer</groupId>
+                    <artifactId>causeway-viewer-webcomponents-vue-security-secman</artifactId>
+                    <version>${project.version}</version>
+                </dependency>
+                <dependency>
+                    <groupId>org.apache.causeway.extensions</groupId>
+                    <artifactId>causeway-extensions-secman-persistence-jpa</artifactId>
+                </dependency>
+                <dependency>
+                    <groupId>org.apache.causeway.extensions</groupId>
+                    <artifactId>causeway-extensions-secman-encryption-spring</artifactId>
+                </dependency>
+                <dependency>
+                    <groupId>com.microsoft.playwright</groupId>
+                    <artifactId>playwright</artifactId>
+                    <version>${playwright.version}</version>
+                    <scope>test</scope>
+                </dependency>
+            </dependencies>
+            <build>
+                <resources>
+                    <resource><directory>src/main/resources</directory></resource>
+                    <resource><directory>src/secured/resources</directory></resource>
+                </resources>
+                <plugins>
+                    <plugin>
+                        <groupId>org.codehaus.mojo</groupId>
+                        <artifactId>build-helper-maven-plugin</artifactId>
+                        <version>3.6.1</version>
+                        <executions>
+                            <execution>
+                                <id>add-secured-vue-source</id>
+                                <phase>generate-sources</phase>
+                                <goals><goal>add-source</goal></goals>
+                                <configuration><sources><source>src/secured/java</source></sources></configuration>
+                            </execution>
+                            <execution>
+                                <id>add-secured-vue-test-source</id>
+                                <phase>generate-test-sources</phase>
+                                <goals><goal>add-test-source</goal></goals>
+                                <configuration>
+                                    <sources>
+                                        <source>src/secured-test/java</source>
+                                        <source>src/secured-playwright/java</source>
+                                    </sources>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
         <profile>
             <id>regenerate-vue-petclinic</id>
             <build>
diff --git a/viewers/webcomponents/sample-vue-petclinic/test.sh b/viewers/webcomponents/sample-vue-petclinic/run-secured.sh
similarity index 60%
copy from viewers/webcomponents/sample-vue-petclinic/test.sh
copy to viewers/webcomponents/sample-vue-petclinic/run-secured.sh
index d943e14f72d..6ed4d6094dc 100755
--- a/viewers/webcomponents/sample-vue-petclinic/test.sh
+++ b/viewers/webcomponents/sample-vue-petclinic/run-secured.sh
@@ -20,28 +20,12 @@
 set -euo pipefail
 
 ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
-MVN="${MVN:-mvn}"
-POM="$ROOT/viewers/webcomponents/pom.xml"
-MODULE="sample-vue-petclinic"
-PROFILE="playwright-vue"
 
 if [[ -z "${JAVA_HOME:-}" && -x /usr/libexec/java_home ]]; then
   export JAVA_HOME="$(/usr/libexec/java_home -v 21)"
 fi
 
-if [[ "${1:-}" == "--install-browser" ]]; then
-  shift
-  "$MVN" -f "$POM" -pl "$MODULE" -am install -DskipTests
-  "$MVN" -f "$ROOT/viewers/webcomponents/sample-vue-petclinic/pom.xml" -P"$PROFILE" \
-    -DskipTests test-compile \
-    -Dexec.mainClass=com.microsoft.playwright.CLI \
-    -Dexec.classpathScope=test \
-    -Dexec.args="install chromium" \
-    exec:java
-fi
-
-exec "$MVN" -f "$POM" -pl "$MODULE" -am -P"$PROFILE" \
-  -Dtest=PetClinicVueApplicationIntegTest,PetClinicVuePlaywrightTest \
-  -Dsurefire.failIfNoSpecifiedTests=false \
-  test \
+exec "${MVN:-mvn}" \
+  -f "$ROOT/viewers/webcomponents/pom.xml" \
+  -Prun-sample-vue-petclinic-secured \
   "$@"
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-3mHRS6NB.js b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-3mHRS6NB.js
deleted file mode 100644
index 680d0754762..00000000000
--- a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-3mHRS6NB.js
+++ /dev/null
@@ -1,2 +0,0 @@
-(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))i(s);new MutationObserver(s=>{for(const r of s)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function n(s){const r={};return s.integrity&&(r.integrity=s.integrity),s.referrerPolicy&&(r.referrerPolicy=s.referr [...]
-//# sourceMappingURL=index-3mHRS6NB.js.map
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-3mHRS6NB.js.map b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-3mHRS6NB.js.map
deleted file mode 100644
index f71c9fd5065..00000000000
--- a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-3mHRS6NB.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"index-3mHRS6NB.js","sources":["../../../../../../frontend/node_modules/@vue/shared/dist/shared.esm-bundler.js","../../../../../../frontend/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js","../../../../../../frontend/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js","../../../../../../frontend/node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js","../../../../../../frontend/node_modules/vue-router/dist/devtools-EWN81iOl.mjs","../../ [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-Bs2eP4Py.js b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-Bs2eP4Py.js
new file mode 100644
index 00000000000..2dc06c2bd37
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-Bs2eP4Py.js
@@ -0,0 +1,2 @@
+(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const r of i)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&s(o)}).observe(document,{childList:!0,subtree:!0});function n(i){const r={};return i.integrity&&(r.integrity=i.integrity),i.referrerPolicy&&(r.referrerPolicy=i.referr [...]
+//# sourceMappingURL=index-Bs2eP4Py.js.map
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-Bs2eP4Py.js.map b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-Bs2eP4Py.js.map
new file mode 100644
index 00000000000..38f5d3a237c
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-Bs2eP4Py.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"index-Bs2eP4Py.js","sources":["../../../../../../frontend/node_modules/@vue/shared/dist/shared.esm-bundler.js","../../../../../../frontend/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js","../../../../../../frontend/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js","../../../../../../frontend/node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js","../../../../../../frontend/node_modules/vue-router/dist/devtools-EWN81iOl.mjs","../../ [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CqcjgOE7.css b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CqcjgOE7.css
deleted file mode 100644
index e940b94c261..00000000000
--- a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CqcjgOE7.css
+++ /dev/null
@@ -1 +0,0 @@
-:root{--causeway-vue-surface: #ffffff;--causeway-vue-text: #1f2937;--causeway-vue-muted: #596579;--causeway-vue-border: #d7dee8;--causeway-vue-danger: #9b1c1c;--causeway-vue-focus: #075ea8;color-scheme:light dark}.causeway-vue-route-page{box-sizing:border-box;min-width:0;width:100%;color:var(--causeway-vue-text)}.causeway-vue-route-page:focus-visible,[data-causeway-router-view]:focus-visible{outline:.2rem solid var(--causeway-vue-focus);outline-offset:.2rem}.causeway-vue-status{margin:1r [...]
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-DAyo-XFi.css b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-DAyo-XFi.css
new file mode 100644
index 00000000000..9c753894135
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-DAyo-XFi.css
@@ -0,0 +1 @@
+:root{--causeway-vue-surface: #ffffff;--causeway-vue-text: #1f2937;--causeway-vue-muted: #596579;--causeway-vue-border: #d7dee8;--causeway-vue-danger: #9b1c1c;--causeway-vue-focus: #075ea8;color-scheme:light dark}.causeway-vue-route-page{box-sizing:border-box;min-width:0;width:100%;color:var(--causeway-vue-text)}.causeway-vue-route-page:focus-visible,[data-causeway-router-view]:focus-visible{outline:.2rem solid var(--causeway-vue-focus);outline-offset:.2rem}.causeway-vue-status{margin:1r [...]
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html
index fd1d7ab30a2..a5f771e0029 100644
--- a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html
@@ -22,12 +22,13 @@
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <meta name="color-scheme" content="light dark">
+  <meta name="causeway-authentication-context" content="">
   <title>Pet Clinic · Vue</title>
   <link rel="stylesheet" href="/causeway-webcomponents/component-styles.css">
   <link rel="stylesheet" href="/causeway-webcomponents/theme.css">
   <link rel="stylesheet" href="/webjars/font-awesome/7.3.0/css/all.min.css">
-  <script type="module" crossorigin src="/vue/assets/index-3mHRS6NB.js"></script>
-  <link rel="stylesheet" crossorigin href="/vue/assets/index-CqcjgOE7.css">
+  <script type="module" crossorigin src="/vue/assets/index-Bs2eP4Py.js"></script>
+  <link rel="stylesheet" crossorigin href="/vue/assets/index-DAyo-XFi.css">
 </head>
 <body>
   <div id="app"></div>
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/secured-playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicVueSecuredPlaywrightAcceptance.java b/viewers/webcomponents/sample-vue-petclinic/src/secured-playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicVueSecuredPlaywrightAcceptance.java
new file mode 100644
index 00000000000..e573e8b31bc
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/secured-playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicVueSecuredPlaywrightAcceptance.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.webcomponents.sample.vue.petclinicsecured;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.microsoft.playwright.Browser;
+import com.microsoft.playwright.BrowserType;
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.Playwright;
+import com.microsoft.playwright.options.WaitUntilState;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest(
+        classes = PetClinicVueSecuredApplication.class,
+        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class PetClinicVueSecuredPlaywrightAcceptance {
+
+    @LocalServerPort
+    private int port;
+
+    private final List<String> browserFailures = new ArrayList<>();
+    private final List<String> graphQlCsrfHeaders = new ArrayList<>();
+    private Playwright playwright;
+    private Browser browser;
+
+    @BeforeAll
+    void startBrowser() {
+        playwright = Playwright.create();
+        final var options = new BrowserType.LaunchOptions()
+                .setHeadless(Boolean.parseBoolean(System.getProperty("playwright.headless", "true")));
+        final var executable = System.getProperty("playwright.chromium.executable", "").trim();
+        final var channel = System.getProperty("playwright.chromium.channel", "").trim();
+        if (!executable.isEmpty()) {
+            options.setExecutablePath(Path.of(executable));
+        } else if (!channel.isEmpty()) {
+            options.setChannel(channel);
+        }
+        browser = playwright.chromium().launch(options);
+    }
+
+    @AfterAll
+    void stopBrowser() {
+        if (browser != null) {
+            browser.close();
+        }
+        if (playwright != null) {
+            playwright.close();
+        }
+    }
+
+    @Test
+    void anonymousLoginDeepLinkMutationCsrfLogoutHistoryAndExpiryJourney() {
+        try (var context = browser.newContext(new Browser.NewContextOptions().setViewportSize(1440, 900))) {
+            final var page = context.newPage();
+            final var menuBarRequests = new ArrayList<String>();
+            page.onPageError(error -> browserFailures.add("page: " + error));
+            page.onConsoleMessage(message -> {
+                if ("error".equals(message.type())) {
+                    browserFailures.add("console: " + message.text());
+                }
+            });
+            page.onRequest(request -> {
+                if (request.url().contains("/graphql") && "POST".equals(request.method())) {
+                    graphQlCsrfHeaders.add(request.headers().get("x-csrf-token"));
+                }
+                if (request.url().contains("/causeway-webcomponents/vaadin-menubar/")) {
+                    menuBarRequests.add(request.url());
+                }
+            });
+
+            final var deepLink = "/vue/object/petclinic.PetOwner/s_owner-mary";
+            page.navigate(origin() + deepLink, new Page.NavigateOptions().setWaitUntil(WaitUntilState.DOMCONTENTLOADED));
+            page.waitForURL("**/vue/login**");
+            assertThat(page.locator("cw-graphql-client").count()).isZero();
+            assertThat(menuBarRequests).isEmpty();
+            page.waitForFunction("() => document.activeElement?.id === 'username'");
+
+            page.locator("#username").fill(PetClinicSecmanDataConfiguration.USERNAME);
+            page.locator("#password").fill("wrong");
+            page.locator("button[type='submit']").click();
+            page.waitForURL("**/vue/login?error=true**");
+            assertThat(page.locator("[role='alert']").textContent()).contains("Sign-in failed");
+            assertThat(menuBarRequests).isEmpty();
+
+            page.locator("#username").fill(PetClinicSecmanDataConfiguration.USERNAME);
+            page.locator("#password").fill(PetClinicSecmanDataConfiguration.PASSWORD);
+            page.locator("button[type='submit']").click();
+            page.waitForURL("**" + deepLink);
+            try {
+                page.waitForFunction("() => typeof document.querySelector('cw-graphql-client')?.executor === 'function'");
+            } catch (com.microsoft.playwright.TimeoutError cause) {
+                throw new AssertionError("Authenticated Vue module did not initialize; failures=" + browserFailures
+                        + "; resources=" + page.evaluate("() => performance.getEntriesByType('resource').map(entry => entry.name).filter(name => name.includes('causeway'))")
+                        + "; element=" + page.locator("cw-graphql-client").evaluate("element => element.outerHTML"), cause);
+            }
+            waitForReadyObject(page);
+            assertThat(page.locator("[data-testid='vue-authentication-shell']").textContent()).contains("sven");
+            page.waitForFunction("() => ['ready','partial-error'].includes(document.querySelector('cw-menubars')?.dataset.menuState)");
+            page.waitForFunction("() => [...document.querySelectorAll('cw-menubar-primary, cw-menubar-secondary, cw-menubar-tertiary')].filter(element => !element.hidden).every(element => element.dataset.causewayMenubarPresentation?.startsWith('vaadin-') && element.querySelector('cw-menubar-control')?.dataset.widgetState === 'ready')");
+            assertThat(menuBarRequests).hasSize(1);
+            assertThat(page.locator("[data-causeway-service-action][data-service-logical-type='causeway.security.LogoutMenu'][data-action-id='logout']").count())
+                    .isZero();
+            assertThat(page.locator("cw-menubar-primary, cw-menubar-secondary, cw-menubar-tertiary")
+                    .evaluateAll("elements => elements.some(element => Object.values(element._projection?.actions ?? {}).some(action => action.serviceLogicalTypeName === 'causeway.security.LogoutMenu' && action.actionId === 'logout'))"))
+                    .isEqualTo(false);
+            assertThat(graphQlCsrfHeaders).isNotEmpty().allSatisfy(value -> assertThat(value).isNotBlank());
+
+            final var graphQlCountBeforeLogoutPolicy = graphQlCsrfHeaders.size();
+            final var frameworkLogoutClaimed = (Boolean) page.locator("[data-testid='petclinic-vue-application-shell']")
+                    .evaluate("""
+                            shell => {
+                              const form = shell.querySelector('[data-causeway-authentication-logout]');
+                              form.addEventListener('submit', event => event.preventDefault(), {once: true});
+                              const event = new CustomEvent('causeway-action-request', {
+                                bubbles: true,
+                                composed: true,
+                                cancelable: true,
+                                detail: {serviceLogicalTypeName: 'causeway.security.LogoutMenu', actionId: 'logout'}
+                              });
+                              shell.dispatchEvent(event);
+                              return event.defaultPrevented;
+                            }
+                            """);
+            assertThat(frameworkLogoutClaimed).isTrue();
+            assertThat(graphQlCsrfHeaders).hasSize(graphQlCountBeforeLogoutPolicy);
+
+            final var property = page.locator("cw-property[id='knownAs']");
+            property.locator("[data-causeway-action='edit']").click();
+            final var editor = property.locator("[data-causeway-editor]");
+            editor.waitFor();
+            editor.evaluate("(element, value) => { element.value = value; element.dispatchEvent(new Event('input', {bubbles:true, composed:true})); element.dispatchEvent(new Event('change', {bubbles:true, composed:true})); }", "Mary Secured");
+            property.locator("[data-causeway-action='save']").click();
+            try {
+                page.waitForFunction("() => { const property = document.querySelector(\"cw-property[id='knownAs']\"); return property?.querySelector('cw-field-editor[data-mode=\"view\"]')?.dataset.value === 'Mary Secured' || property?.querySelector('.causeway-property-value')?.textContent.includes('Mary Secured'); }");
+            } catch (com.microsoft.playwright.TimeoutError cause) {
+                throw new AssertionError("Secured property update did not settle; property=" + property.evaluate("element => element.outerHTML")
+                        + "; failures=" + browserFailures + "; csrfHeaders=" + graphQlCsrfHeaders, cause);
+            }
+
+            final var rejectedStatus = (Number) page.evaluate("""
+                    async () => (await fetch('/graphql', {
+                      method: 'POST',
+                      headers: {'Content-Type': 'application/json'},
+                      body: JSON.stringify({query: 'query Rejected { rich { application { home { kind } } } }'})
+                    })).status
+                    """);
+            assertThat(rejectedStatus.intValue()).isEqualTo(403);
+            browserFailures.removeIf(message -> message.contains("status of 403"));
+
+            context.clearCookies();
+            page.locator(".causeway-shell-brand").click();
+            page.waitForURL("**/vue/login**");
+            assertThat(page.locator("#username").isVisible()).isTrue();
+            assertThat(menuBarRequests).hasSize(1);
+            browserFailures.removeIf(message -> message.contains("status of 401"));
+
+            page.locator("#username").fill(PetClinicSecmanDataConfiguration.USERNAME);
+            page.locator("#password").fill(PetClinicSecmanDataConfiguration.PASSWORD);
+            page.locator("button[type='submit']").click();
+            page.waitForURL("**/vue**");
+            page.waitForFunction("() => ['ready','partial-error'].includes(document.querySelector('cw-menubars')?.dataset.menuState) && document.querySelector('cw-menubar-control')?.dataset.widgetState === 'ready'");
+            assertThat(menuBarRequests).hasSize(2);
+            page.locator("[data-causeway-authentication-logout] button[type='submit']").click();
+            page.waitForURL("**/vue/login?logout=true");
+            assertThat(page.locator("[role='status']").textContent()).contains("signed out");
+            assertThat(menuBarRequests).hasSize(2);
+
+            page.goBack();
+            page.waitForURL("**/vue/login**");
+            assertThat(page.locator("[data-testid='vue-authentication-shell']").count()).isZero();
+            browserFailures.removeIf(message -> message.contains("status of 401"));
+            assertThat(browserFailures).isEmpty();
+        }
+    }
+
+    private void waitForReadyObject(final Page page) {
+        try {
+            page.waitForFunction("() => ['ready','partial-error'].includes(document.querySelector('[data-causeway-route-page]')?.dataset.routeState)");
+        } catch (com.microsoft.playwright.TimeoutError cause) {
+            throw new AssertionError("Secured route did not become ready; url=" + page.url()
+                    + "; route=" + page.locator("#causeway-vue-route").evaluate("element => element.outerHTML")
+                    + "; failures=" + browserFailures
+                    + "; csrfHeaders=" + graphQlCsrfHeaders, cause);
+        }
+    }
+
+    private String origin() {
+        return "http://localhost:" + port;
+    }
+}
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/secured-test/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicVueSecuredApplication_IntegTest.java b/viewers/webcomponents/sample-vue-petclinic/src/secured-test/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicVueSecuredApplication_IntegTest.java
new file mode 100644
index 00000000000..995137bf2c9
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/secured-test/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicVueSecuredApplication_IntegTest.java
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.webcomponents.sample.vue.petclinicsecured;
+
+import java.net.CookieManager;
+import java.net.CookiePolicy;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Pattern;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest(
+        classes = PetClinicVueSecuredApplication.class,
+        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+class PetClinicVueSecuredApplication_IntegTest {
+
+    private static final Pattern CSRF_FIELD = Pattern.compile("name=\"_csrf\" value=\"([^\"]+)\"");
+    private static final Pattern CSRF_CONTEXT = Pattern.compile("\\\"csrfToken\\\":\\\"([^\\\"]+)\\\"");
+
+    @LocalServerPort
+    private int port;
+
+    @Test
+    void realSecmanLoginCsrfGraphQlAndLogoutJourney() throws Exception {
+        final var cookies = new CookieManager(null, CookiePolicy.ACCEPT_ALL);
+        final var client = HttpClient.newBuilder()
+                .cookieHandler(cookies)
+                .followRedirects(HttpClient.Redirect.NEVER)
+                .build();
+
+        final var protectedRoute = get(client, "/vue/object/petclinic.PetOwner/s_owner-mary");
+        assertThat(protectedRoute.statusCode()).isEqualTo(302);
+        assertThat(protectedRoute.headers().firstValue("location").orElseThrow()).contains("/vue/login");
+
+        final var login = get(client, "/vue/login");
+        assertThat(login.statusCode()).isEqualTo(200);
+        assertThat(login.body()).contains("Sign in to Pet Clinic").doesNotContain("cw-graphql-client");
+        final var anonymousSessionId = sessionId(cookies);
+        final var loginCsrf = token(CSRF_FIELD, login.body());
+
+        final var authenticated = login(client, loginCsrf,
+                PetClinicSecmanDataConfiguration.USERNAME,
+                PetClinicSecmanDataConfiguration.PASSWORD,
+                "/vue/object/petclinic.PetOwner/s_owner-mary");
+        assertThat(authenticated.statusCode()).isEqualTo(302);
+        assertThat(authenticated.headers().firstValue("location").orElseThrow())
+                .contains("/vue/object/petclinic.PetOwner/s_owner-mary");
+        assertThat(sessionId(cookies)).isNotEqualTo(anonymousSessionId);
+
+        final var shell = get(client, "/vue/object/petclinic.PetOwner/s_owner-mary");
+        assertThat(shell.statusCode()).isEqualTo(200);
+        assertThat(shell.headers().firstValue("cache-control").orElseThrow()).contains("no-store");
+        assertThat(shell.body())
+                .contains("name=\"causeway-authentication-context\" content=\"/vue/authentication\"")
+                .contains("/vue/assets/")
+                .doesNotContain("csrfToken")
+                .doesNotContain("sven");
+
+        final var context = get(client, "/vue/authentication");
+        assertThat(context.statusCode()).isEqualTo(200);
+        assertThat(context.headers().firstValue("cache-control").orElseThrow()).contains("no-store");
+        assertThat(context.body())
+                .contains("\"username\":\"sven\"")
+                .contains("\"csrfHeaderName\":\"X-CSRF-TOKEN\"")
+                .contains("\"csrfParameterName\":\"_csrf\"")
+                .contains("\"loginPath\":\"/vue/login\"")
+                .contains("\"logoutPath\":\"/vue/logout\"")
+                .doesNotContain("password")
+                .doesNotContain("roles");
+        final var currentCsrf = token(CSRF_CONTEXT, context.body());
+        assertThat(get(client, "/causeway-webcomponents/context-events.mjs").statusCode()).isEqualTo(200);
+        assertThat(get(client, "/causeway-webcomponents/register.mjs").statusCode()).isEqualTo(200);
+
+        final var graphQlWithoutCsrf = graphQl(client, null);
+        assertThat(graphQlWithoutCsrf.statusCode()).isEqualTo(403);
+        final var graphQl = graphQl(client, currentCsrf);
+        assertThat(graphQl.statusCode()).isEqualTo(200);
+        assertThat(graphQl.body()).contains("\"data\"").doesNotContain("Access is denied");
+
+        final var logoutWithoutCsrf = postForm(client, "/vue/logout", "");
+        assertThat(logoutWithoutCsrf.statusCode()).isEqualTo(403);
+        final var logout = postForm(client, "/vue/logout", "_csrf=" + encode(currentCsrf));
+        assertThat(logout.statusCode()).isEqualTo(302);
+        assertThat(logout.headers().firstValue("location").orElseThrow()).contains("/vue/login?logout=true");
+        assertThat(get(client, "/vue").statusCode()).isEqualTo(302);
+    }
+
+    @Test
+    void absentLockedPasswordlessAndWrongPasswordShareGenericFailure() throws Exception {
+        for (final var credentials : new String[][]{
+                {"absent", "pass"},
+                {PetClinicSecmanDataConfiguration.LOCKED_USERNAME, PetClinicSecmanDataConfiguration.PASSWORD},
+                {PetClinicSecmanDataConfiguration.PASSWORDLESS_USERNAME, "pass"},
+                {PetClinicSecmanDataConfiguration.USERNAME, "wrong"}}) {
+            final var cookies = new CookieManager(null, CookiePolicy.ACCEPT_ALL);
+            final var client = HttpClient.newBuilder()
+                    .cookieHandler(cookies)
+                    .followRedirects(HttpClient.Redirect.NEVER)
+                    .build();
+            final var csrf = token(CSRF_FIELD, get(client, "/vue/login").body());
+            final var failure = login(client, csrf, credentials[0], credentials[1], null);
+            assertThat(failure.statusCode()).isEqualTo(302);
+            assertThat(failure.headers().firstValue("location").orElseThrow()).contains("/vue/login?error=true");
+            final var outcome = get(client, failure.headers().firstValue("location").orElseThrow());
+            assertThat(outcome.body())
+                    .contains("Sign-in failed. Check your username and password.")
+                    .doesNotContain(credentials[0]);
+        }
+    }
+
+    private HttpResponse<String> login(
+            final HttpClient client,
+            final String csrf,
+            final String username,
+            final String password,
+            final String destination) throws Exception {
+        var form = "_csrf=" + encode(csrf)
+                + "&username=" + encode(username)
+                + "&password=" + encode(password);
+        if (destination != null) {
+            form += "&continue=" + encode(destination);
+        }
+        return postForm(client, "/vue/login", form);
+    }
+
+    private HttpResponse<String> graphQl(final HttpClient client, final String csrf) throws Exception {
+        final var builder = HttpRequest.newBuilder(uri("/graphql"))
+                .header("Content-Type", "application/json");
+        if (csrf != null) {
+            builder.header("X-CSRF-TOKEN", csrf);
+        }
+        return client.send(builder.POST(HttpRequest.BodyPublishers.ofString(
+                "{\"query\":\"query SecuredHome { rich { application { home { kind logicalTypeName } } } }\"}"))
+                .build(), HttpResponse.BodyHandlers.ofString());
+    }
+
+    private HttpResponse<String> get(final HttpClient client, final String path) throws Exception {
+        final var destination = path.startsWith("http://") || path.startsWith("https://")
+                ? URI.create(path)
+                : uri(path);
+        return client.send(HttpRequest.newBuilder(destination).GET().build(), HttpResponse.BodyHandlers.ofString());
+    }
+
+    private HttpResponse<String> postForm(
+            final HttpClient client,
+            final String path,
+            final String form) throws Exception {
+        return client.send(HttpRequest.newBuilder(uri(path))
+                .header("Content-Type", "application/x-www-form-urlencoded")
+                .POST(HttpRequest.BodyPublishers.ofString(form))
+                .build(), HttpResponse.BodyHandlers.ofString());
+    }
+
+    private URI uri(final String path) {
+        return URI.create("http://localhost:" + port + path);
+    }
+
+    private static String token(final Pattern pattern, final String html) {
+        final var matcher = pattern.matcher(html);
+        assertThat(matcher.find()).as(html).isTrue();
+        return matcher.group(1).replace("&amp;", "&");
+    }
+
+    private static String sessionId(final CookieManager cookies) {
+        return cookies.getCookieStore().getCookies().stream()
+                .filter(cookie -> cookie.getName().equals("JSESSIONID"))
+                .findFirst().orElseThrow().getValue();
+    }
+
+    private static String encode(final String value) {
+        return URLEncoder.encode(value, StandardCharsets.UTF_8);
+    }
+}
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/secured/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicSecmanDataConfiguration.java b/viewers/webcomponents/sample-vue-petclinic/src/secured/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicSecmanDataConfiguration.java
new file mode 100644
index 00000000000..e49a442aef0
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/secured/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicSecmanDataConfiguration.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.webcomponents.sample.vue.petclinicsecured;
+
+import java.util.Locale;
+
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.transaction.annotation.Propagation;
+
+import org.apache.causeway.applib.services.appfeat.ApplicationFeatureSort;
+import org.apache.causeway.applib.services.xactn.TransactionService;
+import org.apache.causeway.applib.value.Password;
+import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermissionMode;
+import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermissionRepository;
+import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermissionRule;
+import org.apache.causeway.extensions.secman.applib.role.dom.ApplicationRoleRepository;
+import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUser;
+import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUserRepository;
+import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUserStatus;
+import org.apache.causeway.viewer.webcomponents.sample.petclinic.domain.PetClinicDomainModule;
+
+@Configuration
+public class PetClinicSecmanDataConfiguration {
+
+    public static final String USERNAME = "sven";
+    public static final String PASSWORD = "pass";
+    public static final String LOCKED_USERNAME = "locked";
+    public static final String PASSWORDLESS_USERNAME = "passwordless";
+    public static final String ROLE_NAME = "petclinic-user";
+
+    @Bean
+    ApplicationRunner loadPetClinicSecurityData(
+            final TransactionService transactionService,
+            final ApplicationUserRepository userRepository,
+            final ApplicationRoleRepository roleRepository,
+            final ApplicationPermissionRepository permissionRepository) {
+        return args -> transactionService.runTransactional(Propagation.REQUIRED, () -> {
+            final var existingRole = roleRepository.findByName(ROLE_NAME);
+            final var role = existingRole.orElseGet(() -> roleRepository.newRole(
+                    ROLE_NAME, "Use the secured Petclinic acceptance application"));
+            if (existingRole.isEmpty()) {
+                permissionRepository.newPermissionNoCheck(
+                        role,
+                        ApplicationPermissionRule.ALLOW,
+                        ApplicationPermissionMode.CHANGING,
+                        ApplicationFeatureSort.NAMESPACE,
+                        PetClinicDomainModule.NAMESPACE);
+            }
+
+            final var unlocked = userRepository.upsertLocal(
+                    USERNAME, Password.of(PASSWORD), ApplicationUserStatus.UNLOCKED);
+            unlocked.setAtPath("/petclinic/europe");
+            unlocked.setLanguage(Locale.ENGLISH);
+            unlocked.setNumberFormat(Locale.GERMANY);
+            unlocked.setTimeFormat(Locale.UK);
+            addRoleIfMissing(roleRepository, role, unlocked);
+
+            final var locked = userRepository.upsertLocal(
+                    LOCKED_USERNAME, Password.of(PASSWORD), ApplicationUserStatus.LOCKED);
+            addRoleIfMissing(roleRepository, role, locked);
+
+            userRepository.upsertLocal(
+                    PASSWORDLESS_USERNAME, null, ApplicationUserStatus.UNLOCKED);
+        });
+    }
+
+    private static void addRoleIfMissing(
+            final ApplicationRoleRepository roleRepository,
+            final org.apache.causeway.extensions.secman.applib.role.dom.ApplicationRole role,
+            final ApplicationUser user) {
+        if (user.getRoles().stream().noneMatch(candidate -> candidate.getName().equals(role.getName()))) {
+            roleRepository.addRoleToUser(role, user);
+        }
+    }
+}
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/secured/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicVueSecuredApplication.java b/viewers/webcomponents/sample-vue-petclinic/src/secured/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicVueSecuredApplication.java
new file mode 100644
index 00000000000..80ff4444357
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/secured/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicVueSecuredApplication.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.webcomponents.sample.vue.petclinicsecured;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.context.annotation.PropertySources;
+
+import org.apache.causeway.core.config.presets.CausewayPresets;
+import org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
+import org.apache.causeway.extensions.secman.encryption.spring.CausewayModuleExtSecmanEncryptionSpring;
+import org.apache.causeway.extensions.secman.jpa.CausewayModuleExtSecmanPersistenceJpa;
+import org.apache.causeway.persistence.jpa.eclipselink.CausewayModulePersistenceJpaEclipselink;
+import org.apache.causeway.viewer.graphql.viewer.CausewayModuleViewerGraphqlViewer;
+import org.apache.causeway.viewer.webcomponents.sample.petclinic.domain.PetClinicDomainModule;
+import org.apache.causeway.viewer.webcomponents.vue.security.secman.CausewayModuleViewerWebcomponentsVueSecuritySecman;
+
+@SpringBootConfiguration
+@EnableAutoConfiguration
+@Import({
+        CausewayModuleCoreRuntimeServices.class,
+        CausewayModulePersistenceJpaEclipselink.class,
+        CausewayModuleExtSecmanPersistenceJpa.class,
+        CausewayModuleExtSecmanEncryptionSpring.class,
+        CausewayModuleViewerGraphqlViewer.class,
+        CausewayModuleViewerWebcomponentsVueSecuritySecman.class,
+        PetClinicDomainModule.class,
+        PetClinicVueSecuredRouteController.class,
+        PetClinicSecmanDataConfiguration.class
+})
+@PropertySources({
+        @PropertySource(CausewayPresets.H2InMemory_withUniqueSchema),
+        @PropertySource(CausewayPresets.SilenceMetaModel),
+        @PropertySource(CausewayPresets.SilenceProgrammingModel),
+        @PropertySource("classpath:secured-application.properties")
+})
+public class PetClinicVueSecuredApplication extends SpringBootServletInitializer {
+
+    public static void main(final String[] args) {
+        SpringApplication.run(PetClinicVueSecuredApplication.class, args);
+    }
+}
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/secured/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicVueSecuredRouteController.java b/viewers/webcomponents/sample-vue-petclinic/src/secured/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicVueSecuredRouteController.java
new file mode 100644
index 00000000000..775144166d0
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/secured/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinicsecured/PetClinicVueSecuredRouteController.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.webcomponents.sample.vue.petclinicsecured;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.http.CacheControl;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+class PetClinicVueSecuredRouteController {
+
+    private static final String AUTHENTICATION_CONTEXT_MARKER =
+            "<meta name=\"causeway-authentication-context\" content=\"\">";
+    private final ClassPathResource applicationDocument = new ClassPathResource("static/vue/index.html");
+
+    @GetMapping(value = {
+            "/vue",
+            "/vue/",
+            "/vue/index.html",
+            "/vue/object/{logicalType}/{objectId}",
+            "/vue/invalid-route"
+    }, produces = MediaType.TEXT_HTML_VALUE)
+    ResponseEntity<String> vueApplication(final HttpServletRequest request) throws IOException {
+        final var document = applicationDocument.getContentAsString(StandardCharsets.UTF_8);
+        if (!document.contains(AUTHENTICATION_CONTEXT_MARKER)) {
+            throw new IllegalStateException("The Vue application document has no authentication-context marker.");
+        }
+        final var contextEndpoint = escape(request.getContextPath() + "/vue/authentication");
+        final var securedDocument = document.replace(
+                AUTHENTICATION_CONTEXT_MARKER,
+                "<meta name=\"causeway-authentication-context\" content=\"" + contextEndpoint + "\">");
+        return ResponseEntity.ok()
+                .cacheControl(CacheControl.noStore())
+                .contentType(MediaType.TEXT_HTML)
+                .body(securedDocument);
+    }
+
+    private static String escape(final String value) {
+        return value.replace("&", "&amp;")
+                .replace("<", "&lt;")
+                .replace(">", "&gt;")
+                .replace("\"", "&quot;")
+                .replace("'", "&#39;");
+    }
+}
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/secured/resources/secured-application.properties b/viewers/webcomponents/sample-vue-petclinic/src/secured/resources/secured-application.properties
new file mode 100644
index 00000000000..dced51cc9e1
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/secured/resources/secured-application.properties
@@ -0,0 +1,23 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+spring.datasource.url=jdbc:h2:mem:securedVuePetclinic;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;INIT=CREATE SCHEMA IF NOT EXISTS causewayExtSecman
+causeway.security.spring.allow-csrf-filters=true
+causeway.viewer.webcomponents.vue.security.secman.brand=Pet Clinic
+server.servlet.session.cookie.http-only=true
+server.servlet.session.cookie.same-site=lax
diff --git a/viewers/webcomponents/sample-vue-petclinic/test.sh b/viewers/webcomponents/sample-vue-petclinic/test.sh
index d943e14f72d..905eef7056c 100755
--- a/viewers/webcomponents/sample-vue-petclinic/test.sh
+++ b/viewers/webcomponents/sample-vue-petclinic/test.sh
@@ -40,8 +40,15 @@ if [[ "${1:-}" == "--install-browser" ]]; then
     exec:java
 fi
 
-exec "$MVN" -f "$POM" -pl "$MODULE" -am -P"$PROFILE" \
+"$MVN" -f "$POM" -pl "$MODULE" -am -P"$PROFILE" \
   -Dtest=PetClinicVueApplicationIntegTest,PetClinicVuePlaywrightTest \
   -Dsurefire.failIfNoSpecifiedTests=false \
   test \
   "$@"
+
+exec "$MVN" -f "$POM" -pl "$MODULE" -am \
+  -Dsample-vue-petclinic.secured=true \
+  -Dtest=PetClinicVueSecuredApplication_IntegTest,PetClinicVueSecuredPlaywrightAcceptance \
+  -Dsurefire.failIfNoSpecifiedTests=false \
+  test \
+  "$@"
diff --git a/viewers/webcomponents/security-secman/README.adoc b/viewers/webcomponents/security-secman/README.adoc
new file mode 100644
index 00000000000..27e780555a1
--- /dev/null
+++ b/viewers/webcomponents/security-secman/README.adoc
@@ -0,0 +1,44 @@
+= Shared SecMan Spring authentication bridge
+
+// Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.
+// See the NOTICE file distributed with this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0.
+
+The `causeway-viewer-webcomponents-security-secman` module is an optional presentation-neutral bridge from local SecMan users to Spring Security browser authentication.
+It is shared by the HTMX and Vue SecMan integrations and has no dependency on either viewer.
+
+== Ownership boundary
+
+The bridge owns controlled anonymous `ApplicationUser` lookup, encrypted-password delivery to a Spring `DaoAuthenticationProvider`, account lock state, a serializable credentials-erasing principal, and conversion to a Causeway `UserMemento`.
+The retained principal captures the effective username, role names, tenancy path, language locale, number locale, and time locale at authentication time.
+Conversion therefore requires no second unscoped SecMan repository lookup.
+
+The bridge deliberately owns no `SecurityFilterChain`, URL matcher, login or logout route, request cache, CSRF transport, cookie policy, session-expiry navigation, HTML, HTMX behavior, Vue behavior, or application redirect.
+A viewer or application integration must own those policies explicitly.
+
+== Installation
+
+Most applications consume the bridge transitively through either `causeway-viewer-webcomponents-htmx-security-secman` or `causeway-viewer-webcomponents-vue-security-secman`.
+A custom web-component host can depend on it directly:
+
+[source,xml]
+----
+<dependency>
+    <groupId>org.apache.causeway.viewer</groupId>
+    <artifactId>causeway-viewer-webcomponents-security-secman</artifactId>
+    <version>${causeway.version}</version>
+</dependency>
+----
+
+Import `CausewayModuleViewerWebcomponentsSecuritySecman` together with the application-selected SecMan persistence and password-encryption modules.
+The common JPA deployment imports `CausewayModuleExtSecmanPersistenceJpa` and `CausewayModuleExtSecmanEncryptionSpring`.
+The application remains responsible for schema provisioning and deterministic or production user, role, permission, tenancy, locale, and password data.
+
+== Failure and credential behavior
+
+Absent, locked, passwordless, and incorrect-password users fail closed through Spring Security without exposing which account condition occurred.
+Unexpected repository or interaction failures become a bounded authentication-service failure rather than exposing persistence details.
+Spring erases encrypted credentials from the authenticated principal, and plaintext credentials are never copied into it.
+
+Applications that do not import this module receive no authentication provider, user-details service, or principal converter from it.
+Existing bypass, bearer, pre-authentication, Wicket, or application-defined security therefore remains authoritative.
diff --git a/viewers/webcomponents/htmx-security-secman/pom.xml b/viewers/webcomponents/security-secman/pom.xml
similarity index 69%
copy from viewers/webcomponents/htmx-security-secman/pom.xml
copy to viewers/webcomponents/security-secman/pom.xml
index a037ed7db25..d89f2ef749e 100644
--- a/viewers/webcomponents/htmx-security-secman/pom.xml
+++ b/viewers/webcomponents/security-secman/pom.xml
@@ -29,20 +29,15 @@
         <version>4.0.0-SNAPSHOT</version>
     </parent>
 
-    <artifactId>causeway-viewer-webcomponents-htmx-security-secman</artifactId>
-    <name>Apache Causeway Viewer - Web Components (HTMX SecMan Security)</name>
+    <artifactId>causeway-viewer-webcomponents-security-secman</artifactId>
+    <name>Apache Causeway Viewer - Web Components (SecMan Spring Security Bridge)</name>
 
     <properties>
-        <jar-plugin.automaticModuleName>org.apache.causeway.viewer.webcomponents.htmx.security.secman</jar-plugin.automaticModuleName>
-        <git-plugin.propertiesDir>org/apache/causeway/viewer/webcomponents/htmx/security/secman</git-plugin.propertiesDir>
+        <jar-plugin.automaticModuleName>org.apache.causeway.viewer.webcomponents.security.secman</jar-plugin.automaticModuleName>
+        <git-plugin.propertiesDir>org/apache/causeway/viewer/webcomponents/security/secman</git-plugin.propertiesDir>
     </properties>
 
     <dependencies>
-        <dependency>
-            <groupId>org.apache.causeway.viewer</groupId>
-            <artifactId>causeway-viewer-webcomponents-htmx</artifactId>
-            <version>${project.version}</version>
-        </dependency>
         <dependency>
             <groupId>org.apache.causeway.security</groupId>
             <artifactId>causeway-security-spring</artifactId>
@@ -55,25 +50,11 @@
             <groupId>org.apache.causeway.extensions</groupId>
             <artifactId>causeway-extensions-secman-applib</artifactId>
         </dependency>
-        <dependency>
-            <groupId>org.apache.causeway.core</groupId>
-            <artifactId>causeway-core-config</artifactId>
-        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-security</artifactId>
         </dependency>
 
-        <dependency>
-            <groupId>org.springframework</groupId>
-            <artifactId>spring-test</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.springframework.security</groupId>
-            <artifactId>spring-security-test</artifactId>
-            <scope>test</scope>
-        </dependency>
         <dependency>
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter-api</artifactId>
@@ -84,11 +65,6 @@
             <artifactId>assertj-core</artifactId>
             <scope>test</scope>
         </dependency>
-        <dependency>
-            <groupId>org.hamcrest</groupId>
-            <artifactId>hamcrest</artifactId>
-            <scope>test</scope>
-        </dependency>
         <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
diff --git a/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/AuthenticationConverterOfSecmanUserDetails.java b/viewers/webcomponents/security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/security/secman/AuthenticationConverterOfSecmanUserDetails.java
similarity index 97%
rename from viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/AuthenticationConverterOfSecmanUserDetails.java
rename to viewers/webcomponents/security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/security/secman/AuthenticationConverterOfSecmanUserDetails.java
index 60a4fad5347..aa278092a1e 100644
--- a/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/AuthenticationConverterOfSecmanUserDetails.java
+++ b/viewers/webcomponents/security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/security/secman/AuthenticationConverterOfSecmanUserDetails.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.htmx.security.secman;
+package org.apache.causeway.viewer.webcomponents.security.secman;
 
 import org.springframework.security.core.GrantedAuthority;
 
diff --git a/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/CausewayModuleViewerWebcomponentsHtmxSecuritySecman.java b/viewers/webcomponents/security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/security/secman/CausewayModuleViewerWebcomponentsSecuritySecman.java
similarity index 71%
copy from viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/CausewayModuleViewerWebcomponentsHtmxSecuritySecman.java
copy to viewers/webcomponents/security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/security/secman/CausewayModuleViewerWebcomponentsSecuritySecman.java
index 8da7f6f1cab..9fe3b166f97 100644
--- a/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/CausewayModuleViewerWebcomponentsHtmxSecuritySecman.java
+++ b/viewers/webcomponents/security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/security/secman/CausewayModuleViewerWebcomponentsSecuritySecman.java
@@ -16,23 +16,19 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.htmx.security.secman;
+package org.apache.causeway.viewer.webcomponents.security.secman;
 
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
 
 import org.apache.causeway.extensions.secman.integration.CausewayModuleExtSecmanIntegration;
 import org.apache.causeway.security.spring.CausewayModuleSecuritySpring;
-import org.apache.causeway.viewer.webcomponents.htmx.CausewayModuleViewerWebcomponentsHtmx;
 
 @Configuration
 @Import({
-        CausewayModuleViewerWebcomponentsHtmx.class,
         CausewayModuleSecuritySpring.class,
         CausewayModuleExtSecmanIntegration.class,
-        HtmxSecmanSecurityConfiguration.class
+        SecmanSpringAuthenticationConfiguration.class
 })
-@EnableConfigurationProperties(HtmxSecmanSecurityProperties.class)
-public class CausewayModuleViewerWebcomponentsHtmxSecuritySecman {
+public class CausewayModuleViewerWebcomponentsSecuritySecman {
 }
diff --git a/viewers/webcomponents/security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/security/secman/SecmanSpringAuthenticationConfiguration.java b/viewers/webcomponents/security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/security/secman/SecmanSpringAuthenticationConfiguration.java
new file mode 100644
index 00000000000..24288eaee53
--- /dev/null
+++ b/viewers/webcomponents/security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/security/secman/SecmanSpringAuthenticationConfiguration.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.webcomponents.security.secman;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import org.apache.causeway.applib.services.iactn.InteractionService;
+import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUserRepository;
+
+@Configuration
+public class SecmanSpringAuthenticationConfiguration {
+
+    static final String AUTHENTICATION_PROVIDER = "webcomponentsSecmanAuthenticationProvider";
+
+    @Bean
+    SecmanUserDetailsService webcomponentsSecmanUserDetailsService(
+            final ApplicationUserRepository applicationUserRepository,
+            final InteractionService interactionService) {
+        return new SecmanUserDetailsService(applicationUserRepository, interactionService);
+    }
+
+    @Bean
+    AuthenticationConverterOfSecmanUserDetails webcomponentsAuthenticationConverterOfSecmanUserDetails() {
+        return new AuthenticationConverterOfSecmanUserDetails();
+    }
+
+    @Bean(AUTHENTICATION_PROVIDER)
+    DaoAuthenticationProvider webcomponentsSecmanAuthenticationProvider(
+            final SecmanUserDetailsService userDetailsService,
+            final @Qualifier("Secman") PasswordEncoder passwordEncoder) {
+        final var provider = new DaoAuthenticationProvider(userDetailsService);
+        provider.setPasswordEncoder(passwordEncoder);
+        provider.setHideUserNotFoundExceptions(true);
+        return provider;
+    }
+}
diff --git a/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/SecmanUserDetails.java b/viewers/webcomponents/security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/security/secman/SecmanUserDetails.java
similarity index 98%
rename from viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/SecmanUserDetails.java
rename to viewers/webcomponents/security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/security/secman/SecmanUserDetails.java
index ced69abb1e8..830fe3ae215 100644
--- a/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/SecmanUserDetails.java
+++ b/viewers/webcomponents/security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/security/secman/SecmanUserDetails.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.htmx.security.secman;
+package org.apache.causeway.viewer.webcomponents.security.secman;
 
 import java.io.Serial;
 import java.io.Serializable;
diff --git a/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/SecmanUserDetailsService.java b/viewers/webcomponents/security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/security/secman/SecmanUserDetailsService.java
similarity index 98%
rename from viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/SecmanUserDetailsService.java
rename to viewers/webcomponents/security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/security/secman/SecmanUserDetailsService.java
index e6aa1076ffb..3456e7e8d58 100644
--- a/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/SecmanUserDetailsService.java
+++ b/viewers/webcomponents/security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/security/secman/SecmanUserDetailsService.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.htmx.security.secman;
+package org.apache.causeway.viewer.webcomponents.security.secman;
 
 import org.springframework.security.authentication.InternalAuthenticationServiceException;
 import org.springframework.security.core.userdetails.UserDetails;
diff --git a/viewers/webcomponents/htmx-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/SecmanAuthenticationBridgeTest.java b/viewers/webcomponents/security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/security/secman/SecmanAuthenticationBridgeTest.java
similarity index 86%
rename from viewers/webcomponents/htmx-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/SecmanAuthenticationBridgeTest.java
rename to viewers/webcomponents/security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/security/secman/SecmanAuthenticationBridgeTest.java
index 3116bf07a4b..604724ed0b7 100644
--- a/viewers/webcomponents/htmx-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/SecmanAuthenticationBridgeTest.java
+++ b/viewers/webcomponents/security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/security/secman/SecmanAuthenticationBridgeTest.java
@@ -16,8 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.htmx.security.secman;
+package org.apache.causeway.viewer.webcomponents.security.secman;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.util.Locale;
 import java.util.Optional;
 import java.util.Set;
@@ -131,6 +135,34 @@ void providerValidatesPasswordRejectsWrongPasswordAndErasesCredentials() {
                 .isInstanceOf(BadCredentialsException.class);
     }
 
+    @Test
+    void erasedPrincipalCanBeSerializedWithoutCredentials() throws Exception {
+        final var details = new SecmanUserDetails(
+                "sven",
+                "encoded",
+                true,
+                Set.of("petclinic-user"),
+                "/clinic/eu",
+                Locale.FRENCH,
+                Locale.GERMANY,
+                Locale.UK);
+        details.eraseCredentials();
+
+        final var bytes = new ByteArrayOutputStream();
+        try (var output = new ObjectOutputStream(bytes)) {
+            output.writeObject(details);
+        }
+        final SecmanUserDetails restored;
+        try (var input = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
+            restored = (SecmanUserDetails) input.readObject();
+        }
+
+        assertThat(restored.getUsername()).isEqualTo("sven");
+        assertThat(restored.getPassword()).isNull();
+        assertThat(restored.getAuthorities()).extracting("authority").containsExactly("petclinic-user");
+        assertThat(restored.atPath()).isEqualTo("/clinic/eu");
+    }
+
     @Test
     void converterRefinesWithoutASecondRepositoryLookup() {
         final var details = new SecmanUserDetails(
diff --git a/viewers/webcomponents/vue-security-secman/README.adoc b/viewers/webcomponents/vue-security-secman/README.adoc
new file mode 100644
index 00000000000..a259effa54c
--- /dev/null
+++ b/viewers/webcomponents/vue-security-secman/README.adoc
@@ -0,0 +1,127 @@
+= Local SecMan authentication for the Vue viewer
+
+// Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.
+// See the NOTICE file distributed with this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0.
+
+The `causeway-viewer-webcomponents-vue-security-secman` module is an optional local username/password integration for application-owned Vue web-component hosts.
+It combines the presentation-neutral SecMan Spring bridge with Vue-route protection, server-rendered login, safe request restoration, bounded CSRF context, and native session logout.
+Generic Vue and foundation modules remain authentication-neutral when this module is absent.
+
+== Installation
+
+Add the Vue integration alongside the application-selected SecMan persistence and encryption implementations:
+
+[source,xml]
+----
+<dependency>
+    <groupId>org.apache.causeway.viewer</groupId>
+    <artifactId>causeway-viewer-webcomponents-vue-security-secman</artifactId>
+    <version>${causeway.version}</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.causeway.extensions</groupId>
+    <artifactId>causeway-extensions-secman-persistence-jpa</artifactId>
+    <version>${causeway.version}</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.causeway.extensions</groupId>
+    <artifactId>causeway-extensions-secman-encryption-spring</artifactId>
+    <version>${causeway.version}</version>
+</dependency>
+----
+
+Import `CausewayModuleViewerWebcomponentsVueSecuritySecman`, the selected SecMan persistence and encryption modules, the GraphQL viewer, and the application-owned Vue route controller.
+The Vue integration imports the shared SecMan Spring bridge itself.
+
+Spring CSRF filters are mandatory, and startup fails unless they are explicitly allowed:
+
+[source,properties]
+----
+causeway.security.spring.allow-csrf-filters=true
+----
+
+== Paths and configuration
+
+The defaults are:
+
+[source,properties]
+----
+causeway.viewer.webcomponents.vue.security.secman.base-path=/vue
+causeway.viewer.webcomponents.vue.security.secman.graph-ql-path=/graphql
+causeway.viewer.webcomponents.vue.security.secman.login-path=/login
+causeway.viewer.webcomponents.vue.security.secman.logout-path=/logout
+causeway.viewer.webcomponents.vue.security.secman.authentication-context-path=/authentication
+causeway.viewer.webcomponents.vue.security.secman.brand=Apache Causeway
+causeway.viewer.webcomponents.vue.security.secman.filter-chain-order=20
+causeway.viewer.webcomponents.vue.security.secman.cookies-to-delete=JSESSIONID
+----
+
+The login, logout, and authentication-context paths are relative to the Vue base path.
+The chain protects the Vue hierarchy, bounded authentication context, and GraphQL hierarchy while permitting login and required immutable `/vue/assets/**`, `/causeway-webcomponents/**`, and Font Awesome resources.
+It does not claim unrelated Wicket, HTMX, actuator, console, application, or bearer-token paths.
+Choose an order that precedes a broader fallback chain without displacing an application chain intended to own the same paths.
+
+== Application document and bootstrap
+
+The application-owned Vue document must identify whether authentication bootstrap is active.
+The ordinary Petclinic document uses an inert empty marker:
+
+[source,html]
+----
+<meta name="causeway-authentication-context" content="">
+----
+
+Its secured route controller serves the same committed production document while binding only the context-path-aware endpoint URL.
+It does not embed a username or CSRF token into the document or assets.
+The Vue bootstrap fetches that authenticated no-store endpoint before mounting and receives only the username, CSRF header name, CSRF parameter name, current token, login path, and logout path.
+
+Bind the resulting executor as a DOM property on the stable provider:
+
+[source,vue]
+----
+<cw-graphql-client
+  data-causeway-shell-client
+  :endpoint="viewer.endpoint"
+  :executor.prop="graphQlExecutor">
+  <!-- application-owned stable shell -->
+</cw-graphql-client>
+----
+
+The executor adds current CSRF evidence to every GraphQL POST.
+A GraphQL `401` performs full-document login navigation with a safe current Vue continuation, while an authenticated `403` remains a bounded authorization or CSRF failure.
+Do not store the context or token in URLs, Vue Router state, local storage, GraphQL documents, variables, or semantic results.
+
+== Login, restoration, and logout
+
+The login page is server rendered, labelled, keyboard accessible, focused on username, and independent of Vue, custom elements, and GraphQL.
+Only canonical same-origin GET destinations beneath the configured Vue base path are restored after successful login.
+Authentication, GraphQL, assets, malformed paths, unsafe methods, unrelated paths, and external destinations fall back to the Vue root.
+Spring migrates the session identifier after login.
+
+The stable Vue shell owns current-user presentation and a native POST logout form carrying the context's CSRF parameter and token.
+The application's typed action policy claims only exact `causeway.security.LogoutMenu#logout` and submits that form before GraphQL invocation.
+Successful logout clears authentication, invalidates the session, applies configured cookie cleanup, and redirects to the signed-out login page.
+A `LocalResourcePathValue` equal to `/logout` remains ordinary safe local-resource navigation and does not acquire authentication semantics.
+Similarly named application actions remain ordinary domain actions.
+
+== HTTPS and production deployment
+
+Transport credentials and session cookies only over HTTPS.
+Keep session cookies `HttpOnly`, enable `Secure` in HTTPS deployments, select a suitable `SameSite` policy, and trust forwarded headers only from a proxy that overwrites untrusted values.
+Review mixed browser-session and bearer-token ownership of a shared GraphQL path explicitly; this module does not combine those models automatically.
+
+== Reference application and rollback
+
+Run the complete secured reference from the repository root with:
+
+[source,bash]
+----
+./viewers/webcomponents/sample-vue-petclinic/run-secured.sh
+----
+
+Sign in as `sven` with password `pass`.
+The sample's `test.sh` runs ordinary and secured integration and headless browser journeys.
+
+To roll back, remove `CausewayModuleViewerWebcomponentsVueSecuritySecman`, the optional dependency, secured route binding, executor decoration, and session chrome, then restore the application's previous security owner for Vue and GraphQL paths.
+No SecMan data migration is required because this integration changes no user, role, permission, tenancy, locale, or password schema.
diff --git a/viewers/webcomponents/htmx-security-secman/pom.xml b/viewers/webcomponents/vue-security-secman/pom.xml
similarity index 78%
copy from viewers/webcomponents/htmx-security-secman/pom.xml
copy to viewers/webcomponents/vue-security-secman/pom.xml
index a037ed7db25..24e6a02132a 100644
--- a/viewers/webcomponents/htmx-security-secman/pom.xml
+++ b/viewers/webcomponents/vue-security-secman/pom.xml
@@ -29,32 +29,20 @@
         <version>4.0.0-SNAPSHOT</version>
     </parent>
 
-    <artifactId>causeway-viewer-webcomponents-htmx-security-secman</artifactId>
-    <name>Apache Causeway Viewer - Web Components (HTMX SecMan Security)</name>
+    <artifactId>causeway-viewer-webcomponents-vue-security-secman</artifactId>
+    <name>Apache Causeway Viewer - Web Components (Vue SecMan Security)</name>
 
     <properties>
-        <jar-plugin.automaticModuleName>org.apache.causeway.viewer.webcomponents.htmx.security.secman</jar-plugin.automaticModuleName>
-        <git-plugin.propertiesDir>org/apache/causeway/viewer/webcomponents/htmx/security/secman</git-plugin.propertiesDir>
+        <jar-plugin.automaticModuleName>org.apache.causeway.viewer.webcomponents.vue.security.secman</jar-plugin.automaticModuleName>
+        <git-plugin.propertiesDir>org/apache/causeway/viewer/webcomponents/vue/security/secman</git-plugin.propertiesDir>
     </properties>
 
     <dependencies>
         <dependency>
             <groupId>org.apache.causeway.viewer</groupId>
-            <artifactId>causeway-viewer-webcomponents-htmx</artifactId>
+            <artifactId>causeway-viewer-webcomponents-security-secman</artifactId>
             <version>${project.version}</version>
         </dependency>
-        <dependency>
-            <groupId>org.apache.causeway.security</groupId>
-            <artifactId>causeway-security-spring</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.causeway.extensions</groupId>
-            <artifactId>causeway-extensions-secman-integration</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.causeway.extensions</groupId>
-            <artifactId>causeway-extensions-secman-applib</artifactId>
-        </dependency>
         <dependency>
             <groupId>org.apache.causeway.core</groupId>
             <artifactId>causeway-core-config</artifactId>
@@ -69,6 +57,11 @@
             <artifactId>spring-test</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-webmvc</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.springframework.security</groupId>
             <artifactId>spring-security-test</artifactId>
@@ -85,13 +78,13 @@
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>org.hamcrest</groupId>
-            <artifactId>hamcrest</artifactId>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>org.mockito</groupId>
-            <artifactId>mockito-core</artifactId>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest</artifactId>
             <scope>test</scope>
         </dependency>
     </dependencies>
diff --git a/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/CausewayModuleViewerWebcomponentsHtmxSecuritySecman.java b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/CausewayModuleViewerWebcomponentsVueSecuritySecman.java
similarity index 61%
copy from viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/CausewayModuleViewerWebcomponentsHtmxSecuritySecman.java
copy to viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/CausewayModuleViewerWebcomponentsVueSecuritySecman.java
index 8da7f6f1cab..775d4813d15 100644
--- a/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/CausewayModuleViewerWebcomponentsHtmxSecuritySecman.java
+++ b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/CausewayModuleViewerWebcomponentsVueSecuritySecman.java
@@ -16,23 +16,19 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.htmx.security.secman;
+package org.apache.causeway.viewer.webcomponents.vue.security.secman;
 
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
 
-import org.apache.causeway.extensions.secman.integration.CausewayModuleExtSecmanIntegration;
-import org.apache.causeway.security.spring.CausewayModuleSecuritySpring;
-import org.apache.causeway.viewer.webcomponents.htmx.CausewayModuleViewerWebcomponentsHtmx;
+import org.apache.causeway.viewer.webcomponents.security.secman.CausewayModuleViewerWebcomponentsSecuritySecman;
 
 @Configuration
 @Import({
-        CausewayModuleViewerWebcomponentsHtmx.class,
-        CausewayModuleSecuritySpring.class,
-        CausewayModuleExtSecmanIntegration.class,
-        HtmxSecmanSecurityConfiguration.class
+        CausewayModuleViewerWebcomponentsSecuritySecman.class,
+        VueSecmanSecurityConfiguration.class
 })
-@EnableConfigurationProperties(HtmxSecmanSecurityProperties.class)
-public class CausewayModuleViewerWebcomponentsHtmxSecuritySecman {
+@EnableConfigurationProperties(VueSecmanSecurityProperties.class)
+public class CausewayModuleViewerWebcomponentsVueSecuritySecman {
 }
diff --git a/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanAuthenticationContextFilter.java b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanAuthenticationContextFilter.java
new file mode 100644
index 00000000000..d3151f471f6
--- /dev/null
+++ b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanAuthenticationContextFilter.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.webcomponents.vue.security.secman;
+
+import java.io.IOException;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.csrf.CsrfToken;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+final class VueSecmanAuthenticationContextFilter extends OncePerRequestFilter {
+
+    private final VueSecmanPaths paths;
+
+    VueSecmanAuthenticationContextFilter(final VueSecmanPaths paths) {
+        this.paths = paths;
+    }
+
+    @Override
+    protected void doFilterInternal(
+            final HttpServletRequest request,
+            final HttpServletResponse response,
+            final FilterChain filterChain) throws ServletException, IOException {
+        final var path = request.getRequestURI().substring(request.getContextPath().length());
+        if (!"GET".equals(request.getMethod()) || !path.equals(paths.authenticationContextPath())) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+        final var csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
+        final var authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (authentication == null || !authentication.isAuthenticated() || csrfToken == null) {
+            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+            return;
+        }
+        response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store, max-age=0");
+        response.setHeader(HttpHeaders.PRAGMA, "no-cache");
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.getWriter().write("""
+                {"username":"%s","csrfHeaderName":"%s","csrfParameterName":"%s","csrfToken":"%s","loginPath":"%s","logoutPath":"%s"}
+                """.formatted(
+                        json(authentication.getName()),
+                        json(csrfToken.getHeaderName()),
+                        json(csrfToken.getParameterName()),
+                        json(csrfToken.getToken()),
+                        json(request.getContextPath() + paths.loginPath()),
+                        json(request.getContextPath() + paths.logoutPath())));
+    }
+
+    private static String json(final String value) {
+        final var escaped = new StringBuilder();
+        for (int i = 0; i < value.length(); i++) {
+            final char character = value.charAt(i);
+            switch (character) {
+                case '"' -> escaped.append("\\\"");
+                case '\\' -> escaped.append("\\\\");
+                case '\b' -> escaped.append("\\b");
+                case '\f' -> escaped.append("\\f");
+                case '\n' -> escaped.append("\\n");
+                case '\r' -> escaped.append("\\r");
+                case '\t' -> escaped.append("\\t");
+                default -> {
+                    if (character < 0x20) {
+                        escaped.append("\\u%04x".formatted((int) character));
+                    } else {
+                        escaped.append(character);
+                    }
+                }
+            }
+        }
+        return escaped.toString();
+    }
+}
diff --git a/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanAuthenticationEntryPoint.java b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanAuthenticationEntryPoint.java
new file mode 100644
index 00000000000..2493dbf1b42
--- /dev/null
+++ b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanAuthenticationEntryPoint.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.webcomponents.vue.security.secman;
+
+import java.io.IOException;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
+
+final class VueSecmanAuthenticationEntryPoint implements AuthenticationEntryPoint {
+
+    private final VueSecmanPaths paths;
+    private final LoginUrlAuthenticationEntryPoint loginEntryPoint;
+
+    VueSecmanAuthenticationEntryPoint(final VueSecmanPaths paths) {
+        this.paths = paths;
+        loginEntryPoint = new LoginUrlAuthenticationEntryPoint(paths.loginPath());
+    }
+
+    @Override
+    public void commence(
+            final HttpServletRequest request,
+            final HttpServletResponse response,
+            final AuthenticationException authException) throws IOException, ServletException {
+        final var requestPath = request.getRequestURI().substring(request.getContextPath().length());
+        if (requestPath.equals(paths.graphQlPath())
+                || requestPath.startsWith(paths.graphQlPath() + "/")
+                || requestPath.equals(paths.authenticationContextPath())) {
+            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+            return;
+        }
+        loginEntryPoint.commence(request, response, authException);
+    }
+}
diff --git a/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanAuthenticationFailureHandler.java b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanAuthenticationFailureHandler.java
new file mode 100644
index 00000000000..bd5169f0e2f
--- /dev/null
+++ b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanAuthenticationFailureHandler.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.webcomponents.vue.security.secman;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+
+final class VueSecmanAuthenticationFailureHandler implements AuthenticationFailureHandler {
+
+    private final VueSecmanPaths paths;
+
+    VueSecmanAuthenticationFailureHandler(final VueSecmanPaths paths) {
+        this.paths = paths;
+    }
+
+    @Override
+    public void onAuthenticationFailure(
+            final HttpServletRequest request,
+            final HttpServletResponse response,
+            final AuthenticationException exception) throws IOException, ServletException {
+        final var requested = request.getParameter("continue");
+        final var continuation = paths.isSafeViewerDestination(requested)
+                ? "&continue=" + URLEncoder.encode(requested, StandardCharsets.UTF_8)
+                : "";
+        response.sendRedirect(response.encodeRedirectURL(
+                request.getContextPath() + paths.loginPath() + "?error=true" + continuation));
+    }
+}
diff --git a/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanAuthenticationSuccessHandler.java b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanAuthenticationSuccessHandler.java
new file mode 100644
index 00000000000..627f9952571
--- /dev/null
+++ b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanAuthenticationSuccessHandler.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.webcomponents.vue.security.secman;
+
+import java.io.IOException;
+import java.net.URI;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.savedrequest.RequestCache;
+
+final class VueSecmanAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
+
+    private final VueSecmanPaths paths;
+    private final RequestCache requestCache;
+
+    VueSecmanAuthenticationSuccessHandler(final VueSecmanPaths paths, final RequestCache requestCache) {
+        this.paths = paths;
+        this.requestCache = requestCache;
+    }
+
+    @Override
+    public void onAuthenticationSuccess(
+            final HttpServletRequest request,
+            final HttpServletResponse response,
+            final Authentication authentication) throws IOException, ServletException {
+        final var requested = request.getParameter("continue");
+        var destination = paths.isSafeViewerDestination(requested) ? requested : null;
+        final var savedRequest = requestCache.getRequest(request, response);
+        if (destination == null && savedRequest != null && "GET".equals(savedRequest.getMethod())) {
+            destination = relativeDestination(savedRequest.getRedirectUrl(), request.getContextPath());
+        }
+        if (!paths.isSafeViewerDestination(destination)) {
+            destination = paths.viewerBasePath();
+        }
+        requestCache.removeRequest(request, response);
+        response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store, max-age=0");
+        response.sendRedirect(response.encodeRedirectURL(request.getContextPath() + destination));
+    }
+
+    private String relativeDestination(final String redirectUrl, final String contextPath) {
+        try {
+            final var uri = URI.create(redirectUrl);
+            var path = uri.getRawPath();
+            if (!contextPath.isEmpty() && path.startsWith(contextPath + "/")) {
+                path = path.substring(contextPath.length());
+            }
+            return uri.getRawQuery() == null ? path : path + "?" + uri.getRawQuery();
+        } catch (IllegalArgumentException ex) {
+            return null;
+        }
+    }
+}
diff --git a/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanLoginPageFilter.java b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanLoginPageFilter.java
new file mode 100644
index 00000000000..854d1899c3e
--- /dev/null
+++ b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanLoginPageFilter.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.webcomponents.vue.security.secman;
+
+import java.io.IOException;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.security.web.csrf.CsrfToken;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+final class VueSecmanLoginPageFilter extends OncePerRequestFilter {
+
+    private final VueSecmanPaths paths;
+    private final VueSecmanSecurityProperties securityProperties;
+
+    VueSecmanLoginPageFilter(
+            final VueSecmanPaths paths,
+            final VueSecmanSecurityProperties securityProperties) {
+        this.paths = paths;
+        this.securityProperties = securityProperties;
+    }
+
+    @Override
+    protected void doFilterInternal(
+            final HttpServletRequest request,
+            final HttpServletResponse response,
+            final FilterChain filterChain) throws ServletException, IOException {
+        final var path = request.getRequestURI().substring(request.getContextPath().length());
+        if (!"GET".equals(request.getMethod())) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+        if (path.equals(paths.loginStylesheetPath())) {
+            response.setHeader(HttpHeaders.CACHE_CONTROL, "public, max-age=3600");
+            response.setContentType("text/css;charset=UTF-8");
+            response.getWriter().write(stylesheet());
+            return;
+        }
+        if (!path.equals(paths.loginPath())) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+        final var csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
+        if (csrfToken == null) {
+            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+            return;
+        }
+        response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store, max-age=0");
+        response.setHeader(HttpHeaders.PRAGMA, "no-cache");
+        response.setContentType(MediaType.TEXT_HTML_VALUE + ";charset=UTF-8");
+        response.getWriter().write(loginPage(request, csrfToken));
+    }
+
+    private String loginPage(final HttpServletRequest request, final CsrfToken csrfToken) {
+        final var invalid = request.getParameter("error") != null;
+        final var signedOut = request.getParameter("logout") != null;
+        final var requestedContinue = request.getParameter("continue");
+        final var safeContinue = paths.isSafeViewerDestination(requestedContinue) ? requestedContinue : null;
+        final var outcome = invalid
+                ? "<p id=\"login-outcome\" class=\"login-error\" role=\"alert\">Sign-in failed. Check your username and password.</p>"
+                : signedOut
+                        ? "<p id=\"login-outcome\" class=\"login-success\" role=\"status\">You have signed out.</p>"
+                        : "";
+        final var describedBy = outcome.isEmpty() ? "" : " aria-describedby=\"login-outcome\"";
+        final var continueField = safeContinue == null
+                ? ""
+                : "<input type=\"hidden\" name=\"continue\" value=\"" + escape(safeContinue) + "\">";
+        return """
+                <!doctype html>
+                <html lang="en">
+                <head>
+                  <meta charset="utf-8">
+                  <meta name="viewport" content="width=device-width, initial-scale=1">
+                  <meta name="color-scheme" content="light dark">
+                  <meta name="robots" content="noindex,nofollow">
+                  <title>Sign in · %s</title>
+                  <link rel="stylesheet" href="%s">
+                </head>
+                <body>
+                  <main class="login-card" aria-labelledby="login-title">
+                    <div class="login-brand" aria-hidden="true">C</div>
+                    <h1 id="login-title">Sign in to %s</h1>
+                    %s
+                    <form method="post" action="%s"%s>
+                      <input type="hidden" name="%s" value="%s">
+                      %s
+                      <label for="username">Username</label>
+                      <input id="username" name="username" type="text" autocomplete="username" required autofocus>
+                      <label for="password">Password</label>
+                      <input id="password" name="password" type="password" autocomplete="current-password" required>
+                      <button type="submit">Sign in</button>
+                    </form>
+                  </main>
+                </body>
+                </html>
+                """.formatted(
+                        escape(securityProperties.getBrand()),
+                        escape(request.getContextPath() + paths.loginStylesheetPath()),
+                        escape(securityProperties.getBrand()),
+                        outcome,
+                        escape(request.getContextPath() + paths.loginPath()),
+                        describedBy,
+                        escape(csrfToken.getParameterName()),
+                        escape(csrfToken.getToken()),
+                        continueField);
+    }
+
+    private static String stylesheet() {
+        return """
+                :root{font-family:system-ui,sans-serif;color-scheme:light dark}*{box-sizing:border-box}
+                body{min-height:100vh;margin:0;display:grid;place-items:center;background:#eef2f7;color:#172033}
+                .login-card{width:min(92vw,28rem);padding:2rem;border:1px solid #c7d0df;border-radius:.75rem;background:white;box-shadow:0 1rem 3rem #20304a22}
+                .login-brand{width:3rem;height:3rem;display:grid;place-items:center;border:3px solid #173f73;border-radius:50%;color:#173f73;font-size:1.4rem;font-weight:800}
+                h1{margin:.9rem 0 1.5rem;font-size:1.55rem}form{display:grid;gap:.65rem}label{font-weight:650}
+                input{min-height:2.75rem;padding:.55rem .7rem;border:1px solid #8592a6;border-radius:.35rem;font:inherit}
+                button{min-height:2.75rem;margin-top:.7rem;border:0;border-radius:.35rem;background:#17569b;color:white;font:inherit;font-weight:700;cursor:pointer}
+                input:focus-visible,button:focus-visible{outline:3px solid #55a7ff;outline-offset:2px}
+                .login-error{padding:.75rem;border-left:4px solid #a4262c;background:#fff0f0}.login-success{padding:.75rem;border-left:4px solid #247a3c;background:#effaf2}
+                @media(prefers-color-scheme:dark){body{background:#111827;color:#e5e7eb}.login-card{background:#1f2937;border-color:#4b5563}.login-brand{color:#93c5fd;border-color:#93c5fd}.login-error{background:#471d21}.login-success{background:#173e25}}
+                """;
+    }
+
+    private static String escape(final String value) {
+        return value == null ? "" : value
+                .replace("&", "&amp;")
+                .replace("<", "&lt;")
+                .replace(">", "&gt;")
+                .replace("\"", "&quot;")
+                .replace("'", "&#39;");
+    }
+}
diff --git a/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanPaths.java b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanPaths.java
new file mode 100644
index 00000000000..12c8fa6266e
--- /dev/null
+++ b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanPaths.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.webcomponents.vue.security.secman;
+
+import java.net.URI;
+
+final class VueSecmanPaths {
+
+    private final String viewerBasePath;
+    private final String graphQlPath;
+    private final String loginPath;
+    private final String logoutPath;
+    private final String authenticationContextPath;
+
+    VueSecmanPaths(final VueSecmanSecurityProperties properties) {
+        viewerBasePath = properties.getBasePath();
+        graphQlPath = properties.getGraphQlPath();
+        loginPath = viewerBasePath + properties.getLoginPath();
+        logoutPath = viewerBasePath + properties.getLogoutPath();
+        authenticationContextPath = viewerBasePath + properties.getAuthenticationContextPath();
+    }
+
+    String viewerBasePath() {
+        return viewerBasePath;
+    }
+
+    String graphQlPath() {
+        return graphQlPath;
+    }
+
+    String loginPath() {
+        return loginPath;
+    }
+
+    String logoutPath() {
+        return logoutPath;
+    }
+
+    String authenticationContextPath() {
+        return authenticationContextPath;
+    }
+
+    String loginStylesheetPath() {
+        return loginPath + ".css";
+    }
+
+    String assetsPath() {
+        return viewerBasePath + "/assets/**";
+    }
+
+    boolean isSafeViewerDestination(final String candidate) {
+        if (candidate == null || candidate.isBlank() || candidate.indexOf('\\') >= 0
+                || candidate.chars().anyMatch(Character::isISOControl)) {
+            return false;
+        }
+        try {
+            final var uri = URI.create(candidate);
+            if (uri.isAbsolute() || uri.getRawAuthority() != null || uri.getRawFragment() != null) {
+                return false;
+            }
+            final var path = uri.getPath();
+            return path != null
+                    && (path.equals(viewerBasePath) || path.startsWith(viewerBasePath + "/"))
+                    && !path.equals(loginPath)
+                    && !path.equals(logoutPath)
+                    && !path.equals(authenticationContextPath)
+                    && !path.equals(graphQlPath)
+                    && !path.startsWith(viewerBasePath + "/assets/");
+        } catch (IllegalArgumentException ex) {
+            return false;
+        }
+    }
+}
diff --git a/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/HtmxSecmanSecurityConfiguration.java b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanSecurityConfiguration.java
similarity index 55%
copy from viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/HtmxSecmanSecurityConfiguration.java
copy to viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanSecurityConfiguration.java
index 443891b3780..4578a99b4df 100644
--- a/viewers/webcomponents/htmx-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/HtmxSecmanSecurityConfiguration.java
+++ b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanSecurityConfiguration.java
@@ -16,14 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.htmx.security.secman;
+package org.apache.causeway.viewer.webcomponents.vue.security.secman;
 
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.List;
 
 import jakarta.servlet.Filter;
-import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 
@@ -36,64 +34,37 @@
 import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.csrf.CsrfFilter;
 import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
 import org.springframework.security.web.savedrequest.RequestCache;
 import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
 import org.springframework.security.web.util.matcher.OrRequestMatcher;
-import org.springframework.security.web.util.matcher.RequestMatcher;
 
-import org.apache.causeway.applib.services.iactn.InteractionService;
 import org.apache.causeway.core.config.CausewayConfiguration;
-import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUserRepository;
-import org.apache.causeway.viewer.webcomponents.htmx.HtmxAuthenticationShell;
-import org.apache.causeway.viewer.webcomponents.htmx.HtmxViewerProperties;
 
 @Configuration
-class HtmxSecmanSecurityConfiguration {
+class VueSecmanSecurityConfiguration {
 
     @Bean
-    HtmxSecmanPaths htmxSecmanPaths(
-            final HtmxViewerProperties viewerProperties,
-            final HtmxSecmanSecurityProperties securityProperties) {
-        return new HtmxSecmanPaths(viewerProperties, securityProperties);
+    VueSecmanPaths vueSecmanPaths(final VueSecmanSecurityProperties securityProperties) {
+        return new VueSecmanPaths(securityProperties);
     }
 
     @Bean
-    HtmxSecmanSecurityValidator htmxSecmanSecurityValidator(
+    VueSecmanSecurityValidator vueSecmanSecurityValidator(
             final CausewayConfiguration causewayConfiguration,
-            final HtmxSecmanSecurityProperties securityProperties,
-            final HtmxSecmanPaths paths) {
-        return new HtmxSecmanSecurityValidator(causewayConfiguration, securityProperties, paths);
+            final VueSecmanSecurityProperties securityProperties,
+            final VueSecmanPaths paths) {
+        return new VueSecmanSecurityValidator(causewayConfiguration, securityProperties, paths);
     }
 
     @Bean
-    SecmanUserDetailsService secmanUserDetailsService(
-            final ApplicationUserRepository applicationUserRepository,
-            final InteractionService interactionService) {
-        return new SecmanUserDetailsService(applicationUserRepository, interactionService);
-    }
-
-    @Bean
-    AuthenticationConverterOfSecmanUserDetails authenticationConverterOfSecmanUserDetails() {
-        return new AuthenticationConverterOfSecmanUserDetails();
-    }
-
-    @Bean
-    HtmxAuthenticationShell secmanHtmxAuthenticationShell(
-            final HtmxViewerProperties viewerProperties,
-            final HtmxSecmanSecurityProperties securityProperties) {
-        return new SecmanHtmxAuthenticationShell(viewerProperties, securityProperties);
-    }
-
-    @Bean
-    RequestCache htmxSecmanRequestCache(final HtmxSecmanPaths paths) {
+    RequestCache vueSecmanRequestCache(final VueSecmanPaths paths) {
         final var requestCache = new HttpSessionRequestCache();
         requestCache.setMatchingRequestParameterName(null);
         requestCache.setRequestMatcher(request -> {
-            if (!"GET".equals(request.getMethod()) || "true".equalsIgnoreCase(request.getHeader("HX-Request"))) {
+            if (!"GET".equals(request.getMethod())) {
                 return false;
             }
             final var requestPath = request.getRequestURI().substring(request.getContextPath().length());
@@ -105,49 +76,31 @@ RequestCache htmxSecmanRequestCache(final HtmxSecmanPaths paths) {
     }
 
     @Bean
-    DaoAuthenticationProvider htmxSecmanAuthenticationProvider(
-            final SecmanUserDetailsService userDetailsService,
-            final @Qualifier("Secman") PasswordEncoder passwordEncoder) {
-        final var provider = new DaoAuthenticationProvider(userDetailsService);
-        provider.setPasswordEncoder(passwordEncoder);
-        provider.setHideUserNotFoundExceptions(true);
-        return provider;
-    }
-
-    @Bean
-    SecurityFilterChain htmxSecmanSecurityFilterChain(
+    SecurityFilterChain vueSecmanSecurityFilterChain(
             final HttpSecurity http,
-            final HtmxSecmanPaths paths,
-            final HtmxSecmanSecurityProperties securityProperties,
-            final HtmxViewerProperties viewerProperties,
-            final DaoAuthenticationProvider authenticationProvider,
+            final VueSecmanPaths paths,
+            final VueSecmanSecurityProperties securityProperties,
+            final @Qualifier("webcomponentsSecmanAuthenticationProvider")
+            DaoAuthenticationProvider authenticationProvider,
             final RequestCache requestCache) throws Exception {
-        final var requestMatcher = PathPatternRequestMatcher.withDefaults();
+        final var matcher = PathPatternRequestMatcher.withDefaults();
         final var viewerRoute = new OrRequestMatcher(
-                requestMatcher.matcher(paths.viewerBasePath()),
-                requestMatcher.matcher(paths.viewerBasePath() + "/**"));
-        final var publicAssetMatchers = new ArrayList<RequestMatcher>(List.of(
-                requestMatcher.matcher("/causeway-htmx/**"),
-                requestMatcher.matcher("/causeway-webcomponents/**"),
-                requestMatcher.matcher("/webjars/htmx.org/**")));
-        final var applicationStylesheet = viewerProperties.getApplicationStylesheet();
-        if (applicationStylesheet != null && applicationStylesheet.startsWith("/")) {
-            publicAssetMatchers.add(requestMatcher.matcher(applicationStylesheet));
-        }
-        final var publicViewerAssets = new OrRequestMatcher(publicAssetMatchers);
+                matcher.matcher(paths.viewerBasePath()),
+                matcher.matcher(paths.viewerBasePath() + "/**"));
         final var graphQlRoute = new OrRequestMatcher(
-                requestMatcher.matcher(paths.graphQlPath()),
-                requestMatcher.matcher(paths.graphQlPath() + "/**"));
-        http.securityMatcher(new OrRequestMatcher(
-                        viewerRoute,
-                        graphQlRoute,
-                        publicViewerAssets))
+                matcher.matcher(paths.graphQlPath()),
+                matcher.matcher(paths.graphQlPath() + "/**"));
+        final var publicAssets = new OrRequestMatcher(
+                matcher.matcher(paths.assetsPath()),
+                matcher.matcher("/causeway-webcomponents/**"),
+                matcher.matcher("/webjars/font-awesome/**"));
+        http.securityMatcher(new OrRequestMatcher(viewerRoute, graphQlRoute, publicAssets))
                 .authorizeHttpRequests(authorize -> authorize
                         .requestMatchers(
-                                requestMatcher.matcher(HttpMethod.GET, paths.loginPath()),
-                                requestMatcher.matcher(HttpMethod.POST, paths.loginPath()),
-                                requestMatcher.matcher(HttpMethod.GET, paths.loginStylesheetPath()),
-                                publicViewerAssets)
+                                matcher.matcher(HttpMethod.GET, paths.loginPath()),
+                                matcher.matcher(HttpMethod.POST, paths.loginPath()),
+                                matcher.matcher(HttpMethod.GET, paths.loginStylesheetPath()),
+                                publicAssets)
                         .permitAll()
                         .anyRequest().authenticated())
                 .authenticationProvider(authenticationProvider)
@@ -166,12 +119,12 @@ SecurityFilterChain htmxSecmanSecurityFilterChain(
                 .requestCache(cache -> cache.requestCache(requestCache))
                 .sessionManagement(session -> session.sessionFixation(fixation -> fixation.migrateSession()))
                 .exceptionHandling(exceptions -> exceptions
-                        .authenticationEntryPoint(new HtmxSecmanAuthenticationEntryPoint(paths)))
+                        .authenticationEntryPoint(new VueSecmanAuthenticationEntryPoint(paths)))
                 .formLogin(form -> form
                         .loginPage(paths.loginPath())
                         .loginProcessingUrl(paths.loginPath())
-                        .failureHandler(new HtmxSecmanAuthenticationFailureHandler(paths))
-                        .successHandler(new HtmxSecmanAuthenticationSuccessHandler(paths, requestCache))
+                        .failureHandler(new VueSecmanAuthenticationFailureHandler(paths))
+                        .successHandler(new VueSecmanAuthenticationSuccessHandler(paths, requestCache))
                         .permitAll())
                 .logout(logout -> logout
                         .logoutUrl(paths.logoutPath())
@@ -180,14 +133,15 @@ SecurityFilterChain htmxSecmanSecurityFilterChain(
                         .deleteCookies(securityProperties.getCookiesToDelete().toArray(String[]::new))
                         .logoutSuccessHandler((request, response, authentication) ->
                                 logoutSucceeded(request, response, paths)))
-                .addFilterAfter(new HtmxSecmanLoginPageFilter(paths, viewerProperties), CsrfFilter.class);
+                .addFilterAfter(new VueSecmanLoginPageFilter(paths, securityProperties), CsrfFilter.class)
+                .addFilterAfter(new VueSecmanAuthenticationContextFilter(paths), CsrfFilter.class);
         return new OrderedSecurityFilterChain(http.build(), securityProperties.getFilterChainOrder());
     }
 
     private static void logoutSucceeded(
             final HttpServletRequest request,
             final HttpServletResponse response,
-            final HtmxSecmanPaths paths) throws IOException {
+            final VueSecmanPaths paths) throws IOException {
         response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store, max-age=0");
         response.sendRedirect(response.encodeRedirectURL(
                 request.getContextPath() + paths.loginPath() + "?logout=true"));
diff --git a/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanSecurityProperties.java b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanSecurityProperties.java
new file mode 100644
index 00000000000..01c969b1e1c
--- /dev/null
+++ b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanSecurityProperties.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.webcomponents.vue.security.secman;
+
+import java.util.List;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(prefix = "causeway.viewer.webcomponents.vue.security.secman")
+public class VueSecmanSecurityProperties {
+
+    public static final int DEFAULT_FILTER_CHAIN_ORDER = 20;
+
+    private String basePath = "/vue";
+    private String graphQlPath = "/graphql";
+    private String loginPath = "/login";
+    private String logoutPath = "/logout";
+    private String authenticationContextPath = "/authentication";
+    private String brand = "Apache Causeway";
+    private int filterChainOrder = DEFAULT_FILTER_CHAIN_ORDER;
+    private List<String> cookiesToDelete = List.of("JSESSIONID");
+
+    public String getBasePath() {
+        return basePath;
+    }
+
+    public void setBasePath(final String basePath) {
+        this.basePath = normalizeAbsolutePath(basePath, "Vue base path");
+    }
+
+    public String getGraphQlPath() {
+        return graphQlPath;
+    }
+
+    public void setGraphQlPath(final String graphQlPath) {
+        this.graphQlPath = normalizeAbsolutePath(graphQlPath, "GraphQL path");
+    }
+
+    public String getLoginPath() {
+        return loginPath;
+    }
+
+    public void setLoginPath(final String loginPath) {
+        this.loginPath = normalizeRelativePath(loginPath, "Login path");
+    }
+
+    public String getLogoutPath() {
+        return logoutPath;
+    }
+
+    public void setLogoutPath(final String logoutPath) {
+        this.logoutPath = normalizeRelativePath(logoutPath, "Logout path");
+    }
+
+    public String getAuthenticationContextPath() {
+        return authenticationContextPath;
+    }
+
+    public void setAuthenticationContextPath(final String authenticationContextPath) {
+        this.authenticationContextPath = normalizeRelativePath(authenticationContextPath, "Authentication context path");
+    }
+
+    public String getBrand() {
+        return brand;
+    }
+
+    public void setBrand(final String brand) {
+        this.brand = brand == null || brand.isBlank() ? "Apache Causeway" : brand.trim();
+    }
+
+    public int getFilterChainOrder() {
+        return filterChainOrder;
+    }
+
+    public void setFilterChainOrder(final int filterChainOrder) {
+        this.filterChainOrder = filterChainOrder;
+    }
+
+    public List<String> getCookiesToDelete() {
+        return cookiesToDelete;
+    }
+
+    public void setCookiesToDelete(final List<String> cookiesToDelete) {
+        this.cookiesToDelete = cookiesToDelete == null ? List.of() : List.copyOf(cookiesToDelete);
+    }
+
+    private static String normalizeAbsolutePath(final String value, final String label) {
+        final var path = normalizeRelativePath(value, label);
+        if (path.length() < 2) {
+            throw new IllegalArgumentException(label + " must be a non-root application-relative path.");
+        }
+        return path;
+    }
+
+    private static String normalizeRelativePath(final String value, final String label) {
+        final var path = value == null ? "" : value.trim();
+        if (!path.startsWith("/") || path.startsWith("//") || path.endsWith("/")
+                || path.contains("?") || path.contains("#") || path.contains("..")
+                || path.indexOf('\\') >= 0 || path.chars().anyMatch(Character::isISOControl)) {
+            throw new IllegalArgumentException(label + " must be a canonical application-relative path.");
+        }
+        return path;
+    }
+}
diff --git a/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanSecurityValidator.java b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanSecurityValidator.java
new file mode 100644
index 00000000000..89ea350d9f7
--- /dev/null
+++ b/viewers/webcomponents/vue-security-secman/src/main/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanSecurityValidator.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.webcomponents.vue.security.secman;
+
+import org.springframework.beans.factory.InitializingBean;
+
+import org.apache.causeway.core.config.CausewayConfiguration;
+
+final class VueSecmanSecurityValidator implements InitializingBean {
+
+    static final String CSRF_PROPERTY = "causeway.security.spring.allow-csrf-filters";
+
+    private final CausewayConfiguration causewayConfiguration;
+    private final VueSecmanSecurityProperties securityProperties;
+    private final VueSecmanPaths paths;
+
+    VueSecmanSecurityValidator(
+            final CausewayConfiguration causewayConfiguration,
+            final VueSecmanSecurityProperties securityProperties,
+            final VueSecmanPaths paths) {
+        this.causewayConfiguration = causewayConfiguration;
+        this.securityProperties = securityProperties;
+        this.paths = paths;
+    }
+
+    @Override
+    public void afterPropertiesSet() {
+        if (!causewayConfiguration.security().spring().allowCsrfFilters()) {
+            throw new IllegalStateException(
+                    "Local Vue SecMan authentication requires " + CSRF_PROPERTY + "=true.");
+        }
+        if (securityProperties.getFilterChainOrder() < -1_000
+                || securityProperties.getFilterChainOrder() > 1_000) {
+            throw new IllegalStateException("Vue SecMan filter-chain order must be between -1000 and 1000.");
+        }
+        final var distinctPaths = new java.util.HashSet<>(java.util.List.of(
+                paths.loginPath(),
+                paths.logoutPath(),
+                paths.authenticationContextPath(),
+                paths.graphQlPath()));
+        if (distinctPaths.size() != 4) {
+            throw new IllegalStateException(
+                    "Vue SecMan login, logout, authentication-context, and GraphQL paths must be distinct.");
+        }
+        for (final var cookieName : securityProperties.getCookiesToDelete()) {
+            if (cookieName == null || !cookieName.matches("[!#$%&'*+.^_`|~0-9A-Za-z-]+")) {
+                throw new IllegalStateException("Vue SecMan cookie cleanup contains an invalid cookie name.");
+            }
+        }
+    }
+}
diff --git a/viewers/webcomponents/htmx-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/HtmxSecmanHttpSecurityTest.java b/viewers/webcomponents/vue-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanHttpSecurityTest.java
similarity index 78%
copy from viewers/webcomponents/htmx-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/HtmxSecmanHttpSecurityTest.java
copy to viewers/webcomponents/vue-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanHttpSecurityTest.java
index dcfc7e0e364..c76dc3f68e5 100644
--- a/viewers/webcomponents/htmx-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/htmx/security/secman/HtmxSecmanHttpSecurityTest.java
+++ b/viewers/webcomponents/vue-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanHttpSecurityTest.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.causeway.viewer.webcomponents.htmx.security.secman;
+package org.apache.causeway.viewer.webcomponents.vue.security.secman;
 
 import java.util.Locale;
 import java.util.Optional;
@@ -49,7 +49,7 @@
 import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUser;
 import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUserRepository;
 import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUserStatus;
-import org.apache.causeway.viewer.webcomponents.htmx.HtmxViewerProperties;
+import org.apache.causeway.viewer.webcomponents.security.secman.SecmanSpringAuthenticationConfiguration;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.hamcrest.Matchers.containsString;
@@ -67,8 +67,8 @@
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
-@SpringJUnitWebConfig(HtmxSecmanHttpSecurityTest.TestConfiguration.class)
-class HtmxSecmanHttpSecurityTest {
+@SpringJUnitWebConfig(VueSecmanHttpSecurityTest.TestConfiguration.class)
+class VueSecmanHttpSecurityTest {
 
     @Autowired private WebApplicationContext context;
     @Autowired private ApplicationUserRepository repository;
@@ -84,18 +84,18 @@ void setUp() {
 
     @Test
     void chainProtectsOnlyViewerAndGraphQlAndKeepsLoginPublic() throws Exception {
-        mvc.perform(get("/htmx/login"))
+        mvc.perform(get("/vue/login"))
                 .andExpect(status().isOk())
                 .andExpect(header().string("Cache-Control", containsString("no-store")))
                 .andExpect(content().string(containsString("Sign in to Petclinic")))
                 .andExpect(content().string(containsString("name=\"_csrf\"")))
                 .andExpect(content().string(org.hamcrest.Matchers.not(containsString("cw-graphql-client"))));
-        mvc.perform(get("/htmx/login.css"))
+        mvc.perform(get("/vue/login.css"))
                 .andExpect(status().isOk())
                 .andExpect(content().contentTypeCompatibleWith("text/css"));
-        mvc.perform(get("/htmx/object/42"))
+        mvc.perform(get("/vue/object/42"))
                 .andExpect(status().isFound())
-                .andExpect(header().string("Location", "/htmx/login"));
+                .andExpect(header().string("Location", "/vue/login"));
         mvc.perform(post("/graphql").contentType(MediaType.APPLICATION_JSON).content("{}"))
                 .andExpect(status().isUnauthorized());
         mvc.perform(get("/unrelated"))
@@ -104,25 +104,35 @@ void chainProtectsOnlyViewerAndGraphQlAndKeepsLoginPublic() throws Exception {
     }
 
     @Test
-    void fragmentAuthenticationLossSignalsFullBrowserRedirect() throws Exception {
-        mvc.perform(get("/htmx/object/42").header("HX-Request", "true"))
-                .andExpect(status().isUnauthorized())
-                .andExpect(header().string("HX-Redirect", "/htmx/login?continue=%2Fhtmx%2Fobject%2F42"));
+    void authenticationContextRequiresSessionAndReturnsOnlyBoundedNoStoreState() throws Exception {
+        mvc.perform(get("/vue/authentication"))
+                .andExpect(status().isUnauthorized());
+
+        mvc.perform(get("/vue/authentication").with(user("sven")))
+                .andExpect(status().isOk())
+                .andExpect(header().string("Cache-Control", containsString("no-store")))
+                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+                .andExpect(content().string(containsString("\"username\":\"sven\"")))
+                .andExpect(content().string(containsString("\"csrfHeaderName\":\"X-CSRF-TOKEN\"")))
+                .andExpect(content().string(containsString("\"loginPath\":\"/vue/login\"")))
+                .andExpect(content().string(containsString("\"logoutPath\":\"/vue/logout\"")))
+                .andExpect(content().string(org.hamcrest.Matchers.not(containsString("password"))))
+                .andExpect(content().string(org.hamcrest.Matchers.not(containsString("roles"))));
     }
 
     @Test
     void loginRejectsMissingCsrfAndInvalidCredentialsGenerically() throws Exception {
-        mvc.perform(post("/htmx/login").param("username", "absent").param("password", "wrong"))
+        mvc.perform(post("/vue/login").param("username", "absent").param("password", "wrong"))
                 .andExpect(status().isForbidden());
-        mvc.perform(post("/htmx/login").with(csrf().useInvalidToken())
+        mvc.perform(post("/vue/login").with(csrf().useInvalidToken())
                         .param("username", "absent").param("password", "wrong"))
                 .andExpect(status().isForbidden());
         when(repository.findByUsername("absent")).thenReturn(Optional.empty());
-        mvc.perform(post("/htmx/login").with(csrf())
+        mvc.perform(post("/vue/login").with(csrf())
                         .param("username", "absent").param("password", "wrong"))
                 .andExpect(status().isFound())
-                .andExpect(redirectedUrl("/htmx/login?error=true"));
-        mvc.perform(get("/htmx/login").param("error", "true"))
+                .andExpect(redirectedUrl("/vue/login?error=true"));
+        mvc.perform(get("/vue/login").param("error", "true"))
                 .andExpect(status().isOk())
                 .andExpect(content().string(containsString("Sign-in failed. Check your username and password.")))
                 .andExpect(content().string(org.hamcrest.Matchers.not(containsString("absent"))));
@@ -134,13 +144,13 @@ void validLoginMigratesSessionAndRestoresSafeDeepLink() throws Exception {
         when(repository.findByUsername("sven")).thenReturn(Optional.of(applicationUser));
         final var session = new MockHttpSession();
         final var initialId = session.getId();
-        mvc.perform(get("/htmx/object/42?tab=visits").session(session))
+        mvc.perform(get("/vue/object/42?tab=visits").session(session))
                 .andExpect(status().isFound());
 
-        final var result = mvc.perform(post("/htmx/login").session(session).with(csrf())
+        final var result = mvc.perform(post("/vue/login").session(session).with(csrf())
                         .param("username", "sven").param("password", "correct horse"))
                 .andExpect(status().isFound())
-                .andExpect(redirectedUrl("/htmx/object/42?tab=visits"))
+                .andExpect(redirectedUrl("/vue/object/42?tab=visits"))
                 .andReturn();
 
         assertThat(result.getRequest().getSession(false).getId()).isNotEqualTo(initialId);
@@ -151,12 +161,12 @@ void unsafeContinueFallsBackToViewerRoot() throws Exception {
         final var applicationUser = applicationUser("sven", "correct horse");
         when(repository.findByUsername("sven")).thenReturn(Optional.of(applicationUser));
 
-        mvc.perform(post("/htmx/login").with(csrf())
+        mvc.perform(post("/vue/login").with(csrf())
                         .param("username", "sven")
                         .param("password", "correct horse")
-                        .param("continue", "https://attacker.example/htmx"))
+                        .param("continue", "https://attacker.example/vue"))
                 .andExpect(status().isFound())
-                .andExpect(redirectedUrl("/htmx"));
+                .andExpect(redirectedUrl("/vue"));
     }
 
     @Test
@@ -173,25 +183,25 @@ void csrfProtectsGraphQlAndLogoutAndLogoutRemovesAuthentication() throws Excepti
                 .andExpect(status().isOk())
                 .andExpect(content().string("graphql"));
 
-        final var login = mvc.perform(post("/htmx/login").with(csrf())
+        final var login = mvc.perform(post("/vue/login").with(csrf())
                         .param("username", "sven").param("password", "correct horse"))
                 .andReturn();
         final var session = (MockHttpSession) login.getRequest().getSession(false);
-        mvc.perform(post("/htmx/logout").session(session))
+        mvc.perform(post("/vue/logout").session(session))
                 .andExpect(status().isForbidden());
-        mvc.perform(post("/htmx/logout").session(session).with(csrf()))
+        mvc.perform(post("/vue/logout").session(session).with(csrf()))
                 .andExpect(status().isFound())
-                .andExpect(redirectedUrl("/htmx/login?logout=true"))
+                .andExpect(redirectedUrl("/vue/login?logout=true"))
                 .andExpect(header().string("Cache-Control", containsString("no-store")));
-        mvc.perform(get("/htmx"))
+        mvc.perform(get("/vue"))
                 .andExpect(status().isFound());
     }
 
     @Test
     void authenticatedForbiddenOutcomeIsNotReportedAsSessionExpiry() throws Exception {
-        mvc.perform(get("/htmx/forbidden").with(user("sven")))
+        mvc.perform(get("/vue/forbidden").with(user("sven")))
                 .andExpect(status().isForbidden())
-                .andExpect(header().doesNotExist("HX-Redirect"));
+                .andExpect(header().doesNotExist("Location"));
     }
 
     private ApplicationUser applicationUser(final String username, final String password) {
@@ -209,21 +219,19 @@ private ApplicationUser applicationUser(final String username, final String pass
     @Configuration
     @EnableWebMvc
     @EnableWebSecurity
-    @org.springframework.context.annotation.Import(HtmxSecmanSecurityConfiguration.class)
+    @org.springframework.context.annotation.Import({
+            SecmanSpringAuthenticationConfiguration.class,
+            VueSecmanSecurityConfiguration.class
+    })
     static class TestConfiguration {
 
         @Bean
-        HtmxViewerProperties htmxViewerProperties() {
-            final var properties = new HtmxViewerProperties();
+        VueSecmanSecurityProperties vueSecmanSecurityProperties() {
+            final var properties = new VueSecmanSecurityProperties();
             properties.setBrand("Petclinic");
             return properties;
         }
 
-        @Bean
-        HtmxSecmanSecurityProperties htmxSecmanSecurityProperties() {
-            return new HtmxSecmanSecurityProperties();
-        }
-
         @Bean
         CausewayConfiguration causewayConfiguration() {
             final var configuration = mock(CausewayConfiguration.class);
@@ -262,12 +270,12 @@ TestEndpoints testEndpoints() {
 
     @RestController
     static class TestEndpoints {
-        @GetMapping({"/htmx", "/htmx/object/42"})
+        @GetMapping({"/vue", "/vue/object/42"})
         String viewer() {
             return "viewer";
         }
 
-        @GetMapping("/htmx/forbidden")
+        @GetMapping("/vue/forbidden")
         org.springframework.http.ResponseEntity<Void> forbidden() {
             return org.springframework.http.ResponseEntity.status(403).build();
         }
diff --git a/viewers/webcomponents/vue-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanSecurityFoundationTest.java b/viewers/webcomponents/vue-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanSecurityFoundationTest.java
new file mode 100644
index 00000000000..b568c4bb83f
--- /dev/null
+++ b/viewers/webcomponents/vue-security-secman/src/test/java/org/apache/causeway/viewer/webcomponents/vue/security/secman/VueSecmanSecurityFoundationTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.webcomponents.vue.security.secman;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.causeway.core.config.CausewayConfiguration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class VueSecmanSecurityFoundationTest {
+
+    @Test
+    void validatorRequiresExplicitCsrfSetting() {
+        final var properties = new VueSecmanSecurityProperties();
+        final var paths = new VueSecmanPaths(properties);
+
+        assertThatThrownBy(() -> new VueSecmanSecurityValidator(configuration(false), properties, paths)
+                .afterPropertiesSet())
+                .isInstanceOf(IllegalStateException.class)
+                .hasMessageContaining(VueSecmanSecurityValidator.CSRF_PROPERTY);
+    }
+
+    @Test
+    void validatorAcceptsBoundedDistinctConfiguration() {
+        final var properties = new VueSecmanSecurityProperties();
+        final var paths = new VueSecmanPaths(properties);
+
+        new VueSecmanSecurityValidator(configuration(true), properties, paths).afterPropertiesSet();
+    }
+
+    @Test
+    void validatorRejectsPathAndChainConflicts() {
+        final var properties = new VueSecmanSecurityProperties();
+        properties.setLogoutPath(properties.getLoginPath());
+
+        assertThatThrownBy(() -> new VueSecmanSecurityValidator(
+                configuration(true), properties, new VueSecmanPaths(properties)).afterPropertiesSet())
+                .hasMessageContaining("must be distinct");
+
+        properties.setLogoutPath("/logout");
+        properties.setFilterChainOrder(1_001);
+        assertThatThrownBy(() -> new VueSecmanSecurityValidator(
+                configuration(true), properties, new VueSecmanPaths(properties)).afterPropertiesSet())
+                .hasMessageContaining("between -1000 and 1000");
+    }
+
+    @Test
+    void destinationPolicyAllowsOnlyCanonicalVueRoutes() {
+        final var paths = new VueSecmanPaths(new VueSecmanSecurityProperties());
+
+        assertThat(paths.isSafeViewerDestination("/vue/object/petclinic.Pet/1?tab=visits")).isTrue();
+        assertThat(paths.isSafeViewerDestination("/vue")).isTrue();
+        assertThat(paths.isSafeViewerDestination("https://attacker.example/vue")).isFalse();
+        assertThat(paths.isSafeViewerDestination("//attacker.example/vue")).isFalse();
+        assertThat(paths.isSafeViewerDestination("/vue/login")).isFalse();
+        assertThat(paths.isSafeViewerDestination("/vue/logout")).isFalse();
+        assertThat(paths.isSafeViewerDestination("/vue/authentication")).isFalse();
+        assertThat(paths.isSafeViewerDestination("/vue/assets/app.js")).isFalse();
+        assertThat(paths.isSafeViewerDestination("/graphql")).isFalse();
+        assertThat(paths.isSafeViewerDestination("/wicket")).isFalse();
+    }
+
+    @Test
+    void propertiesRejectMalformedPaths() {
+        final var properties = new VueSecmanSecurityProperties();
+        assertThatThrownBy(() -> properties.setBasePath("//attacker.example"))
+                .isInstanceOf(IllegalArgumentException.class);
+        assertThatThrownBy(() -> properties.setAuthenticationContextPath("/../authentication"))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    private static CausewayConfiguration configuration(final boolean allowCsrfFilters) {
+        final var configuration = mock(CausewayConfiguration.class);
+        final var security = mock(CausewayConfiguration.Security.class);
+        final var spring = mock(CausewayConfiguration.Security.Spring.class);
+        when(configuration.security()).thenReturn(security);
+        when(security.spring()).thenReturn(spring);
+        when(spring.allowCsrfFilters()).thenReturn(allowCsrfFilters);
+        return configuration;
+    }
+}
diff --git a/viewers/webcomponents/vue/README.adoc b/viewers/webcomponents/vue/README.adoc
index a8ce1b12141..f6d760f4fc1 100644
--- a/viewers/webcomponents/vue/README.adoc
+++ b/viewers/webcomponents/vue/README.adoc
@@ -136,6 +136,17 @@ The default result policy resolves those values as same-origin full-document tar
 A local-resource path named `/logout` remains ordinary navigation and is never treated as authentication.
 Hosts may claim result policy first to replace or further restrict this behavior.
 
+== Secured host composition
+
+Authentication remains an application responsibility.
+A secured application can fetch its current session and CSRF context before mounting, bind a CSRF-decorating executor with `:executor.prop` on its stable `<cw-graphql-client>`, render current-user and native POST logout controls in its authored shell, and claim exact framework Logout through the typed action policy.
+The viewer neither fetches credentials nor CSRF evidence, creates endpoints, persists tokens, or infers authentication from routes or `LocalResourcePathValue` results.
+A host that does not supply this context makes no authentication bootstrap request and retains ordinary executor behavior.
+
+The optional `causeway-viewer-webcomponents-vue-security-secman` server module provides one local SecMan and Spring Security implementation of these application-owned integration points.
+It serves a JavaScript-independent login page and bounded no-store authentication context, protects Vue and GraphQL routes, restores safe Vue GET destinations, and owns CSRF-protected session logout.
+See `../vue-security-secman/README.adoc` and the secured Vue Petclinic variant for a complete host composition.
+
 The canonical route grammar is `<base-path>/object/<logical-type>/<identifier>` using independently canonical UTF-8 percent-encoded segments.
 Malformed, unsafe, non-canonical, or oversized segments fail before an object context is mounted.
 


(causeway) 09/16: feat(webcomponents): support logout policy and local redirects

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit a7a6cc96501e4d961633266cdd8626a65a849466
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 3 13:36:36 2026 +0100

    feat(webcomponents): support logout policy and local redirects
---
 .../tasks.md                                       |  54 +-
 viewers/webcomponents/foundation/USAGE.adoc        |  13 +-
 .../webcomponents/foundation/src/action-result.mjs |  70 +++
 .../foundation/src/host-operation-policy.mjs       |  43 ++
 viewers/webcomponents/foundation/src/index.mjs     |   3 +
 .../foundation/src/local-resource-policy.mjs       |  99 ++++
 .../foundation/src/object-context-controller.mjs   |  26 +-
 .../foundation/src/service-action-context.mjs      |  34 +-
 viewers/webcomponents/foundation/src/types.mjs     |   2 +
 .../test/application-menu-client.test.mjs          |  50 ++
 .../test/host-navigation-policy.test.mjs           |  99 ++++
 .../webcomponents/htmx-security-secman/README.adoc |   2 +
 viewers/webcomponents/htmx/README.adoc             |   6 +
 .../causeway/webcomponents/htmx/document.html      |   2 +-
 .../resources/causeway-htmx/causeway-htmx.mjs      |  46 +-
 .../htmx/HtmxViewerControllerTest.java             |   1 +
 .../petclinic/PetClinicHtmxPlaywrightTest.java     |  30 +
 .../sample/petclinic/domain/ViewerFallback.java    |  23 +
 .../resources/petclinic-local-resource.html        |  29 +
 .../sample-vue-petclinic/frontend/src/main.ts      |   1 +
 .../resources/static/vue/assets/index-CklV2Pe0.js  |   2 -
 .../resources/static/vue/assets/index-CyDhqcW-.js  |   2 +
 ...index-CklV2Pe0.js.map => index-CyDhqcW-.js.map} |   2 +-
 .../src/main/resources/static/vue/index.html       |   2 +-
 .../vue/petclinic/PetClinicVuePlaywrightTest.java  |  42 +-
 viewers/webcomponents/vue/README.adoc              |  15 +-
 viewers/webcomponents/vue/dist/causeway-vue.js     | 603 ++++++++++++---------
 viewers/webcomponents/vue/dist/causeway-vue.js.map |   2 +-
 viewers/webcomponents/vue/dist/contracts.d.ts      |  18 +-
 viewers/webcomponents/vue/dist/contracts.d.ts.map  |   2 +-
 viewers/webcomponents/vue/dist/host-operation.d.ts |   8 +
 .../webcomponents/vue/dist/host-operation.d.ts.map |   1 +
 viewers/webcomponents/vue/dist/index.d.ts          |   2 +
 viewers/webcomponents/vue/dist/index.d.ts.map      |   2 +-
 viewers/webcomponents/vue/dist/local-resource.d.ts |  21 +
 .../webcomponents/vue/dist/local-resource.d.ts.map |   1 +
 viewers/webcomponents/vue/dist/plugin.d.ts.map     |   2 +-
 viewers/webcomponents/vue/dist/policy.d.ts         |   1 +
 viewers/webcomponents/vue/dist/policy.d.ts.map     |   2 +-
 viewers/webcomponents/vue/src/contracts.ts         |  20 +-
 viewers/webcomponents/vue/src/host-operation.ts    |  43 ++
 viewers/webcomponents/vue/src/index.ts             |   2 +
 viewers/webcomponents/vue/src/local-resource.ts    |  97 ++++
 viewers/webcomponents/vue/src/plugin.ts            |   1 +
 viewers/webcomponents/vue/src/policy.ts            |  68 ++-
 .../webcomponents/vue/test/local-resource.test.ts  |  59 ++
 .../vue/test/policy-integration.test.ts            |  69 +++
 47 files changed, 1365 insertions(+), 357 deletions(-)

diff --git a/openspec/changes/support-web-component-logout-and-local-redirects/tasks.md b/openspec/changes/support-web-component-logout-and-local-redirects/tasks.md
index cd3626c4188..1869c3f4d6a 100644
--- a/openspec/changes/support-web-component-logout-and-local-redirects/tasks.md
+++ b/openspec/changes/support-web-component-logout-and-local-redirects/tasks.md
@@ -1,44 +1,44 @@
 ## 1. Reproduce and Bound the Contracts
 
-- [ ] 1.1 Add focused browser or integration coverage that reproduces the current HTMX and Vue framework Logout GraphQL failure before changing dispatch policy.
-- [ ] 1.2 Record the exact `causeway.security.LogoutMenu#logout` identity, cancelable action-request contract, `LocalResourcePathValue` GraphQL shape, and existing authenticated HTMX exclusion as compatibility baselines.
-- [ ] 1.3 Add negative fixtures for a similarly named application action and a non-logout local-resource path so exact matching remains testable.
+- [x] 1.1 Add focused browser or integration coverage that reproduces the current HTMX and Vue framework Logout GraphQL failure before changing dispatch policy.
+- [x] 1.2 Record the exact `causeway.security.LogoutMenu#logout` identity, cancelable action-request contract, `LocalResourcePathValue` GraphQL shape, and existing authenticated HTMX exclusion as compatibility baselines.
+- [x] 1.3 Add negative fixtures for a similarly named application action and a non-logout local-resource path so exact matching remains testable.
 
 ## 2. Foundation Action and Result Semantics
 
-- [ ] 2.1 Centralize the exact reserved framework Logout action identity and helper without importing core security into browser components.
-- [ ] 2.2 Verify and, where necessary, strengthen action controls so a synchronously claimed or canceled `causeway-action-request` performs no validation, invocation, or synthetic result emission.
-- [ ] 2.3 Centralize object and service action-result normalization and classify exact `LocalResourcePathValue` output as an immutable local-resource semantic result.
-- [ ] 2.4 Fail closed for malformed local-resource payloads while preserving existing scalar, object, collection, void, Blob, Clob, and unsupported structured-result behavior.
-- [ ] 2.5 Add foundation unit and integration tests for exact Logout matching, pre-invocation cancellation, result classification, and nonmatching actions.
+- [x] 2.1 Centralize the exact reserved framework Logout action identity and helper without importing core security into browser components.
+- [x] 2.2 Verify and, where necessary, strengthen action controls so a synchronously claimed or canceled `causeway-action-request` performs no validation, invocation, or synthetic result emission.
+- [x] 2.3 Centralize object and service action-result normalization and classify exact `LocalResourcePathValue` output as an immutable local-resource semantic result.
+- [x] 2.4 Fail closed for malformed local-resource payloads while preserving existing scalar, object, collection, void, Blob, Clob, and unsupported structured-result behavior.
+- [x] 2.5 Add foundation unit and integration tests for exact Logout matching, pre-invocation cancellation, result classification, and nonmatching actions.
 
 ## 3. Safe Local-Resource Navigation
 
-- [ ] 3.1 Implement a reusable local-resource target resolver that accepts an explicit application-local base and rejects malformed, scheme-relative, cross-origin, credential-bearing, escaping, or unsupported targets.
-- [ ] 3.2 Implement `SAME_WINDOW` full-document navigation and opener-isolated `NEW_WINDOW` behavior without passing resource targets to an object router.
-- [ ] 3.3 Preserve host policy claims and bounded error reporting so applications can replace or further restrict default navigation without mutating canonical results.
-- [ ] 3.4 Add unit coverage for root and nested deployment bases, context preservation exactly once, both opening strategies, popup-policy behavior, and unsafe-target rejection.
+- [x] 3.1 Implement a reusable local-resource target resolver that accepts an explicit application-local base and rejects malformed, scheme-relative, cross-origin, credential-bearing, escaping, or unsupported targets.
+- [x] 3.2 Implement `SAME_WINDOW` full-document navigation and opener-isolated `NEW_WINDOW` behavior without passing resource targets to an object router.
+- [x] 3.3 Preserve host policy claims and bounded error reporting so applications can replace or further restrict default navigation without mutating canonical results.
+- [x] 3.4 Add unit coverage for root and nested deployment bases, context preservation exactly once, both opening strategies, popup-policy behavior, and unsafe-target rejection.
 
 ## 4. HTMX Host Policy
 
-- [ ] 4.1 Remove the unclaimed framework Logout action from generic HTMX semantic menu controls while leaving similarly named application actions untouched.
-- [ ] 4.2 Add a fail-closed HTMX action-request guard that blocks stale or custom framework Logout requests before GraphQL and composes with the existing authentication metadata handler.
-- [ ] 4.3 Extend the HTMX document/runtime contract with an authoritative servlet-context local-resource base and handle local-resource semantic results through the shared resolver.
-- [ ] 4.4 Verify that authenticated HTMX SecMan continues to use its accessible CSRF-protected POST logout control and that generic HTMX adds no security endpoint or session behavior.
-- [ ] 4.5 Add HTMX unit, controller, integration, and headless browser coverage for default Logout suppression, stale-request blocking, claimed authenticated logout, local redirects, nested contexts, and unsafe targets.
+- [x] 4.1 Remove the unclaimed framework Logout action from generic HTMX semantic menu controls while leaving similarly named application actions untouched.
+- [x] 4.2 Add a fail-closed HTMX action-request guard that blocks stale or custom framework Logout requests before GraphQL and composes with the existing authentication metadata handler.
+- [x] 4.3 Extend the HTMX document/runtime contract with an authoritative servlet-context local-resource base and handle local-resource semantic results through the shared resolver.
+- [x] 4.4 Verify that authenticated HTMX SecMan continues to use its accessible CSRF-protected POST logout control and that generic HTMX adds no security endpoint or session behavior.
+- [x] 4.5 Add HTMX unit, controller, integration, and headless browser coverage for default Logout suppression, stale-request blocking, claimed authenticated logout, local redirects, nested contexts, and unsafe targets.
 
 ## 5. Vue Host Policy
 
-- [ ] 5.1 Add a typed pre-invocation Vue action policy with canonical detail, single-claim semantics, immutable policy context, disposal safety, and error-policy integration.
-- [ ] 5.2 Remove the unclaimed framework Logout action from Vue semantic menu controls and block direct or stale requests before GraphQL while preserving nonmatching actions.
-- [ ] 5.3 Add a documented Vue application-local resource base or resolver option independent of the Vue object-route base.
-- [ ] 5.4 Handle local-resource semantic results through host result claims and the shared safe-navigation contract.
-- [ ] 5.5 Add Vue unit, declaration, package-consumer, and headless browser coverage for action claims, default Logout suppression, no GraphQL invocation, local redirects, nested deployment, unsafe targets, and route lifecycle stability.
+- [x] 5.1 Add a typed pre-invocation Vue action policy with canonical detail, single-claim semantics, immutable policy context, disposal safety, and error-policy integration.
+- [x] 5.2 Remove the unclaimed framework Logout action from Vue semantic menu controls and block direct or stale requests before GraphQL while preserving nonmatching actions.
+- [x] 5.3 Add a documented Vue application-local resource base or resolver option independent of the Vue object-route base.
+- [x] 5.4 Handle local-resource semantic results through host result claims and the shared safe-navigation contract.
+- [x] 5.5 Add Vue unit, declaration, package-consumer, and headless browser coverage for action claims, default Logout suppression, no GraphQL invocation, local redirects, nested deployment, unsafe targets, and route lifecycle stability.
 
 ## 6. Samples, Documentation, and Verification
 
-- [ ] 6.1 Add or reuse bounded typed `LocalResourcePath` acceptance actions without placing a misleading framework Logout action in an unauthenticated sample menu.
-- [ ] 6.2 Update foundation, HTMX, Vue, and security documentation to distinguish host-owned secure logout from ordinary local-resource navigation and explain deployment-base configuration.
-- [ ] 6.3 Regenerate and verify committed frontend assets and confirm stale-generated-output checks pass.
-- [ ] 6.4 Run foundation and viewer frontend tests, Vue declaration/build/pack verification, relevant Maven integration suites, and HTMX and Vue headless Playwright suites.
-- [ ] 6.5 Run secured HTMX logout regression coverage, script syntax checks, Apache RAT, IDE inspections/build, strict OpenSpec validation, and whitespace checks.
+- [x] 6.1 Add or reuse bounded typed `LocalResourcePath` acceptance actions without placing a misleading framework Logout action in an unauthenticated sample menu.
+- [x] 6.2 Update foundation, HTMX, Vue, and security documentation to distinguish host-owned secure logout from ordinary local-resource navigation and explain deployment-base configuration.
+- [x] 6.3 Regenerate and verify committed frontend assets and confirm stale-generated-output checks pass.
+- [x] 6.4 Run foundation and viewer frontend tests, Vue declaration/build/pack verification, relevant Maven integration suites, and HTMX and Vue headless Playwright suites.
+- [x] 6.5 Run secured HTMX logout regression coverage, script syntax checks, Apache RAT, IDE inspections/build, strict OpenSpec validation, and whitespace checks.
diff --git a/viewers/webcomponents/foundation/USAGE.adoc b/viewers/webcomponents/foundation/USAGE.adoc
index f1b8a52023e..769c51c4af7 100644
--- a/viewers/webcomponents/foundation/USAGE.adoc
+++ b/viewers/webcomponents/foundation/USAGE.adoc
@@ -245,7 +245,9 @@ Safe actions use their advertised rich query invocation, while mutating actions
 No object bookmark or `_target` is manufactured for a service action.
 `causeway-action-request`, `causeway-action-prompt-state-change`, and `causeway-action-result` retain their established names.
 Service interaction events add `target: {kind: "service", logicalTypeName}`, `serviceLogicalTypeName`, and `identity: null`; object interaction identities remain unchanged.
-Routing, result rendering, browser history, home behavior, and navigation remain host policy.
+Routing, result rendering, browser history, home behavior, authentication, and navigation remain host policy.
+Every action publishes one cancelable `causeway-action-request` before validation or invocation; a host that synchronously claims or cancels that request prevents validation, GraphQL invocation, and synthetic result emission.
+The reserved framework operation helper matches only `causeway.security.LogoutMenu#logout`, so similarly labelled application actions remain ordinary domain actions.
 Selecting an enabled menu action closes its transient menu before interaction presentation, clicking outside closes an expanded menu without moving focus, and cancellation restores focus to the visible originating disclosure.
 
 Each non-empty bar renders a labelled `nav` landmark.
@@ -576,6 +578,15 @@ Direct compositions expose `data-causeway-associated-member`, `data-causeway-act
 Use `--causeway-associated-action-gap` and `--causeway-associated-action-margin-block-end` to adjust wrapping presentation without inline styles or toolkit-specific elements.
 Generated `<cw-object>` layouts retain semantically equivalent associated-member and action-group hooks from effective grid XML.
 
+== Typed action results and local resources
+
+Object and service interactions use the same immutable result normalization.
+An exact GraphQL `LocalResourcePathValue` wrapper becomes `{kind: "local-resource", value: {path, openUrlStrategy}}`; malformed wrappers become bounded unsupported results rather than scalar or object values.
+Foundation exports a host-facing resolver and navigator that require an explicit application-local base, preserve a nested deployment context exactly once, and reject malformed, scheme-relative, cross-origin, credential-bearing, escaping, or unsupported targets.
+`SAME_WINDOW` performs full-document navigation and `NEW_WINDOW` requests an opener-isolated window.
+These values are navigation results, not authentication instructions: a path equal to `/logout` does not acquire logout semantics.
+Secure logout remains a host concern because only the host owns CSRF evidence, sessions, provider logout, and post-logout destinations.
+
 == Standalone action-result collections
 
 `<cw-standalone-collection>` presents a normalized collection-valued action result without pretending that the result is an object member.
diff --git a/viewers/webcomponents/foundation/src/action-result.mjs b/viewers/webcomponents/foundation/src/action-result.mjs
new file mode 100644
index 00000000000..c44ed08bc27
--- /dev/null
+++ b/viewers/webcomponents/foundation/src/action-result.mjs
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {namedType} from './introspection.mjs';
+import {InteractionResultKind} from './types.mjs';
+
+export const LOCAL_RESOURCE_PATH_GRAPHQL_TYPE = 'LocalResourcePathValue';
+export const LOCAL_RESOURCE_STRATEGIES = Object.freeze(['SAME_WINDOW', 'NEW_WINDOW']);
+
+export function normalizeActionResult(value, typeRef) {
+  const inner = innermostType(typeRef);
+  const list = unwrapNonNull(typeRef)?.kind === 'LIST';
+  if (value == null) {
+    return Object.freeze({kind: InteractionResultKind.VOID, value: null});
+  }
+  if (list || Array.isArray(value)) {
+    return Object.freeze({kind: InteractionResultKind.COLLECTION, value});
+  }
+  if (namedType(typeRef) === LOCAL_RESOURCE_PATH_GRAPHQL_TYPE) {
+    return normalizeLocalResourceResult(value);
+  }
+  if (inner?.kind === 'SCALAR' || inner?.kind === 'ENUM' || typeof value !== 'object') {
+    return Object.freeze({kind: InteractionResultKind.SCALAR, value});
+  }
+  return Object.freeze({kind: InteractionResultKind.OBJECT, value});
+}
+
+function normalizeLocalResourceResult(value) {
+  const path = typeof value?.path === 'string' ? value.path.trim() : '';
+  const openUrlStrategy = typeof value?.openUrlStrategy === 'string' ? value.openUrlStrategy : '';
+  if (!path || !LOCAL_RESOURCE_STRATEGIES.includes(openUrlStrategy)) {
+    return Object.freeze({
+      kind: InteractionResultKind.UNSUPPORTED,
+      value: null,
+      reason: 'LOCAL_RESOURCE_RESULT_INVALID'
+    });
+  }
+  return Object.freeze({
+    kind: InteractionResultKind.LOCAL_RESOURCE,
+    value: Object.freeze({path, openUrlStrategy})
+  });
+}
+
+function unwrapNonNull(typeRef) {
+  let current = typeRef;
+  while (current?.kind === 'NON_NULL') current = current.ofType;
+  return current;
+}
+
+function innermostType(typeRef) {
+  let current = typeRef;
+  while (current?.ofType) current = current.ofType;
+  return current;
+}
diff --git a/viewers/webcomponents/foundation/src/host-operation-policy.mjs b/viewers/webcomponents/foundation/src/host-operation-policy.mjs
new file mode 100644
index 00000000000..52aaea054f8
--- /dev/null
+++ b/viewers/webcomponents/foundation/src/host-operation-policy.mjs
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export const FRAMEWORK_LOGOUT_ACTION = Object.freeze({
+  serviceLogicalTypeName: 'causeway.security.LogoutMenu',
+  actionId: 'logout'
+});
+
+export function isFrameworkLogoutAction(value) {
+  return value?.serviceLogicalTypeName === FRAMEWORK_LOGOUT_ACTION.serviceLogicalTypeName
+    && value?.actionId === FRAMEWORK_LOGOUT_ACTION.actionId;
+}
+
+export function removeFrameworkLogoutMenuActions(root = globalThis.document) {
+  if (!root?.querySelectorAll) return 0;
+  let removed = 0;
+  for (const control of root.querySelectorAll('[data-causeway-service-action]')) {
+    if (!isFrameworkLogoutAction({
+      serviceLogicalTypeName: control.getAttribute('data-service-logical-type'),
+      actionId: control.getAttribute('data-action-id')
+    })) continue;
+    const region = control.closest?.('[data-causeway-service-action-region]') ?? control;
+    region.remove?.();
+    removed += 1;
+  }
+  return removed;
+}
diff --git a/viewers/webcomponents/foundation/src/index.mjs b/viewers/webcomponents/foundation/src/index.mjs
index 9ec3985b730..c9967a8b88d 100644
--- a/viewers/webcomponents/foundation/src/index.mjs
+++ b/viewers/webcomponents/foundation/src/index.mjs
@@ -18,6 +18,7 @@
  */
 
 export * from './action-element.mjs';
+export * from './action-result.mjs';
 export * from './action-results-element.mjs';
 export * from './action-widget.mjs';
 export * from './breadcrumbs-element.mjs';
@@ -37,9 +38,11 @@ export * from './graphql-client.mjs';
 export * from './graphql-client-element.mjs';
 export * from './graphql-executor.mjs';
 export * from './grid-widget.mjs';
+export * from './host-operation-policy.mjs';
 export * from './interaction-controller-element.mjs';
 export * from './interaction-operations.mjs';
 export * from './introspection.mjs';
+export * from './local-resource-policy.mjs';
 export * from './menu-context-controller.mjs';
 export * from './menu-layout.mjs';
 export * from './menubar-element.mjs';
diff --git a/viewers/webcomponents/foundation/src/local-resource-policy.mjs b/viewers/webcomponents/foundation/src/local-resource-policy.mjs
new file mode 100644
index 00000000000..7bb1d6cd928
--- /dev/null
+++ b/viewers/webcomponents/foundation/src/local-resource-policy.mjs
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {LOCAL_RESOURCE_STRATEGIES} from './action-result.mjs';
+
+export class LocalResourceNavigationError extends Error {
+  constructor(code, message) {
+    super(message);
+    this.name = 'LocalResourceNavigationError';
+    this.code = code;
+  }
+}
+
+export function resolveLocalResourceTarget(value, {
+  location = globalThis.location,
+  applicationBase = '/'
+} = {}) {
+  const path = typeof value?.path === 'string' ? value.path.trim() : '';
+  const strategy = value?.openUrlStrategy;
+  if (!path || !LOCAL_RESOURCE_STRATEGIES.includes(strategy)) {
+    throw navigationError('LOCAL_RESOURCE_INVALID', 'The local-resource result is incomplete.');
+  }
+  if (/^[a-z][a-z\d+.-]*:/i.test(path) || path.startsWith('//') || path.includes('\\') || /[\u0000-\u001f\u007f]/.test(path)) {
+    throw navigationError('LOCAL_RESOURCE_TARGET_UNSAFE', 'The local-resource target is not an application-local path.');
+  }
+  const origin = new URL(location?.href ?? String(location));
+  const base = resolveApplicationBase(applicationBase, origin);
+  const basePath = normalizedBasePath(base.pathname);
+  const supplied = path.startsWith('/') ? path : `/${path}`;
+  const alreadyBased = basePath !== '/' && (supplied === basePath || supplied.startsWith(`${basePath}/`) || supplied.startsWith(`${basePath}?`) || supplied.startsWith(`${basePath}#`));
+  const relative = alreadyBased ? supplied : `${basePath === '/' ? '' : basePath}/${supplied.replace(/^\/+/, '')}`;
+  const target = new URL(relative, origin.origin);
+  if (target.origin !== origin.origin || target.username || target.password || !withinBase(target.pathname, basePath)) {
+    throw navigationError('LOCAL_RESOURCE_TARGET_UNSAFE', 'The local-resource target escapes the configured application boundary.');
+  }
+  return Object.freeze({url: target, openUrlStrategy: strategy});
+}
+
+export function navigateLocalResource(value, {
+  location = globalThis.location,
+  applicationBase = '/',
+  open = globalThis.open?.bind(globalThis)
+} = {}) {
+  const target = resolveLocalResourceTarget(value, {location, applicationBase});
+  if (target.openUrlStrategy === 'SAME_WINDOW') {
+    if (typeof location?.assign !== 'function') {
+      throw navigationError('LOCAL_RESOURCE_NAVIGATION_UNAVAILABLE', 'Same-window navigation is unavailable.');
+    }
+    location.assign(target.url.href);
+    return target;
+  }
+  if (typeof open !== 'function') {
+    throw navigationError('LOCAL_RESOURCE_NAVIGATION_UNAVAILABLE', 'New-window navigation is unavailable.');
+  }
+  const opened = open(target.url.href, '_blank', 'noopener,noreferrer');
+  if (opened && 'opener' in opened) opened.opener = null;
+  return target;
+}
+
+function resolveApplicationBase(applicationBase, origin) {
+  const text = String(applicationBase ?? '/').trim() || '/';
+  if (text.startsWith('//') || /[\u0000-\u001f\u007f]/.test(text)) {
+    throw navigationError('LOCAL_RESOURCE_BASE_INVALID', 'The application-local resource base is invalid.');
+  }
+  const base = new URL(text.endsWith('/') ? text : `${text}/`, origin.origin);
+  if (base.origin !== origin.origin || base.username || base.password) {
+    throw navigationError('LOCAL_RESOURCE_BASE_INVALID', 'The application-local resource base must be same-origin.');
+  }
+  return base;
+}
+
+function normalizedBasePath(pathname) {
+  const normalized = pathname.replace(/\/+$/, '');
+  return normalized || '/';
+}
+
+function withinBase(pathname, basePath) {
+  return basePath === '/' || pathname === basePath || pathname.startsWith(`${basePath}/`);
+}
+
+function navigationError(code, message) {
+  return new LocalResourceNavigationError(code, message);
+}
diff --git a/viewers/webcomponents/foundation/src/object-context-controller.mjs b/viewers/webcomponents/foundation/src/object-context-controller.mjs
index 393bddf05c8..7e790792400 100644
--- a/viewers/webcomponents/foundation/src/object-context-controller.mjs
+++ b/viewers/webcomponents/foundation/src/object-context-controller.mjs
@@ -25,6 +25,7 @@ import {
   extractActionInvocationResult,
   secureActionInvocationResult
 } from './action-dispatch.mjs';
+import {normalizeActionResult} from './action-result.mjs';
 import {createSemanticEvent, OBJECT_CONTEXT_STATE_EVENT} from './context-events.mjs';
 import {fieldsByName, namedType} from './introspection.mjs';
 import {
@@ -47,7 +48,7 @@ import {
 } from './selection.mjs';
 import {assertGraphQLName} from './schema-names.mjs';
 import {fetchStructuralResource, StructuralResourceError} from './structural-resource.mjs';
-import {InteractionResultKind, InteractionStatus, ObjectContextStatus, RequirementStatus} from './types.mjs';
+import {InteractionStatus, ObjectContextStatus, RequirementStatus} from './types.mjs';
 
 export {StructuralResourceError} from './structural-resource.mjs';
 
@@ -1186,29 +1187,6 @@ function interactionResult(status, data = null, errors = [], operation = null) {
   });
 }
 
-function normalizeActionResult(value, typeRef) {
-  const inner = innermostType(typeRef);
-  const list = unwrapNonNull(typeRef)?.kind === 'LIST';
-  if (value == null) {
-    return Object.freeze({kind: InteractionResultKind.VOID, value: null});
-  }
-  if (list || Array.isArray(value)) {
-    return Object.freeze({kind: InteractionResultKind.COLLECTION, value});
-  }
-  if (inner?.kind === 'SCALAR' || inner?.kind === 'ENUM' || typeof value !== 'object') {
-    return Object.freeze({kind: InteractionResultKind.SCALAR, value});
-  }
-  return Object.freeze({kind: InteractionResultKind.OBJECT, value});
-}
-
-function unwrapNonNull(typeRef) {
-  let current = typeRef;
-  while (current?.kind === 'NON_NULL') {
-    current = current.ofType;
-  }
-  return current;
-}
-
 function commandError(error) {
   if (typeof error === 'string') {
     return Object.freeze({message: error, path: [], extensions: Object.freeze({})});
diff --git a/viewers/webcomponents/foundation/src/service-action-context.mjs b/viewers/webcomponents/foundation/src/service-action-context.mjs
index 04be92e72eb..1c29e6330a0 100644
--- a/viewers/webcomponents/foundation/src/service-action-context.mjs
+++ b/viewers/webcomponents/foundation/src/service-action-context.mjs
@@ -25,6 +25,7 @@ import {
   extractActionInvocationResult,
   secureActionInvocationResult
 } from './action-dispatch.mjs';
+import {normalizeActionResult} from './action-result.mjs';
 import {fieldsByName, namedType} from './introspection.mjs';
 import {
   argumentsFromValues,
@@ -33,7 +34,7 @@ import {
   normalizeAutoCompleteWindow,
   resultSelectionForType
 } from './interaction-operations.mjs';
-import {InteractionResultKind, InteractionStatus} from './types.mjs';
+import {InteractionStatus} from './types.mjs';
 
 export class ServiceActionContextController {
   constructor({
@@ -494,37 +495,6 @@ function interactionResult(status, data = null, errors = [], operation = null) {
   });
 }
 
-function normalizeActionResult(value, typeRef) {
-  const inner = innermostType(typeRef);
-  const list = unwrapNonNull(typeRef)?.kind === 'LIST';
-  if (value == null) {
-    return Object.freeze({kind: InteractionResultKind.VOID, value: null});
-  }
-  if (list || Array.isArray(value)) {
-    return Object.freeze({kind: InteractionResultKind.COLLECTION, value});
-  }
-  if (inner?.kind === 'SCALAR' || inner?.kind === 'ENUM' || typeof value !== 'object') {
-    return Object.freeze({kind: InteractionResultKind.SCALAR, value});
-  }
-  return Object.freeze({kind: InteractionResultKind.OBJECT, value});
-}
-
-function unwrapNonNull(typeRef) {
-  let current = typeRef;
-  while (current?.kind === 'NON_NULL') {
-    current = current.ofType;
-  }
-  return current;
-}
-
-function innermostType(typeRef) {
-  let current = typeRef;
-  while (current?.ofType) {
-    current = current.ofType;
-  }
-  return current;
-}
-
 function commandError(error) {
   if (typeof error === 'string') {
     return Object.freeze({message: error, path: [], extensions: Object.freeze({})});
diff --git a/viewers/webcomponents/foundation/src/types.mjs b/viewers/webcomponents/foundation/src/types.mjs
index 35fea3cd611..05e6e3f15b2 100644
--- a/viewers/webcomponents/foundation/src/types.mjs
+++ b/viewers/webcomponents/foundation/src/types.mjs
@@ -236,6 +236,8 @@ export const InteractionResultKind = Object.freeze({
   OBJECT: 'object',
   COLLECTION: 'collection',
   SCALAR: 'scalar',
+  LOCAL_RESOURCE: 'local-resource',
+  UNSUPPORTED: 'unsupported',
   VOID: 'void'
 });
 
diff --git a/viewers/webcomponents/foundation/test/application-menu-client.test.mjs b/viewers/webcomponents/foundation/test/application-menu-client.test.mjs
index 365c48c93bf..d80e3a4ab9a 100644
--- a/viewers/webcomponents/foundation/test/application-menu-client.test.mjs
+++ b/viewers/webcomponents/foundation/test/application-menu-client.test.mjs
@@ -147,6 +147,56 @@ test('service action adapter reuses parameter preparation, validation, safe invo
   assert.deepEqual(context.interactionTarget, {kind: 'service', logicalTypeName: SAMPLE_SERVICE_LOGICAL_TYPE});
 });
 
+test('service actions preserve typed local-resource results distinctly', async () => {
+  const types = createMenuGraphQLTypes();
+  const actionTypeName = `${SAMPLE_SERVICE_TYPE}__openGuide__gqlv_action`;
+  const invokeTypeName = `${actionTypeName}_invoke`;
+  const ref = (kind, name, ofType = null) => ({kind, name, ofType});
+  const scalarRef = name => ref('SCALAR', name);
+  const namedRef = name => ref('OBJECT', name);
+  const field = (name, type) => ({name, description: null, args: [], type});
+  const objectType = (name, fields) => ({
+    name, kind: 'OBJECT', description: null, fields, inputFields: [], enumValues: [], possibleTypes: []
+  });
+  types.get(SAMPLE_SERVICE_TYPE).fields.push(field('openGuide', namedRef(actionTypeName)));
+  types.set(actionTypeName, objectType(actionTypeName, [
+    field('hidden', scalarRef('Boolean')),
+    field('disabled', scalarRef('String')),
+    field('metadata', namedRef('RichMemberMetadata')),
+    field('validate', scalarRef('String')),
+    field('invoke', namedRef(invokeTypeName))
+  ]));
+  types.set(invokeTypeName, objectType(invokeTypeName, [field('results', namedRef('LocalResourcePathValue'))]));
+  types.set('LocalResourcePathValue', objectType('LocalResourcePathValue', [
+    field('path', scalarRef('String')),
+    field('openUrlStrategy', ref('ENUM', 'OpenUrlStrategy'))
+  ]));
+  const base = createMenuGraphQLExecutor({types});
+  let invocation;
+  const executor = async request => {
+    if (request.operationName === 'CausewayInvokeServiceAction' && request.document.includes('openGuide')) {
+      invocation = request;
+      return {data: {rich: {causeway_webcomponents_sample_SampleMenu: {
+        openGuide: {invoke: {results: {path: '/guide', openUrlStrategy: 'SAME_WINDOW'}}}
+      }}}};
+    }
+    return base(request);
+  };
+  const context = new ServiceActionContextController({
+    client: new CausewayGraphQLClient({executor}),
+    logicalTypeName: SAMPLE_SERVICE_LOGICAL_TYPE
+  });
+
+  const result = await context.invokeAction('openGuide', {});
+
+  assert.equal(result.status, InteractionStatus.SUCCESS);
+  assert.deepEqual(result.data, {
+    kind: 'local-resource', value: {path: '/guide', openUrlStrategy: 'SAME_WINDOW'}
+  });
+  assert.match(invocation.document, /openUrlStrategy/);
+  assert.match(invocation.document, /path/);
+});
+
 test('service collection action requests declared wrappers in its single invocation response', async () => {
   const types = createMenuGraphQLTypes();
   const personType = 'rich__sample_Person';
diff --git a/viewers/webcomponents/foundation/test/host-navigation-policy.test.mjs b/viewers/webcomponents/foundation/test/host-navigation-policy.test.mjs
new file mode 100644
index 00000000000..c0377257740
--- /dev/null
+++ b/viewers/webcomponents/foundation/test/host-navigation-policy.test.mjs
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import {normalizeActionResult} from '../src/action-result.mjs';
+import {isFrameworkLogoutAction} from '../src/host-operation-policy.mjs';
+import {navigateLocalResource, resolveLocalResourceTarget} from '../src/local-resource-policy.mjs';
+
+const object = name => ({kind: 'OBJECT', name, ofType: null});
+const scalar = name => ({kind: 'SCALAR', name, ofType: null});
+const nonNull = type => ({kind: 'NON_NULL', name: null, ofType: type});
+
+const localResource = (path = '/guide', openUrlStrategy = 'SAME_WINDOW') => ({path, openUrlStrategy});
+const browserLocation = (href = 'https://example.test/viewer/object/Thing/1') => ({href, assign() {}});
+
+test('framework Logout matching requires the exact canonical identity', () => {
+  assert.equal(isFrameworkLogoutAction({serviceLogicalTypeName: 'causeway.security.LogoutMenu', actionId: 'logout'}), true);
+  assert.equal(isFrameworkLogoutAction({serviceLogicalTypeName: 'example.LogoutMenu', actionId: 'logout'}), false);
+  assert.equal(isFrameworkLogoutAction({serviceLogicalTypeName: 'causeway.security.LogoutMenu', actionId: 'signOut'}), false);
+  assert.equal(isFrameworkLogoutAction({serviceLogicalTypeName: 'causeway.security.LogoutMenu', actionId: 'logout', path: '/other'}), true);
+  assert.equal(isFrameworkLogoutAction({kind: 'local-resource', value: localResource('/logout')}), false);
+});
+
+test('typed local-resource results are distinct, immutable, and exact-type only', () => {
+  const result = normalizeActionResult(localResource('/guide', 'NEW_WINDOW'), nonNull(object('LocalResourcePathValue')));
+  assert.deepEqual(result, {kind: 'local-resource', value: {path: '/guide', openUrlStrategy: 'NEW_WINDOW'}});
+  assert.equal(Object.isFrozen(result), true);
+  assert.equal(Object.isFrozen(result.value), true);
+  assert.deepEqual(normalizeActionResult(localResource(), object('OtherValue')), {kind: 'object', value: localResource()});
+  assert.deepEqual(normalizeActionResult('done', scalar('String')), {kind: 'scalar', value: 'done'});
+  assert.deepEqual(normalizeActionResult(null, object('LocalResourcePathValue')), {kind: 'void', value: null});
+});
+
+test('malformed typed local-resource results fail closed', () => {
+  for (const value of [{}, {path: '/guide'}, {path: '', openUrlStrategy: 'SAME_WINDOW'}, {path: '/guide', openUrlStrategy: 'OTHER'}]) {
+    assert.deepEqual(normalizeActionResult(value, object('LocalResourcePathValue')), {
+      kind: 'unsupported', value: null, reason: 'LOCAL_RESOURCE_RESULT_INVALID'
+    });
+  }
+});
+
+test('local-resource resolution preserves root and nested application bases exactly once', () => {
+  const location = browserLocation();
+  assert.equal(resolveLocalResourceTarget(localResource('/guide'), {location}).url.href, 'https://example.test/guide');
+  assert.equal(resolveLocalResourceTarget(localResource('/logout'), {location}).url.href, 'https://example.test/logout');
+  assert.equal(resolveLocalResourceTarget(localResource('/guide'), {location, applicationBase: '/app'}).url.href, 'https://example.test/app/guide');
+  assert.equal(resolveLocalResourceTarget(localResource('/app/guide'), {location, applicationBase: '/app'}).url.href, 'https://example.test/app/guide');
+  assert.equal(resolveLocalResourceTarget(localResource('guide?mode=full#top'), {location, applicationBase: '/app/'}).url.href,
+    'https://example.test/app/guide?mode=full#top');
+});
+
+test('local-resource resolution rejects unsafe and escaping targets', () => {
+  const location = browserLocation();
+  for (const path of ['https://evil.test/x', '//evil.test/x', 'javascript:alert(1)', '..\\secret', '../secret', '/app/../secret']) {
+    assert.throws(() => resolveLocalResourceTarget(localResource(path), {location, applicationBase: '/app'}), /application-local|escapes/);
+  }
+  assert.throws(() => resolveLocalResourceTarget(localResource('/guide', 'OTHER'), {location}), /incomplete/);
+  assert.throws(() => resolveLocalResourceTarget(localResource('/guide'), {location, applicationBase: 'https://evil.test/app'}), /same-origin/);
+});
+
+test('local-resource navigation uses full-document and opener-isolated browser operations', () => {
+  const assigned = [];
+  const location = {href: 'https://example.test/viewer', assign: value => assigned.push(value)};
+  navigateLocalResource(localResource('/guide'), {location, applicationBase: '/app'});
+  assert.deepEqual(assigned, ['https://example.test/app/guide']);
+
+  const openedWindow = {opener: {}};
+  const opened = [];
+  navigateLocalResource(localResource('/guide', 'NEW_WINDOW'), {
+    location,
+    applicationBase: '/app',
+    open: (...args) => { opened.push(args); return openedWindow; }
+  });
+  assert.deepEqual(opened, [['https://example.test/app/guide', '_blank', 'noopener,noreferrer']]);
+  assert.equal(openedWindow.opener, null);
+  assert.deepEqual(assigned, ['https://example.test/app/guide']);
+  assert.doesNotThrow(() => navigateLocalResource(localResource('/guide', 'NEW_WINDOW'), {
+    location,
+    applicationBase: '/app',
+    open: () => null
+  }));
+});
diff --git a/viewers/webcomponents/htmx-security-secman/README.adoc b/viewers/webcomponents/htmx-security-secman/README.adoc
index 108c100550f..66fc7b3ff02 100644
--- a/viewers/webcomponents/htmx-security-secman/README.adoc
+++ b/viewers/webcomponents/htmx-security-secman/README.adoc
@@ -101,6 +101,8 @@ The login page is server rendered and makes no GraphQL request before authentica
 Spring Security owns credential processing, request caching, session-identifier migration, security-context persistence, CSRF, and logout invalidation.
 Only canonical same-origin GET routes beneath the configured HTMX base path are restored after login.
 The HTMX host owns login presentation, current-user chrome, session-expiry navigation, exact suppression of `causeway.security.LogoutMenu#logout`, and the native POST logout control.
+That control remains independent of typed `LocalResourcePathValue` action results: secure logout uses current CSRF evidence and Spring Security session invalidation, while local-resource results are only safe same-origin navigation.
+A local-resource path equal to `/logout` does not acquire authentication semantics.
 Invalid passwords, absent users, locked users, passwordless users, and internal lookup failures all produce the same public credential failure.
 
 == Provisional bridge boundary
diff --git a/viewers/webcomponents/htmx/README.adoc b/viewers/webcomponents/htmx/README.adoc
index e4b3b5657b0..82e0e6c1e56 100644
--- a/viewers/webcomponents/htmx/README.adoc
+++ b/viewers/webcomponents/htmx/README.adoc
@@ -164,9 +164,15 @@ Absent, hidden, invalid, unsupported, or partially failing home state leaves an
 The viewer does not invent a home service-action descriptor.
 
 Default result policy routes object results, presents scalar and collection results in the shell result region, and announces void completion before authoritatively replacing the current route context while preserving the result announcement.
+Typed `LocalResourcePathValue` results use the servlet context path published as `data-causeway-application-resource-base` and perform safe same-origin full-document navigation; they never enter HTMX fragment or object routing.
+Unsafe local-resource targets fail closed in the result region, and `NEW_WINDOW` requests an opener-isolated window.
 An application can provide `window.causewayHtmxPolicy.navigate`, `handleHome`, or `handleResult` functions and return `true` to claim the corresponding policy decision.
 These policy functions receive semantic public payloads and do not replace component interaction behavior.
 
+The generic host always removes and blocks the exact framework action `causeway.security.LogoutMenu#logout` before GraphQL invocation.
+Without an authentication integration it exposes no logout endpoint and announces that logout is unavailable; similarly named application actions remain available.
+Secure authentication integrations own their logout form, HTTP method, CSRF evidence, session cleanup, and redirect independently of ordinary local-resource results.
+
 == Styling and accessibility
 
 The shell loads `/causeway-webcomponents/theme.css` and `/causeway-htmx/causeway-htmx.css`.
diff --git a/viewers/webcomponents/htmx/src/main/resources/META-INF/causeway/webcomponents/htmx/document.html b/viewers/webcomponents/htmx/src/main/resources/META-INF/causeway/webcomponents/htmx/document.html
index 2001600d9bb..77b669e7352 100644
--- a/viewers/webcomponents/htmx/src/main/resources/META-INF/causeway/webcomponents/htmx/document.html
+++ b/viewers/webcomponents/htmx/src/main/resources/META-INF/causeway/webcomponents/htmx/document.html
@@ -14,7 +14,7 @@
   limitations under the License.
 -->
 <!doctype html>
-<html lang="{{causeway.language}}" data-causeway-htmx-base="{{causeway.basePath}}" data-causeway-canonical-path="{{causeway.canonicalPath}}"{{causeway.widgetAttributes}}>
+<html lang="{{causeway.language}}" data-causeway-htmx-base="{{causeway.basePath}}" data-causeway-application-resource-base="{{causeway.contextPath}}" data-causeway-canonical-path="{{causeway.canonicalPath}}"{{causeway.widgetAttributes}}>
 <head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1">
diff --git a/viewers/webcomponents/htmx/src/main/resources/META-INF/resources/causeway-htmx/causeway-htmx.mjs b/viewers/webcomponents/htmx/src/main/resources/META-INF/resources/causeway-htmx/causeway-htmx.mjs
index 80e7857bf43..4121ae394a2 100644
--- a/viewers/webcomponents/htmx/src/main/resources/META-INF/resources/causeway-htmx/causeway-htmx.mjs
+++ b/viewers/webcomponents/htmx/src/main/resources/META-INF/resources/causeway-htmx/causeway-htmx.mjs
@@ -39,6 +39,11 @@ import {
   configureCausewayMenubarWidgets
 } from '../causeway-webcomponents/menubar-widget.mjs';
 import {defineCausewayWebComponents} from '../causeway-webcomponents/register.mjs';
+import {
+  isFrameworkLogoutAction,
+  removeFrameworkLogoutMenuActions
+} from '../causeway-webcomponents/host-operation-policy.mjs';
+import {navigateLocalResource} from '../causeway-webcomponents/local-resource-policy.mjs';
 import {
   applyStandaloneCollectionPresentation,
   standaloneCollectionPresentation
@@ -73,6 +78,7 @@ const resultRegion = uniqueShellLandmark('#causeway-result');
 const loadingRegion = uniqueShellLandmark('#causeway-route-loading');
 const menuBoundary = uniqueShellLandmark('cw-menubars');
 const basePath = document.documentElement.dataset.causewayHtmxBase;
+const applicationResourceBase = document.documentElement.dataset.causewayApplicationResourceBase || '/';
 const applicationTitle = document.title.trim();
 const referenceWidgetMode = document.documentElement.dataset.causewayReferenceWidgets;
 const collectionGridMode = document.documentElement.dataset.causewayCollectionGrid;
@@ -138,7 +144,8 @@ document.addEventListener(CAUSEWAY_MENUBAR_WIDGET_POLICY_EVENT, event => {
 configureCausewayMenubarWidgets({
   enabled: applicationMenubarMode === 'vaadin',
   moduleUrl: applicationMenubarModuleUrl,
-  excludeAction: detail => Boolean(authentication && isExcludedAction(authentication, detail))
+  excludeAction: detail => isFrameworkLogoutAction(detail)
+    || Boolean(authentication && isExcludedAction(authentication, detail))
 });
 let activeRequest = null;
 let navigationGeneration = 0;
@@ -429,6 +436,15 @@ function presentResult(detail) {
   if (typeof policy?.handleResult === 'function' && policy.handleResult(detail) === true) {
     return;
   }
+  if (detail?.result?.kind === 'local-resource') {
+    try {
+      navigateLocalResource(detail.result.value, {applicationBase: applicationResourceBase});
+    } catch (error) {
+      document.documentElement.dataset.causewayLocalResourceError = String(error?.code ?? 'LOCAL_RESOURCE_NAVIGATION_FAILED');
+      announce('The local resource could not be opened.');
+    }
+    return;
+  }
   const identity = resultObjectIdentity(detail?.result);
   if (identity) {
     navigate(canonicalObjectPath(basePath, identity));
@@ -454,6 +470,9 @@ function presentResult(detail) {
   } else if (result?.kind === 'scalar') {
     output.textContent = result.value == null ? '' : String(result.value);
     replaceResultPresentation(destination, heading, output, resultDismissButton(destination, detail));
+  } else if (result?.kind === 'unsupported') {
+    output.textContent = 'This result cannot be presented.';
+    replaceResultPresentation(destination, heading, output, resultDismissButton(destination, detail));
   } else {
     output.textContent = 'Completed';
     replaceResultPresentation(destination, heading, output, resultDismissButton(destination, detail));
@@ -572,19 +591,34 @@ document.addEventListener(ACTION_REQUEST_EVENT, event => {
 }, {capture: true});
 
 document.addEventListener(ACTION_REQUEST_EVENT, event => {
-  if (!authentication) {
-    return;
-  }
-  if (!isExcludedAction(authentication, event.detail)) {
+  const detail = event.detail;
+  const policy = globalThis.causewayHtmxPolicy;
+  try {
+    if (typeof policy?.handleAction === 'function' && policy.handleAction(detail) === true) {
+      event.preventDefault();
+      return;
+    }
+  } catch {
+    event.preventDefault();
+    event.stopImmediatePropagation();
+    document.documentElement.dataset.causewayActionPolicyError = 'ACTION_POLICY_FAILED';
+    announce('The action host policy could not complete.');
     return;
   }
+  if (!isFrameworkLogoutAction(detail)) return;
   event.preventDefault();
   event.stopImmediatePropagation();
-  document.querySelector('[data-causeway-logout-form]')?.requestSubmit?.();
+  if (authentication && isExcludedAction(authentication, detail)) {
+    document.querySelector('[data-causeway-logout-form]')?.requestSubmit?.();
+  } else {
+    document.documentElement.dataset.causewayLogoutUnavailable = 'true';
+    announce('Logout requires a host authentication integration.');
+  }
 }, {capture: true});
 
 document.addEventListener(MENU_BARS_STATE_EVENT, () => globalThis.setTimeout(() => {
   collapseNarrowBars();
+  removeFrameworkLogoutMenuActions(document);
   applyAuthenticationMenuPolicy(authentication, document);
 }, 0));
 
diff --git a/viewers/webcomponents/htmx/src/test/java/org/apache/causeway/viewer/webcomponents/htmx/HtmxViewerControllerTest.java b/viewers/webcomponents/htmx/src/test/java/org/apache/causeway/viewer/webcomponents/htmx/HtmxViewerControllerTest.java
index 90639469638..88e97efcaef 100644
--- a/viewers/webcomponents/htmx/src/test/java/org/apache/causeway/viewer/webcomponents/htmx/HtmxViewerControllerTest.java
+++ b/viewers/webcomponents/htmx/src/test/java/org/apache/causeway/viewer/webcomponents/htmx/HtmxViewerControllerTest.java
@@ -45,6 +45,7 @@ void returnsCompleteStableShellForOrdinaryObjectRequest() {
         assertThat(response.getBody())
                 .contains("<!doctype html>")
                 .contains("<cw-menubars>")
+                .contains("data-causeway-application-resource-base=\"/app\"")
                 .contains("<main id=\"causeway-route\"")
                 .contains("data-page-kind=\"generic\"")
                 .contains("data-page-source=\"generic\"")
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java b/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java
index d3f05ecbf3c..6a339d0b02f 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java
+++ b/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java
@@ -1499,6 +1499,36 @@ private void assertMovableModalPrompt() {
         assertThat(after.get(3).doubleValue()).isLessThanOrEqualTo(viewport.get(1).doubleValue() + 0.5);
     }
 
+    @Test
+    @Order(10)
+    void frameworkLogoutFailsClosedWhileApplicationActionsAndLocalResourcesRemainAvailable() {
+        page.navigate(url("/htmx/object/petclinic.ViewerFallback/s_viewer-fallback"),
+                new Page.NavigateOptions().setWaitUntil(WaitUntilState.DOMCONTENTLOADED));
+        waitForPageKind("generic");
+        waitForMenus();
+        page.waitForFunction("() => document.querySelector('cw-object cw-action#openLocalResource')?.componentState?.status === 'ready'");
+
+        assertThat(page.locator("cw-menubars [data-service-logical-type='causeway.security.LogoutMenu'][data-action-id='logout']").count()).isZero();
+        final var logoutInvocationsBefore = graphQLRequestCount("CausewayInvokeServiceAction", "LogoutMenu");
+        page.locator("#causeway-route").evaluate("element => element.dispatchEvent(new CustomEvent('causeway-action-request', { bubbles: true, composed: true, cancelable: true, detail: { serviceLogicalTypeName: 'causeway.security.LogoutMenu', actionId: 'logout', context: {} } }))");
+        page.waitForTimeout(50);
+        assertThat(graphQLRequestCount("CausewayInvokeServiceAction", "LogoutMenu")).isEqualTo(logoutInvocationsBefore);
+        assertThat(page.locator("#causeway-route-announcement").innerText()).contains("host authentication");
+
+        page.locator("cw-object cw-action#logout [data-causeway-action-control]").click();
+        waitForShellResult("logout", "Application action completed");
+
+        final var localResourcePopup = page.waitForPopup(() ->
+                page.locator("cw-object cw-action#openLocalResourceInNewWindow [data-causeway-action-control]").click());
+        localResourcePopup.locator("h1").waitFor();
+        assertThat(localResourcePopup.locator("h1").innerText()).isEqualTo("Pet Clinic local resource");
+        localResourcePopup.close();
+
+        page.locator("cw-object cw-action#openLocalResource [data-causeway-action-control]").click();
+        page.waitForURL("**/petclinic-local-resource.html");
+        assertThat(page.locator("h1").innerText()).isEqualTo("Pet Clinic local resource");
+    }
+
     @SuppressWarnings("unchecked")
     private void assertApplicationUsesAvailableWidth() {
         page.setViewportSize(1800, 900);
diff --git a/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/ViewerFallback.java b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/ViewerFallback.java
index 70a65d94843..5182a9f1591 100644
--- a/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/ViewerFallback.java
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/java/org/apache/causeway/viewer/webcomponents/sample/petclinic/domain/ViewerFallback.java
@@ -25,9 +25,14 @@
 import jakarta.persistence.Table;
 import jakarta.persistence.Version;
 
+import org.apache.causeway.applib.annotation.Action;
+import org.apache.causeway.applib.annotation.ActionLayout;
 import org.apache.causeway.applib.annotation.DomainObject;
 import org.apache.causeway.applib.annotation.DomainObjectLayout;
 import org.apache.causeway.applib.annotation.ObjectSupport;
+import org.apache.causeway.applib.annotation.SemanticsOf;
+import org.apache.causeway.applib.value.LocalResourcePath;
+import org.apache.causeway.applib.value.OpenUrlStrategy;
 
 @Entity
 @Table(name = "petclinic_viewer_fallback")
@@ -73,4 +78,22 @@ public long getVersion() {
     public String getMessage() {
         return message;
     }
+
+    @Action(semantics = SemanticsOf.SAFE)
+    @ActionLayout(named = "Open local resource")
+    public LocalResourcePath openLocalResource() {
+        return new LocalResourcePath("/petclinic-local-resource.html", OpenUrlStrategy.SAME_WINDOW);
+    }
+
+    @Action(semantics = SemanticsOf.SAFE)
+    @ActionLayout(named = "Open local resource in new window")
+    public LocalResourcePath openLocalResourceInNewWindow() {
+        return new LocalResourcePath("/petclinic-local-resource.html", OpenUrlStrategy.NEW_WINDOW);
+    }
+
+    @Action(semantics = SemanticsOf.SAFE)
+    @ActionLayout(named = "Logout")
+    public String logout() {
+        return "Application action completed";
+    }
 }
diff --git a/viewers/webcomponents/sample-petclinic-domain/src/main/resources/META-INF/resources/petclinic-local-resource.html b/viewers/webcomponents/sample-petclinic-domain/src/main/resources/META-INF/resources/petclinic-local-resource.html
new file mode 100644
index 00000000000..5eedf92bf8c
--- /dev/null
+++ b/viewers/webcomponents/sample-petclinic-domain/src/main/resources/META-INF/resources/petclinic-local-resource.html
@@ -0,0 +1,29 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to you under the Apache License, Version 2.0.
+  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>Pet Clinic local resource</title>
+</head>
+<body>
+  <main>
+    <h1>Pet Clinic local resource</h1>
+    <p>This same-origin page verifies framework-neutral local-resource action results.</p>
+  </main>
+</body>
+</html>
diff --git a/viewers/webcomponents/sample-vue-petclinic/frontend/src/main.ts b/viewers/webcomponents/sample-vue-petclinic/frontend/src/main.ts
index 39814f53db0..6f07856e6dd 100644
--- a/viewers/webcomponents/sample-vue-petclinic/frontend/src/main.ts
+++ b/viewers/webcomponents/sample-vue-petclinic/frontend/src/main.ts
@@ -54,6 +54,7 @@ const viewer = createCausewayVueViewer({
   router,
   endpoint: '/graphql',
   basePath: '/vue',
+  applicationResourceBase: '/',
   pages: {
     'petclinic.HomePage': HomePage,
     'petclinic.PetOwner': PetOwnerPage,
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CklV2Pe0.js b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CklV2Pe0.js
deleted file mode 100644
index 9955a28b772..00000000000
--- a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CklV2Pe0.js
+++ /dev/null
@@ -1,2 +0,0 @@
-(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const r of i)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&s(o)}).observe(document,{childList:!0,subtree:!0});function n(i){const r={};return i.integrity&&(r.integrity=i.integrity),i.referrerPolicy&&(r.referrerPolicy=i.referr [...]
-//# sourceMappingURL=index-CklV2Pe0.js.map
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CyDhqcW-.js b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CyDhqcW-.js
new file mode 100644
index 00000000000..b98593a707a
--- /dev/null
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CyDhqcW-.js
@@ -0,0 +1,2 @@
+(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const r of i)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&s(o)}).observe(document,{childList:!0,subtree:!0});function n(i){const r={};return i.integrity&&(r.integrity=i.integrity),i.referrerPolicy&&(r.referrerPolicy=i.referr [...]
+//# sourceMappingURL=index-CyDhqcW-.js.map
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CklV2Pe0.js.map b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CyDhqcW-.js.map
similarity index 69%
rename from viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CklV2Pe0.js.map
rename to viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CyDhqcW-.js.map
index 87a5b0c0cbc..c993c0378c5 100644
--- a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CklV2Pe0.js.map
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/assets/index-CyDhqcW-.js.map
@@ -1 +1 @@
-{"version":3,"file":"index-CklV2Pe0.js","sources":["../../../../../../frontend/node_modules/@vue/shared/dist/shared.esm-bundler.js","../../../../../../frontend/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js","../../../../../../frontend/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js","../../../../../../frontend/node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js","../../../../../../frontend/node_modules/vue-router/dist/devtools-EWN81iOl.mjs","../../ [...]
\ No newline at end of file
+{"version":3,"file":"index-CyDhqcW-.js","sources":["../../../../../../frontend/node_modules/@vue/shared/dist/shared.esm-bundler.js","../../../../../../frontend/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js","../../../../../../frontend/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js","../../../../../../frontend/node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js","../../../../../../frontend/node_modules/vue-router/dist/devtools-EWN81iOl.mjs","../../ [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html
index fa4b8b83fb4..f4e98dca52c 100644
--- a/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html
+++ b/viewers/webcomponents/sample-vue-petclinic/src/main/resources/static/vue/index.html
@@ -26,7 +26,7 @@
   <link rel="stylesheet" href="/causeway-webcomponents/component-styles.css">
   <link rel="stylesheet" href="/causeway-webcomponents/theme.css">
   <link rel="stylesheet" href="/webjars/font-awesome/7.3.0/css/all.min.css">
-  <script type="module" crossorigin src="/vue/assets/index-CklV2Pe0.js"></script>
+  <script type="module" crossorigin src="/vue/assets/index-CyDhqcW-.js"></script>
   <link rel="stylesheet" crossorigin href="/vue/assets/index-CqcjgOE7.css">
 </head>
 <body>
diff --git a/viewers/webcomponents/sample-vue-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVuePlaywrightTest.java b/viewers/webcomponents/sample-vue-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVuePlaywrightTest.java
index de80c48a92e..d9b54415e30 100644
--- a/viewers/webcomponents/sample-vue-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVuePlaywrightTest.java
+++ b/viewers/webcomponents/sample-vue-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/vue/petclinic/PetClinicVuePlaywrightTest.java
@@ -49,6 +49,7 @@ class PetClinicVuePlaywrightTest {
     private int port;
 
     private final List<String> browserFailures = new ArrayList<>();
+    private final List<String> graphQLRequests = new ArrayList<>();
     private Playwright playwright;
     private Browser browser;
     private Page page;
@@ -74,6 +75,7 @@ void stopBrowser() {
     @BeforeEach
     void openPage() {
         browserFailures.clear();
+        graphQLRequests.clear();
         page = browser.newPage(new Browser.NewPageOptions().setViewportSize(1440, 900));
         page.onPageError(error -> browserFailures.add(error));
         page.onConsoleMessage(message -> {
@@ -81,6 +83,11 @@ void openPage() {
                 browserFailures.add("console: " + message.text());
             }
         });
+        page.onRequest(request -> {
+            if (request.url().contains("/graphql") && request.postData() != null) {
+                graphQLRequests.add(request.postData());
+            }
+        });
         page.onRequestFailed(request -> {
             final var failure = request.failure();
             if (!(request.url().contains("/graphql") && failure != null && failure.contains("ERR_ABORTED"))) {
@@ -256,6 +263,39 @@ void scalarActionResultsUseTheActivePageOutlet() {
                 .contains("allOwners result", "Completed in Vue");
     }
 
+    @Test
+    void frameworkLogoutFailsClosedWhileApplicationActionsAndLocalResourcesRemainAvailable() {
+        open("/vue/object/petclinic.ViewerFallback/s_viewer-fallback");
+        page.locator("[data-page-kind='generic'][data-route-state='ready']").waitFor();
+        page.waitForFunction("() => document.querySelector('cw-object cw-action#openLocalResource')?.componentState?.status === 'ready'");
+        page.locator("cw-menubars[data-menu-state='ready']").waitFor();
+
+        assertThat(page.locator("cw-menubars [data-service-logical-type='causeway.security.LogoutMenu'][data-action-id='logout']").count()).isZero();
+        final var logoutInvocationsBefore = graphQLRequests.stream()
+                .filter(body -> body.contains("CausewayInvokeServiceAction") && body.contains("LogoutMenu"))
+                .count();
+        final var logoutAnnouncement = (String) page.locator("[data-testid='petclinic-vue-application-shell']").evaluate("element => { element.dispatchEvent(new CustomEvent('causeway-action-request', { bubbles: true, composed: true, cancelable: true, detail: { serviceLogicalTypeName: 'causeway.security.LogoutMenu', actionId: 'logout', context: {} } })); return element.querySelector('[data-causeway-route-announcement]').textContent; }");
+        page.waitForTimeout(50);
+        assertThat(graphQLRequests.stream()
+                .filter(body -> body.contains("CausewayInvokeServiceAction") && body.contains("LogoutMenu"))
+                .count()).isEqualTo(logoutInvocationsBefore);
+        assertThat(logoutAnnouncement).contains("host authentication");
+
+        page.locator("cw-object cw-action#logout [data-causeway-action-control]").click();
+        page.waitForFunction("() => document.body.innerText.includes('Application action completed')");
+        assertThat(page.locator("cw-action-results:not([hidden])").innerText()).contains("Application action completed");
+
+        final var localResourcePopup = page.waitForPopup(() ->
+                page.locator("cw-object cw-action#openLocalResourceInNewWindow [data-causeway-action-control]").click());
+        localResourcePopup.locator("h1").waitFor();
+        assertThat(localResourcePopup.locator("h1").innerText()).isEqualTo("Pet Clinic local resource");
+        localResourcePopup.close();
+
+        page.locator("cw-object cw-action#openLocalResource [data-causeway-action-control]").click();
+        page.waitForURL("**/petclinic-local-resource.html");
+        assertThat(page.locator("h1").innerText()).isEqualTo("Pet Clinic local resource");
+    }
+
     @Test
     void directRefreshAndInvalidRoutesRemainWithinTheApplicationShell() {
         open("/vue/object/petclinic.Pet/s_pet-basil");
@@ -279,7 +319,7 @@ void bothToolkitPoliciesPreserveAccessibleShellLandmarksAndMenus() {
         for (final String toolkit : List.of("native", "vaadin")) {
             open("/vue/?toolkit=" + toolkit);
             assertThat(page.locator("html").getAttribute("data-causeway-component-toolkit")).isEqualTo(toolkit);
-            assertThat(page.locator("header").count()).isEqualTo(1);
+            assertThat(page.locator("header.causeway-shell-header").count()).isEqualTo(1);
             assertThat(page.locator("main#causeway-vue-route").count()).isEqualTo(1);
             assertThat(page.locator("footer").count()).isEqualTo(1);
             page.locator("cw-menubars[data-menu-state='ready']").waitFor();
diff --git a/viewers/webcomponents/vue/README.adoc b/viewers/webcomponents/vue/README.adoc
index 739f9efdb6c..a8ce1b12141 100644
--- a/viewers/webcomponents/vue/README.adoc
+++ b/viewers/webcomponents/vue/README.adoc
@@ -1,5 +1,9 @@
 = Apache Causeway Vue Web Component Viewer
 
+// Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.
+// See the NOTICE file distributed with this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0.
+
 The package provides the thin Vue 3 and Vue Router 4 integration for Apache Causeway semantic web components.
 It owns route and host policy but does not duplicate GraphQL or domain state.
 
@@ -38,6 +42,7 @@ const viewer = createCausewayVueViewer({
   router,
   endpoint: '/graphql',
   basePath: '/vue',
+  applicationResourceBase: '/',
   pages: {
     'petclinic.PetOwner': PetOwnerPage,
     'petclinic.HomePage': () => import('./pages/HomePage.vue')
@@ -120,8 +125,16 @@ Registrations are immutable after plugin creation and do not use type inheritanc
 == Public exports
 
 The package exports route codecs, route names and records, the viewer plugin factory, shell binding helpers, boundary validators, policy helpers, public TypeScript contracts, and the generic route components.
-Applications may replace navigation, home, result, and error policy through viewer options.
+Applications may replace pre-invocation action, navigation, home, result, and error policy through viewer options.
+The typed action policy receives canonical action-request detail, an idempotent single-claim capability, and immutable route context; a synchronous claim prevents validation and GraphQL invocation, while an unclaimed asynchronous decision resumes an ordinary action exactly once if the route remains current.
 Claimed semantic events are not also handled by the default policy.
+The default policy removes and blocks only the exact framework action `causeway.security.LogoutMenu#logout`; applications with authentication claim that action and own the secure logout workflow.
+Without such a policy, framework Logout fails closed before GraphQL and similarly named application actions remain ordinary.
+
+`applicationResourceBase` is independent of `basePath` and identifies the application-local deployment base used for typed `LocalResourcePathValue` results.
+The default result policy resolves those values as same-origin full-document targets, rejects unsafe or escaping paths, and uses opener isolation for `NEW_WINDOW`; it does not send them through Vue Router.
+A local-resource path named `/logout` remains ordinary navigation and is never treated as authentication.
+Hosts may claim result policy first to replace or further restrict this behavior.
 
 The canonical route grammar is `<base-path>/object/<logical-type>/<identifier>` using independently canonical UTF-8 percent-encoded segments.
 Malformed, unsafe, non-canonical, or oversized segments fail before an object context is mounted.
diff --git a/viewers/webcomponents/vue/dist/causeway-vue.js b/viewers/webcomponents/vue/dist/causeway-vue.js
index d856f4251e5..f8ec5ba221b 100644
--- a/viewers/webcomponents/vue/dist/causeway-vue.js
+++ b/viewers/webcomponents/vue/dist/causeway-vue.js
@@ -1,28 +1,82 @@
-import { inject as re, onMounted as V, onBeforeUnmount as X, defineComponent as j, ref as A, openBlock as O, createElementBlock as q, createElementVNode as T, toDisplayString as ie, computed as R, h as b, defineAsyncComponent as ce, watch as se, createBlock as ue, resolveDynamicComponent as le, nextTick as de } from "vue";
-import { useRoute as pe } from "vue-router";
-const G = "cw-object-context[data-causeway-route-context]", fe = "cw-interaction-controller[data-causeway-route-interactions]";
-function ge(e, t) {
-  const a = [...e.querySelectorAll(G)].filter((r) => !r.parentElement?.closest(G));
+import { inject as le, onMounted as W, onBeforeUnmount as ee, defineComponent as C, ref as L, openBlock as N, createElementBlock as I, createElementVNode as A, toDisplayString as de, computed as P, h as S, defineAsyncComponent as pe, watch as fe, createBlock as ge, resolveDynamicComponent as he, nextTick as ye } from "vue";
+import { useRoute as ve } from "vue-router";
+const k = "cw-object-context[data-causeway-route-context]", we = "cw-interaction-controller[data-causeway-route-interactions]";
+function me(e, t) {
+  const a = [...e.querySelectorAll(k)].filter((r) => !r.parentElement?.closest(k));
   if (a.length === 0) return Object.freeze({ valid: !1, classification: "missing-context" });
   if (a.length !== 1) return Object.freeze({ valid: !1, classification: "duplicate-context" });
   const n = a[0];
-  return n.getAttribute("logical-type") !== t.logicalTypeName || n.getAttribute("object-id") !== t.objectId ? Object.freeze({ valid: !1, classification: "identity" }) : [...n.querySelectorAll(fe)].filter((r) => r.closest(G) === n).length !== 1 ? Object.freeze({ valid: !1, classification: "interactions" }) : Object.freeze({ valid: !0, context: n });
+  return n.getAttribute("logical-type") !== t.logicalTypeName || n.getAttribute("object-id") !== t.objectId ? Object.freeze({ valid: !1, classification: "identity" }) : [...n.querySelectorAll(we)].filter((r) => r.closest(k) === n).length !== 1 ? Object.freeze({ valid: !1, classification: "interactions" }) : Object.freeze({ valid: !0, context: n });
 }
-function C(e, t) {
+function j(e, t) {
   const a = e.querySelectorAll(t);
   return a.length === 1 ? a[0] : null;
 }
-function Z(e) {
-  const t = C(e, "cw-graphql-client[data-causeway-shell-client]"), a = C(e, "[data-causeway-router-view]"), n = C(e, "[data-causeway-route-loading]"), o = C(e, "[data-causeway-route-announcement]"), r = C(e, "cw-action-results[data-causeway-shell-result]");
+function te(e) {
+  const t = j(e, "cw-graphql-client[data-causeway-shell-client]"), a = j(e, "[data-causeway-router-view]"), n = j(e, "[data-causeway-route-loading]"), o = j(e, "[data-causeway-route-announcement]"), r = j(e, "cw-action-results[data-causeway-shell-result]");
   if (!t || !a || !n || !o || !r || !t.contains(a) || !t.contains(r))
     throw new Error("The authored Vue application shell is invalid.");
   return Object.freeze({ shell: e, client: t, route: a, loading: n, announcement: o, result: r });
 }
-const H = /* @__PURE__ */ Symbol("causeway-vue-viewer"), U = 4096, Q = 4096, he = "The requested application route is invalid.";
-function h() {
-  return new Error(he);
+const ae = /* @__PURE__ */ Symbol("causeway-vue-viewer"), D = Object.freeze({
+  serviceLogicalTypeName: "causeway.security.LogoutMenu",
+  actionId: "logout"
+});
+function ne(e) {
+  return e?.serviceLogicalTypeName === D.serviceLogicalTypeName && e?.actionId === D.actionId;
+}
+function M(e) {
+  let t = 0;
+  for (const a of e.querySelectorAll("[data-causeway-service-action]"))
+    ne({
+      serviceLogicalTypeName: a.getAttribute("data-service-logical-type") ?? void 0,
+      actionId: a.getAttribute("data-action-id") ?? ""
+    }) && ((a.closest("[data-causeway-service-action-region]") ?? a).remove(), t += 1);
+  return t;
+}
+class be extends Error {
+  code;
+  constructor(t, a) {
+    super(a), this.name = "LocalResourceNavigationError", this.code = t;
+  }
 }
-function ye(e) {
+function Ee(e, t = {}) {
+  const a = t.location ?? window.location, n = typeof e?.path == "string" ? e.path.trim() : "", o = e?.openUrlStrategy;
+  if (!n || !["SAME_WINDOW", "NEW_WINDOW"].includes(o ?? ""))
+    throw R("LOCAL_RESOURCE_INVALID", "The local-resource result is incomplete.");
+  if (/^[a-z][a-z\d+.-]*:/i.test(n) || n.startsWith("//") || n.includes("\\") || /[\u0000-\u001f\u007f]/.test(n))
+    throw R("LOCAL_RESOURCE_TARGET_UNSAFE", "The local-resource target is not an application-local path.");
+  const r = new URL(a.href), u = _e(t.applicationBase ?? "/", r).pathname.replace(/\/+$/, "") || "/", l = n.startsWith("/") ? n : `/${n}`, h = u !== "/" && (l === u || l.startsWith(`${u}/`) || l.startsWith(`${u}?`) || l.startsWith(`${u}#`)) ? l : `${u === "/" ? "" : u}/${l.replace(/^\/+/, "")}`, v = new URL(h, r.origin);
+  if (v.origin !== r.origin || v.username || v.password || u !== "/" && v.pathname !== u && !v.pathname.startsWith(`${u}/`))
+    throw R("LOCAL_RESOURCE_TARGET_UNSAFE", "The local-resource target escapes the configured application boundary.");
+  return Object.freeze({ url: v, openUrlStrategy: o });
+}
+function Te(e, t = {}) {
+  const a = t.location ?? window.location, n = Ee(e, { location: a, applicationBase: t.applicationBase });
+  if (n.openUrlStrategy === "SAME_WINDOW") {
+    a.assign(n.url.href);
+    return;
+  }
+  const r = (t.open ?? window.open.bind(window))(n.url.href, "_blank", "noopener,noreferrer");
+  r && (r.opener = null);
+}
+function _e(e, t) {
+  const a = String(e).trim() || "/";
+  if (a.startsWith("//") || /[\u0000-\u001f\u007f]/.test(a))
+    throw R("LOCAL_RESOURCE_BASE_INVALID", "The application-local resource base is invalid.");
+  const n = new URL(a.endsWith("/") ? a : `${a}/`, t.origin);
+  if (n.origin !== t.origin || n.username || n.password)
+    throw R("LOCAL_RESOURCE_BASE_INVALID", "The application-local resource base must be same-origin.");
+  return n;
+}
+function R(e, t) {
+  return new be(e, t);
+}
+const F = 4096, oe = 4096, Se = "The requested application route is invalid.";
+function b() {
+  return new Error(Se);
+}
+function Ae(e) {
   for (let t = 0; t < e.length; t += 1) {
     const a = e.charCodeAt(t);
     if (a >= 55296 && a <= 56319) {
@@ -34,59 +88,59 @@ function ye(e) {
   }
   return !1;
 }
-function I(e) {
+function q(e) {
   const t = String(e ?? "");
-  if (!t || t.length > U || t === "." || t === ".." || /[\\/\u0000-\u001f\u007f-\u009f]/u.test(t) || ye(t) || new TextEncoder().encode(t).length > U) throw h();
+  if (!t || t.length > F || t === "." || t === ".." || /[\\/\u0000-\u001f\u007f-\u009f]/u.test(t) || Ae(t) || new TextEncoder().encode(t).length > F) throw b();
   let a;
   try {
     a = encodeURIComponent(t).replace(/[!'()*]/g, (n) => `%${n.charCodeAt(0).toString(16).toUpperCase()}`);
   } catch {
-    throw h();
+    throw b();
   }
-  if (a.length > Q) throw h();
+  if (a.length > oe) throw b();
   return a;
 }
-function B(e) {
-  if (!e || e.length > Q || !/^(?:[^%]|%[0-9A-F]{2})+$/u.test(e))
-    throw h();
+function K(e) {
+  if (!e || e.length > oe || !/^(?:[^%]|%[0-9A-F]{2})+$/u.test(e))
+    throw b();
   let t;
   try {
     t = decodeURIComponent(e);
   } catch {
-    throw h();
+    throw b();
   }
-  if (I(t) !== e) throw h();
+  if (q(t) !== e) throw b();
   return t;
 }
-function L(e) {
+function U(e) {
   let t = String(e ?? "").trim();
   if (!t.startsWith("/") || t.startsWith("//") || /[?#\\\u0000-\u001f]/u.test(t))
     throw new Error("The Vue viewer base path is invalid.");
   for (; t.length > 1 && t.endsWith("/"); ) t = t.slice(0, -1);
   return t;
 }
-function ve(e, t) {
-  const a = L(e), n = `/object/${I(t?.logicalTypeName)}/${I(t?.id ?? t?.objectId)}`;
+function Ce(e, t) {
+  const a = U(e), n = `/object/${q(t?.logicalTypeName)}/${q(t?.id ?? t?.objectId)}`;
   return a === "/" ? n : `${a}${n}`;
 }
-function z(e) {
-  return ve("/", e);
+function B(e) {
+  return Ce("/", e);
 }
-function J(e, t = "/") {
-  const a = String(e ?? ""), n = L(t), o = n === "/" ? "/object/" : `${n}/object/`;
-  if (!a.startsWith(o)) throw h();
-  const u = a.slice(o.length).split("/");
-  if (u.length !== 2) throw h();
+function re(e, t = "/") {
+  const a = String(e ?? ""), n = U(t), o = n === "/" ? "/object/" : `${n}/object/`;
+  if (!a.startsWith(o)) throw b();
+  const p = a.slice(o.length).split("/");
+  if (p.length !== 2) throw b();
   return Object.freeze({
-    logicalTypeName: B(u[0]),
-    objectId: B(u[1])
+    logicalTypeName: K(p[0]),
+    objectId: K(p[1])
   });
 }
-function we(e) {
-  return z(e);
+function Oe(e) {
+  return B(e);
 }
-const D = "causeway-navigation-request", M = "causeway-action-request", W = "causeway-action-result", F = "causeway-object-context-state-change";
-function ee() {
+const Y = "causeway-navigation-request", G = "causeway-action-request", X = "causeway-action-result", Z = "causeway-object-context-state-change", H = "causeway-menu-bars-state-change";
+function V() {
   let e = !1;
   return {
     get claimed() {
@@ -97,7 +151,7 @@ function ee() {
     }
   };
 }
-function S(e) {
+function _(e) {
   return Object.freeze({
     router: e.router,
     basePath: e.basePath,
@@ -105,96 +159,133 @@ function S(e) {
     routeGeneration: e.state.routeGeneration
   });
 }
-function me(e) {
+function je(e) {
   if (e?.kind !== "object" || !e.value || typeof e.value != "object") return null;
   const t = e.value._meta, a = t?.logicalTypeName, n = t?.id;
   return typeof a != "string" || typeof n != "string" || !a || !n ? null : Object.freeze({ logicalTypeName: a, id: n, title: String(t?.title ?? n) });
 }
-function be(e) {
+function xe(e) {
   return [...e.querySelector("[data-causeway-router-view]")?.querySelectorAll("cw-action-results[data-causeway-page-result]") ?? []].filter((a) => a.isConnected);
 }
-function Ee(e) {
+function Le(e) {
   const t = e.querySelectorAll("cw-action-results[data-causeway-shell-result]");
   return t.length === 1 ? t[0] : null;
 }
-function k(e) {
-  const t = be(e);
+function $(e) {
+  const t = xe(e);
   if (t.length > 1) throw new Error("The active Vue route has duplicate action-result outlets.");
   if (t.length === 1) return t[0];
-  const a = Ee(e);
+  const a = Le(e);
   if (!a?.isConnected) throw new Error("The authored Vue shell result outlet is unavailable.");
   return a;
 }
-function K(e, ...t) {
+function Q(e, ...t) {
   const a = e;
   a.replacePresentation ? a.replacePresentation(...t) : e.replaceChildren(...t), e.hidden = t.length === 0;
 }
-function te(e, t) {
+function ie(e, t) {
   const a = t.result, n = document.createElement("h2");
   if (n.textContent = t.actionId ? `${t.actionId} result` : "Action result", a?.kind === "collection") {
     const r = document.createElement("cw-standalone-collection");
-    r.setAttribute("data-testid", "causeway-standalone-action-result"), r.setAttribute("named", String(t.resultPresentation?.named ?? n.textContent)), r.result = a, K(e, r);
+    r.setAttribute("data-testid", "causeway-standalone-action-result"), r.setAttribute("named", String(t.resultPresentation?.named ?? n.textContent)), r.result = a, Q(e, r);
     return;
   }
   const o = document.createElement("output");
-  o.textContent = a?.kind === "scalar" ? String(a.value ?? "") : a?.kind === "void" ? "Completed" : "This result cannot be presented.", K(e, n, o);
+  o.textContent = a?.kind === "scalar" ? String(a.value ?? "") : a?.kind === "void" ? "Completed" : "This result cannot be presented.", Q(e, n, o);
 }
-async function Te(e, t) {
+async function Ne(e, t) {
   const a = e.state.shell, o = a?.querySelector("[data-causeway-router-view]")?.querySelector("cw-object-context[data-causeway-route-context]"), r = o?.context;
   if (!o || !r?.refresh) return;
-  const u = e.state.routeGeneration, l = (i) => {
-    if (u !== e.state.routeGeneration) return s();
-    const d = i.detail?.state;
-    if (d?.status !== "terminal-error") {
-      (d?.status === "ready" || d?.status === "partial-error") && s();
+  const p = e.state.routeGeneration, u = (m) => {
+    if (p !== e.state.routeGeneration) return l();
+    const h = m.detail?.state;
+    if (h?.status !== "terminal-error") {
+      (h?.status === "ready" || h?.status === "partial-error") && l();
       return;
     }
-    s(), (d.errors?.[0]?.extensions?.classification ?? d.errors?.[0]?.extensions?.code ?? d.error?.code) === "NOT_FOUND" && e.router.replace("/");
-  }, s = () => o.removeEventListener(F, l);
-  o.addEventListener(F, l), r.refresh(), te(k(a), t);
-}
-async function Y(e, t) {
-  const a = ee();
-  await e.policies.navigate?.(t, a, S(e)) === !0 && a.claim(), a.claimed || await e.router.push(z(t));
-}
-function ae(e, t) {
-  const a = /* @__PURE__ */ new WeakMap();
-  let n = null;
-  const o = (l) => {
-    const s = l.detail;
+    l(), (h.errors?.[0]?.extensions?.classification ?? h.errors?.[0]?.extensions?.code ?? h.error?.code) === "NOT_FOUND" && e.router.replace("/");
+  }, l = () => o.removeEventListener(Z, u);
+  o.addEventListener(Z, u), r.refresh(), ie($(a), t);
+}
+async function J(e, t) {
+  const a = V();
+  await e.policies.navigate?.(t, a, _(e)) === !0 && a.claim(), a.claimed || await e.router.push(B(t));
+}
+function ce(e, t) {
+  const a = /* @__PURE__ */ new WeakMap(), n = /* @__PURE__ */ new WeakSet();
+  let o = null, r = !0;
+  const p = () => {
+    const f = t.querySelector("[data-causeway-route-announcement]");
+    f && (f.textContent = "Logout requires a host authentication integration."), t.dataset.causewayLogoutUnavailable = "true";
+  }, u = (f, d) => {
+    if (!r || d !== e.state.routeGeneration || !(f.target instanceof EventTarget)) return;
+    const i = new CustomEvent(G, {
+      bubbles: !0,
+      composed: !0,
+      cancelable: !0,
+      detail: f.detail
+    });
+    n.add(i), f.target.dispatchEvent(i);
+  }, l = (f) => {
+    const d = f, i = d.detail;
     try {
-      const i = k(t);
-      s?.context && typeof s.context == "object" ? a.set(s.context, i) : n = i;
-    } catch (i) {
-      e.policies.error?.(i, S(e));
+      const g = $(t);
+      i?.context && typeof i.context == "object" ? a.set(i.context, g) : o = g;
+    } catch (g) {
+      e.policies.error?.(g, _(e));
     }
-  }, r = (l) => {
-    const s = l, i = s.detail?.target;
-    !i?.logicalTypeName || !(i.id ?? i.objectId) || (s.preventDefault(), Y(e, i).catch((d) => e.policies.error?.(d, S(e))));
-  }, u = (l) => {
-    const s = l, i = s.detail ?? {};
+    if (n.has(d)) return;
+    const y = ne(i), s = e.policies.action;
+    if (!s) {
+      y && (d.preventDefault(), p());
+      return;
+    }
+    const c = V(), w = e.state.routeGeneration;
+    try {
+      const g = s(i, c, _(e));
+      if (g && typeof g.then == "function") {
+        d.preventDefault(), Promise.resolve(g).then((E) => {
+          E === !0 && c.claim(), !c.claimed && !y ? u(d, w) : c.claimed || p();
+        }).catch((E) => e.policies.error?.(E, _(e)));
+        return;
+      }
+      g === !0 && c.claim(), (c.claimed || y) && d.preventDefault(), y && !c.claimed && p();
+    } catch (g) {
+      d.preventDefault(), e.policies.error?.(g, _(e));
+    }
+  }, m = (f) => {
+    const d = f, i = d.detail?.target;
+    !i?.logicalTypeName || !(i.id ?? i.objectId) || (d.preventDefault(), J(e, i).catch((y) => e.policies.error?.(y, _(e))));
+  }, h = (f) => {
+    const d = f, i = d.detail ?? {};
     (async () => {
-      const d = ee();
-      if (await e.policies.result?.(i, d, S(e)) === !0 && d.claim(), d.claimed) return;
-      const f = me(i.result);
-      if (f) return Y(e, f);
-      const g = i.context && a.get(i.context) || n || k(t);
-      i.result?.kind === "void" ? await Te(e, i) : te(g, i), s.target?.dismissResult?.();
-    })().catch((d) => e.policies.error?.(d, S(e)));
-  };
-  return t.addEventListener(M, o, { capture: !0 }), t.addEventListener(D, r), t.addEventListener(W, u), () => {
-    t.removeEventListener(M, o, { capture: !0 }), t.removeEventListener(D, r), t.removeEventListener(W, u);
+      const y = V();
+      if (await e.policies.result?.(i, y, _(e)) === !0 && y.claim(), y.claimed) return;
+      if (i.result?.kind === "local-resource") {
+        Te(i.result.value, { applicationBase: e.applicationResourceBase }), d.target?.dismissResult?.();
+        return;
+      }
+      const c = je(i.result);
+      if (c) return J(e, c);
+      const w = i.context && a.get(i.context) || o || $(t);
+      i.result?.kind === "void" ? await Ne(e, i) : ie(w, i), d.target?.dismissResult?.();
+    })().catch((y) => e.policies.error?.(y, _(e)));
+  }, v = () => queueMicrotask(() => {
+    r && M(t);
+  });
+  return M(t), t.addEventListener(G, l, { capture: !0 }), t.addEventListener(Y, m), t.addEventListener(X, h), t.addEventListener(H, v), () => {
+    r = !1, t.removeEventListener(G, l, { capture: !0 }), t.removeEventListener(Y, m), t.removeEventListener(X, h), t.removeEventListener(H, v);
   };
 }
-const je = /^[A-Za-z_][A-Za-z0-9_$-]*(?:\.[A-Za-z_][A-Za-z0-9_$-]*)*$/u;
-function _e(e) {
+const Re = /^[A-Za-z_][A-Za-z0-9_$-]*(?:\.[A-Za-z_][A-Za-z0-9_$-]*)*$/u;
+function Pe(e) {
   return e ? e instanceof Map ? [...e.entries()] : Array.isArray(e) ? e : Object.entries(e) : [];
 }
-function Ce(e) {
+function Ie(e) {
   const t = /* @__PURE__ */ new Map();
-  for (const [a, n] of _e(e)) {
+  for (const [a, n] of Pe(e)) {
     const o = String(a ?? "").trim();
-    if (!je.test(o)) throw new Error("A Vue page registration has an invalid logical type.");
+    if (!Re.test(o)) throw new Error("A Vue page registration has an invalid logical type.");
     if (!n || typeof n != "object" && typeof n != "function")
       throw new Error(`The Vue page registration for ${o} is unsupported.`);
     if (t.has(o)) throw new Error(`The Vue page registration for ${o} is duplicated.`);
@@ -202,10 +293,10 @@ function Ce(e) {
   }
   return t;
 }
-function xe(e) {
+function Ue(e) {
   return typeof e == "function";
 }
-function ze(e) {
+function Ye(e) {
   if (!e?.router) throw new Error("The Vue viewer requires the application router.");
   const t = String(e.endpoint ?? "").trim();
   if (!t) throw new Error("The Vue viewer GraphQL endpoint is required.");
@@ -214,38 +305,39 @@ function ze(e) {
   return n = Object.freeze({
     plugin: {
       install(r) {
-        r.provide(H, n);
+        r.provide(ae, n);
       }
     },
     router: e.router,
     endpoint: t,
-    basePath: L(e.basePath ?? (e.router.options.history.base || "/")),
-    pages: Ce(e.pages),
+    basePath: U(e.basePath ?? (e.router.options.history.base || "/")),
+    applicationResourceBase: U(e.applicationResourceBase ?? "/"),
+    pages: Ie(e.pages),
     policies: Object.freeze({ ...e.policies }),
     developmentDiagnostics: e.developmentDiagnostics ?? !0,
     state: a
   }), n;
 }
-function $() {
-  const e = re(H);
+function z() {
+  const e = le(ae);
   if (!e) throw new Error("The Causeway Vue viewer plugin is not installed.");
   return e;
 }
-function $e(e) {
-  const t = $();
+function Xe(e) {
+  const t = z();
   let a = null;
-  return V(() => {
+  return W(() => {
     const n = e.value;
     if (!n) throw new Error("The authored Vue application shell is missing.");
-    Z(n), t.state.shell = n, a = ae(t, n);
-  }), X(() => {
+    te(n), t.state.shell = n, a = ce(t, n);
+  }), ee(() => {
     a?.(), a = null, t.state.shell === e.value && (t.state.shell = null);
   }), e;
 }
-function Ue(e, t) {
-  const a = Z(t);
+function Ze(e, t) {
+  const a = te(t);
   e.state.shell = t;
-  const n = ae(e, t);
+  const n = ce(e, t);
   return {
     landmarks: a,
     dispose() {
@@ -253,67 +345,67 @@ function Ue(e, t) {
     }
   };
 }
-const Se = ["data-route-state"], Ae = /* @__PURE__ */ j({
+const ke = ["data-route-state"], Ge = /* @__PURE__ */ C({
   __name: "HomePage",
   setup(e) {
-    const t = $(), a = A(null), n = A("loading"), o = A("Loading the application home page…");
-    function r(u) {
-      let l = null;
-      return u.dispatchEvent(new CustomEvent("causeway-graphql-client-request", {
+    const t = z(), a = L(null), n = L("loading"), o = L("Loading the application home page…");
+    function r(p) {
+      let u = null;
+      return p.dispatchEvent(new CustomEvent("causeway-graphql-client-request", {
         bubbles: !0,
         composed: !0,
-        detail: { provide(s) {
-          l ??= s;
+        detail: { provide(l) {
+          u ??= l;
         } }
-      })), l;
+      })), u;
     }
-    return V(async () => {
-      const u = ++t.state.routeGeneration, l = a.value ? r(a.value) : null;
-      if (!l) {
+    return W(async () => {
+      const p = ++t.state.routeGeneration, u = a.value ? r(a.value) : null;
+      if (!u) {
         n.value = "unsupported", o.value = "Choose an application action to begin.";
         return;
       }
       try {
-        const s = await l.describeApplicationEntry();
-        if (u !== t.state.routeGeneration) return;
-        if (!s?.supported) {
+        const l = await u.describeApplicationEntry();
+        if (p !== t.state.routeGeneration) return;
+        if (!l?.supported) {
           n.value = "unsupported", o.value = "Choose an application action to begin.";
           return;
         }
-        const i = await l.readApplicationEntry({ description: s });
-        if (u !== t.state.routeGeneration) return;
-        const d = i?.data?.home, E = d?.object?._meta, f = E?.logicalTypeName ?? d?.logicalTypeName, g = E?.id, y = { claimed: !1, claim() {
+        const m = await u.readApplicationEntry({ description: l });
+        if (p !== t.state.routeGeneration) return;
+        const h = m?.data?.home, v = h?.object?._meta, f = v?.logicalTypeName ?? h?.logicalTypeName, d = v?.id, i = { claimed: !1, claim() {
           return this.claimed ? !1 : (this.claimed = !0, !0);
-        } }, P = { router: t.router, basePath: t.basePath, shell: t.state.shell, routeGeneration: u };
-        if (await t.policies.home?.(i?.data, y, P) === !0 && y.claim(), u !== t.state.routeGeneration || y.claimed) return;
-        if (d?.kind === "OBJECT" && f && g) {
-          await t.router.replace(z({ logicalTypeName: f, id: g }));
+        } }, y = { router: t.router, basePath: t.basePath, shell: t.state.shell, routeGeneration: p };
+        if (await t.policies.home?.(m?.data, i, y) === !0 && i.claim(), p !== t.state.routeGeneration || i.claimed) return;
+        if (h?.kind === "OBJECT" && f && d) {
+          await t.router.replace(B({ logicalTypeName: f, id: d }));
           return;
         }
-        n.value = i?.errors?.length ? "partial-error" : "ready", o.value = "Choose an application action to begin.";
-      } catch (s) {
-        if (u !== t.state.routeGeneration) return;
-        t.policies.error?.(s, { router: t.router, basePath: t.basePath, shell: t.state.shell, routeGeneration: u }), n.value = "partial-error", o.value = "The home page is unavailable; application menus remain available.";
+        n.value = m?.errors?.length ? "partial-error" : "ready", o.value = "Choose an application action to begin.";
+      } catch (l) {
+        if (p !== t.state.routeGeneration) return;
+        t.policies.error?.(l, { router: t.router, basePath: t.basePath, shell: t.state.shell, routeGeneration: p }), n.value = "partial-error", o.value = "The home page is unavailable; application menus remain available.";
       }
-    }), (u, l) => (O(), q("section", {
+    }), (p, u) => (N(), I("section", {
       ref_key: "page",
       ref: a,
       class: "causeway-vue-route-page causeway-vue-status",
       "data-route-state": n.value,
       tabindex: "-1"
     }, [
-      l[0] || (l[0] = T("h1", null, "Welcome", -1)),
-      T("p", null, ie(o.value), 1)
-    ], 8, Se));
+      u[0] || (u[0] = A("h1", null, "Welcome", -1)),
+      A("p", null, de(o.value), 1)
+    ], 8, ke));
   }
-}), Oe = {
+}), qe = {
   class: "causeway-vue-route-page causeway-vue-route-object",
   "data-causeway-route-page": "",
   "data-page-kind": "generic",
   "data-route-state": "loading",
   tabindex: "-1",
   "aria-label": "Object page"
-}, Pe = ["logical-type", "object-id"], Ne = /* @__PURE__ */ j({
+}, Ve = ["logical-type", "object-id"], $e = /* @__PURE__ */ C({
   __name: "GenericObjectPage",
   props: {
     logicalTypeName: {},
@@ -321,128 +413,128 @@ const Se = ["data-route-state"], Ae = /* @__PURE__ */ j({
     routeKey: {}
   },
   setup(e) {
-    return (t, a) => (O(), q("section", Oe, [
-      T("cw-object-context", {
+    return (t, a) => (N(), I("section", qe, [
+      A("cw-object-context", {
         "data-causeway-route-context": "",
         "logical-type": e.logicalTypeName,
         "object-id": e.objectId
       }, [...a[0] || (a[0] = [
-        T("cw-object", { editable: "" }, null, -1),
-        T("cw-interaction-controller", { "data-causeway-route-interactions": "" }, null, -1)
-      ])], 8, Pe)
+        A("cw-object", { editable: "" }, null, -1),
+        A("cw-interaction-controller", { "data-causeway-route-interactions": "" }, null, -1)
+      ])], 8, Ve)
     ]));
   }
-}), Re = ["data-route-key"], qe = {
+}), We = ["data-route-key"], Be = {
   key: 0,
   class: "causeway-vue-route-page causeway-vue-status causeway-vue-status-danger",
   "data-route-state": "terminal-error",
   tabindex: "-1",
   role: "alert"
-}, Ge = /* @__PURE__ */ j({
+}, ze = /* @__PURE__ */ C({
   __name: "ObjectRoutePage",
   setup(e) {
-    const t = pe(), a = $(), n = A(null), o = A(null), r = R(() => J(t.path, "/")), u = j(() => () => b("section", {
+    const t = ve(), a = z(), n = L(null), o = L(null), r = P(() => re(t.path, "/")), p = C(() => () => S("section", {
       class: "causeway-vue-route-page causeway-vue-status",
       "data-route-state": "loading",
       tabindex: -1,
       role: "status"
-    }, [b("h1", "Loading page…")])), l = j(() => () => b("section", {
+    }, [S("h1", "Loading page…")])), u = C(() => () => S("section", {
       class: "causeway-vue-route-page causeway-vue-status causeway-vue-status-danger",
       "data-route-state": "terminal-error",
       tabindex: -1,
       role: "alert"
-    }, [b("h1", "Page unavailable"), b("p", "The application page could not be loaded.")])), s = R(() => we(r.value)), i = R(() => a.pages.get(r.value.logicalTypeName)), d = R(() => {
-      const c = i.value;
-      return c ? xe(c) ? ce({
+    }, [S("h1", "Page unavailable"), S("p", "The application page could not be loaded.")])), l = P(() => Oe(r.value)), m = P(() => a.pages.get(r.value.logicalTypeName)), h = P(() => {
+      const s = m.value;
+      return s ? Ue(s) ? pe({
         loader: async () => {
-          const p = await c();
-          return "default" in p ? p.default : p;
+          const c = await s();
+          return "default" in c ? c.default : c;
         },
         delay: 0,
         timeout: 3e4,
-        loadingComponent: u,
-        errorComponent: l,
-        onError(p, v, w) {
-          a.policies.error?.(p, {
+        loadingComponent: p,
+        errorComponent: u,
+        onError(c, w, g) {
+          a.policies.error?.(c, {
             router: a.router,
             basePath: a.basePath,
             shell: a.state.shell,
             routeGeneration: a.state.routeGeneration
-          }), w();
+          }), g();
         }
-      }) : c : Ne;
+      }) : s : $e;
     });
-    function E() {
-      const c = a.state.shell;
+    function v() {
+      const s = a.state.shell;
       return {
-        route: c?.querySelector("[data-causeway-router-view]") ?? null,
-        loading: c?.querySelector("[data-causeway-route-loading]") ?? null,
-        announcement: c?.querySelector("[data-causeway-route-announcement]") ?? null
+        route: s?.querySelector("[data-causeway-router-view]") ?? null,
+        loading: s?.querySelector("[data-causeway-route-loading]") ?? null,
+        announcement: s?.querySelector("[data-causeway-route-announcement]") ?? null
       };
     }
-    function f(c, p) {
-      const v = E().announcement;
-      v && (v.textContent = "", requestAnimationFrame(() => {
-        p === a.state.routeGeneration && v.isConnected && (v.textContent = c);
+    function f(s, c) {
+      const w = v().announcement;
+      w && (w.textContent = "", requestAnimationFrame(() => {
+        c === a.state.routeGeneration && w.isConnected && (w.textContent = s);
       }));
     }
-    function g(c) {
-      const p = E();
-      p.route?.setAttribute("aria-busy", String(c)), p.loading && (p.loading.hidden = !c);
+    function d(s) {
+      const c = v();
+      c.route?.setAttribute("aria-busy", String(s)), c.loading && (c.loading.hidden = !s);
     }
-    async function y() {
-      const c = a.state.routeGeneration;
-      if (o.value = null, await de(), c !== a.state.routeGeneration || !n.value) return;
-      const p = ge(n.value, r.value);
-      p.valid || (o.value = p.classification ?? "invalid", g(!1), f("Page unavailable", c));
+    async function i() {
+      const s = a.state.routeGeneration;
+      if (o.value = null, await ye(), s !== a.state.routeGeneration || !n.value) return;
+      const c = me(n.value, r.value);
+      c.valid || (o.value = c.classification ?? "invalid", d(!1), f("Page unavailable", s));
     }
-    function P(c) {
-      const p = a.state.routeGeneration, v = c, w = v.detail?.state, N = v.target?.closest("[data-causeway-route-page]");
-      if (!w || !N || !n.value?.contains(N)) return;
-      let m = String(w.status ?? "terminal-error");
-      if (m === "terminal-error") {
-        const _ = w.errors?.[0]?.extensions?.classification ?? w.errors?.[0]?.extensions?.code ?? w.error?.code;
-        (_ === "NOT_FOUND" || _ === "ACCESS_DENIED") && (m = "unavailable");
+    function y(s) {
+      const c = a.state.routeGeneration, w = s, g = w.detail?.state, E = w.target?.closest("[data-causeway-route-page]");
+      if (!g || !E || !n.value?.contains(E)) return;
+      let T = String(g.status ?? "terminal-error");
+      if (T === "terminal-error") {
+        const O = g.errors?.[0]?.extensions?.classification ?? g.errors?.[0]?.extensions?.code ?? g.error?.code;
+        (O === "NOT_FOUND" || O === "ACCESS_DENIED") && (T = "unavailable");
       }
-      if (N.dataset.routeState = m, m === "ready" || m === "partial-error") {
-        g(!1);
-        const _ = w.snapshot?.data?._meta?.title;
-        _ && (document.title = String(_));
-        for (const oe of N.querySelectorAll("cw-collection:not([active])"))
-          oe.activate?.();
-        f(m === "ready" ? "Page ready" : "Page ready with partial information", p);
-      } else (m === "terminal-error" || m === "unavailable") && (g(!1), f("Page unavailable", p));
+      if (E.dataset.routeState = T, T === "ready" || T === "partial-error") {
+        d(!1);
+        const O = g.snapshot?.data?._meta?.title;
+        O && (document.title = String(O));
+        for (const ue of E.querySelectorAll("cw-collection:not([active])"))
+          ue.activate?.();
+        f(T === "ready" ? "Page ready" : "Page ready with partial information", c);
+      } else (T === "terminal-error" || T === "unavailable") && (d(!1), f("Page unavailable", c));
     }
-    return se(() => t.fullPath, () => {
-      a.state.routeGeneration += 1, g(!0), f("Loading page", a.state.routeGeneration), y();
-    }, { immediate: !0 }), V(async () => {
-      n.value?.addEventListener("causeway-object-context-state-change", P), await y();
-      const c = n.value?.querySelector(
+    return fe(() => t.fullPath, () => {
+      a.state.routeGeneration += 1, d(!0), f("Loading page", a.state.routeGeneration), i();
+    }, { immediate: !0 }), W(async () => {
+      n.value?.addEventListener("causeway-object-context-state-change", y), await i();
+      const s = n.value?.querySelector(
         "cw-object-context[data-causeway-route-context]"
       );
-      c?.context?.state && c.dispatchEvent(new CustomEvent("causeway-object-context-state-change", {
+      s?.context?.state && s.dispatchEvent(new CustomEvent("causeway-object-context-state-change", {
         bubbles: !0,
         composed: !0,
-        detail: { state: c.context.state, context: c.context }
+        detail: { state: s.context.state, context: s.context }
       })), n.value?.querySelector("[data-causeway-route-page]")?.focus({ preventScroll: !0 });
-    }), X(() => n.value?.removeEventListener("causeway-object-context-state-change", P)), (c, p) => (O(), q("div", {
+    }), ee(() => n.value?.removeEventListener("causeway-object-context-state-change", y)), (s, c) => (N(), I("div", {
       ref_key: "host",
       ref: n,
       class: "causeway-vue-object-route",
-      "data-route-key": s.value
+      "data-route-key": l.value
     }, [
-      o.value ? (O(), q("section", qe, [...p[0] || (p[0] = [
-        T("h1", null, "Page unavailable", -1),
-        T("p", null, "The application page has an invalid semantic context boundary.", -1)
-      ])])) : (O(), ue(le(d.value), {
-        key: s.value,
+      o.value ? (N(), I("section", Be, [...c[0] || (c[0] = [
+        A("h1", null, "Page unavailable", -1),
+        A("p", null, "The application page has an invalid semantic context boundary.", -1)
+      ])])) : (N(), ge(he(h.value), {
+        key: l.value,
         "logical-type-name": r.value.logicalTypeName,
         "object-id": r.value.objectId,
-        "route-key": s.value,
-        onVnodeMounted: y,
-        onVnodeUpdated: y
+        "route-key": l.value,
+        onVnodeMounted: i,
+        onVnodeUpdated: i
       }, null, 8, ["logical-type-name", "object-id", "route-key"]))
-    ], 8, Re));
+    ], 8, We));
   }
 }), x = Object.freeze({
   home: "causeway-home",
@@ -450,40 +542,40 @@ const Se = ["data-route-state"], Ae = /* @__PURE__ */ j({
   invalid: "causeway-invalid-route",
   notFound: "causeway-not-found"
 });
-function ne(e, t, a, n) {
-  return j({
+function se(e, t, a, n) {
+  return C({
     name: `Causeway${e.replace(/\W/gu, "")}Page`,
     setup() {
-      return () => b("section", {
+      return () => S("section", {
         class: ["causeway-vue-route-page", "causeway-vue-status", a === "invalid-route" && "causeway-vue-status-danger"],
         "data-route-state": a,
         tabindex: -1,
         role: n
-      }, [b("h1", e), b("p", t)]);
+      }, [S("h1", e), S("p", t)]);
     }
   });
 }
-const Ie = ne(
+const De = se(
   "Invalid route",
   "The requested application route is invalid.",
   "invalid-route",
   "alert"
-), ke = ne(
+), Me = se(
   "Page unavailable",
   "The requested page is unavailable.",
   "not-found",
   "alert"
 );
-function Be(e = {}) {
+function He(e = {}) {
   return [
-    { path: "/", name: x.home, component: e.homeComponent ?? Ae },
+    { path: "/", name: x.home, component: e.homeComponent ?? Ge },
     {
       path: "/object/:logicalTypeName/:objectId",
       name: x.object,
-      component: Ge,
+      component: ze,
       beforeEnter(t) {
         try {
-          return J(t.path, "/"), !0;
+          return re(t.path, "/"), !0;
         } catch {
           return { name: x.invalid, replace: !0 };
         }
@@ -492,48 +584,55 @@ function Be(e = {}) {
     {
       path: "/invalid-route",
       name: x.invalid,
-      component: Ie
+      component: De
     },
     {
       path: "/:pathMatch(.*)*",
       name: x.notFound,
-      component: e.notFoundComponent ?? ke
+      component: e.notFoundComponent ?? Me
     }
   ];
 }
 export {
-  M as ACTION_REQUEST_EVENT,
-  W as ACTION_RESULT_EVENT,
+  G as ACTION_REQUEST_EVENT,
+  X as ACTION_RESULT_EVENT,
   x as CAUSEWAY_ROUTE_NAMES,
-  H as CAUSEWAY_VIEWER_KEY,
-  Ne as CausewayGenericObjectPage,
-  Ae as CausewayHomePage,
-  Ie as CausewayInvalidRoutePage,
-  ke as CausewayNotFoundPage,
-  Ge as CausewayObjectRoutePage,
-  he as INVALID_ROUTE_MESSAGE,
-  D as NAVIGATION_REQUEST_EVENT,
-  F as OBJECT_CONTEXT_STATE_EVENT,
-  G as ROUTE_CONTEXT_SELECTOR,
-  fe as ROUTE_INTERACTIONS_SELECTOR,
-  Ue as bindCausewayShell,
-  ve as canonicalObjectPath,
-  we as canonicalRouteKey,
-  z as canonicalRouterObjectPath,
-  Be as createCausewayRouteRecords,
-  ze as createCausewayVueViewer,
-  B as decodeRouteSegment,
-  I as encodeRouteSegment,
-  ae as installSemanticBridge,
-  xe as isPageLoader,
-  L as normalizeBasePath,
-  Ce as normalizePageRegistry,
-  J as parseCanonicalObjectPath,
-  te as presentSemanticResult,
-  k as resolveResultOutlet,
-  $e as useCausewayShell,
-  $ as useCausewayViewer,
-  ge as validateRouteBoundary,
-  Z as validateShellBoundary
+  ae as CAUSEWAY_VIEWER_KEY,
+  $e as CausewayGenericObjectPage,
+  Ge as CausewayHomePage,
+  De as CausewayInvalidRoutePage,
+  Me as CausewayNotFoundPage,
+  ze as CausewayObjectRoutePage,
+  D as FRAMEWORK_LOGOUT_ACTION,
+  Se as INVALID_ROUTE_MESSAGE,
+  be as LocalResourceNavigationError,
+  H as MENU_BARS_STATE_EVENT,
+  Y as NAVIGATION_REQUEST_EVENT,
+  Z as OBJECT_CONTEXT_STATE_EVENT,
+  k as ROUTE_CONTEXT_SELECTOR,
+  we as ROUTE_INTERACTIONS_SELECTOR,
+  Ze as bindCausewayShell,
+  Ce as canonicalObjectPath,
+  Oe as canonicalRouteKey,
+  B as canonicalRouterObjectPath,
+  He as createCausewayRouteRecords,
+  Ye as createCausewayVueViewer,
+  K as decodeRouteSegment,
+  q as encodeRouteSegment,
+  ce as installSemanticBridge,
+  ne as isFrameworkLogoutAction,
+  Ue as isPageLoader,
+  Te as navigateLocalResource,
+  U as normalizeBasePath,
+  Ie as normalizePageRegistry,
+  re as parseCanonicalObjectPath,
+  ie as presentSemanticResult,
+  M as removeFrameworkLogoutMenuActions,
+  Ee as resolveLocalResourceTarget,
+  $ as resolveResultOutlet,
+  Xe as useCausewayShell,
+  z as useCausewayViewer,
+  me as validateRouteBoundary,
+  te as validateShellBoundary
 };
 //# sourceMappingURL=causeway-vue.js.map
diff --git a/viewers/webcomponents/vue/dist/causeway-vue.js.map b/viewers/webcomponents/vue/dist/causeway-vue.js.map
index 63164cafb1c..42539e2e459 100644
--- a/viewers/webcomponents/vue/dist/causeway-vue.js.map
+++ b/viewers/webcomponents/vue/dist/causeway-vue.js.map
@@ -1 +1 @@
-{"version":3,"file":"causeway-vue.js","sources":["../src/boundary.ts","../src/contracts.ts","../src/route-codec.ts","../src/policy.ts","../src/registry.ts","../src/plugin.ts","../src/components/HomePage.vue","../src/components/GenericObjectPage.vue","../src/components/ObjectRoutePage.vue","../src/routes.ts"],"sourcesContent":["/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work f [...]
\ No newline at end of file
+{"version":3,"file":"causeway-vue.js","sources":["../src/boundary.ts","../src/contracts.ts","../src/host-operation.ts","../src/local-resource.ts","../src/route-codec.ts","../src/policy.ts","../src/registry.ts","../src/plugin.ts","../src/components/HomePage.vue","../src/components/GenericObjectPage.vue","../src/components/ObjectRoutePage.vue","../src/routes.ts"],"sourcesContent":["/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements. [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/contracts.d.ts b/viewers/webcomponents/vue/dist/contracts.d.ts
index dd9d303cc40..f97475da63e 100644
--- a/viewers/webcomponents/vue/dist/contracts.d.ts
+++ b/viewers/webcomponents/vue/dist/contracts.d.ts
@@ -7,9 +7,22 @@ export interface CausewayObjectTarget {
     readonly objectId?: string;
     readonly title?: string;
 }
+export interface CausewayLocalResourceTarget {
+    readonly path: string;
+    readonly openUrlStrategy: 'SAME_WINDOW' | 'NEW_WINDOW';
+}
 export interface CausewaySemanticResult {
-    readonly kind: 'object' | 'scalar' | 'collection' | 'void' | string;
+    readonly kind: 'object' | 'scalar' | 'collection' | 'local-resource' | 'unsupported' | 'void' | string;
     readonly value?: unknown;
+    readonly reason?: string;
+}
+export interface CausewayActionRequest {
+    readonly actionId: string;
+    readonly serviceLogicalTypeName?: string;
+    readonly identity?: CausewayObjectTarget | null;
+    readonly target?: object | null;
+    readonly context?: object;
+    readonly presentation?: Record<string, unknown>;
 }
 export interface CausewayEventClaim {
     readonly claimed: boolean;
@@ -22,6 +35,7 @@ export interface CausewayPolicyContext {
     readonly routeGeneration: number;
 }
 export interface CausewayViewerPolicies {
+    readonly action?: (detail: CausewayActionRequest, claim: CausewayEventClaim, context: CausewayPolicyContext) => boolean | void | Promise<boolean | void>;
     readonly navigate?: (target: CausewayObjectTarget, claim: CausewayEventClaim, context: CausewayPolicyContext) => boolean | void | Promise<boolean | void>;
     readonly home?: (entry: unknown, claim: CausewayEventClaim, context: CausewayPolicyContext) => boolean | void | Promise<boolean | void>;
     readonly result?: (detail: unknown, claim: CausewayEventClaim, context: CausewayPolicyContext) => boolean | void | Promise<boolean | void>;
@@ -31,6 +45,7 @@ export interface CausewayViewerOptions {
     readonly router: Router;
     readonly endpoint: string;
     readonly basePath?: string;
+    readonly applicationResourceBase?: string;
     readonly pages?: CausewayPageRegistryInput;
     readonly policies?: CausewayViewerPolicies;
     readonly developmentDiagnostics?: boolean;
@@ -40,6 +55,7 @@ export interface CausewayViewerRuntime {
     readonly router: Router;
     readonly endpoint: string;
     readonly basePath: string;
+    readonly applicationResourceBase: string;
     readonly pages: ReadonlyMap<string, CausewayPageRegistration>;
     readonly policies: CausewayViewerPolicies;
     readonly developmentDiagnostics: boolean;
diff --git a/viewers/webcomponents/vue/dist/contracts.d.ts.map b/viewers/webcomponents/vue/dist/contracts.d.ts.map
index 3e13a37b16b..cc329acf2a9 100644
--- a/viewers/webcomponents/vue/dist/contracts.d.ts.map
+++ b/viewers/webcomponents/vue/dist/contracts.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAC,YAAY,EAAE,MAAM,EAAC,MAAM,KAAK,CAAC;AAC9C,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAC,wBAAwB,EAAE,yBAAyB,EAAC,MAAM,YAAY,CAAC;AAEpF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC [...]
\ No newline at end of file
+{"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAC,YAAY,EAAE,MAAM,EAAC,MAAM,KAAK,CAAC;AAC9C,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAC,wBAAwB,EAAE,yBAAyB,EAAC,MAAM,YAAY,CAAC;AAEpF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,2BAA2B;IAC1C [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/host-operation.d.ts b/viewers/webcomponents/vue/dist/host-operation.d.ts
new file mode 100644
index 00000000000..523ee8be80d
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/host-operation.d.ts
@@ -0,0 +1,8 @@
+import { CausewayActionRequest } from './contracts';
+export declare const FRAMEWORK_LOGOUT_ACTION: Readonly<{
+    serviceLogicalTypeName: "causeway.security.LogoutMenu";
+    actionId: "logout";
+}>;
+export declare function isFrameworkLogoutAction(value: Partial<CausewayActionRequest> | undefined): boolean;
+export declare function removeFrameworkLogoutMenuActions(root: ParentNode): number;
+//# sourceMappingURL=host-operation.d.ts.map
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/host-operation.d.ts.map b/viewers/webcomponents/vue/dist/host-operation.d.ts.map
new file mode 100644
index 00000000000..5f52a28275c
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/host-operation.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"host-operation.d.ts","sourceRoot":"","sources":["../src/host-operation.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,aAAa,CAAC;AAEvD,eAAO,MAAM,uBAAuB;;;EAGlC,CAAC;AAEH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,CAAC,qBAAqB,CAAC,GAAG,SAAS,GAAG,OAAO,CAGlG;AAED,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAWzE"}
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/index.d.ts b/viewers/webcomponents/vue/dist/index.d.ts
index 24629dc9f88..48c54b57d59 100644
--- a/viewers/webcomponents/vue/dist/index.d.ts
+++ b/viewers/webcomponents/vue/dist/index.d.ts
@@ -1,5 +1,7 @@
 export * from './boundary';
 export * from './contracts';
+export * from './host-operation';
+export * from './local-resource';
 export * from './plugin';
 export * from './policy';
 export * from './registry';
diff --git a/viewers/webcomponents/vue/dist/index.d.ts.map b/viewers/webcomponents/vue/dist/index.d.ts.map
index 31e0ec2bf7a..8c3e29a97ff 100644
--- a/viewers/webcomponents/vue/dist/index.d.ts.map
+++ b/viewers/webcomponents/vue/dist/index.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAmBA,OAAO,aAAa,CAAC;AAErB,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,OAAO,EAAC,OAAO,IAAI,yBAAyB,EAAC,MAAM,oCAAoC,CAAC;AACxF,OAAO,EAAC,OAAO,IAAI,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAC,OAAO,IAAI,uBAAuB,EAAC,MAAM,kCAAkC,CAAC"}
\ No newline at end of file
+{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAmBA,OAAO,aAAa,CAAC;AAErB,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,OAAO,EAAC,OAAO,IAAI,yBAAyB,EAAC,MAAM,oCAAoC,CAAC;AACxF,OAAO,EAAC,OAAO,IAAI,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAC,OAAO,IAAI,uBAAuB,EAAC,MAAM,kCAAkC,CAAC"}
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/local-resource.d.ts b/viewers/webcomponents/vue/dist/local-resource.d.ts
new file mode 100644
index 00000000000..8e00c70053e
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/local-resource.d.ts
@@ -0,0 +1,21 @@
+import { CausewayLocalResourceTarget } from './contracts';
+export declare class LocalResourceNavigationError extends Error {
+    readonly code: string;
+    constructor(code: string, message: string);
+}
+interface LocationTarget {
+    readonly href: string;
+    assign(url: string): void;
+}
+interface NavigateOptions {
+    readonly location?: LocationTarget;
+    readonly applicationBase?: string;
+    readonly open?: (url?: string | URL, target?: string, features?: string) => Window | null;
+}
+export declare function resolveLocalResourceTarget(value: CausewayLocalResourceTarget | undefined, options?: Pick<NavigateOptions, 'location' | 'applicationBase'>): Readonly<{
+    url: URL;
+    openUrlStrategy: 'SAME_WINDOW' | 'NEW_WINDOW';
+}>;
+export declare function navigateLocalResource(value: CausewayLocalResourceTarget | undefined, options?: NavigateOptions): void;
+export {};
+//# sourceMappingURL=local-resource.d.ts.map
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/local-resource.d.ts.map b/viewers/webcomponents/vue/dist/local-resource.d.ts.map
new file mode 100644
index 00000000000..1cedd139cb1
--- /dev/null
+++ b/viewers/webcomponents/vue/dist/local-resource.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"local-resource.d.ts","sourceRoot":"","sources":["../src/local-resource.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAC,2BAA2B,EAAC,MAAM,aAAa,CAAC;AAE7D,qBAAa,4BAA6B,SAAQ,KAAK;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAK1C;AAED,UAAU,cAAc;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,UAAU,eAAe;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC;IACnC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,C [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/plugin.d.ts.map b/viewers/webcomponents/vue/dist/plugin.d.ts.map
index 95ca872aa7c..c948371a041 100644
--- a/viewers/webcomponents/vue/dist/plugin.d.ts.map
+++ b/viewers/webcomponents/vue/dist/plugin.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAkD,KAAK,GAAG,EAAC,MAAM,KAAK,CAAC;AAC9E,OAAO,EAAsB,KAAK,qBAAqB,EAAE,KAAK,qBAAqB,EAAC,MAAM,aAAa,CAAC;AACxG,OAAO,EAAwB,KAAK,cAAc,EAAC,MAAM,YAAY,CAAC;AAKtE,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,qBAAqB,GAAG,qBAAqB,CAsB7F;AAED,wBAAgB,iBAAiB,IAAI,qBAAqB,CAIzD;AAED,wBAAgB,gBAAgB,CAAC,cAAc,EAAE,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,CAgBjG;AAED,wBAAgB,iBAAiB,CAAC, [...]
\ No newline at end of file
+{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAkD,KAAK,GAAG,EAAC,MAAM,KAAK,CAAC;AAC9E,OAAO,EAAsB,KAAK,qBAAqB,EAAE,KAAK,qBAAqB,EAAC,MAAM,aAAa,CAAC;AACxG,OAAO,EAAwB,KAAK,cAAc,EAAC,MAAM,YAAY,CAAC;AAKtE,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,qBAAqB,GAAG,qBAAqB,CAuB7F;AAED,wBAAgB,iBAAiB,IAAI,qBAAqB,CAIzD;AAED,wBAAgB,gBAAgB,CAAC,cAAc,EAAE,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,CAgBjG;AAED,wBAAgB,iBAAiB,CAAC, [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/dist/policy.d.ts b/viewers/webcomponents/vue/dist/policy.d.ts
index c44f04a1b92..ba60dee00c0 100644
--- a/viewers/webcomponents/vue/dist/policy.d.ts
+++ b/viewers/webcomponents/vue/dist/policy.d.ts
@@ -3,6 +3,7 @@ export declare const NAVIGATION_REQUEST_EVENT = "causeway-navigation-request";
 export declare const ACTION_REQUEST_EVENT = "causeway-action-request";
 export declare const ACTION_RESULT_EVENT = "causeway-action-result";
 export declare const OBJECT_CONTEXT_STATE_EVENT = "causeway-object-context-state-change";
+export declare const MENU_BARS_STATE_EVENT = "causeway-menu-bars-state-change";
 interface ActionResultDetail {
     readonly actionId?: string;
     readonly context?: object;
diff --git a/viewers/webcomponents/vue/dist/policy.d.ts.map b/viewers/webcomponents/vue/dist/policy.d.ts.map
index c549f5fba3a..2d5070da678 100644
--- a/viewers/webcomponents/vue/dist/policy.d.ts.map
+++ b/viewers/webcomponents/vue/dist/policy.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAIV,sBAAsB,EACtB,qBAAqB,EACtB,MAAM,aAAa,CAAC;AAGrB,eAAO,MAAM,wBAAwB,gCAAgC,CAAC;AACtE,eAAO,MAAM,oBAAoB,4BAA4B,CAAC;AAC9D,eAAO,MAAM,mBAAmB,2BAA2B,CAAC;AAC5D,eAAO,MAAM,0BAA0B,yCAAyC,CAAC;AAEjF,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,sBAAsB,CAAC;IACzC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC, [...]
\ No newline at end of file
+{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAMV,sBAAsB,EACtB,qBAAqB,EACtB,MAAM,aAAa,CAAC;AAKrB,eAAO,MAAM,wBAAwB,gCAAgC,CAAC;AACtE,eAAO,MAAM,oBAAoB,4BAA4B,CAAC;AAC9D,eAAO,MAAM,mBAAmB,2BAA2B,CAAC;AAC5D,eAAO,MAAM,0BAA0B,yCAAyC,CAAC;AACjF,eAAO,MAAM,qBAAqB,oCAAoC,CAAC;AAEvE,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,sBAAsB,CAAC;IACzC,QA [...]
\ No newline at end of file
diff --git a/viewers/webcomponents/vue/src/contracts.ts b/viewers/webcomponents/vue/src/contracts.ts
index 2cc3c18b577..0711f94ac5c 100644
--- a/viewers/webcomponents/vue/src/contracts.ts
+++ b/viewers/webcomponents/vue/src/contracts.ts
@@ -28,9 +28,24 @@ export interface CausewayObjectTarget {
   readonly title?: string;
 }
 
+export interface CausewayLocalResourceTarget {
+  readonly path: string;
+  readonly openUrlStrategy: 'SAME_WINDOW' | 'NEW_WINDOW';
+}
+
 export interface CausewaySemanticResult {
-  readonly kind: 'object' | 'scalar' | 'collection' | 'void' | string;
+  readonly kind: 'object' | 'scalar' | 'collection' | 'local-resource' | 'unsupported' | 'void' | string;
   readonly value?: unknown;
+  readonly reason?: string;
+}
+
+export interface CausewayActionRequest {
+  readonly actionId: string;
+  readonly serviceLogicalTypeName?: string;
+  readonly identity?: CausewayObjectTarget | null;
+  readonly target?: object | null;
+  readonly context?: object;
+  readonly presentation?: Record<string, unknown>;
 }
 
 export interface CausewayEventClaim {
@@ -46,6 +61,7 @@ export interface CausewayPolicyContext {
 }
 
 export interface CausewayViewerPolicies {
+  readonly action?: (detail: CausewayActionRequest, claim: CausewayEventClaim, context: CausewayPolicyContext) => boolean | void | Promise<boolean | void>;
   readonly navigate?: (target: CausewayObjectTarget, claim: CausewayEventClaim, context: CausewayPolicyContext) => boolean | void | Promise<boolean | void>;
   readonly home?: (entry: unknown, claim: CausewayEventClaim, context: CausewayPolicyContext) => boolean | void | Promise<boolean | void>;
   readonly result?: (detail: unknown, claim: CausewayEventClaim, context: CausewayPolicyContext) => boolean | void | Promise<boolean | void>;
@@ -56,6 +72,7 @@ export interface CausewayViewerOptions {
   readonly router: Router;
   readonly endpoint: string;
   readonly basePath?: string;
+  readonly applicationResourceBase?: string;
   readonly pages?: CausewayPageRegistryInput;
   readonly policies?: CausewayViewerPolicies;
   readonly developmentDiagnostics?: boolean;
@@ -66,6 +83,7 @@ export interface CausewayViewerRuntime {
   readonly router: Router;
   readonly endpoint: string;
   readonly basePath: string;
+  readonly applicationResourceBase: string;
   readonly pages: ReadonlyMap<string, CausewayPageRegistration>;
   readonly policies: CausewayViewerPolicies;
   readonly developmentDiagnostics: boolean;
diff --git a/viewers/webcomponents/vue/src/host-operation.ts b/viewers/webcomponents/vue/src/host-operation.ts
new file mode 100644
index 00000000000..c7d204e9bc1
--- /dev/null
+++ b/viewers/webcomponents/vue/src/host-operation.ts
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type {CausewayActionRequest} from './contracts';
+
+export const FRAMEWORK_LOGOUT_ACTION = Object.freeze({
+  serviceLogicalTypeName: 'causeway.security.LogoutMenu',
+  actionId: 'logout'
+});
+
+export function isFrameworkLogoutAction(value: Partial<CausewayActionRequest> | undefined): boolean {
+  return value?.serviceLogicalTypeName === FRAMEWORK_LOGOUT_ACTION.serviceLogicalTypeName
+    && value?.actionId === FRAMEWORK_LOGOUT_ACTION.actionId;
+}
+
+export function removeFrameworkLogoutMenuActions(root: ParentNode): number {
+  let removed = 0;
+  for (const control of root.querySelectorAll<HTMLElement>('[data-causeway-service-action]')) {
+    if (!isFrameworkLogoutAction({
+      serviceLogicalTypeName: control.getAttribute('data-service-logical-type') ?? undefined,
+      actionId: control.getAttribute('data-action-id') ?? ''
+    })) continue;
+    (control.closest<HTMLElement>('[data-causeway-service-action-region]') ?? control).remove();
+    removed += 1;
+  }
+  return removed;
+}
diff --git a/viewers/webcomponents/vue/src/index.ts b/viewers/webcomponents/vue/src/index.ts
index ff16b6a8778..c731e194077 100644
--- a/viewers/webcomponents/vue/src/index.ts
+++ b/viewers/webcomponents/vue/src/index.ts
@@ -21,6 +21,8 @@ import './theme.css';
 
 export * from './boundary';
 export * from './contracts';
+export * from './host-operation';
+export * from './local-resource';
 export * from './plugin';
 export * from './policy';
 export * from './registry';
diff --git a/viewers/webcomponents/vue/src/local-resource.ts b/viewers/webcomponents/vue/src/local-resource.ts
new file mode 100644
index 00000000000..2adcb965272
--- /dev/null
+++ b/viewers/webcomponents/vue/src/local-resource.ts
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type {CausewayLocalResourceTarget} from './contracts';
+
+export class LocalResourceNavigationError extends Error {
+  readonly code: string;
+
+  constructor(code: string, message: string) {
+    super(message);
+    this.name = 'LocalResourceNavigationError';
+    this.code = code;
+  }
+}
+
+interface LocationTarget {
+  readonly href: string;
+  assign(url: string): void;
+}
+
+interface NavigateOptions {
+  readonly location?: LocationTarget;
+  readonly applicationBase?: string;
+  readonly open?: (url?: string | URL, target?: string, features?: string) => Window | null;
+}
+
+export function resolveLocalResourceTarget(
+  value: CausewayLocalResourceTarget | undefined,
+  options: Pick<NavigateOptions, 'location' | 'applicationBase'> = {}
+): Readonly<{url: URL; openUrlStrategy: 'SAME_WINDOW' | 'NEW_WINDOW'}> {
+  const location = options.location ?? window.location;
+  const path = typeof value?.path === 'string' ? value.path.trim() : '';
+  const strategy = value?.openUrlStrategy;
+  if (!path || !['SAME_WINDOW', 'NEW_WINDOW'].includes(strategy ?? '')) {
+    throw failure('LOCAL_RESOURCE_INVALID', 'The local-resource result is incomplete.');
+  }
+  if (/^[a-z][a-z\d+.-]*:/i.test(path) || path.startsWith('//') || path.includes('\\') || /[\u0000-\u001f\u007f]/.test(path)) {
+    throw failure('LOCAL_RESOURCE_TARGET_UNSAFE', 'The local-resource target is not an application-local path.');
+  }
+  const origin = new URL(location.href);
+  const base = resolveApplicationBase(options.applicationBase ?? '/', origin);
+  const basePath = base.pathname.replace(/\/+$/, '') || '/';
+  const supplied = path.startsWith('/') ? path : `/${path}`;
+  const alreadyBased = basePath !== '/'
+    && (supplied === basePath || supplied.startsWith(`${basePath}/`) || supplied.startsWith(`${basePath}?`) || supplied.startsWith(`${basePath}#`));
+  const relative = alreadyBased ? supplied : `${basePath === '/' ? '' : basePath}/${supplied.replace(/^\/+/, '')}`;
+  const target = new URL(relative, origin.origin);
+  if (target.origin !== origin.origin || target.username || target.password
+      || (basePath !== '/' && target.pathname !== basePath && !target.pathname.startsWith(`${basePath}/`))) {
+    throw failure('LOCAL_RESOURCE_TARGET_UNSAFE', 'The local-resource target escapes the configured application boundary.');
+  }
+  return Object.freeze({url: target, openUrlStrategy: strategy as 'SAME_WINDOW' | 'NEW_WINDOW'});
+}
+
+export function navigateLocalResource(value: CausewayLocalResourceTarget | undefined, options: NavigateOptions = {}): void {
+  const location = options.location ?? window.location;
+  const target = resolveLocalResourceTarget(value, {location, applicationBase: options.applicationBase});
+  if (target.openUrlStrategy === 'SAME_WINDOW') {
+    location.assign(target.url.href);
+    return;
+  }
+  const open = options.open ?? window.open.bind(window);
+  const opened = open(target.url.href, '_blank', 'noopener,noreferrer');
+  if (opened) opened.opener = null;
+}
+
+function resolveApplicationBase(applicationBase: string, origin: URL): URL {
+  const text = String(applicationBase).trim() || '/';
+  if (text.startsWith('//') || /[\u0000-\u001f\u007f]/.test(text)) {
+    throw failure('LOCAL_RESOURCE_BASE_INVALID', 'The application-local resource base is invalid.');
+  }
+  const base = new URL(text.endsWith('/') ? text : `${text}/`, origin.origin);
+  if (base.origin !== origin.origin || base.username || base.password) {
+    throw failure('LOCAL_RESOURCE_BASE_INVALID', 'The application-local resource base must be same-origin.');
+  }
+  return base;
+}
+
+function failure(code: string, message: string): LocalResourceNavigationError {
+  return new LocalResourceNavigationError(code, message);
+}
diff --git a/viewers/webcomponents/vue/src/plugin.ts b/viewers/webcomponents/vue/src/plugin.ts
index dccdfc4f8d1..b758603a0aa 100644
--- a/viewers/webcomponents/vue/src/plugin.ts
+++ b/viewers/webcomponents/vue/src/plugin.ts
@@ -40,6 +40,7 @@ export function createCausewayVueViewer(options: CausewayViewerOptions): Causewa
     router: options.router,
     endpoint,
     basePath: normalizeBasePath(options.basePath ?? (options.router.options.history.base || '/')),
+    applicationResourceBase: normalizeBasePath(options.applicationResourceBase ?? '/'),
     pages: normalizePageRegistry(options.pages),
     policies: Object.freeze({...options.policies}),
     developmentDiagnostics: options.developmentDiagnostics ?? true,
diff --git a/viewers/webcomponents/vue/src/policy.ts b/viewers/webcomponents/vue/src/policy.ts
index 56dea4749da..bec10d4dd2c 100644
--- a/viewers/webcomponents/vue/src/policy.ts
+++ b/viewers/webcomponents/vue/src/policy.ts
@@ -18,18 +18,23 @@
  */
 
 import type {
+  CausewayActionRequest,
   CausewayEventClaim,
+  CausewayLocalResourceTarget,
   CausewayObjectTarget,
   CausewayPolicyContext,
   CausewaySemanticResult,
   CausewayViewerRuntime
 } from './contracts';
+import {isFrameworkLogoutAction, removeFrameworkLogoutMenuActions} from './host-operation';
+import {navigateLocalResource} from './local-resource';
 import {canonicalRouterObjectPath} from './route-codec';
 
 export const NAVIGATION_REQUEST_EVENT = 'causeway-navigation-request';
 export const ACTION_REQUEST_EVENT = 'causeway-action-request';
 export const ACTION_RESULT_EVENT = 'causeway-action-result';
 export const OBJECT_CONTEXT_STATE_EVENT = 'causeway-object-context-state-change';
+export const MENU_BARS_STATE_EVENT = 'causeway-menu-bars-state-change';
 
 interface ActionResultDetail {
   readonly actionId?: string;
@@ -149,9 +154,28 @@ async function applyNavigation(runtime: CausewayViewerRuntime, target: CausewayO
 
 export function installSemanticBridge(runtime: CausewayViewerRuntime, shell: HTMLElement): () => void {
   const destinations = new WeakMap<object, HTMLElement>();
+  const resumedActionEvents = new WeakSet<Event>();
   let unscopedDestination: HTMLElement | null = null;
+  let active = true;
+  const announceUnavailableLogout = () => {
+    const announcement = shell.querySelector<HTMLElement>('[data-causeway-route-announcement]');
+    if (announcement) announcement.textContent = 'Logout requires a host authentication integration.';
+    shell.dataset.causewayLogoutUnavailable = 'true';
+  };
+  const resumeAction = (event: CustomEvent<CausewayActionRequest>, generation: number) => {
+    if (!active || generation !== runtime.state.routeGeneration || !(event.target instanceof EventTarget)) return;
+    const replay = new CustomEvent<CausewayActionRequest>(ACTION_REQUEST_EVENT, {
+      bubbles: true,
+      composed: true,
+      cancelable: true,
+      detail: event.detail
+    });
+    resumedActionEvents.add(replay);
+    event.target.dispatchEvent(replay);
+  };
   const onActionRequest = (rawEvent: Event) => {
-    const detail = (rawEvent as CustomEvent).detail as ActionResultDetail | undefined;
+    const event = rawEvent as CustomEvent<CausewayActionRequest>;
+    const detail = event.detail;
     try {
       const outlet = resolveResultOutlet(shell);
       if (detail?.context && typeof detail.context === 'object') destinations.set(detail.context, outlet);
@@ -159,6 +183,36 @@ export function installSemanticBridge(runtime: CausewayViewerRuntime, shell: HTM
     } catch (error) {
       runtime.policies.error?.(error, context(runtime));
     }
+    if (resumedActionEvents.has(event)) return;
+    const frameworkLogout = isFrameworkLogoutAction(detail);
+    const actionPolicy = runtime.policies.action;
+    if (!actionPolicy) {
+      if (frameworkLogout) {
+        event.preventDefault();
+        announceUnavailableLogout();
+      }
+      return;
+    }
+    const claim = createClaim();
+    const generation = runtime.state.routeGeneration;
+    try {
+      const handled = actionPolicy(detail, claim, context(runtime));
+      if (handled && typeof (handled as Promise<boolean | void>).then === 'function') {
+        event.preventDefault();
+        void Promise.resolve(handled).then(value => {
+          if (value === true) claim.claim();
+          if (!claim.claimed && !frameworkLogout) resumeAction(event, generation);
+          else if (!claim.claimed) announceUnavailableLogout();
+        }).catch(error => runtime.policies.error?.(error, context(runtime)));
+        return;
+      }
+      if (handled === true) claim.claim();
+      if (claim.claimed || frameworkLogout) event.preventDefault();
+      if (frameworkLogout && !claim.claimed) announceUnavailableLogout();
+    } catch (error) {
+      event.preventDefault();
+      runtime.policies.error?.(error, context(runtime));
+    }
   };
   const onNavigation = (rawEvent: Event) => {
     const event = rawEvent as CustomEvent;
@@ -175,6 +229,11 @@ export function installSemanticBridge(runtime: CausewayViewerRuntime, shell: HTM
       const handled = await runtime.policies.result?.(detail, claim, context(runtime));
       if (handled === true) claim.claim();
       if (claim.claimed) return;
+      if (detail.result?.kind === 'local-resource') {
+        navigateLocalResource(detail.result.value as CausewayLocalResourceTarget, {applicationBase: runtime.applicationResourceBase});
+        (event.target as {dismissResult?: () => void} | null)?.dismissResult?.();
+        return;
+      }
       const identity = objectIdentity(detail.result);
       if (identity) return applyNavigation(runtime, identity);
       const destination = detail.context && destinations.get(detail.context) || unscopedDestination || resolveResultOutlet(shell);
@@ -183,12 +242,19 @@ export function installSemanticBridge(runtime: CausewayViewerRuntime, shell: HTM
       (event.target as {dismissResult?: () => void} | null)?.dismissResult?.();
     })().catch(error => runtime.policies.error?.(error, context(runtime)));
   };
+  const onMenuState = () => queueMicrotask(() => {
+    if (active) removeFrameworkLogoutMenuActions(shell);
+  });
+  removeFrameworkLogoutMenuActions(shell);
   shell.addEventListener(ACTION_REQUEST_EVENT, onActionRequest, {capture: true});
   shell.addEventListener(NAVIGATION_REQUEST_EVENT, onNavigation);
   shell.addEventListener(ACTION_RESULT_EVENT, onResult);
+  shell.addEventListener(MENU_BARS_STATE_EVENT, onMenuState);
   return () => {
+    active = false;
     shell.removeEventListener(ACTION_REQUEST_EVENT, onActionRequest, {capture: true});
     shell.removeEventListener(NAVIGATION_REQUEST_EVENT, onNavigation);
     shell.removeEventListener(ACTION_RESULT_EVENT, onResult);
+    shell.removeEventListener(MENU_BARS_STATE_EVENT, onMenuState);
   };
 }
diff --git a/viewers/webcomponents/vue/test/local-resource.test.ts b/viewers/webcomponents/vue/test/local-resource.test.ts
new file mode 100644
index 00000000000..18122a77eea
--- /dev/null
+++ b/viewers/webcomponents/vue/test/local-resource.test.ts
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {describe, expect, it, vi} from 'vitest';
+import {navigateLocalResource, resolveLocalResourceTarget} from '../src/local-resource';
+
+const target = (path = '/guide', openUrlStrategy: 'SAME_WINDOW' | 'NEW_WINDOW' = 'SAME_WINDOW') => ({path, openUrlStrategy});
+const location = (href = 'https://example.test/vue/object/example.Type/1') => ({href, assign: vi.fn()});
+
+describe('local-resource navigation', () => {
+  it('resolves root and nested deployment bases exactly once', () => {
+    expect(resolveLocalResourceTarget(target(), {location: location(), applicationBase: '/'}).url.href).toBe('https://example.test/guide');
+    expect(resolveLocalResourceTarget(target('/logout'), {location: location(), applicationBase: '/'}).url.href).toBe('https://example.test/logout');
+    expect(resolveLocalResourceTarget(target(), {location: location(), applicationBase: '/app'}).url.href).toBe('https://example.test/app/guide');
+    expect(resolveLocalResourceTarget(target('/app/guide'), {location: location(), applicationBase: '/app'}).url.href).toBe('https://example.test/app/guide');
+  });
+
+  it('rejects malformed, escaping, and cross-origin targets', () => {
+    for (const path of ['https://evil.test/x', '//evil.test/x', 'javascript:alert(1)', '../secret', '/app/../secret', '..\\secret']) {
+      expect(() => resolveLocalResourceTarget(target(path), {location: location(), applicationBase: '/app'})).toThrow();
+    }
+    expect(() => resolveLocalResourceTarget(target('/guide'), {
+      location: location(), applicationBase: 'https://evil.test/app'
+    })).toThrow(/same-origin/);
+  });
+
+  it('uses document navigation and opener isolation without touching a router', () => {
+    const current = location();
+    navigateLocalResource(target('/guide'), {location: current, applicationBase: '/app'});
+    expect(current.assign).toHaveBeenCalledWith('https://example.test/app/guide');
+
+    const opened = {opener: {} as unknown};
+    const open = vi.fn(() => opened as unknown as Window);
+    navigateLocalResource(target('/guide', 'NEW_WINDOW'), {location: current, applicationBase: '/app', open});
+    expect(open).toHaveBeenCalledWith('https://example.test/app/guide', '_blank', 'noopener,noreferrer');
+    expect(opened.opener).toBeNull();
+    expect(() => navigateLocalResource(target('/guide', 'NEW_WINDOW'), {
+      location: current,
+      applicationBase: '/app',
+      open: () => null
+    })).not.toThrow();
+  });
+});
diff --git a/viewers/webcomponents/vue/test/policy-integration.test.ts b/viewers/webcomponents/vue/test/policy-integration.test.ts
index cabbb84d358..763d95590bc 100644
--- a/viewers/webcomponents/vue/test/policy-integration.test.ts
+++ b/viewers/webcomponents/vue/test/policy-integration.test.ts
@@ -21,6 +21,7 @@ import {createMemoryHistory, createRouter} from 'vue-router';
 import {beforeEach, describe, expect, it, vi} from 'vitest';
 import {createCausewayVueViewer} from '../src/plugin';
 import {
+  ACTION_REQUEST_EVENT,
   ACTION_RESULT_EVENT,
   installSemanticBridge,
   NAVIGATION_REQUEST_EVENT,
@@ -121,4 +122,72 @@ describe('semantic policy bridge', () => {
     await new Promise(resolve => setTimeout(resolve, 0));
     expect(viewer.router.currentRoute.value.path).toBe('/');
   });
+
+  it('fails framework Logout closed before invocation and removes its menu control', async () => {
+    const viewer = await runtime();
+    const shell = authoredShell();
+    const region = document.createElement('div');
+    region.setAttribute('data-causeway-service-action-region', '');
+    region.innerHTML = '<button data-causeway-service-action data-service-logical-type="causeway.security.LogoutMenu" data-action-id="logout">Logout</button>';
+    shell.append(region);
+    const dispose = installSemanticBridge(viewer, shell);
+    expect(region.isConnected).toBe(false);
+
+    const request = new CustomEvent(ACTION_REQUEST_EVENT, {
+      bubbles: true,
+      composed: true,
+      cancelable: true,
+      detail: {serviceLogicalTypeName: 'causeway.security.LogoutMenu', actionId: 'logout', context: {}}
+    });
+    shell.dispatchEvent(request);
+    expect(request.defaultPrevented).toBe(true);
+    expect(shell.dataset.causewayLogoutUnavailable).toBe('true');
+    expect(shell.querySelector('[data-causeway-route-announcement]')?.textContent).toContain('host authentication');
+    dispose();
+  });
+
+  it('lets synchronous and asynchronous action policies claim or resume exactly once', async () => {
+    const claimed = vi.fn((_detail, claim) => claim.claim());
+    const claimedViewer = await runtime({action: claimed});
+    const claimedShell = authoredShell();
+    const disposeClaimed = installSemanticBridge(claimedViewer, claimedShell);
+    const claimedRequest = new CustomEvent(ACTION_REQUEST_EVENT, {
+      bubbles: true, cancelable: true, detail: {actionId: 'run', serviceLogicalTypeName: 'example.Service', context: {}}
+    });
+    claimedShell.dispatchEvent(claimedRequest);
+    expect(claimedRequest.defaultPrevented).toBe(true);
+    expect(claimed).toHaveBeenCalledOnce();
+    disposeClaimed();
+
+    const asyncViewer = await runtime({action: async () => false});
+    const asyncShell = authoredShell();
+    const source = document.createElement('button');
+    asyncShell.append(source);
+    const seen: Event[] = [];
+    source.addEventListener(ACTION_REQUEST_EVENT, event => seen.push(event));
+    const disposeAsync = installSemanticBridge(asyncViewer, asyncShell);
+    source.dispatchEvent(new CustomEvent(ACTION_REQUEST_EVENT, {
+      bubbles: true, cancelable: true, detail: {actionId: 'run', serviceLogicalTypeName: 'example.Service', context: {}}
+    }));
+    await new Promise(resolve => setTimeout(resolve, 0));
+    expect(seen).toHaveLength(2);
+    expect(seen[0].defaultPrevented).toBe(true);
+    expect(seen[1].defaultPrevented).toBe(false);
+    disposeAsync();
+  });
+
+  it('lets the result policy claim local-resource navigation', async () => {
+    const result = vi.fn((_detail, claim) => claim.claim());
+    const viewer = await runtime({result});
+    const shell = authoredShell();
+    const dispose = installSemanticBridge(viewer, shell);
+    shell.dispatchEvent(new CustomEvent(ACTION_RESULT_EVENT, {
+      bubbles: true,
+      detail: {result: {kind: 'local-resource', value: {path: '/guide', openUrlStrategy: 'SAME_WINDOW'}}}
+    }));
+    await new Promise(resolve => setTimeout(resolve, 0));
+    expect(result).toHaveBeenCalledOnce();
+    expect(viewer.router.currentRoute.value.path).toBe('/');
+    dispose();
+  });
 });


(causeway) 11/16: docs(openspec): archive web component logout and redirects

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit b7453c794bee06df1dd01bccb0ca56845d67db1d
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 3 15:09:02 2026 +0100

    docs(openspec): archive web component logout and redirects
---
 .../.openspec.yaml                                 |  0
 .../design.md                                      |  0
 .../proposal.md                                    |  0
 .../specs/domain-web-components/spec.md            |  0
 .../generic-htmx-web-component-viewer/spec.md      |  0
 .../specs/generic-vue-web-component-viewer/spec.md |  0
 .../tasks.md                                       |  0
 openspec/specs/domain-web-components/spec.md       | 54 ++++++++++++++
 .../generic-htmx-web-component-viewer/spec.md      | 59 +++++++++++++++
 .../specs/generic-vue-web-component-viewer/spec.md | 83 ++++++++++++++++++++++
 10 files changed, 196 insertions(+)

diff --git a/openspec/changes/support-web-component-logout-and-local-redirects/.openspec.yaml b/openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/.openspec.yaml
similarity index 100%
rename from openspec/changes/support-web-component-logout-and-local-redirects/.openspec.yaml
rename to openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/.openspec.yaml
diff --git a/openspec/changes/support-web-component-logout-and-local-redirects/design.md b/openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/design.md
similarity index 100%
rename from openspec/changes/support-web-component-logout-and-local-redirects/design.md
rename to openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/design.md
diff --git a/openspec/changes/support-web-component-logout-and-local-redirects/proposal.md b/openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/proposal.md
similarity index 100%
rename from openspec/changes/support-web-component-logout-and-local-redirects/proposal.md
rename to openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/proposal.md
diff --git a/openspec/changes/support-web-component-logout-and-local-redirects/specs/domain-web-components/spec.md b/openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/specs/domain-web-components/spec.md
similarity index 100%
rename from openspec/changes/support-web-component-logout-and-local-redirects/specs/domain-web-components/spec.md
rename to openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/specs/domain-web-components/spec.md
diff --git a/openspec/changes/support-web-component-logout-and-local-redirects/specs/generic-htmx-web-component-viewer/spec.md b/openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/specs/generic-htmx-web-component-viewer/spec.md
similarity index 100%
rename from openspec/changes/support-web-component-logout-and-local-redirects/specs/generic-htmx-web-component-viewer/spec.md
rename to openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/specs/generic-htmx-web-component-viewer/spec.md
diff --git a/openspec/changes/support-web-component-logout-and-local-redirects/specs/generic-vue-web-component-viewer/spec.md b/openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/specs/generic-vue-web-component-viewer/spec.md
similarity index 100%
rename from openspec/changes/support-web-component-logout-and-local-redirects/specs/generic-vue-web-component-viewer/spec.md
rename to openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/specs/generic-vue-web-component-viewer/spec.md
diff --git a/openspec/changes/support-web-component-logout-and-local-redirects/tasks.md b/openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/tasks.md
similarity index 100%
rename from openspec/changes/support-web-component-logout-and-local-redirects/tasks.md
rename to openspec/changes/archive/2026-09-03-support-web-component-logout-and-local-redirects/tasks.md
diff --git a/openspec/specs/domain-web-components/spec.md b/openspec/specs/domain-web-components/spec.md
index 746e320ff08..ef51798ceac 100644
--- a/openspec/specs/domain-web-components/spec.md
+++ b/openspec/specs/domain-web-components/spec.md
@@ -2725,3 +2725,57 @@ The reload SHALL retain current collection criteria and page policy while collap
 - **WHEN** a mutation refresh retires the focused preview subtree
 - **THEN** the stable collection host is available as the programmatic fallback focus target
 - **AND** an action-result host uses that connected collection fallback instead of retaining the soon-to-be-disconnected action control
+
+### Requirement: Host-owned action interception boundary
+
+The foundation SHALL publish canonical target identity before any service or object action invocation and SHALL allow an enclosing host to claim and cancel the request synchronously.
+A claimed or canceled request MUST NOT execute GraphQL validation or invocation and MUST NOT emit a synthetic successful action result.
+The exact framework Logout operation SHALL be identified by service logical type `causeway.security.LogoutMenu` and action id `logout`, not by its label, result path, or a partial identity match.
+
+#### Scenario: Host claims an action request
+
+- **WHEN** a host claims or cancels a published action request using its canonical target and action identity
+- **THEN** the component does not validate or invoke that action through GraphQL
+- **AND** the host remains responsible for any replacement behavior, announcement, and navigation
+
+#### Scenario: Framework Logout is requested
+
+- **WHEN** a component publishes an action request for `causeway.security.LogoutMenu#logout`
+- **THEN** the request contains sufficient exact identity for the host to recognize the reserved authentication operation before invocation
+- **AND** no path string or post-invocation result is required to recognize it
+
+#### Scenario: Application action has a logout-like name
+
+- **WHEN** an application action is labeled Logout or returns `/logout` but does not have the exact framework service and action identity
+- **THEN** the foundation does not classify it as the reserved framework Logout operation
+- **AND** ordinary host and domain-action policy continues to apply
+
+### Requirement: Local resource action-result semantics
+
+The foundation SHALL classify an action result of exact GraphQL output type `LocalResourcePathValue` as a distinct local-resource semantic result before applying generic structured-object classification.
+The result SHALL preserve only the authoritative `path` and `openUrlStrategy` fields and MUST NOT be treated as a navigable domain object, ordinary scalar, or authentication instruction.
+Object and service action contexts SHALL apply the same classification contract.
+
+#### Scenario: Typed local resource result is returned
+
+- **WHEN** a successfully invoked action has exact output type `LocalResourcePathValue` and returns valid path and strategy fields
+- **THEN** the emitted action result has the local-resource kind with those fields preserved
+- **AND** the result does not acquire object identity or route semantics
+
+#### Scenario: Structured non-resource result is returned
+
+- **WHEN** a structured result has a different GraphQL output type
+- **THEN** it retains the applicable existing object or unsupported-result behavior
+- **AND** field names resembling `path` or `openUrlStrategy` do not opt it into local-resource semantics
+
+#### Scenario: Local resource result is malformed
+
+- **WHEN** a `LocalResourcePathValue` result lacks a non-empty path or supported opening strategy
+- **THEN** the result fails closed with a bounded unsupported-result diagnostic
+- **AND** no navigation request is emitted
+
+#### Scenario: Result kinds are regression tested
+
+- **WHEN** foundation result-normalization tests execute
+- **THEN** local-resource, scalar, object, collection, void, and other structured result classifications remain deterministic
+- **AND** service and object action execution produce equivalent classifications
diff --git a/openspec/specs/generic-htmx-web-component-viewer/spec.md b/openspec/specs/generic-htmx-web-component-viewer/spec.md
index 55c1c67d0a4..b6c4efd15dd 100644
--- a/openspec/specs/generic-htmx-web-component-viewer/spec.md
+++ b/openspec/specs/generic-htmx-web-component-viewer/spec.md
@@ -1474,3 +1474,62 @@ The shell MUST NOT contain a document root, document head, executable script, ro
 - **WHEN** a shell contains a reserved `{{causeway.*}}` token outside the documented closed vocabulary
 - **THEN** validation fails with a bounded unresolved-binding diagnostic
 - **AND** no expression, request parameter, configuration property, metamodel value, persistence value, or GraphQL result is evaluated
+
+### Requirement: Generic HTMX framework Logout safety
+
+The generic HTMX viewer SHALL treat `causeway.security.LogoutMenu#logout` as a host-owned authentication operation and MUST NOT invoke it through GraphQL by default.
+When no authentication integration claims that operation, the viewer SHALL omit its ordinary menu affordance and SHALL cancel stale or custom action requests for the same exact identity.
+An installed authentication integration SHALL remain responsible for presenting an accessible logout control, submitting the configured method and current CSRF evidence, clearing the session, and selecting the post-logout destination.
+
+#### Scenario: Generic HTMX viewer has no authentication integration
+
+- **WHEN** application menus contain the framework Logout action but the HTMX host has no registered logout capability
+- **THEN** the action is absent from ordinary semantic menu controls
+- **AND** the viewer does not imply that a browser session can be ended
+
+#### Scenario: Stale Logout request is published
+
+- **WHEN** stale or custom markup publishes an action request for the exact framework Logout identity without a host logout claim
+- **THEN** the HTMX viewer cancels the request before GraphQL validation or invocation
+- **AND** it emits a bounded unavailable-operation announcement or diagnostic without a successful result
+
+#### Scenario: Authenticated HTMX integration is active
+
+- **WHEN** an installed HTMX authentication integration claims Logout and supplies its host-owned control
+- **THEN** the existing protected logout flow remains available outside domain action invocation
+- **AND** its endpoint, HTTP method, CSRF, session, cookie, and redirect policies remain authoritative
+
+#### Scenario: Similar application action is present
+
+- **WHEN** another service exposes a similarly named action or a local-resource result containing `/logout`
+- **THEN** the HTMX viewer does not suppress or reinterpret it as framework Logout
+- **AND** normal action and local-resource policies apply
+
+### Requirement: HTMX local-resource result navigation
+
+The HTMX viewer SHALL resolve a local-resource semantic result against the current application context and SHALL perform full-document navigation according to its supported opening strategy.
+It MUST validate the resolved target as same-origin and application-local before navigation and MUST NOT interpret any target path as an authentication operation.
+
+#### Scenario: Same-window local resource is returned
+
+- **WHEN** an action returns a valid local-resource result with `SAME_WINDOW`
+- **THEN** the HTMX host navigates the current browsing context to the context-aware local target
+- **AND** it does not issue an HTMX fragment request or canonical object-route transition
+
+#### Scenario: New-window local resource is returned
+
+- **WHEN** an action returns a valid local-resource result with `NEW_WINDOW`
+- **THEN** the HTMX host requests a new opener-isolated browsing context for the local target
+- **AND** the current object route remains unchanged
+
+#### Scenario: Local resource target is unsafe
+
+- **WHEN** a result path is malformed, scheme-relative, cross-origin, credential-bearing, outside the configured application-local boundary, or paired with an unknown strategy
+- **THEN** the HTMX host refuses navigation and reports a bounded result-policy error
+- **AND** no target value is repaired, rewritten to another origin, or treated as Logout
+
+#### Scenario: Application uses a servlet context path
+
+- **WHEN** the HTMX application is deployed beneath a non-root servlet context and returns an application-local resource path
+- **THEN** resolution preserves the authoritative servlet context exactly once
+- **AND** the viewer does not confuse the HTMX route base with the application context
diff --git a/openspec/specs/generic-vue-web-component-viewer/spec.md b/openspec/specs/generic-vue-web-component-viewer/spec.md
index 7ff3612bcbb..0c384cc8bf2 100644
--- a/openspec/specs/generic-vue-web-component-viewer/spec.md
+++ b/openspec/specs/generic-vue-web-component-viewer/spec.md
@@ -381,3 +381,86 @@ The first generic Vue viewer SHALL document browser client rendering as its supp
 - **WHEN** an application requires Nuxt, SSR, streaming hydration, or server-only route data
 - **THEN** the viewer reports that capability as separate compatibility work
 - **AND** does not silently provide inconsistent custom-element hydration
+
+### Requirement: Vue host-owned action policy
+
+The Vue viewer SHALL expose a pre-invocation action policy that receives canonical action identity, a single-claim token, and the current immutable viewer-policy context.
+A claimed action MUST NOT proceed to foundation GraphQL validation or invocation, and asynchronous policy failure SHALL reach the configured viewer error policy without creating a successful action result.
+The policy SHALL compose with application-owned routing and lifecycle disposal without introducing authentication state into Vue route components.
+
+#### Scenario: Vue action policy claims a request
+
+- **WHEN** the configured Vue action policy claims a service or object action request
+- **THEN** ordinary GraphQL dispatch is canceled before invocation
+- **AND** the application policy owns any replacement effect
+
+#### Scenario: Vue action policy does not claim a request
+
+- **WHEN** the configured action policy returns without claiming an ordinary action
+- **THEN** existing parameter, validation, confirmation, invocation, result, refresh, and navigation behavior continues
+- **AND** the action is not duplicated
+
+#### Scenario: Vue action policy fails
+
+- **WHEN** a synchronous or asynchronous action policy throws or rejects
+- **THEN** the viewer reports the error through its configured error policy
+- **AND** fail-closed actions such as framework Logout do not fall through to GraphQL invocation
+
+### Requirement: Generic Vue framework Logout safety
+
+The generic Vue viewer SHALL classify `causeway.security.LogoutMenu#logout` as a host-owned authentication operation and MUST NOT invoke it through GraphQL by default.
+When no application authentication policy claims that operation, the viewer SHALL omit its ordinary menu affordance and SHALL cancel stale or custom requests for the exact identity.
+A future authentication integration SHALL own endpoint selection, HTTP method, current anti-forgery evidence, session cleanup, and post-logout navigation.
+
+#### Scenario: Vue application has no authentication integration
+
+- **WHEN** application menus contain the framework Logout action and no Vue host logout capability is configured
+- **THEN** the action is absent from ordinary semantic menu controls
+- **AND** direct or stale requests cannot reach GraphQL invocation
+
+#### Scenario: Vue application supplies a logout policy
+
+- **WHEN** an application explicitly registers a policy that claims the framework Logout identity
+- **THEN** the viewer delegates the operation before GraphQL dispatch
+- **AND** the application remains responsible for a secure accessible logout affordance and complete browser-session behavior
+
+#### Scenario: Similar application action is present
+
+- **WHEN** another service exposes a similarly named action or a local-resource result containing `/logout`
+- **THEN** the Vue viewer does not suppress or reinterpret it as framework Logout
+- **AND** ordinary action or local-resource policy applies
+
+### Requirement: Vue local-resource result navigation
+
+The Vue viewer SHALL interpret a local-resource semantic result through a bounded host policy and a documented application-local resource base.
+Its default behavior SHALL perform validated full-document same-origin navigation according to `SAME_WINDOW` or `NEW_WINDOW` and MUST NOT send the target through Vue Router.
+
+#### Scenario: Same-window local resource is returned
+
+- **WHEN** an action returns a valid local-resource result with `SAME_WINDOW`
+- **THEN** the Vue host performs full-document navigation to the resolved application-local target
+- **AND** the canonical object router does not claim the target
+
+#### Scenario: New-window local resource is returned
+
+- **WHEN** an action returns a valid local-resource result with `NEW_WINDOW`
+- **THEN** the Vue host requests a new opener-isolated browsing context for the resolved target
+- **AND** the current route generation remains active unless ordinary lifecycle policy later changes it
+
+#### Scenario: Application claims local-resource navigation
+
+- **WHEN** the configured result policy claims a valid local-resource result
+- **THEN** default browser navigation does not run
+- **AND** the application can apply stricter deployment policy without mutating the canonical result
+
+#### Scenario: Local resource target is unsafe
+
+- **WHEN** a path is malformed, scheme-relative, cross-origin, credential-bearing, outside the configured application-local boundary, or paired with an unknown strategy
+- **THEN** the Vue viewer refuses navigation and reports the failure through its error policy
+- **AND** it does not repair the value, route it as a domain object, or infer Logout semantics
+
+#### Scenario: Vue application uses a nested deployment context
+
+- **WHEN** the application configures a non-root local-resource base and receives an application-local path
+- **THEN** resolution preserves that deployment context exactly once
+- **AND** it remains independent of the Vue object-route base path


(causeway) 04/16: docs(openspec): archive generic Vue web component viewer

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit bef375a250df0a897e2088a16226915865c174a3
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 3 09:31:22 2026 +0100

    docs(openspec): archive generic Vue web component viewer
---
 .../.openspec.yaml                                                 | 0
 .../2026-09-03-add-generic-vue-web-component-viewer}/design.md     | 0
 .../2026-09-03-add-generic-vue-web-component-viewer}/proposal.md   | 0
 .../specs/generic-vue-web-component-viewer/spec.md                 | 0
 .../2026-09-03-add-generic-vue-web-component-viewer}/tasks.md      | 0
 .../specs/generic-vue-web-component-viewer/spec.md                 | 7 ++++++-
 6 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/openspec/changes/add-generic-vue-web-component-viewer/.openspec.yaml b/openspec/changes/archive/2026-09-03-add-generic-vue-web-component-viewer/.openspec.yaml
similarity index 100%
rename from openspec/changes/add-generic-vue-web-component-viewer/.openspec.yaml
rename to openspec/changes/archive/2026-09-03-add-generic-vue-web-component-viewer/.openspec.yaml
diff --git a/openspec/changes/add-generic-vue-web-component-viewer/design.md b/openspec/changes/archive/2026-09-03-add-generic-vue-web-component-viewer/design.md
similarity index 100%
rename from openspec/changes/add-generic-vue-web-component-viewer/design.md
rename to openspec/changes/archive/2026-09-03-add-generic-vue-web-component-viewer/design.md
diff --git a/openspec/changes/add-generic-vue-web-component-viewer/proposal.md b/openspec/changes/archive/2026-09-03-add-generic-vue-web-component-viewer/proposal.md
similarity index 100%
rename from openspec/changes/add-generic-vue-web-component-viewer/proposal.md
rename to openspec/changes/archive/2026-09-03-add-generic-vue-web-component-viewer/proposal.md
diff --git a/openspec/changes/add-generic-vue-web-component-viewer/specs/generic-vue-web-component-viewer/spec.md b/openspec/changes/archive/2026-09-03-add-generic-vue-web-component-viewer/specs/generic-vue-web-component-viewer/spec.md
similarity index 100%
copy from openspec/changes/add-generic-vue-web-component-viewer/specs/generic-vue-web-component-viewer/spec.md
copy to openspec/changes/archive/2026-09-03-add-generic-vue-web-component-viewer/specs/generic-vue-web-component-viewer/spec.md
diff --git a/openspec/changes/add-generic-vue-web-component-viewer/tasks.md b/openspec/changes/archive/2026-09-03-add-generic-vue-web-component-viewer/tasks.md
similarity index 100%
rename from openspec/changes/add-generic-vue-web-component-viewer/tasks.md
rename to openspec/changes/archive/2026-09-03-add-generic-vue-web-component-viewer/tasks.md
diff --git a/openspec/changes/add-generic-vue-web-component-viewer/specs/generic-vue-web-component-viewer/spec.md b/openspec/specs/generic-vue-web-component-viewer/spec.md
similarity index 98%
rename from openspec/changes/add-generic-vue-web-component-viewer/specs/generic-vue-web-component-viewer/spec.md
rename to openspec/specs/generic-vue-web-component-viewer/spec.md
index 299abc8f28b..af6fba198a5 100644
--- a/openspec/changes/add-generic-vue-web-component-viewer/specs/generic-vue-web-component-viewer/spec.md
+++ b/openspec/specs/generic-vue-web-component-viewer/spec.md
@@ -1,4 +1,9 @@
-## ADDED Requirements
+# generic-vue-web-component-viewer Specification
+
+## Purpose
+Provide an optional client-rendered Vue 3 and Vue Router 4 host viewer that owns application routing, shell composition, and lifecycle policy while preserving semantic web components as the authority for domain behavior.
+
+## Requirements
 
 ### Requirement: Optional packaged Vue viewer
 


(causeway) 15/16: docs(openspec): archive secured Vue Petclinic variant

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 6606499af6c44264116c4e2481e336a4f6c6f828
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 3 17:25:59 2026 +0100

    docs(openspec): archive secured Vue Petclinic variant
---
 .../2026-09-03-add-secured-vue-petclinic-variant}/.openspec.yaml          | 0
 .../2026-09-03-add-secured-vue-petclinic-variant}/design.md               | 0
 .../2026-09-03-add-secured-vue-petclinic-variant}/proposal.md             | 0
 .../specs/generic-vue-web-component-viewer/spec.md                        | 0
 .../specs/htmx-viewer-local-secman-authentication/spec.md                 | 0
 .../specs/vue-viewer-local-secman-authentication/spec.md                  | 0
 .../specs/web-component-secman-spring-authentication-bridge/spec.md       | 0
 .../2026-09-03-add-secured-vue-petclinic-variant}/tasks.md                | 0
 8 files changed, 0 insertions(+), 0 deletions(-)

diff --git a/openspec/changes/add-secured-vue-petclinic-variant/.openspec.yaml b/openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/.openspec.yaml
similarity index 100%
rename from openspec/changes/add-secured-vue-petclinic-variant/.openspec.yaml
rename to openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/.openspec.yaml
diff --git a/openspec/changes/add-secured-vue-petclinic-variant/design.md b/openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/design.md
similarity index 100%
rename from openspec/changes/add-secured-vue-petclinic-variant/design.md
rename to openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/design.md
diff --git a/openspec/changes/add-secured-vue-petclinic-variant/proposal.md b/openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/proposal.md
similarity index 100%
rename from openspec/changes/add-secured-vue-petclinic-variant/proposal.md
rename to openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/proposal.md
diff --git a/openspec/changes/add-secured-vue-petclinic-variant/specs/generic-vue-web-component-viewer/spec.md b/openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/specs/generic-vue-web-component-viewer/spec.md
similarity index 100%
rename from openspec/changes/add-secured-vue-petclinic-variant/specs/generic-vue-web-component-viewer/spec.md
rename to openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/specs/generic-vue-web-component-viewer/spec.md
diff --git a/openspec/changes/add-secured-vue-petclinic-variant/specs/htmx-viewer-local-secman-authentication/spec.md b/openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/specs/htmx-viewer-local-secman-authentication/spec.md
similarity index 100%
rename from openspec/changes/add-secured-vue-petclinic-variant/specs/htmx-viewer-local-secman-authentication/spec.md
rename to openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/specs/htmx-viewer-local-secman-authentication/spec.md
diff --git a/openspec/changes/add-secured-vue-petclinic-variant/specs/vue-viewer-local-secman-authentication/spec.md b/openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/specs/vue-viewer-local-secman-authentication/spec.md
similarity index 100%
rename from openspec/changes/add-secured-vue-petclinic-variant/specs/vue-viewer-local-secman-authentication/spec.md
rename to openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/specs/vue-viewer-local-secman-authentication/spec.md
diff --git a/openspec/changes/add-secured-vue-petclinic-variant/specs/web-component-secman-spring-authentication-bridge/spec.md b/openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/specs/web-component-secman-spring-authentication-bridge/spec.md
similarity index 100%
rename from openspec/changes/add-secured-vue-petclinic-variant/specs/web-component-secman-spring-authentication-bridge/spec.md
rename to openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/specs/web-component-secman-spring-authentication-bridge/spec.md
diff --git a/openspec/changes/add-secured-vue-petclinic-variant/tasks.md b/openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/tasks.md
similarity index 100%
rename from openspec/changes/add-secured-vue-petclinic-variant/tasks.md
rename to openspec/changes/archive/2026-09-03-add-secured-vue-petclinic-variant/tasks.md