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/08/29 23:25:42 UTC

(causeway) branch htmx updated (5d0bcd51690 -> bd886b59587)

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 5d0bcd51690 docs(openspec): archive execution outbox test compilation
     new 28ae131a2ed docs(openspec): propose collection grid controls and paging
     new 1cb2d6b3e8d feat(webcomponents): add collection paging and grid controls
     new bd886b59587 docs(openspec): archive collection grid controls and paging

The 3 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                                      | 89 ++++++++++++++++++++++
 .../proposal.md                                    | 30 ++++++++
 .../specs/domain-web-components/spec.md            | 57 ++++++++++++++
 .../generic-htmx-web-component-viewer/spec.md      | 24 ++++++
 .../specs/vaadin-collection-grid-adapter/spec.md   | 42 ++++++++++
 .../tasks.md                                       | 20 +++++
 openspec/specs/domain-web-components/spec.md       | 56 ++++++++++++++
 .../generic-htmx-web-component-viewer/spec.md      | 23 ++++++
 .../specs/vaadin-collection-grid-adapter/spec.md   | 41 ++++++++++
 viewers/webcomponents/foundation/USAGE.adoc        | 14 +++-
 .../foundation/src/collection-element.mjs          | 79 ++++++++++++++++++-
 .../src/collection-grid-qualification.mjs          |  6 +-
 .../webcomponents/foundation/src/grid-widget.mjs   |  7 +-
 .../test/collection-grid-qualification.test.mjs    |  7 ++
 .../foundation/test/collection.test.mjs            | 71 ++++++++++++++++-
 .../test/grid-production-isolation.test.mjs        |  5 +-
 .../foundation/test/grid-widget.test.mjs           | 14 ++++
 .../webcomponents/pages/petclinic.HomePage.html    |  4 +-
 .../webcomponents/pages/petclinic.PetOwner.html    |  4 +-
 .../petclinic/PetClinicHtmxPlaywrightTest.java     | 29 +++++--
 .../PetClinicHtmxApplication_IntegTest.java        | 11 +--
 22 files changed, 604 insertions(+), 29 deletions(-)
 copy openspec/changes/archive/{2026-08-29-add-breadcrumbs-web-component => 2026-08-30-add-collection-grid-controls-and-paging}/.openspec.yaml (100%)
 create mode 100644 openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/design.md
 create mode 100644 openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/proposal.md
 create mode 100644 openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/specs/domain-web-components/spec.md
 create mode 100644 openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/specs/generic-htmx-web-component-viewer/spec.md
 create mode 100644 openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/specs/vaadin-collection-grid-adapter/spec.md
 create mode 100644 openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/tasks.md


(causeway) 02/03: feat(webcomponents): add collection paging and grid controls

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 1cb2d6b3e8de03b27a290e6a9f2e27d9071694c9
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Aug 30 01:21:35 2026 +0200

    feat(webcomponents): add collection paging and grid controls
---
 .../design.md                                      |  1 +
 .../tasks.md                                       | 18 ++---
 viewers/webcomponents/foundation/USAGE.adoc        | 14 +++-
 .../foundation/src/collection-element.mjs          | 79 ++++++++++++++++++++--
 .../src/collection-grid-qualification.mjs          |  6 +-
 .../webcomponents/foundation/src/grid-widget.mjs   |  7 +-
 .../test/collection-grid-qualification.test.mjs    |  7 ++
 .../foundation/test/collection.test.mjs            | 71 ++++++++++++++++++-
 .../test/grid-production-isolation.test.mjs        |  5 +-
 .../foundation/test/grid-widget.test.mjs           | 14 ++++
 .../webcomponents/pages/petclinic.HomePage.html    |  4 +-
 .../webcomponents/pages/petclinic.PetOwner.html    |  4 +-
 .../petclinic/PetClinicHtmxPlaywrightTest.java     | 29 ++++++--
 .../PetClinicHtmxApplication_IntegTest.java        | 11 +--
 14 files changed, 232 insertions(+), 38 deletions(-)

diff --git a/openspec/changes/add-collection-grid-controls-and-paging/design.md b/openspec/changes/add-collection-grid-controls-and-paging/design.md
index 1f467680bf3..aab1d6e4d78 100644
--- a/openspec/changes/add-collection-grid-controls-and-paging/design.md
+++ b/openspec/changes/add-collection-grid-controls-and-paging/design.md
@@ -34,6 +34,7 @@ Absent, blank, malformed, zero, negative, or oversized values will be treated as
 A valid value will be supplied to the initial load and retained by previous, next, refresh, and generation changes.
 
 The collection host will force bounded Grid presentation for an opted-in paged collection even when a total count is available, because Causeway-owned paging controls are the public behavior being requested.
+The same Causeway-owned controls will remain available around native table fallback so responsive width, ordering qualification, or toolkit policy cannot silently remove opted-in paging.
 The server-returned window metadata remains authoritative for actual requested size, maximum size, offsets, and terminal-page state.
 
 This is preferred to reusing `size`, which is currently inert application markup and does not communicate that visible paging controls are requested.
diff --git a/openspec/changes/add-collection-grid-controls-and-paging/tasks.md b/openspec/changes/add-collection-grid-controls-and-paging/tasks.md
index 69fe3a113ee..7eafb4b5747 100644
--- a/openspec/changes/add-collection-grid-controls-and-paging/tasks.md
+++ b/openspec/changes/add-collection-grid-controls-and-paging/tasks.md
@@ -1,20 +1,20 @@
 ## 1. Public collection contract
 
-- [ ] 1.1 Add observed, reactive `paged`, `resizable-columns`, and `reorderable-columns` collection attributes with bounded normalization and default-off Grid controls.
-- [ ] 1.2 Apply valid page sizes to initial and subsequent loads, reset changed paging to offset zero, and force Causeway-owned bounded presentation without weakening stale-request protection.
-- [ ] 1.3 Document the new collection attributes and the deliberate sorting and filtering exclusion.
+- [x] 1.1 Add observed, reactive `paged`, `resizable-columns`, and `reorderable-columns` collection attributes with bounded normalization and default-off Grid controls.
+- [x] 1.2 Apply valid page sizes to initial and subsequent loads, reset changed paging to offset zero, and force Causeway-owned bounded presentation without weakening stale-request protection.
+- [x] 1.3 Document the new collection attributes and the deliberate sorting and filtering exclusion.
 
 ## 2. Private Grid adapter
 
-- [ ] 2.1 Thread normalized control options through the frozen adapter presentation contract.
-- [ ] 2.2 Map resizing to generated Vaadin columns and reordering to the Vaadin Grid while preserving private events, native fallback, and declarative column authority.
-- [ ] 2.3 Add adapter and collection tests for defaults, opt-in mapping, bounded paging, reactivity, invalid values, and fallback behavior.
+- [x] 2.1 Thread normalized control options through the frozen adapter presentation contract.
+- [x] 2.2 Map resizing to generated Vaadin columns and reordering to the Vaadin Grid while preserving private events, native fallback, and declarative column authority.
+- [x] 2.3 Add adapter and collection tests for defaults, opt-in mapping, bounded paging, reactivity, invalid values, and fallback behavior.
 
 ## 3. Petclinic demonstration
 
-- [ ] 3.1 Replace inert collection range attributes with selective `paged` overrides on the global owner list and owner visit history while leaving upcoming visits and pets unpaged.
-- [ ] 3.2 Update Petclinic resource composition, integration, and Playwright assertions for configured page size and accessible paging without changing associated actions.
+- [x] 3.1 Replace inert collection range attributes with selective `paged` overrides on the global owner list and owner visit history while leaving upcoming visits and pets unpaged.
+- [x] 3.2 Update Petclinic resource composition, integration, and Playwright assertions for configured page size and accessible paging without changing associated actions.
 
 ## 4. Verification
 
-- [ ] 4.1 Run foundation Node and Maven tests, Petclinic integration and browser tests, RAT, IntelliJ inspections or compilation, diff checks, and strict OpenSpec validation.
+- [x] 4.1 Run foundation Node and Maven tests, Petclinic integration and browser tests, RAT, IntelliJ inspections or compilation, diff checks, and strict OpenSpec validation.
diff --git a/viewers/webcomponents/foundation/USAGE.adoc b/viewers/webcomponents/foundation/USAGE.adoc
index 32c7f9ad99e..4e66d138ce0 100644
--- a/viewers/webcomponents/foundation/USAGE.adoc
+++ b/viewers/webcomponents/foundation/USAGE.adoc
@@ -486,7 +486,8 @@ An authored property or collection can place object actions directly beneath the
   <cw-action id="updateName"></cw-action>
 </cw-property>
 
-<cw-collection id="pets" named="Companion animals" described-as="Pets registered to this owner." description-as="tooltip" active>
+<cw-collection id="pets" named="Companion animals" described-as="Pets registered to this owner." description-as="tooltip" active
+               paged="10" resizable-columns reorderable-columns>
   <cw-collection-column id="name" label="Name"></cw-collection-column>
   <cw-collection-column id="species" label="Species"></cw-collection-column>
   <cw-action id="addPet"></cw-action>
@@ -537,18 +538,25 @@ A later requirement outside the hydrated selection produces only a missing-field
 The collection's `rowContexts` property exposes the currently hydrated controllers to host adapters and is cleared on rerender or disconnect.
 
 When targeted introspection finds the additive rich `window(offset, size)` field, collections use bounded secondary window operations and retain the documented legacy-list fallback.
+`paged="N"` requests bounded windows of `N` rows and renders Causeway-owned previous and next controls when the advertised window permits them.
+The page size must be an integer from 1 through 100; absent, blank, malformed, fractional, zero, negative, and oversized values provide no override.
+Changing the effective page size retires stale collection work and restarts at offset zero.
+Paging remains available in native table presentation and does not grant sorting or filtering semantics.
 
 Under the default Vaadin component policy, an active connected collection wider than 48rem may use the internal independently packaged Grid closure when its window ordering is `CONFIGURED`, its semantic columns are fully projectable, and its total or paging metadata is safe.
-A safe positive total selects virtual Grid and maps only bounded range callbacks through the Causeway-owned range broker.
+A safe positive total selects virtual Grid and maps only bounded range callbacks through the Causeway-owned range broker unless explicit `paged` presentation requires the bounded Grid.
 An unavailable total selects bounded Grid only when normalized previous or next metadata exists, renders only the current window, and keeps accessible Causeway previous and next controls without inventing a logical size.
 Safe-zero, terminal-empty, narrow, inactive, hidden, legacy-list, encounter-ordered, malformed-window, unsupported-column, renderer-failure, family-failure, and explicit-native collections retain the established native presentation.
 The initial authoritative range seeds the bounded cache, identical ranges deduplicate, overlapping work has independent cancellation, retained windows and hydrated contexts are capped, and route, object, member, column, renderer, policy, width, refresh, or connection changes retire stale work.
 Causeway remains authoritative for rows, values, semantic renderers, object links, errors, focus identity, paging, associated actions, GraphQL, routes, authorization, and lifecycle.
 Applications continue to declare only `<cw-collection>` and `<cw-collection-column>` and must not consume internal Grid tags, item objects, data providers, render callbacks, events, properties, methods, or Shadow DOM.
+`resizable-columns` maps each generated internal Grid column to Vaadin's resize affordance, and `reorderable-columns` maps the generated Grid to Vaadin's column-reordering affordance.
+Both attributes are opt-in, default off, and have no effect in native fallback presentation.
 Set `causeway.viewer.webcomponents.htmx.component-toolkit=native` for complete collection and control rollback without changing GraphQL, routes, persisted data, or application markup.
 The Grid closure is same-origin and route-lazy, has a 196608-byte gzip cap, and participates with Menu Bar in the 631808-byte aggregate closure cap and exact-hash CSP policy.
 See `vaadin-grid/README.adoc` for pinned inputs, accepted checksum, legal metadata, CSP evidence, verification, and audit commands.
-Sorting, filtering, selection, editing, details, drag, resizing, reordering, Grid Pro, Flow, Binder, telemetry, CDN resources, server-side Vaadin state, and backend query pushdown remain outside the component contract.
+Sorting, filtering, selection, editing, details, drag, Grid Pro, Flow, Binder, telemetry, CDN resources, server-side Vaadin state, and backend query pushdown remain outside the component contract.
+Column resizing and column reordering are supported only through the bounded public attributes above; no other Vaadin Grid API becomes application-facing.
 
 == Observable state, accessibility, and styling
 
diff --git a/viewers/webcomponents/foundation/src/collection-element.mjs b/viewers/webcomponents/foundation/src/collection-element.mjs
index 42601b33972..3cd74262f9b 100644
--- a/viewers/webcomponents/foundation/src/collection-element.mjs
+++ b/viewers/webcomponents/foundation/src/collection-element.mjs
@@ -75,7 +75,17 @@ export function captureDeclarativeCollectionColumns(root = globalThis.document)
 
 export class CausewayCollectionElement extends CausewayContextConsumerElement {
   static get observedAttributes() {
-    return ['id', 'named', 'described-as', 'description-as', 'label', 'active'];
+    return [
+      'id',
+      'named',
+      'described-as',
+      'description-as',
+      'label',
+      'active',
+      'paged',
+      'resizable-columns',
+      'reorderable-columns'
+    ];
   }
 
   constructor() {
@@ -207,6 +217,36 @@ export class CausewayCollectionElement extends CausewayContextConsumerElement {
     }
   }
 
+  get paged() {
+    return normalizedPageSize(this.getAttribute('paged'));
+  }
+
+  set paged(value) {
+    if (value == null || value === '') {
+      this.removeAttribute('paged');
+    } else {
+      this.setAttribute('paged', String(value));
+    }
+  }
+
+  get resizableColumns() {
+    return this.hasAttribute('resizable-columns');
+  }
+
+  set resizableColumns(value) {
+    if (value === true) this.setAttribute('resizable-columns', '');
+    else this.removeAttribute('resizable-columns');
+  }
+
+  get reorderableColumns() {
+    return this.hasAttribute('reorderable-columns');
+  }
+
+  set reorderableColumns(value) {
+    if (value === true) this.setAttribute('reorderable-columns', '');
+    else this.removeAttribute('reorderable-columns');
+  }
+
   get columns() {
     return Object.freeze(this._columns.map(column => Object.freeze({...column})));
   }
@@ -307,7 +347,20 @@ export class CausewayCollectionElement extends CausewayContextConsumerElement {
       this.reconnectRequirement();
     } else if (name === 'active' && this.active && this.componentState?.status === 'ready') {
       void this.load();
+    } else if (name === 'paged') {
+      this.gridHostRevision += 1;
+      this.loadAbortController?.abort();
+      this.loadAbortController = null;
+      this.#disconnectRangeBroker();
+      if (this.active && this.componentState?.status === 'ready') {
+        void this.load({force: true, offset: 0, size: this.paged});
+      } else {
+        this.renderComponentState(this.componentState);
+      }
     } else {
+      if (name === 'resizable-columns' || name === 'reorderable-columns') {
+        this.gridHostRevision += 1;
+      }
       this.renderComponentState(this.componentState);
     }
   }
@@ -337,6 +390,7 @@ export class CausewayCollectionElement extends CausewayContextConsumerElement {
 
   async load({force = false, offset = 0, size = null} = {}) {
     const context = this._resolvedContext;
+    const requestedSize = size ?? this.paged;
     if (!context || this.componentState?.status !== 'ready') {
       return null;
     }
@@ -354,7 +408,7 @@ export class CausewayCollectionElement extends CausewayContextConsumerElement {
         member: this.id,
         columns: this._columns,
         offset,
-        size,
+        size: requestedSize,
         requestKey: this,
         force,
         signal: abortController.signal
@@ -470,7 +524,8 @@ export class CausewayCollectionElement extends CausewayContextConsumerElement {
     }
     const rows = collectionRows(this.collectionState);
     if (rows.length === 0) {
-      renderMemberPrimary(this, shell('<span class="causeway-empty" role="status">No items</span>'));
+      const pager = this.paged ? this.#renderBoundedPager(this.collectionState.window) : '';
+      renderMemberPrimary(this, shell(`<span class="causeway-empty" role="status">No items</span>${pager}`));
       return;
     }
     const errors = this.collectionState.errors?.length
@@ -483,7 +538,8 @@ export class CausewayCollectionElement extends CausewayContextConsumerElement {
     const content = this._columns.length > 0
       ? this.#renderTable(rows)
       : this.#renderDefaultRows(rows);
-    renderMemberPrimary(this, shell(`${content}${errors}`));
+    const pager = this.paged ? this.#renderBoundedPager(this.collectionState.window) : '';
+    renderMemberPrimary(this, shell(`${content}${pager}${errors}`));
     this.#restoreNativeFocus();
   }
 
@@ -504,6 +560,8 @@ export class CausewayCollectionElement extends CausewayContextConsumerElement {
       labelledBy: this.labelId,
       describedBy,
       testId: this.getAttribute('data-testid') ? `${this.getAttribute('data-testid')}-grid` : '',
+      resizableColumns: this.resizableColumns,
+      reorderableColumns: this.reorderableColumns,
       rangeProvider: request => this.#projectRange(request, revision)
     };
   }
@@ -538,6 +596,7 @@ export class CausewayCollectionElement extends CausewayContextConsumerElement {
   }
 
   #renderBoundedPager(window) {
+    if (!window) return '';
     const range = window.rangeStart == null
       ? 'No items'
       : `Items ${window.rangeStart}–${window.rangeEnd}`;
@@ -681,6 +740,7 @@ export class CausewayCollectionElement extends CausewayContextConsumerElement {
       hasVisibleColumn: this._gridProjection.columns.length > 0
         && rows.some(row => row?._meta?.logicalTypeName && row?._meta?.id),
       renderersSupported: this._gridProjection.supported,
+      bounded: this.paged != null,
       rows,
       window: this.collectionState.window,
       lifecycle: {
@@ -827,6 +887,17 @@ function gridRowKey(row) {
   return `${metadata.logicalTypeName ?? ''}:${metadata.id ?? ''}`;
 }
 
+const MAX_DECLARATIVE_PAGE_SIZE = 100;
+
+function normalizedPageSize(value) {
+  const normalized = String(value ?? '').trim();
+  if (!/^\d+$/.test(normalized)) return null;
+  const parsed = Number(normalized);
+  return Number.isSafeInteger(parsed) && parsed >= 1 && parsed <= MAX_DECLARATIVE_PAGE_SIZE
+    ? parsed
+    : null;
+}
+
 function sameWindowContract(authoritative, candidate) {
   if (!authoritative || !candidate) return false;
   return authoritative.ordering === candidate.ordering
diff --git a/viewers/webcomponents/foundation/src/collection-grid-qualification.mjs b/viewers/webcomponents/foundation/src/collection-grid-qualification.mjs
index 4a7db101fb8..c75c3772fa1 100644
--- a/viewers/webcomponents/foundation/src/collection-grid-qualification.mjs
+++ b/viewers/webcomponents/foundation/src/collection-grid-qualification.mjs
@@ -36,7 +36,11 @@ export function qualifyCausewayCollectionGrid(candidate = {}) {
     return native(base, 'columns-unsupported');
   }
   if (candidate.renderersSupported !== true) return native(base, 'renderers-unsupported');
-  if (count === 'available') return grid(base, CausewayCollectionPresentation.GRID_VIRTUAL);
+  if (count === 'available') {
+    return grid(base, candidate.bounded === true
+      ? CausewayCollectionPresentation.GRID_BOUNDED
+      : CausewayCollectionPresentation.GRID_VIRTUAL);
+  }
   if (window.hasPrevious === true || window.hasNext === true) {
     return grid(base, CausewayCollectionPresentation.GRID_BOUNDED);
   }
diff --git a/viewers/webcomponents/foundation/src/grid-widget.mjs b/viewers/webcomponents/foundation/src/grid-widget.mjs
index e01514b2f66..e1d3202117a 100644
--- a/viewers/webcomponents/foundation/src/grid-widget.mjs
+++ b/viewers/webcomponents/foundation/src/grid-widget.mjs
@@ -176,13 +176,14 @@ export class CausewayCollectionGridElement extends HTMLElement {
     if (presentation.testId) control.setAttribute('data-testid', presentation.testId);
     else control.removeAttribute('data-testid');
     control.pageSize = presentation.pageSize;
+    control.columnReorderingAllowed = presentation.reorderableColumns;
     control.activeItem = null;
     control.selectedItems = [];
     control.rowDetailsRenderer = null;
     for (const descriptor of presentation.columns) {
       const column = document.createElement('vaadin-grid-column');
       column.header = descriptor.label;
-      column.resizable = false;
+      column.resizable = presentation.resizableColumns;
       column.sortable = false;
       column.frozen = false;
       column.renderer = (root, _column, model) => {
@@ -262,7 +263,9 @@ function freezePresentation(value = {}) {
     rangeProvider: typeof value.rangeProvider === 'function' ? value.rangeProvider : async () => ({rows: []}),
     labelledBy: String(value.labelledBy ?? ''),
     describedBy: String(value.describedBy ?? ''),
-    testId: String(value.testId ?? '')
+    testId: String(value.testId ?? ''),
+    resizableColumns: value.resizableColumns === true,
+    reorderableColumns: value.reorderableColumns === true
   });
 }
 
diff --git a/viewers/webcomponents/foundation/test/collection-grid-qualification.test.mjs b/viewers/webcomponents/foundation/test/collection-grid-qualification.test.mjs
index 2c8d1a1ac88..27147e05302 100644
--- a/viewers/webcomponents/foundation/test/collection-grid-qualification.test.mjs
+++ b/viewers/webcomponents/foundation/test/collection-grid-qualification.test.mjs
@@ -56,6 +56,13 @@ test('stable positive total and deterministic ordering qualify virtual Grid', ()
   assert.equal(Object.isFrozen(result.lifecycle), true);
 });
 
+test('explicit bounded paging suppresses virtual Grid even when total count is available', () => {
+  const result = qualifyCausewayCollectionGrid(candidate({bounded: true}));
+  assert.equal(result.qualified, true);
+  assert.equal(result.presentation, CausewayCollectionPresentation.GRID_BOUNDED);
+  assert.equal(result.count, 'available');
+});
+
 test('unavailable total with normalized paging qualifies bounded Grid without invented size', () => {
   const window = {...candidate().window, totalCount: null};
   const result = qualifyCausewayCollectionGrid(candidate({window}));
diff --git a/viewers/webcomponents/foundation/test/collection.test.mjs b/viewers/webcomponents/foundation/test/collection.test.mjs
index 561c030adac..304891a3cce 100644
--- a/viewers/webcomponents/foundation/test/collection.test.mjs
+++ b/viewers/webcomponents/foundation/test/collection.test.mjs
@@ -585,7 +585,41 @@ test('collection component resolves canonical and HTML headings without unmodifi
   document.body.appendChild(collection);
   await waitFor(() => collection.innerHTML.includes('Department staff'));
 
-  assert.deepEqual(CausewayCollectionElement.observedAttributes, ['id', 'named', 'described-as', 'description-as', 'label', 'active']);
+  assert.deepEqual(CausewayCollectionElement.observedAttributes, [
+    'id',
+    'named',
+    'described-as',
+    'description-as',
+    'label',
+    'active',
+    'paged',
+    'resizable-columns',
+    'reorderable-columns'
+  ]);
+  for (const [value, expected] of [
+    ['1', 1],
+    ['100', 100],
+    ['0', null],
+    ['101', null],
+    ['1.5', null],
+    ['1e2', null],
+    ['many', null],
+    [' ', null]
+  ]) {
+    collection.setAttribute('paged', value);
+    assert.equal(collection.paged, expected);
+  }
+  collection.paged = null;
+  assert.equal(collection.hasAttribute('paged'), false);
+  assert.equal(collection.resizableColumns, false);
+  assert.equal(collection.reorderableColumns, false);
+  collection.resizableColumns = true;
+  collection.reorderableColumns = true;
+  assert.equal(collection.resizableColumns, true);
+  assert.equal(collection.reorderableColumns, true);
+  collection.resizableColumns = false;
+  collection.reorderableColumns = false;
+
   assert.match(collection.innerHTML, /class="causeway-collection-label causeway-member-tooltip"/);
   assert.match(collection.innerHTML, /class="causeway-collection-description">Current staff members\.<\/p>/);
   assert.match(collection.innerHTML, /data-tooltip="Current staff members\.\s+Cannot edit a mixed-in collection\."/);
@@ -809,6 +843,8 @@ test('collection host owns immutable Grid qualification diagnostics policy recov
   assert.deepEqual(gridAdapter.presentation.columns.map(column => column.member), ['_meta', 'name']);
   assert.equal(gridAdapter.presentation.rows[0].identity.id, 'staff-1');
   assert.equal(gridAdapter.presentation.totalCount, 40);
+  assert.equal(gridAdapter.presentation.resizableColumns, false);
+  assert.equal(gridAdapter.presentation.reorderableColumns, false);
   assert.equal(rangeRequests.length, 1);
 
   const range = await collection.requestCollectionRange({offset: 20, size: 20});
@@ -852,6 +888,39 @@ test('collection host owns immutable Grid qualification diagnostics policy recov
   assert.equal(collection.gridQualification.reason, 'policy-native');
   configureCausewayGridWidgets({enabled: true});
   assert.equal(collection.gridQualification.presentation, 'grid-bounded');
+
+  collection.resizableColumns = true;
+  collection.reorderableColumns = true;
+  assert.equal(gridAdapter.presentation.resizableColumns, true);
+  assert.equal(gridAdapter.presentation.reorderableColumns, true);
+
+  const requestsBeforePaging = rangeRequests.length;
+  collection.paged = 10;
+  await waitFor(() => rangeRequests.length === requestsBeforePaging + 1
+    && collection.collectionState.status === 'ready'
+    && rangeRequests.at(-1).size === 10);
+  assert.equal(collection.paged, 10);
+  assert.equal(rangeRequests.at(-1).offset, 0);
+  assert.equal(rangeRequests.at(-1).size, 10);
+  assert.equal(rangeRequests.at(-1).force, true);
+  assert.equal(collection.gridQualification.presentation, 'grid-bounded');
+  assert.match(collection.innerHTML, /data-causeway-grid-next/);
+
+  collection.acceptGridResponsiveState(false);
+  assert.equal(collection.gridQualification.reason, 'narrow');
+  assert.match(collection.innerHTML, /data-causeway-grid-next/, 'declarative paging remains available with native presentation');
+  collection.acceptGridResponsiveState(true);
+
+  const requestsBeforeInvalidPaging = rangeRequests.length;
+  collection.paged = 101;
+  await waitFor(() => rangeRequests.length === requestsBeforeInvalidPaging + 1
+    && collection.collectionState.status === 'ready'
+    && rangeRequests.at(-1).size == null);
+  assert.equal(collection.paged, null);
+  collection.resizableColumns = false;
+  collection.reorderableColumns = false;
+  assert.equal(gridAdapter.presentation.resizableColumns, false);
+  assert.equal(gridAdapter.presentation.reorderableColumns, false);
   document.body.removeChild(collection);
 });
 
diff --git a/viewers/webcomponents/foundation/test/grid-production-isolation.test.mjs b/viewers/webcomponents/foundation/test/grid-production-isolation.test.mjs
index 2e4fc114dac..7579dd45afb 100644
--- a/viewers/webcomponents/foundation/test/grid-production-isolation.test.mjs
+++ b/viewers/webcomponents/foundation/test/grid-production-isolation.test.mjs
@@ -44,8 +44,9 @@ test('Grid adapter remains presentation-only and disables unqualified affordance
   const source = await readFile(path.join(foundation, 'src', 'grid-widget.mjs'), 'utf8');
   assert.doesNotMatch(source, /GraphQL|repository|persistence|canonicalObjectPath|navigation-request/i);
   assert.match(source, /column\.sortable = false/);
-  assert.match(source, /column\.resizable = false/);
+  assert.match(source, /column\.resizable = presentation\.resizableColumns/);
+  assert.match(source, /control\.columnReorderingAllowed = presentation\.reorderableColumns/);
   assert.match(source, /control\.selectedItems = \[\]/);
   assert.match(source, /control\.rowDetailsRenderer = null/);
-  assert.doesNotMatch(source, /sortable\s*=\s*true|resizable\s*=\s*true|selectionMode|dragFilter|dropFilter/);
+  assert.doesNotMatch(source, /sortable\s*=\s*true|selectionMode|dragFilter|dropFilter/);
 });
diff --git a/viewers/webcomponents/foundation/test/grid-widget.test.mjs b/viewers/webcomponents/foundation/test/grid-widget.test.mjs
index effc0238ed3..73634c0d8b7 100644
--- a/viewers/webcomponents/foundation/test/grid-widget.test.mjs
+++ b/viewers/webcomponents/foundation/test/grid-widget.test.mjs
@@ -103,6 +103,20 @@ test('bounded Grid upgrades lazily with immutable rows columns and relationships
   assert.equal(control.getAttribute('data-testid'), 'people-grid');
   assert.equal(control.childNodes.length, 1);
   assert.equal(control.childNodes[0].header, 'Name');
+  assert.equal(control.childNodes[0].resizable, false);
+  assert.equal(control.columnReorderingAllowed, false);
+
+  adapter.presentation = {
+    ...boundedPresentation(),
+    resizableColumns: true,
+    reorderableColumns: true
+  };
+  assert.equal(control.childNodes[0].resizable, true);
+  assert.equal(control.columnReorderingAllowed, true);
+
+  adapter.presentation = boundedPresentation();
+  assert.equal(control.childNodes[0].resizable, false);
+  assert.equal(control.columnReorderingAllowed, false);
   document.body.removeChild(adapter);
 });
 
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/META-INF/causeway/webcomponents/pages/petclinic.HomePage.html b/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/META-INF/causeway/webcomponents/pages/petclinic.HomePage.html
index 4549c8529e0..7fc207a3807 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/META-INF/causeway/webcomponents/pages/petclinic.HomePage.html
+++ b/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/META-INF/causeway/webcomponents/pages/petclinic.HomePage.html
@@ -18,7 +18,7 @@
   <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 offset="0" size="10">
+      <cw-collection id="petOwners" label="Pet owners" active paged="2">
         <cw-collection-column id="name" label="Owner"></cw-collection-column>
         <cw-collection-column id="telephoneNumber" label="Telephone"></cw-collection-column>
         <cw-collection-column id="emailAddress" label="Email"></cw-collection-column>
@@ -26,7 +26,7 @@ <h2 id="petclinic-owner-heading">Pet owners</h2>
     </section>
     <section aria-labelledby="petclinic-visit-heading">
       <h2 id="petclinic-visit-heading">Upcoming visits</h2>
-      <cw-collection id="futureVisits" named="Next appointments" active offset="0" size="10">
+      <cw-collection id="futureVisits" named="Next appointments" active>
         <cw-collection-column id="visitAt" label="When"></cw-collection-column>
         <cw-collection-column id="reason" label="Reason"></cw-collection-column>
         <cw-collection-column id="notes" label="Notes"></cw-collection-column>
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/META-INF/causeway/webcomponents/pages/petclinic.PetOwner.html b/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/META-INF/causeway/webcomponents/pages/petclinic.PetOwner.html
index 3c8a91e595b..1a54b426278 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/META-INF/causeway/webcomponents/pages/petclinic.PetOwner.html
+++ b/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/META-INF/causeway/webcomponents/pages/petclinic.PetOwner.html
@@ -55,7 +55,7 @@ <h2 id="petclinic-owner-details-heading">Details</h2>
     <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 offset="0" size="10">
+        <cw-collection id="pets" named="Companion animals" active>
           <cw-collection-column id="name" label="Name"></cw-collection-column>
           <cw-collection-column id="species" label="Species"></cw-collection-column>
           <cw-collection-column id="notes" label="Notes"></cw-collection-column>
@@ -68,7 +68,7 @@ <h2 id="petclinic-owner-pets-heading">Pets</h2>
         <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 offset="0" size="10">
+                       active paged="1">
           <cw-collection-column id="visitAt" label="When"></cw-collection-column>
           <cw-collection-column id="reason" label="Reason"></cw-collection-column>
           <cw-collection-column id="notes" label="Notes"></cw-collection-column>
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 2356e7e63a3..ff8d71ab8dd 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
@@ -197,8 +197,10 @@ void routesHomeObjectsHistoryCollectionsAndResponsiveLayout() {
         assertThat(page.locator(ROUTE_PAGE).getAttribute("data-page-source")).isEqualTo("resource");
         assertThat(page.locator("[data-testid='petclinic-custom-home']").isVisible()).isTrue();
         assertFocused(ROUTE_PAGE);
-        waitForCollectionRows("petOwners", 4);
+        waitForCollectionRows("petOwners", 2);
         waitForCollectionRows("futureVisits", 3);
+        assertThat(page.locator("cw-collection[id='petOwners']").getAttribute("paged")).isEqualTo("2");
+        assertThat(page.locator("cw-collection[id='petOwners'] [data-causeway-grid-next]").isVisible()).isTrue();
         assertThat(page.locator("cw-collection[id='futureVisits'] .causeway-collection-label").innerText())
                 .isEqualTo("Next appointments");
         assertThat(page.locator("cw-collection[id='futureVisits'] .causeway-collection-description").innerText())
@@ -211,6 +213,10 @@ void routesHomeObjectsHistoryCollectionsAndResponsiveLayout() {
                 .isEqualTo(nativeToolkit() ? 0 : 1);
         assertThat(toolkitRequests.stream().noneMatch(url -> !url.contains("/vaadin-menubar/"))).isTrue();
 
+        page.locator("cw-collection[id='petOwners'] [data-causeway-grid-next]").click();
+        page.waitForFunction("() => document.querySelector(\"cw-collection[id='petOwners']\")?.collectionState?.window?.offset === 2");
+        waitForCollectionRows("petOwners", 2);
+        assertThat(page.locator("cw-collection[id='petOwners'] [data-causeway-grid-previous]").isVisible()).isTrue();
         clickObjectLink("Mary Smith");
         waitForRoute("petclinic.PetOwner", "s_owner-mary");
         assertThat(page.locator(ROUTE_PAGE).getAttribute("data-page-kind")).isEqualTo("custom");
@@ -228,13 +234,22 @@ void routesHomeObjectsHistoryCollectionsAndResponsiveLayout() {
                 """)).isEqualTo(true);
         assertFocused(ROUTE_PAGE);
         waitForCollectionRows("pets", 2);
-        waitForCollectionRows("visits", 2);
+        waitForCollectionRows("visits", 1);
+        assertThat(page.locator("cw-collection[id='visits']").getAttribute("paged")).isEqualTo("1");
+        assertThat(page.locator("cw-collection[id='visits'] [data-causeway-grid-next]").isVisible()).isTrue();
+        page.locator("cw-collection[id='visits'] [data-causeway-grid-next]").click();
+        page.waitForFunction("() => document.querySelector(\"cw-collection[id='visits']\")?.collectionState?.window?.offset === 1");
+        waitForCollectionRows("visits", 1);
+        assertThat(page.locator("cw-collection[id='visits'] [data-causeway-grid-previous]").isVisible()).isTrue();
+        page.locator("cw-collection[id='visits'] [data-causeway-grid-previous]").click();
+        page.waitForFunction("() => document.querySelector(\"cw-collection[id='visits']\")?.collectionState?.window?.offset === 0");
+        waitForCollectionRows("visits", 1);
         assertCollectionHeading("pets", "Companion animals", "Pets currently registered to this owner.");
         assertCollectionHeading("visits", "Visit history", "All visits recorded for this owner's pets.");
-        assertThat(page.locator("cw-collection .causeway-collection-disabled-reason").count()).isZero();
+        final var visitDisabledReason = page.locator("cw-collection[id='visits'] .causeway-visually-hidden");
+        assertThat(visitDisabledReason.textContent()).contains("Cannot edit a mixed-in collection.");
+        assertThat(visitDisabledReason.getAttribute("class")).contains("causeway-visually-hidden");
         assertThat(page.locator("cw-collection [title*='Cannot edit']").count()).isZero();
-        assertThat(page.locator("cw-collection").allTextContents())
-                .noneMatch(text -> text.contains("Cannot edit a mixed-in collection"));
         assertCollectionPresentation("pets", "grid");
         assertCollectionPresentation("visits", "ordering-not-deterministic");
         assertThat(page.locator(".petclinic-page-toolbar cw-action[id='delete']").count()).isEqualTo(1);
@@ -259,7 +274,7 @@ void routesHomeObjectsHistoryCollectionsAndResponsiveLayout() {
         page.setViewportSize(700, 900);
         page.waitForFunction("() => [...document.querySelectorAll(\"cw-collection[id='pets'], cw-collection[id='visits']\")].every(element => element.dataset.causewayGridResponsive === 'narrow' && !element.querySelector('cw-collection-grid'))");
         waitForCollectionRows("pets", 2);
-        waitForCollectionRows("visits", 2);
+        waitForCollectionRows("visits", 1);
         assertThat(graphQLRequests.size()).isEqualTo(readsBeforeResponsiveSwitch);
         page.setViewportSize(1800, 900);
         if (!nativeToolkit()) {
@@ -479,7 +494,7 @@ void propertyEditingAndPromptFocusRemainDeterministic() {
         final var notes = page.locator("cw-property[id='notes']");
         final var notesEdit = notes.locator("[data-causeway-action='edit']");
         revealContainingTab(notes, notesEdit);
-        assertThat(notes.locator(".causeway-property-label").getAttribute("title"))
+        assertThat(notes.locator(".causeway-property-description").textContent())
                 .isEqualTo("Additional notes about this pet owner.");
         assertWideMultilinePropertyLayout(notes);
         notesEdit.click();
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 04a2291ccf8..3a15898aca8 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
@@ -216,22 +216,23 @@ void packagesHtmlPagesAndRetainsLayoutFallbackResources() throws Exception {
                 .contains("id=\"knownAs\" editable\n                       described-as=\"The familiar or preferred name used by this owner.\"")
                 .contains("id=\"notes\" editable multi-line=\"5\"")
                 .contains("id=\"lastVisit\" editable label-position=\"TOP\"")
-                .contains("<cw-collection id=\"pets\" named=\"Companion animals\"")
+                .contains("<cw-collection id=\"pets\" named=\"Companion animals\" active>")
                 .contains("<cw-action id=\"addPet\"")
                 .contains("<cw-action id=\"removePet\"")
                 .contains("<cw-collection id=\"visits\" named=\"Visit history\"")
                 .contains("described-as=\"All visits recorded for this owner's pets.\"")
+                .contains("active paged=\"1\"")
                 .contains("<cw-action id=\"bookVisit\"")
-                .doesNotContain("petclinic-associated-actions", "petclinic-member-composition");
+                .doesNotContain("petclinic-associated-actions", "petclinic-member-composition", " offset=", " size=");
         final String homeHtml;
         try (var input = loader.getResource(
                 "META-INF/causeway/webcomponents/pages/petclinic.HomePage.html").openStream()) {
             homeHtml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
         }
         assertThat(homeHtml)
-                .contains("<cw-collection id=\"futureVisits\" named=\"Next appointments\"")
-                .contains("<cw-collection id=\"petOwners\" label=\"Pet owners\"")
-                .doesNotContain("id=\"petOwners\" named=", "id=\"petOwners\" described-as=");
+                .contains("<cw-collection id=\"futureVisits\" named=\"Next appointments\" active>")
+                .contains("<cw-collection id=\"petOwners\" label=\"Pet owners\" active paged=\"2\">")
+                .doesNotContain("id=\"petOwners\" named=", "id=\"petOwners\" described-as=", " offset=", " size=");
         assertThat(ownerHtml.indexOf("id=\"addPet\""))
                 .isLessThan(ownerHtml.indexOf("id=\"removePet\""));
         assertThat(get("/META-INF/causeway/webcomponents/pages/petclinic.PetOwner.html").statusCode())


(causeway) 03/03: docs(openspec): archive collection grid controls and paging

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 bd886b595877d22e7474cc42b3a9b3375a29c7a6
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Aug 30 01:23:34 2026 +0200

    docs(openspec): archive collection grid controls and paging
---
 .../.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/vaadin-collection-grid-adapter/spec.md   |  0
 .../tasks.md                                       |  0
 openspec/specs/domain-web-components/spec.md       | 56 ++++++++++++++++++++++
 .../generic-htmx-web-component-viewer/spec.md      | 23 +++++++++
 .../specs/vaadin-collection-grid-adapter/spec.md   | 41 ++++++++++++++++
 10 files changed, 120 insertions(+)

diff --git a/openspec/changes/add-collection-grid-controls-and-paging/.openspec.yaml b/openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/.openspec.yaml
similarity index 100%
rename from openspec/changes/add-collection-grid-controls-and-paging/.openspec.yaml
rename to openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/.openspec.yaml
diff --git a/openspec/changes/add-collection-grid-controls-and-paging/design.md b/openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/design.md
similarity index 100%
rename from openspec/changes/add-collection-grid-controls-and-paging/design.md
rename to openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/design.md
diff --git a/openspec/changes/add-collection-grid-controls-and-paging/proposal.md b/openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/proposal.md
similarity index 100%
rename from openspec/changes/add-collection-grid-controls-and-paging/proposal.md
rename to openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/proposal.md
diff --git a/openspec/changes/add-collection-grid-controls-and-paging/specs/domain-web-components/spec.md b/openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/specs/domain-web-components/spec.md
similarity index 100%
rename from openspec/changes/add-collection-grid-controls-and-paging/specs/domain-web-components/spec.md
rename to openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/specs/domain-web-components/spec.md
diff --git a/openspec/changes/add-collection-grid-controls-and-paging/specs/generic-htmx-web-component-viewer/spec.md b/openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/specs/generic-htmx-web-component-viewer/spec.md
similarity index 100%
rename from openspec/changes/add-collection-grid-controls-and-paging/specs/generic-htmx-web-component-viewer/spec.md
rename to openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/specs/generic-htmx-web-component-viewer/spec.md
diff --git a/openspec/changes/add-collection-grid-controls-and-paging/specs/vaadin-collection-grid-adapter/spec.md b/openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/specs/vaadin-collection-grid-adapter/spec.md
similarity index 100%
rename from openspec/changes/add-collection-grid-controls-and-paging/specs/vaadin-collection-grid-adapter/spec.md
rename to openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/specs/vaadin-collection-grid-adapter/spec.md
diff --git a/openspec/changes/add-collection-grid-controls-and-paging/tasks.md b/openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/tasks.md
similarity index 100%
rename from openspec/changes/add-collection-grid-controls-and-paging/tasks.md
rename to openspec/changes/archive/2026-08-30-add-collection-grid-controls-and-paging/tasks.md
diff --git a/openspec/specs/domain-web-components/spec.md b/openspec/specs/domain-web-components/spec.md
index de2fa95d7b0..f598d55f3fd 100644
--- a/openspec/specs/domain-web-components/spec.md
+++ b/openspec/specs/domain-web-components/spec.md
@@ -1731,3 +1731,59 @@ A non-blank collection-level reason MUST remain available on demand from the sem
 - **WHEN** an individual row member or associated action has its own disabled state and reason
 - **THEN** that control retains its established semantic state and explanation
 - **AND** collection-level tooltip presentation does not broaden authorization or enable mutation
+
+### Requirement: Declarative collection paging
+`<cw-collection>` SHALL accept an optional `paged` attribute containing a bounded positive integer page size.
+A valid override SHALL apply consistently to initial loading, previous and next navigation, refresh, and current-generation reloads.
+
+#### Scenario: Valid page size is declared
+- **WHEN** an active collection declares `paged="10"`
+- **THEN** its first collection-window request starts at offset zero with size ten
+- **AND** previous and next requests preserve the authoritative normalized page size
+
+#### Scenario: Page size is absent
+- **WHEN** a collection does not declare `paged`
+- **THEN** established default collection loading and Grid qualification remain unchanged
+- **AND** no paging controls are introduced solely by this change
+
+#### Scenario: Page size is invalid
+- **WHEN** `paged` is blank, malformed, zero, negative, or above the accepted public bound
+- **THEN** the component treats it as absent
+- **AND** does not issue an unbounded or invalid request from that attribute
+
+#### Scenario: Page size changes reactively
+- **WHEN** a connected collection's effective `paged` value changes
+- **THEN** current loading and range generations are retired before loading offset zero with the new effective size
+- **AND** stale rows, focus, errors, or paging controls cannot replace the new state
+
+### Requirement: Declarative collection Grid controls
+`<cw-collection>` SHALL accept optional `resizable-columns` and `reorderable-columns` attributes for qualified Grid presentation.
+The attributes MUST NOT expose toolkit elements, events, or item objects and MUST NOT alter native fallback semantics.
+
+#### Scenario: Resizing is enabled
+- **WHEN** a qualified Grid collection declares `resizable-columns`
+- **THEN** each accepted generated Grid column is resizable
+- **AND** Causeway cell rendering and column selection remain authoritative
+
+#### Scenario: Reordering is enabled
+- **WHEN** a qualified Grid collection declares `reorderable-columns`
+- **THEN** the current Grid permits pointer and keyboard-supported column reordering provided by the toolkit
+- **AND** declarative order is restored after adapter replacement, native fallback, refresh, or navigation
+
+#### Scenario: Controls are absent
+- **WHEN** neither Grid-control attribute is declared
+- **THEN** resizing and reordering remain disabled
+- **AND** existing collection presentation is unchanged
+
+#### Scenario: Native presentation is active
+- **WHEN** responsive, policy, capability, or failure qualification selects native collection presentation
+- **THEN** Grid-control attributes do not create toolkit controls or alter the native table or list
+- **AND** later safe Grid qualification may apply the current attributes once
+
+### Requirement: Collection sorting and filtering remain collection-wide concerns
+The component SHALL NOT expose sorting or filtering behavior that applies only to a loaded window while appearing to cover the complete collection.
+
+#### Scenario: Current server contract lacks sort and filter inputs
+- **WHEN** the collection-window operation accepts offset and size but no ordering or filtering criteria
+- **THEN** the component does not enable Vaadin sorting or filtering controls
+- **AND** deterministic server ordering remains authoritative across windows
diff --git a/openspec/specs/generic-htmx-web-component-viewer/spec.md b/openspec/specs/generic-htmx-web-component-viewer/spec.md
index 8a9d89ed0b4..dc245d42a07 100644
--- a/openspec/specs/generic-htmx-web-component-viewer/spec.md
+++ b/openspec/specs/generic-htmx-web-component-viewer/spec.md
@@ -679,3 +679,26 @@ Petclinic integration and browser acceptance SHALL verify canonical and HTML-aut
 - **WHEN** integration and browser tests inspect selected home and owner collections
 - **THEN** names and descriptions follow documented precedence and selective application
 - **AND** existing rows, links, paging, responsive behavior, add/remove/book actions, and error monitoring remain valid
+
+### Requirement: Selective Petclinic collection paging overrides
+Petclinic HTML resource pages SHALL demonstrate declarative paging on collections that can grow materially while leaving smaller or summary collections on established loading behavior.
+
+#### Scenario: Global owner list renders
+- **WHEN** the Petclinic home page composes the owner collection
+- **THEN** its HTML override declares a bounded `paged` size
+- **AND** does not rely on inert `offset` or `size` attributes
+
+#### Scenario: Owner visit history renders
+- **WHEN** an owner page composes visit history
+- **THEN** that collection declares a bounded `paged` size
+- **AND** its associated actions and semantic columns remain unchanged
+
+#### Scenario: Small or summary collection renders
+- **WHEN** Petclinic composes an owner's pets or the upcoming-visit summary
+- **THEN** the HTML override does not declare `paged`
+- **AND** the collection retains established default loading behavior
+
+#### Scenario: Browser navigates a paged collection
+- **WHEN** normalized metadata reports another page
+- **THEN** the application exposes accessible Causeway previous and next controls and the configured range size
+- **AND** navigation does not duplicate rows, associated actions, requests, or page-level headings
diff --git a/openspec/specs/vaadin-collection-grid-adapter/spec.md b/openspec/specs/vaadin-collection-grid-adapter/spec.md
index 27655835924..443c1840f10 100644
--- a/openspec/specs/vaadin-collection-grid-adapter/spec.md
+++ b/openspec/specs/vaadin-collection-grid-adapter/spec.md
@@ -241,3 +241,44 @@ Default and native collection journeys SHALL remain release-qualified across key
 - **THEN** events and diagnostics contain only family, phase, bounded classification, and revision metadata
 - **AND** row values, protected values, GraphQL variables, serialized snapshots, and full server errors remain absent
 
+### Requirement: Approved Grid column controls
+The private Grid adapter SHALL map only reviewed Causeway presentation options to pinned Vaadin Grid properties.
+It SHALL keep toolkit controls disabled unless the owning `<cw-collection>` explicitly opts in.
+
+#### Scenario: Resizable columns are requested
+- **WHEN** the adapter receives a presentation with column resizing enabled
+- **THEN** every generated `vaadin-grid-column` has `resizable` enabled
+- **AND** the setting is removed when a later presentation disables resizing
+
+#### Scenario: Column reordering is requested
+- **WHEN** the adapter receives a presentation with column reordering enabled
+- **THEN** the generated `vaadin-grid` has `columnReorderingAllowed` enabled
+- **AND** raw reorder events and toolkit column state remain private implementation details
+
+#### Scenario: Adapter presentation is replaced
+- **WHEN** responsive mode, policy, route generation, collection state, or attributes replace the current adapter presentation
+- **THEN** the current Causeway options are applied exactly once to the current Grid and columns
+- **AND** stale toolkit controls cannot reapply a previous option set
+
+### Requirement: Explicit paging selects bounded Grid mode
+A valid Causeway paging override SHALL select bounded Grid presentation and SHALL retain Causeway-owned previous and next controls.
+Vaadin Grid MUST NOT create an independent paging state machine.
+
+#### Scenario: Paged collection has a safe total
+- **WHEN** a qualified collection declares a valid page size and the window reports a stable total
+- **THEN** the adapter receives only the current bounded window as items
+- **AND** Causeway-owned paging controls navigate normalized server offsets
+
+#### Scenario: Unpaged collection has a safe total
+- **WHEN** a qualified collection has no page-size override and reports a stable total
+- **THEN** established virtual Grid qualification remains available
+- **AND** the existing bounded range broker remains authoritative
+
+### Requirement: Unsupported sort and filter hints are not advertised
+The Grid adapter SHALL NOT enable sorter or filter controls until the Causeway range provider can apply their criteria across the complete collection.
+
+#### Scenario: Adapter creates columns under the current range contract
+- **WHEN** generated columns target an offset-and-size-only range provider
+- **THEN** no sorter or filter element is attached
+- **AND** the data provider does not ignore visible user sorting or filtering intent
+


(causeway) 01/03: docs(openspec): propose collection grid controls and paging

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 28ae131a2ede32c4613afd03416a20ab70dff698
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sat Aug 29 23:07:19 2026 +0200

    docs(openspec): propose collection grid controls and paging
---
 .../.openspec.yaml                                 |  2 +
 .../design.md                                      | 88 ++++++++++++++++++++++
 .../proposal.md                                    | 30 ++++++++
 .../specs/domain-web-components/spec.md            | 57 ++++++++++++++
 .../generic-htmx-web-component-viewer/spec.md      | 24 ++++++
 .../specs/vaadin-collection-grid-adapter/spec.md   | 42 +++++++++++
 .../tasks.md                                       | 20 +++++
 7 files changed, 263 insertions(+)

diff --git a/openspec/changes/add-collection-grid-controls-and-paging/.openspec.yaml b/openspec/changes/add-collection-grid-controls-and-paging/.openspec.yaml
new file mode 100644
index 00000000000..50adc91034f
--- /dev/null
+++ b/openspec/changes/add-collection-grid-controls-and-paging/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-08-29
diff --git a/openspec/changes/add-collection-grid-controls-and-paging/design.md b/openspec/changes/add-collection-grid-controls-and-paging/design.md
new file mode 100644
index 00000000000..1f467680bf3
--- /dev/null
+++ b/openspec/changes/add-collection-grid-controls-and-paging/design.md
@@ -0,0 +1,88 @@
+## Context
+
+`CausewayCollectionElement` already loads GraphQL collection windows and owns bounded previous/next controls, while `CausewayCollectionGridElement` privately creates Vaadin Grid controls for qualified wide collections.
+The public collection contract does not currently observe page-size or Grid interaction attributes, and Petclinic's existing `offset` and `size` attributes are inert.
+
+Vaadin Grid directly supports `columnReorderingAllowed` on the grid and `resizable` on each column.
+Its sorting and filtering controls instead emit data-provider hints that must be applied to the entire data set.
+The current GraphQL collection-window operation accepts only `offset` and `size`, so applying sort or filter only to a loaded window would produce misleading collection-wide behavior.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Add a bounded declarative page-size override to `<cw-collection>`.
+- Keep paging state and requests owned by Causeway rather than Vaadin.
+- Map explicit column resizing and reordering options through the private adapter.
+- Demonstrate paging selectively in Petclinic HTML overrides.
+- Preserve native fallback, responsive qualification, accessibility, focus, authorization, and bounded range behavior.
+
+**Non-Goals:**
+
+- Adding GraphQL ordering or filtering inputs.
+- Enabling page-local sorting or filtering that could be mistaken for collection-wide behavior.
+- Exposing raw Vaadin controls or events to applications.
+- Persisting user column widths or order across rerenders or routes.
+- Changing collections that do not opt into `paged`.
+
+## Decisions
+
+### Use `paged` as a bounded positive page-size attribute
+
+`paged="N"` will parse an integer from 1 through 100.
+Absent, blank, malformed, zero, negative, or oversized values will be treated as no override rather than producing unbounded requests.
+A valid value will be supplied to the initial load and retained by previous, next, refresh, and generation changes.
+
+The collection host will force bounded Grid presentation for an opted-in paged collection even when a total count is available, because Causeway-owned paging controls are the public behavior being requested.
+The server-returned window metadata remains authoritative for actual requested size, maximum size, offsets, and terminal-page state.
+
+This is preferred to reusing `size`, which is currently inert application markup and does not communicate that visible paging controls are requested.
+
+### Keep Grid controls explicit and adapter-private
+
+`resizable-columns` will set each generated Vaadin column's `resizable` property.
+`reorderable-columns` will set the Vaadin grid's `columnReorderingAllowed` property.
+Both attributes default to absent and false, apply only while Grid is qualified, and trigger a safe rerender when changed.
+Native table/list fallback remains unchanged.
+
+Column order changes remain presentation-local to the current Grid instance.
+Causeway's declarative column selection and GraphQL projection order remain authoritative after rerender, refresh, fallback, or navigation.
+
+### Descope sorting and filtering
+
+No `sortable` or `filterable` public behavior will be added in this change.
+Vaadin's sorter and filter elements pass sort orders and filters to the data provider, but the Causeway range provider has no corresponding server contract.
+Ignoring those hints or applying them to one page would violate deterministic cross-window behavior.
+
+A later change may introduce collection-wide sorting or filtering only after the GraphQL operation, authorization behavior, canonical value comparison, range-cache keys, and stale-request semantics are specified end to end.
+
+### Apply paging selectively in Petclinic
+
+The global owner list and owner visit history will opt into bounded paging with a small demonstration size.
+The upcoming-visit summary and an owner's usually small pet list will remain unpaged.
+Existing inert `offset` and `size` attributes will be removed from touched collection markup.
+
+## Risks / Trade-offs
+
+- [Risk] A configured GraphQL maximum below the declarative page size can reject the first request.
+  → Mitigation: keep the public bound conservative, surface the existing collection error state, and use Petclinic values below the default server maximum.
+- [Risk] Column reordering appears persistent while the current adapter is connected but resets after rerender.
+  → Mitigation: document it as presentation-local and preserve declarative order as authoritative.
+- [Risk] Changing paging attributes during an in-flight request can publish stale data.
+  → Mitigation: use the existing load revision, abort controller, range-broker teardown, and host revision before reloading from offset zero.
+- [Risk] Toolkit properties drift in a future Vaadin upgrade.
+  → Mitigation: adapter tests assert the reviewed `resizable` and `columnReorderingAllowed` mappings against the pinned closure.
+
+## Migration Plan
+
+1. Add and test normalized collection presentation attributes.
+2. Thread the page size into collection loading and bounded qualification.
+3. Map the approved controls in the private Grid adapter.
+4. Update usage documentation and selective Petclinic markup and regression tests.
+5. Regenerate no toolkit assets because the required APIs already exist in the pinned Grid closure.
+
+Rollback removes the new attributes and restores the prior Petclinic markup without data migration.
+
+## Open Questions
+
+None.
diff --git a/openspec/changes/add-collection-grid-controls-and-paging/proposal.md b/openspec/changes/add-collection-grid-controls-and-paging/proposal.md
new file mode 100644
index 00000000000..11ce2a03a3e
--- /dev/null
+++ b/openspec/changes/add-collection-grid-controls-and-paging/proposal.md
@@ -0,0 +1,30 @@
+## Why
+
+`<cw-collection>` already owns bounded collection windows and an internal Vaadin Grid adapter, but application markup cannot choose a page size or enable the Grid's safe column sizing controls.
+Petclinic currently declares inert `offset` and `size` attributes, so representative pages do not demonstrate selective collection paging through the public component contract.
+
+## What Changes
+
+- Add a bounded `paged="N"` attribute to `<cw-collection>` that requests the initial and subsequent Causeway-owned collection windows with page size `N`.
+- Add `resizable-columns` and `reorderable-columns` attributes that map through the private adapter boundary to Vaadin Grid column resizing and reordering.
+- Keep filtering disabled and do not expose it because the current GraphQL collection-window contract has no filter input.
+- Keep sorting unchanged and do not claim collection-wide sorting because the current GraphQL collection-window contract has no ordering input; sorting only a loaded page would be misleading.
+- Replace inert Petclinic `offset` and `size` markup with selective `paged` overrides on collections where paging is useful, while leaving smaller collections unpaged.
+- Add component, adapter, integration, and browser regression coverage for bounded paging and opted-in Grid controls.
+
+## Capabilities
+
+### New Capabilities
+
+None.
+
+### Modified Capabilities
+
+- `domain-web-components`: Add declarative collection page-size and Grid-control attributes with bounded parsing and reactive behavior.
+- `vaadin-collection-grid-adapter`: Map approved resizing and reordering options through the private adapter while retaining Causeway-owned paging and fallback.
+- `generic-htmx-web-component-viewer`: Demonstrate selective collection paging through Petclinic HTML resource overrides.
+
+## Impact
+
+The change affects the web-component foundation collection host, private Vaadin Grid adapter, component documentation and tests, and selected Petclinic HTML pages and tests.
+It does not change the GraphQL schema, collection ordering, filtering, native narrow presentation, toolkit packaging, production width policy, or application access to raw Vaadin elements.
diff --git a/openspec/changes/add-collection-grid-controls-and-paging/specs/domain-web-components/spec.md b/openspec/changes/add-collection-grid-controls-and-paging/specs/domain-web-components/spec.md
new file mode 100644
index 00000000000..f86cfb33a07
--- /dev/null
+++ b/openspec/changes/add-collection-grid-controls-and-paging/specs/domain-web-components/spec.md
@@ -0,0 +1,57 @@
+## ADDED Requirements
+
+### Requirement: Declarative collection paging
+`<cw-collection>` SHALL accept an optional `paged` attribute containing a bounded positive integer page size.
+A valid override SHALL apply consistently to initial loading, previous and next navigation, refresh, and current-generation reloads.
+
+#### Scenario: Valid page size is declared
+- **WHEN** an active collection declares `paged="10"`
+- **THEN** its first collection-window request starts at offset zero with size ten
+- **AND** previous and next requests preserve the authoritative normalized page size
+
+#### Scenario: Page size is absent
+- **WHEN** a collection does not declare `paged`
+- **THEN** established default collection loading and Grid qualification remain unchanged
+- **AND** no paging controls are introduced solely by this change
+
+#### Scenario: Page size is invalid
+- **WHEN** `paged` is blank, malformed, zero, negative, or above the accepted public bound
+- **THEN** the component treats it as absent
+- **AND** does not issue an unbounded or invalid request from that attribute
+
+#### Scenario: Page size changes reactively
+- **WHEN** a connected collection's effective `paged` value changes
+- **THEN** current loading and range generations are retired before loading offset zero with the new effective size
+- **AND** stale rows, focus, errors, or paging controls cannot replace the new state
+
+### Requirement: Declarative collection Grid controls
+`<cw-collection>` SHALL accept optional `resizable-columns` and `reorderable-columns` attributes for qualified Grid presentation.
+The attributes MUST NOT expose toolkit elements, events, or item objects and MUST NOT alter native fallback semantics.
+
+#### Scenario: Resizing is enabled
+- **WHEN** a qualified Grid collection declares `resizable-columns`
+- **THEN** each accepted generated Grid column is resizable
+- **AND** Causeway cell rendering and column selection remain authoritative
+
+#### Scenario: Reordering is enabled
+- **WHEN** a qualified Grid collection declares `reorderable-columns`
+- **THEN** the current Grid permits pointer and keyboard-supported column reordering provided by the toolkit
+- **AND** declarative order is restored after adapter replacement, native fallback, refresh, or navigation
+
+#### Scenario: Controls are absent
+- **WHEN** neither Grid-control attribute is declared
+- **THEN** resizing and reordering remain disabled
+- **AND** existing collection presentation is unchanged
+
+#### Scenario: Native presentation is active
+- **WHEN** responsive, policy, capability, or failure qualification selects native collection presentation
+- **THEN** Grid-control attributes do not create toolkit controls or alter the native table or list
+- **AND** later safe Grid qualification may apply the current attributes once
+
+### Requirement: Collection sorting and filtering remain collection-wide concerns
+The component SHALL NOT expose sorting or filtering behavior that applies only to a loaded window while appearing to cover the complete collection.
+
+#### Scenario: Current server contract lacks sort and filter inputs
+- **WHEN** the collection-window operation accepts offset and size but no ordering or filtering criteria
+- **THEN** the component does not enable Vaadin sorting or filtering controls
+- **AND** deterministic server ordering remains authoritative across windows
diff --git a/openspec/changes/add-collection-grid-controls-and-paging/specs/generic-htmx-web-component-viewer/spec.md b/openspec/changes/add-collection-grid-controls-and-paging/specs/generic-htmx-web-component-viewer/spec.md
new file mode 100644
index 00000000000..87c7c191f80
--- /dev/null
+++ b/openspec/changes/add-collection-grid-controls-and-paging/specs/generic-htmx-web-component-viewer/spec.md
@@ -0,0 +1,24 @@
+## ADDED Requirements
+
+### Requirement: Selective Petclinic collection paging overrides
+Petclinic HTML resource pages SHALL demonstrate declarative paging on collections that can grow materially while leaving smaller or summary collections on established loading behavior.
+
+#### Scenario: Global owner list renders
+- **WHEN** the Petclinic home page composes the owner collection
+- **THEN** its HTML override declares a bounded `paged` size
+- **AND** does not rely on inert `offset` or `size` attributes
+
+#### Scenario: Owner visit history renders
+- **WHEN** an owner page composes visit history
+- **THEN** that collection declares a bounded `paged` size
+- **AND** its associated actions and semantic columns remain unchanged
+
+#### Scenario: Small or summary collection renders
+- **WHEN** Petclinic composes an owner's pets or the upcoming-visit summary
+- **THEN** the HTML override does not declare `paged`
+- **AND** the collection retains established default loading behavior
+
+#### Scenario: Browser navigates a paged collection
+- **WHEN** normalized metadata reports another page
+- **THEN** the application exposes accessible Causeway previous and next controls and the configured range size
+- **AND** navigation does not duplicate rows, associated actions, requests, or page-level headings
diff --git a/openspec/changes/add-collection-grid-controls-and-paging/specs/vaadin-collection-grid-adapter/spec.md b/openspec/changes/add-collection-grid-controls-and-paging/specs/vaadin-collection-grid-adapter/spec.md
new file mode 100644
index 00000000000..ed23cce6cb6
--- /dev/null
+++ b/openspec/changes/add-collection-grid-controls-and-paging/specs/vaadin-collection-grid-adapter/spec.md
@@ -0,0 +1,42 @@
+## ADDED Requirements
+
+### Requirement: Approved Grid column controls
+The private Grid adapter SHALL map only reviewed Causeway presentation options to pinned Vaadin Grid properties.
+It SHALL keep toolkit controls disabled unless the owning `<cw-collection>` explicitly opts in.
+
+#### Scenario: Resizable columns are requested
+- **WHEN** the adapter receives a presentation with column resizing enabled
+- **THEN** every generated `vaadin-grid-column` has `resizable` enabled
+- **AND** the setting is removed when a later presentation disables resizing
+
+#### Scenario: Column reordering is requested
+- **WHEN** the adapter receives a presentation with column reordering enabled
+- **THEN** the generated `vaadin-grid` has `columnReorderingAllowed` enabled
+- **AND** raw reorder events and toolkit column state remain private implementation details
+
+#### Scenario: Adapter presentation is replaced
+- **WHEN** responsive mode, policy, route generation, collection state, or attributes replace the current adapter presentation
+- **THEN** the current Causeway options are applied exactly once to the current Grid and columns
+- **AND** stale toolkit controls cannot reapply a previous option set
+
+### Requirement: Explicit paging selects bounded Grid mode
+A valid Causeway paging override SHALL select bounded Grid presentation and SHALL retain Causeway-owned previous and next controls.
+Vaadin Grid MUST NOT create an independent paging state machine.
+
+#### Scenario: Paged collection has a safe total
+- **WHEN** a qualified collection declares a valid page size and the window reports a stable total
+- **THEN** the adapter receives only the current bounded window as items
+- **AND** Causeway-owned paging controls navigate normalized server offsets
+
+#### Scenario: Unpaged collection has a safe total
+- **WHEN** a qualified collection has no page-size override and reports a stable total
+- **THEN** established virtual Grid qualification remains available
+- **AND** the existing bounded range broker remains authoritative
+
+### Requirement: Unsupported sort and filter hints are not advertised
+The Grid adapter SHALL NOT enable sorter or filter controls until the Causeway range provider can apply their criteria across the complete collection.
+
+#### Scenario: Adapter creates columns under the current range contract
+- **WHEN** generated columns target an offset-and-size-only range provider
+- **THEN** no sorter or filter element is attached
+- **AND** the data provider does not ignore visible user sorting or filtering intent
diff --git a/openspec/changes/add-collection-grid-controls-and-paging/tasks.md b/openspec/changes/add-collection-grid-controls-and-paging/tasks.md
new file mode 100644
index 00000000000..69fe3a113ee
--- /dev/null
+++ b/openspec/changes/add-collection-grid-controls-and-paging/tasks.md
@@ -0,0 +1,20 @@
+## 1. Public collection contract
+
+- [ ] 1.1 Add observed, reactive `paged`, `resizable-columns`, and `reorderable-columns` collection attributes with bounded normalization and default-off Grid controls.
+- [ ] 1.2 Apply valid page sizes to initial and subsequent loads, reset changed paging to offset zero, and force Causeway-owned bounded presentation without weakening stale-request protection.
+- [ ] 1.3 Document the new collection attributes and the deliberate sorting and filtering exclusion.
+
+## 2. Private Grid adapter
+
+- [ ] 2.1 Thread normalized control options through the frozen adapter presentation contract.
+- [ ] 2.2 Map resizing to generated Vaadin columns and reordering to the Vaadin Grid while preserving private events, native fallback, and declarative column authority.
+- [ ] 2.3 Add adapter and collection tests for defaults, opt-in mapping, bounded paging, reactivity, invalid values, and fallback behavior.
+
+## 3. Petclinic demonstration
+
+- [ ] 3.1 Replace inert collection range attributes with selective `paged` overrides on the global owner list and owner visit history while leaving upcoming visits and pets unpaged.
+- [ ] 3.2 Update Petclinic resource composition, integration, and Playwright assertions for configured page size and accessible paging without changing associated actions.
+
+## 4. Verification
+
+- [ ] 4.1 Run foundation Node and Maven tests, Petclinic integration and browser tests, RAT, IntelliJ inspections or compilation, diff checks, and strict OpenSpec validation.